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}