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.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021import static org.apache.juneau.marshall.Constants.*;
022
023import java.lang.reflect.*;
024import java.util.*;
025
026import org.apache.juneau.commons.reflect.*;
027import org.apache.juneau.http.*;
028import org.apache.juneau.marshall.*;
029import org.apache.juneau.marshall.collections.*;
030import org.apache.juneau.marshall.httppart.*;
031import org.apache.juneau.rest.server.*;
032import org.apache.juneau.rest.server.httppart.*;
033import org.apache.juneau.rest.server.util.*;
034
035/**
036 * Resolves method parameters and parameter types annotated with {@link Path} on {@link RestOp}-annotated Java methods.
037 *
038 * <p>
039 * The parameter value is resolved using:
040 * <p class='bjava'>
041 *    <jv>opSession</jv>
042 *       .{@link RestOpSession#getRequest() getRequest}()
043 *       .{@link RestRequest#getPathParams() getPathParams}()
044 *       .{@link RequestPathParamList#get(String) get}(<jv>name</jv>)
045 *       .{@link RequestPathParam#as(Class) as}(<jv>type</jv>);
046 * </p>
047 *
048 * <p>
049 * {@link HttpPartSchema schema} is derived from the {@link Path} annotation.
050 *
051 * <h5 class='section'>See Also:</h5><ul>
052 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JavaMethodParameters">Java Method Parameters</a>
053 * </ul>
054 */
055public class PathArg implements RestOpArg {
056
057   private static final AnnotationProvider AP = AnnotationProvider.INSTANCE;
058
059   /**
060    * Static creator.
061    *
062    * @param paramInfo The Java method parameter being resolved. Must not be <jk>null</jk>.
063    * @param annotations The annotations to apply to any new part parsers.
064    * @param pathMatcher Path matcher for the specified method. Can be <jk>null</jk> (the name must then be specified on the {@link Path} annotation).
065    * @return A new {@link PathArg}, or <jk>null</jk> if the parameter is not annotated with {@link Path}.
066    */
067   public static PathArg create(ParameterInfo paramInfo, AnnotationWorkList annotations, UrlPathMatcher pathMatcher) {
068      if (AP.has(Path.class, paramInfo))
069         return new PathArg(paramInfo, annotations, pathMatcher);
070      return null;
071   }
072
073   private static Optional<String> findName(ParameterInfo pi) {
074      // @formatter:off
075      return AP.find(Path.class, pi)
076         .stream()
077         .map(AnnotationInfo::inner)
078         .filter(x -> isAnyNotBlank(x.value(), x.name()))
079         .findFirst()
080         .map(x -> firstNonBlank(x.name(), x.value()));
081      // @formatter:on
082   }
083
084   private static Optional<String> findDef(ParameterInfo pi) {
085      // @formatter:off
086      return AP.find(Path.class, pi)
087         .stream()
088         .map(AnnotationInfo::inner)
089         .filter(x -> ine(x.def()) && neq(NONE, x.def()))
090         .findFirst()
091         .map(Path::def);
092      // @formatter:on
093   }
094
095   private static Path getClassLevelPath(ParameterInfo pi, String paramName) {
096      var declaringClass = pi.getMethod().getDeclaringClass();
097      if (declaringClass == null)
098         return null;
099      var restAnnotation = declaringClass.getAnnotations(Rest.class).findFirst().map(AnnotationInfo::inner).orElse(null);
100      if (restAnnotation == null)
101         return null;
102      for (var p : restAnnotation.pathParams()) {
103         var pName = firstNonEmpty(p.name(), p.value());
104         if (eq(paramName, pName))
105            return p;
106      }
107      return null;
108   }
109
110   private final HttpPartParser partParser;
111   private final HttpPartSchema schema;
112   private final String name;
113   private final String def;
114   private final Type type;
115
116   /**
117    * Constructor.
118    *
119    * @param paramInfo The Java method parameter being resolved. Must not be <jk>null</jk>.
120    * @param annotations The annotations to apply to any new part parsers.
121    * @param pathMatcher Path matcher for the specified method. Can be <jk>null</jk> (the name must then be specified on the {@link Path} annotation).
122    */
123   protected PathArg(ParameterInfo paramInfo, AnnotationWorkList annotations, UrlPathMatcher pathMatcher) {
124      this.name = getName(paramInfo, pathMatcher);
125
126      var classLevelPath = getClassLevelPath(paramInfo, name);
127
128      var schemaBuilder = HttpPartSchema.create();
129      if (classLevelPath != null)
130         schemaBuilder.apply(classLevelPath);
131      schemaBuilder.applyAll(Path.class, paramInfo);
132      this.schema = schemaBuilder.build();
133
134      this.def = findDef(paramInfo).or(() -> o(classLevelPath).filter(p -> ine(p.def()) && neq(NONE, p.def())).map(Path::def)).orElse(null);
135      this.type = paramInfo.getParameterType().innerType();
136      @SuppressWarnings({
137         "unchecked" // Type erasure on reflective/generic cast; element type is verified at call site
138      })
139      var pp = (Class<? extends HttpPartParser>)schema.getParser();
140      this.partParser = nn(pp) ? HttpPartParser.creator().type(pp).apply(annotations).create() : null;
141   }
142
143   @Override /* Overridden from RestOpArg */
144   public Object resolve(RestOpSession opSession) throws Exception {
145      var req = opSession.getRequest();
146      if (eq(name, "*")) {
147         var m = new JsonMap();
148         req.getPathParams().forEach(x -> m.put(x.getName(), x.getValue()));
149         return req.getMarshallingSession().convertToType(m, type);
150      }
151      var ps = partParser == null ? req.getPartParserSession() : partParser.getPartSession();
152      return req.getPathParams().get(name).parser(ps).schema(schema).def(def).as(type).orElse(null);
153   }
154
155   private static String getName(ParameterInfo pi, UrlPathMatcher pathMatcher) {
156      var p = findName(pi).orElse(null);
157      if (nn(p))
158         return p;
159      if (nn(pathMatcher)) {
160         var idx = 0;
161         var i = pi.getIndex();
162         var mi = pi.getMethod();
163
164         for (var j = 0; j < i; j++) {
165            var hasAnnotation = AP.has(Path.class, mi.getParameter(j));
166            if (hasAnnotation)
167               idx++;
168         }
169
170         var vars = pathMatcher.getVars();
171         if (vars.length <= idx)
172            throw new ArgException(pi, "Number of attribute parameters exceeds the number of URL pattern variables.  vars.length=%s, idx=%s", vars.length, idx);
173
174         var idxs = String.valueOf(idx);
175         for (var v : vars)
176            if (isNumeric(v) && v.equals(idxs))
177               return v;
178
179         return pathMatcher.getVars()[idx];
180      }
181      throw new ArgException(pi, "@Path used without name or value");
182   }
183}