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.QueryAnnotation.*;
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 Query} 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#getQueryParams() getQueryParams}()
045 *       .{@link RequestQueryParams#get(String) get}(<jv>name</jv>)
046 *       .{@link RequestQueryParam#as(Class) as}(<jv>type</jv>);
047 * </p>
048 *
049 * <p>
050 * {@link HttpPartSchema schema} is derived from the {@link Query} 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 QueryArg 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 QueryArg}, or <jk>null</jk> if the parameter is not annotated with {@link Query}.
069    */
070   public static QueryArg create(ParameterInfo paramInfo, AnnotationWorkList annotations) {
071      if (AP.has(Query.class, paramInfo))
072         return new QueryArg(paramInfo, annotations);
073      return null;
074   }
075
076   /**
077    * Gets the merged @Query annotation combining class-level and parameter-level values.
078    *
079    * @param pi The parameter info.
080    * @param paramName The query parameter name.
081    * @return Merged annotation, or null if no class-level defaults exist.
082    */
083   private static Query getMergedQuery(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 @Query from class-level queryParams array
095      var classLevelQuery = (Query)null;
096      for (var q : restAnnotation.queryParams()) {
097         var qName = firstNonEmpty(q.name(), q.value());
098         if (paramName.equals(qName)) {
099            classLevelQuery = q;
100            break;
101         }
102      }
103
104      if (classLevelQuery == null)
105         return null;
106
107      // Get parameter-level @Query
108      var paramQuery = AP.find(Query.class, pi).stream().findFirst().map(AnnotationInfo::inner).orElse(null);
109
110      if (paramQuery == null) {
111         // No parameter-level @Query, use class-level as-is
112         return classLevelQuery;
113      }
114
115      // Merge the two annotations: parameter-level takes precedence
116      return mergeAnnotations(classLevelQuery, paramQuery);
117   }
118
119   /**
120    * Merges two @Query 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 Query mergeAnnotations(Query classLevel, Query paramLevel) {
127      // @formatter:off
128      return QueryAnnotation.create()
129         .name(firstNonEmpty(paramLevel.name(), paramLevel.value(), classLevel.name(), classLevel.value()))
130         .value(firstNonEmpty(paramLevel.value(), paramLevel.name(), classLevel.value(), classLevel.name()))
131         .def(firstNonEmpty(paramLevel.def(), classLevel.def()))
132         .description(paramLevel.description().length > 0 ? paramLevel.description() : classLevel.description())
133         .parser(paramLevel.parser() != HttpPartParser.Void.class ? paramLevel.parser() : classLevel.parser())
134         .serializer(paramLevel.serializer() != HttpPartSerializer.Void.class ? paramLevel.serializer() : classLevel.serializer())
135         .schema(mergeSchemas(classLevel.schema(), paramLevel.schema()))
136         .build();
137      // @formatter:on
138   }
139
140   /**
141    * Merges two @Schema annotations, with param-level taking precedence.
142    *
143    * @param classLevel The class-level default.
144    * @param paramLevel The parameter-level override.
145    * @return Merged annotation.
146    */
147   private static Schema mergeSchemas(Schema classLevel, Schema paramLevel) {
148      // If parameter has a non-default schema, use it; otherwise use class-level
149      if (! SchemaAnnotation.empty(paramLevel))
150         return paramLevel;
151      return classLevel;
152   }
153
154   private final boolean multi;
155   private final HttpPartParser partParser;
156   private final HttpPartSchema schema;
157   private final String name, def;
158   private final ClassInfo type;
159
160   /**
161    * Constructor.
162    *
163    * @param pi The Java method parameter being resolved.
164    * @param annotations The annotations to apply to any new part parsers.
165    */
166   protected QueryArg(ParameterInfo pi, AnnotationWorkList annotations) {
167      // Get the query name from the parameter
168      this.name = findName(pi).orElseThrow(() -> new ArgException(pi, "@Query used without name or value"));
169
170      // Check for class-level defaults and merge if found
171      var mergedQuery = getMergedQuery(pi, name);
172
173      // Use merged query annotation for all lookups
174      this.def = nn(mergedQuery) && ! mergedQuery.def().isEmpty() ? mergedQuery.def() : findDef(pi).orElse(null);
175      this.type = pi.getParameterType();
176      this.schema = nn(mergedQuery) ? HttpPartSchema.create(mergedQuery) : HttpPartSchema.create(Query.class, pi);
177      var pp = schema.getParser();
178      this.partParser = nn(pp) ? HttpPartParser.creator().type(pp).apply(annotations).create() : null;
179      this.multi = schema.getCollectionFormat() == HttpPartCollectionFormat.MULTI;
180
181      if (multi && ! type.isCollectionOrArray())
182         throw new ArgException(pi, "Use of multipart flag on @Query parameter that is not an array or Collection");
183   }
184
185   @SuppressWarnings({ "rawtypes", "unchecked" })
186   @Override /* Overridden from RestOpArg */
187   public Object resolve(RestOpSession opSession) throws Exception {
188      var req = opSession.getRequest();
189      var ps = partParser == null ? req.getPartParserSession() : partParser.getPartSession();
190      var rh = req.getQueryParams();
191      var bs = req.getBeanSession();
192      var cm = bs.getClassMeta(type.innerType());
193
194      if (multi) {
195         var c = cm.isArray() ? list() : (Collection)(cm.canCreateNewInstance() ? cm.newInstance() : new JsonList());
196         rh.getAll(name).stream().map(x -> x.parser(ps).schema(schema).as(cm.getElementType()).orElse(null)).forEach(x -> c.add(x));
197         return cm.isArray() ? toArray(c, cm.getElementType().inner()) : c;
198      }
199
200      if (cm.isMapOrBean() && isOneOf(name, "*", "")) {
201         var m = new JsonMap();
202         rh.forEach(e -> m.put(e.getName(), e.parser(ps).schema(schema == null ? null : schema.getProperty(e.getName())).as(cm.getValueType()).orElse(null)));
203         return req.getBeanSession().convertToType(m, cm);
204      }
205
206      return rh.getLast(name).parser(ps).schema(schema).def(def).as(type.innerType()).orElse(null);
207   }
208}