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.Constants.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021import static org.apache.juneau.commons.utils.Utils.*;
022import static org.apache.juneau.http.annotation.PathAnnotation.*;
023
024import java.lang.reflect.*;
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.*;
035import org.apache.juneau.rest.util.*;
036
037/**
038 * Resolves method parameters and parameter types annotated with {@link Path} on {@link RestOp}-annotated Java methods.
039 *
040 * <p>
041 * The parameter value is resolved using:
042 * <p class='bjava'>
043 *    <jv>opSession</jv>
044 *       .{@link RestOpSession#getRequest() getRequest}()
045 *       .{@link RestRequest#getPathParams() getPathParams}()
046 *       .{@link RequestPathParams#get(String) get}(<jv>name</jv>)
047 *       .{@link RequestPathParam#as(Class) as}(<jv>type</jv>);
048 * </p>
049 *
050 * <p>
051 * {@link HttpPartSchema schema} is derived from the {@link Path} annotation.
052 *
053 * <h5 class='section'>See Also:</h5><ul>
054 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JavaMethodParameters">Java Method Parameters</a>
055 * </ul>
056 */
057public class PathArg implements RestOpArg {
058
059   private static final AnnotationProvider AP = AnnotationProvider.INSTANCE;
060
061   /**
062    * Static creator.
063    *
064    * @param paramInfo The Java method parameter being resolved.
065    * @param annotations The annotations to apply to any new part parsers.
066    * @param pathMatcher Path matcher for the specified method.
067    * @return A new {@link PathArg}, or <jk>null</jk> if the parameter is not annotated with {@link Path}.
068    */
069   public static PathArg create(ParameterInfo paramInfo, AnnotationWorkList annotations, UrlPathMatcher pathMatcher) {
070      if (AP.has(Path.class, paramInfo))
071         return new PathArg(paramInfo, annotations, pathMatcher);
072      return null;
073   }
074
075   /**
076    * Gets the merged @Path annotation combining class-level and parameter-level values.
077    *
078    * @param pi The parameter info.
079    * @param paramName The path parameter name.
080    * @return Merged annotation, or null if no class-level defaults exist.
081    */
082   private static Path getMergedPath(ParameterInfo pi, String paramName) {
083      // Get the declaring class
084      var declaringClass = pi.getMethod().getDeclaringClass();
085      if (declaringClass == null)
086         return null;
087
088      // Find @Rest annotation on the class
089      var restAnnotation = declaringClass.getAnnotations(Rest.class).findFirst().map(AnnotationInfo::inner).orElse(null);
090      if (restAnnotation == null)
091         return null;
092
093      // Find matching @Path from class-level pathParams array
094      var classLevelPath = (Path)null;
095      for (var p : restAnnotation.pathParams()) {
096         var pName = firstNonEmpty(p.name(), p.value());
097         if (paramName.equals(pName)) {
098            classLevelPath = p;
099            break;
100         }
101      }
102
103      if (classLevelPath == null)
104         return null;
105
106      // Get parameter-level @Path
107      var paramPath = AP.find(Path.class, pi).stream().findFirst().map(AnnotationInfo::inner).orElse(null);
108
109      if (paramPath == null) {
110         // No parameter-level @Path, use class-level as-is
111         return classLevelPath;
112      }
113
114      // Merge the two annotations: parameter-level takes precedence
115      return mergeAnnotations(classLevelPath, paramPath);
116   }
117
118   /**
119    * Merges two @Path annotations, with param-level taking precedence over class-level.
120    *
121    * @param classLevel The class-level default.
122    * @param paramLevel The parameter-level override.
123    * @return Merged annotation.
124    */
125   private static Path mergeAnnotations(Path classLevel, Path paramLevel) {
126      // @formatter:off
127      return PathAnnotation.create()
128         .name(firstNonEmpty(paramLevel.name(), paramLevel.value(), classLevel.name(), classLevel.value()))
129         .value(firstNonEmpty(paramLevel.value(), paramLevel.name(), classLevel.value(), classLevel.name()))
130         .def(firstNonEmpty(paramLevel.def(), classLevel.def()))
131         .description(paramLevel.description().length > 0 ? paramLevel.description() : classLevel.description())
132         .parser(paramLevel.parser() != HttpPartParser.Void.class ? paramLevel.parser() : classLevel.parser())
133         .serializer(paramLevel.serializer() != HttpPartSerializer.Void.class ? paramLevel.serializer() : classLevel.serializer())
134         .schema(mergeSchemas(classLevel.schema(), paramLevel.schema()))
135         .build();
136      // @formatter:on
137   }
138
139   /**
140    * Merges two @Schema annotations, with param-level taking precedence.
141    *
142    * @param classLevel The class-level default.
143    * @param paramLevel The parameter-level override.
144    * @return Merged annotation.
145    */
146   private static Schema mergeSchemas(Schema classLevel, Schema paramLevel) {
147      // If parameter has a non-default schema, use it; otherwise use class-level
148      if (! SchemaAnnotation.empty(paramLevel))
149         return paramLevel;
150      return classLevel;
151   }
152
153   private final HttpPartParser partParser;
154   private final HttpPartSchema schema;
155   private final String name, def;
156   private final Type type;
157
158   /**
159    * Constructor.
160    *
161    * @param paramInfo The Java method parameter being resolved.
162    * @param annotations The annotations to apply to any new part parsers.
163    * @param pathMatcher Path matcher for the specified method.
164    */
165   protected PathArg(ParameterInfo paramInfo, AnnotationWorkList annotations, UrlPathMatcher pathMatcher) {
166      // Get the path parameter name
167      this.name = getName(paramInfo, pathMatcher);
168
169      // Check for class-level defaults and merge if found
170      var mergedPath = getMergedPath(paramInfo, name);
171
172      // Use merged path annotation for all lookups
173      var pathDef = nn(mergedPath) ? mergedPath.def() : null;
174      this.def = nn(pathDef) && neq(NONE, pathDef) ? pathDef : findDef(paramInfo).orElse(null);
175      this.type = paramInfo.getParameterType().innerType();
176      this.schema = nn(mergedPath) ? HttpPartSchema.create(mergedPath) : HttpPartSchema.create(Path.class, paramInfo);
177      var pp = schema.getParser();
178      this.partParser = nn(pp) ? HttpPartParser.creator().type(pp).apply(annotations).create() : null;
179   }
180
181   @Override /* Overridden from RestOpArg */
182   public Object resolve(RestOpSession opSession) throws Exception {
183      var req = opSession.getRequest();
184      if (name.equals("*")) {
185         var m = new JsonMap();
186         req.getPathParams().stream().forEach(x -> m.put(x.getName(), x.getValue()));
187         return req.getBeanSession().convertToType(m, type);
188      }
189      var ps = partParser == null ? req.getPartParserSession() : partParser.getPartSession();
190      return req.getPathParams().get(name).parser(ps).schema(schema).def(def).as(type).orElse(null);
191   }
192
193   private static String getName(ParameterInfo pi, UrlPathMatcher pathMatcher) {
194      var p = findName(pi).orElse(null);
195      if (nn(p))
196         return p;
197      if (nn(pathMatcher)) {
198         var idx = 0;
199         var i = pi.getIndex();
200         var mi = pi.getMethod();
201
202         for (var j = 0; j < i; j++) {
203            var hasAnnotation = AP.has(Path.class, mi.getParameter(j));
204            if (hasAnnotation)
205               idx++;
206         }
207
208         var vars = pathMatcher.getVars();
209         if (vars.length <= idx)
210            throw new ArgException(pi, "Number of attribute parameters exceeds the number of URL pattern variables.  vars.length={0}, idx={1}", vars.length, idx);
211
212         // Check for {#} variables.
213         var idxs = String.valueOf(idx);
214         for (var var : vars)
215            if (isNumeric(var) && var.equals(idxs))
216               return var;
217
218         return pathMatcher.getVars()[idx];
219      }
220      throw new ArgException(pi, "@Path used without name or value");
221   }
222}