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}