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 &mdash; {@code true} iff this parameter carries a Jakarta Bean Validation
125    * {@code @Valid} (or equivalent) marker. Off-by-default opt-in &mdash; 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}