001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.arg; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022 023import java.util.*; 024 025import org.apache.juneau.commons.*; 026import org.apache.juneau.commons.httppart.*; 027import org.apache.juneau.commons.reflect.*; 028import org.apache.juneau.http.*; 029import org.apache.juneau.marshall.*; 030import org.apache.juneau.marshall.collections.*; 031import org.apache.juneau.marshall.httppart.*; 032import org.apache.juneau.rest.server.*; 033import org.apache.juneau.rest.server.httppart.*; 034import org.apache.juneau.rest.server.validation.*; 035 036/** 037 * Resolves method parameters and parameter types annotated with {@link FormData} on {@link RestOp}-annotated Java methods. 038 * 039 * <p> 040 * The parameter value is resolved using: 041 * <p class='bjava'> 042 * <jv>opSession</jv> 043 * .{@link RestOpSession#getRequest() getRequest}() 044 * .{@link RestRequest#getFormParams() getFormParams}() 045 * .{@link RequestFormParamList#get(String) get}(<jv>name</jv>) 046 * .{@link RequestFormParam#as(Class) as}(<jv>type</jv>); 047 * </p> 048 * 049 * <p> 050 * {@link HttpPartSchema schema} is derived from the {@link FormData} annotation. 051 * 052 * <p> 053 * If the {@link Schema#collectionFormat()} value is {@link HttpPartCollectionFormat#MULTI}, then the data type can be a {@link Collection} or array. 054 * 055 * <h5 class='section'>See Also:</h5><ul> 056 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JavaMethodParameters">Java Method Parameters</a> 057 * </ul> 058 */ 059@SuppressWarnings({ 060 "unchecked" // The schema parser is cast to Class<? extends HttpPartParser>; the annotation contract guarantees the bound 061}) 062public class FormDataArg implements RestOpArg { 063 064 private static final AnnotationProvider AP = AnnotationProvider.INSTANCE; 065 066 /** 067 * Static creator. 068 * 069 * @param paramInfo The Java method parameter being resolved. Must not be <jk>null</jk>. 070 * @param annotations The annotations to apply to any new part parsers. 071 * @return A new {@link FormDataArg}, or <jk>null</jk> if the parameter is not annotated with {@link FormData}. 072 */ 073 public static FormDataArg create(ParameterInfo paramInfo, AnnotationWorkList annotations) { 074 if (AP.has(FormData.class, paramInfo)) 075 return new FormDataArg(paramInfo, annotations); 076 return null; 077 } 078 079 private static Optional<String> findName(ParameterInfo pi) { 080 // @formatter:off 081 return AP.find(FormData.class, pi) 082 .stream() 083 .map(AnnotationInfo::inner) 084 .filter(x -> isAnyNotBlank(x.value(), x.name())) 085 .findFirst() 086 .map(x -> firstNonBlank(x.name(), x.value())); 087 // @formatter:on 088 } 089 090 private static Optional<String> findDef(ParameterInfo pi) { 091 // @formatter:off 092 return AP.find(FormData.class, pi) 093 .stream() 094 .map(AnnotationInfo::inner) 095 .filter(x -> ine(x.def())) 096 .findFirst() 097 .map(FormData::def); 098 // @formatter:on 099 } 100 101 private static FormData getClassLevelFormData(ParameterInfo pi, String paramName) { 102 var declaringClass = pi.getMethod().getDeclaringClass(); 103 if (declaringClass == null) 104 return null; 105 var restAnnotation = declaringClass.getAnnotations(Rest.class).findFirst().map(AnnotationInfo::inner).orElse(null); 106 if (restAnnotation == null) 107 return null; 108 for (var f : restAnnotation.formDataParams()) { 109 var fName = firstNonEmpty(f.name(), f.value()); 110 if (eq(paramName, fName)) 111 return f; 112 } 113 return null; 114 } 115 116 private final boolean multi; 117 private final HttpPartParser partParser; 118 private final HttpPartSchema schema; 119 private final String name; 120 private final String def; 121 private final ClassInfo type; 122 123 /** 124 * Pre-computed flag — {@code true} iff this parameter carries a Jakarta Bean Validation 125 * {@code @Valid} (or equivalent) marker. Off-by-default opt-in — see {@link BeanValidator}. 126 */ 127 private final boolean validate; 128 129 /** 130 * Constructor. 131 * 132 * @param pi The Java method parameter being resolved. Must not be <jk>null</jk>. 133 * @param annotations The annotations to apply to any new part parsers. 134 */ 135 protected FormDataArg(ParameterInfo pi, AnnotationWorkList annotations) { 136 this.name = findName(pi).orElseThrow(() -> new ArgException(pi, "@FormData used without name or value")); 137 138 var classLevelFormData = getClassLevelFormData(pi, name); 139 140 var schemaBuilder = HttpPartSchema.create(); 141 if (classLevelFormData != null) 142 schemaBuilder.apply(classLevelFormData); 143 schemaBuilder.applyAll(FormData.class, pi); 144 this.schema = schemaBuilder.build(); 145 146 this.def = findDef(pi).or(() -> o(classLevelFormData).filter(f -> ine(f.def())).map(FormData::def)).orElse(null); 147 this.type = pi.getParameterType(); 148 var pp = (Class<? extends HttpPartParser>)schema.getParser(); 149 this.partParser = nn(pp) ? HttpPartParser.creator().type(pp).apply(annotations).create() : null; 150 this.multi = schema.getCollectionFormat() == HttpPartCollectionFormat.MULTI; 151 this.validate = BeanValidator.isValidationRequested(pi); 152 153 if (multi && ! type.isCollectionOrArray()) 154 throw new ArgException(pi, "Use of multipart flag on @FormData parameter that is not an array or Collection"); 155 } 156 157 @SuppressWarnings({ 158 "rawtypes", // Raw types necessary for REST argument resolution with generic types 159 "resource" // as(Class)/as(Type,Type...) calls below are generic and conservatively treated as Closeable; the resolved form value is handed to the invoked op method, not owned/closed here. 160 }) 161 @Override /* Overridden from RestOpArg */ 162 public Object resolve(RestOpSession opSession) throws Exception { 163 var req = opSession.getRequest(); 164 var ps = partParser == null ? req.getPartParserSession() : partParser.getPartSession(); 165 var rh = req.getFormParams(); 166 var bs = req.getMarshallingSession(); 167 var cm = bs.getClassMeta(type.innerType()); 168 169 Object result; 170 if (multi) { 171 Collection c; 172 if (cm.isArray()) { 173 c = list(); 174 } else if (cm.canCreateNewInstance()) { 175 c = (Collection)cm.newInstance(); 176 } else { 177 c = new JsonList(); 178 } 179 rh.getAll(name).stream().map(x -> x.parser(ps).schema(schema).as(cm.getElementType()).orElse(null)).forEach(c::add); 180 result = cm.isArray() ? toArray(c, cm.getElementType().inner()) : c; 181 } else if (cm.isMapOrBean() && isOneOf(name, "*", "")) { 182 var m = new JsonMap(); 183 rh.forEach(e -> m.put(e.getName(), e.parser(ps).schema(schema == null ? null : schema.getProperty(e.getName())).as(cm.getValueType()).orElse(null))); 184 result = req.getMarshallingSession().convertToType(m, cm); 185 } else { 186 result = rh.getLast(name).parser(ps).schema(schema).def(def).as(type.innerType()).orElse(null); 187 } 188 return validate ? BeanValidator.validate(result, opSession.getBeanStore()) : result; 189 } 190}