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.arg; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021import static org.apache.juneau.commons.utils.Utils.*; 022import static org.apache.juneau.http.annotation.FormDataAnnotation.*; 023 024import java.util.*; 025 026import org.apache.juneau.*; 027import org.apache.juneau.annotation.*; 028import org.apache.juneau.collections.*; 029import org.apache.juneau.commons.reflect.*; 030import org.apache.juneau.http.annotation.*; 031import org.apache.juneau.httppart.*; 032import org.apache.juneau.rest.*; 033import org.apache.juneau.rest.annotation.*; 034import org.apache.juneau.rest.httppart.*; 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 RequestFormParams#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 */ 059public class FormDataArg implements RestOpArg { 060 061 private static final AnnotationProvider AP = AnnotationProvider.INSTANCE; 062 063 /** 064 * Static creator. 065 * 066 * @param paramInfo The Java method parameter being resolved. 067 * @param annotations The annotations to apply to any new part parsers. 068 * @return A new {@link FormDataArg}, or <jk>null</jk> if the parameter is not annotated with {@link FormData}. 069 */ 070 public static FormDataArg create(ParameterInfo paramInfo, AnnotationWorkList annotations) { 071 if (AP.has(FormData.class, paramInfo)) 072 return new FormDataArg(paramInfo, annotations); 073 return null; 074 } 075 076 /** 077 * Gets the merged @FormData annotation combining class-level and parameter-level values. 078 * 079 * @param pi The parameter info. 080 * @param paramName The form data parameter name. 081 * @return Merged annotation, or null if no class-level defaults exist. 082 */ 083 private static FormData getMergedFormData(ParameterInfo pi, String paramName) { 084 // Get the declaring class 085 var declaringClass = pi.getMethod().getDeclaringClass(); 086 if (declaringClass == null) 087 return null; 088 089 // Find @Rest annotation on the class 090 var restAnnotation = declaringClass.getAnnotations(Rest.class).findFirst().map(AnnotationInfo::inner).orElse(null); 091 if (restAnnotation == null) 092 return null; 093 094 // Find matching @FormData from class-level formDataParams array 095 var classLevelFormData = (FormData)null; 096 for (var f : restAnnotation.formDataParams()) { 097 var fName = firstNonEmpty(f.name(), f.value()); 098 if (eq(paramName, fName)) { 099 classLevelFormData = f; 100 break; 101 } 102 } 103 104 if (classLevelFormData == null) 105 return null; 106 107 // Get parameter-level @FormData 108 var paramFormData = AP.find(FormData.class, pi).stream().findFirst().map(AnnotationInfo::inner).orElse(null); 109 110 if (paramFormData == null) { 111 // No parameter-level @FormData, use class-level as-is 112 return classLevelFormData; 113 } 114 115 // Merge the two annotations: parameter-level takes precedence 116 return mergeAnnotations(classLevelFormData, paramFormData); 117 } 118 119 /** 120 * Merges two @FormData annotations, with param-level taking precedence over class-level. 121 * 122 * @param classLevel The class-level default. 123 * @param paramLevel The parameter-level override. 124 * @return Merged annotation. 125 */ 126 private static FormData mergeAnnotations(FormData classLevel, FormData paramLevel) { 127 return FormDataAnnotation.create().name(firstNonEmpty(paramLevel.name(), paramLevel.value(), classLevel.name(), classLevel.value())) 128 .value(firstNonEmpty(paramLevel.value(), paramLevel.name(), classLevel.value(), classLevel.name())).def(firstNonEmpty(paramLevel.def(), classLevel.def())) 129 .description(paramLevel.description().length > 0 ? paramLevel.description() : classLevel.description()) 130 .parser(paramLevel.parser() != HttpPartParser.Void.class ? paramLevel.parser() : classLevel.parser()) 131 .serializer(paramLevel.serializer() != HttpPartSerializer.Void.class ? paramLevel.serializer() : classLevel.serializer()).schema(mergeSchemas(classLevel.schema(), paramLevel.schema())) 132 .build(); 133 } 134 135 /** 136 * Merges two @Schema annotations, with param-level taking precedence. 137 * 138 * @param classLevel The class-level default. 139 * @param paramLevel The parameter-level override. 140 * @return Merged annotation. 141 */ 142 private static Schema mergeSchemas(Schema classLevel, Schema paramLevel) { 143 // If parameter has a non-default schema, use it; otherwise use class-level 144 if (! SchemaAnnotation.empty(paramLevel)) 145 return paramLevel; 146 return classLevel; 147 } 148 149 private final boolean multi; 150 151 private final HttpPartParser partParser; 152 153 private final HttpPartSchema schema; 154 155 private final String name, def; 156 157 private final ClassInfo type; 158 159 /** 160 * Constructor. 161 * 162 * @param pi The Java method parameter being resolved. 163 * @param annotations The annotations to apply to any new part parsers. 164 */ 165 protected FormDataArg(ParameterInfo pi, AnnotationWorkList annotations) { 166 // Get the form data parameter name 167 this.name = findName(pi).orElseThrow(() -> new ArgException(pi, "@FormData used without name or value")); 168 169 // Check for class-level defaults and merge if found 170 FormData mergedFormData = getMergedFormData(pi, name); 171 172 // Use merged form data annotation for all lookups 173 this.def = nn(mergedFormData) && ! mergedFormData.def().isEmpty() ? mergedFormData.def() : findDef(pi).orElse(null); 174 this.type = pi.getParameterType(); 175 this.schema = nn(mergedFormData) ? HttpPartSchema.create(mergedFormData) : HttpPartSchema.create(FormData.class, pi); 176 Class<? extends HttpPartParser> pp = schema.getParser(); 177 this.partParser = nn(pp) ? HttpPartParser.creator().type(pp).apply(annotations).create() : null; 178 this.multi = schema.getCollectionFormat() == HttpPartCollectionFormat.MULTI; 179 180 if (multi && ! type.isCollectionOrArray()) 181 throw new ArgException(pi, "Use of multipart flag on @FormData parameter that is not an array or Collection"); 182 } 183 184 @SuppressWarnings({ "rawtypes", "unchecked" }) 185 @Override /* Overridden from RestOpArg */ 186 public Object resolve(RestOpSession opSession) throws Exception { 187 RestRequest req = opSession.getRequest(); 188 HttpPartParserSession ps = partParser == null ? req.getPartParserSession() : partParser.getPartSession(); 189 RequestFormParams rh = req.getFormParams(); 190 BeanSession bs = req.getBeanSession(); 191 var cm = bs.getClassMeta(type.innerType()); 192 193 if (multi) { 194 Collection c = cm.isArray() ? list() : (Collection)(cm.canCreateNewInstance() ? cm.newInstance() : new JsonList()); 195 rh.getAll(name).stream().map(x -> x.parser(ps).schema(schema).as(cm.getElementType()).orElse(null)).forEach(x -> c.add(x)); 196 return cm.isArray() ? toArray(c, cm.getElementType().inner()) : c; 197 } 198 199 if (cm.isMapOrBean() && isOneOf(name, "*", "")) { 200 var m = new JsonMap(); 201 rh.forEach(e -> m.put(e.getName(), e.parser(ps).schema(schema == null ? null : schema.getProperty(e.getName())).as(cm.getValueType()).orElse(null))); 202 return req.getBeanSession().convertToType(m, cm); 203 } 204 205 return rh.getLast(name).parser(ps).schema(schema).def(def).as(type.innerType()).orElse(null); 206 } 207}