001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.rest; 014 015import static org.apache.juneau.internal.StringUtils.*; 016 017import java.lang.reflect.*; 018import java.util.*; 019 020import javax.servlet.http.*; 021 022import org.apache.juneau.*; 023import org.apache.juneau.httppart.*; 024import org.apache.juneau.oapi.*; 025import org.apache.juneau.parser.*; 026import org.apache.juneau.http.exception.*; 027 028/** 029 * Contains information about the matched path on the HTTP request. 030 * 031 * <p> 032 * Provides access to the matched path variables and path match remainder. 033 * 034 * <ul class='seealso'> 035 * <li class='link'>{@doc juneau-rest-server.RestMethod.RequestPathMatch} 036 * </ul> 037 */ 038public class RequestPath extends TreeMap<String,String> { 039 private static final long serialVersionUID = 1L; 040 041 /** 042 * Request attribute name for passing path variables from parent to child. 043 */ 044 static final String REST_PATHVARS_ATTR = "juneau.pathVars"; 045 046 private final RestRequest req; 047 private HttpPartParser parser; 048 049 RequestPath(RestRequest req) { 050 super(String.CASE_INSENSITIVE_ORDER); 051 this.req = req; 052 @SuppressWarnings("unchecked") 053 Map<String,String> parentVars = (Map<String,String>)req.getAttribute(REST_PATHVARS_ATTR); 054 if (parentVars != null) 055 for (Map.Entry<String,String> e : parentVars.entrySet()) 056 put(e.getKey(), e.getValue()); 057 } 058 059 RequestPath parser(HttpPartParser parser) { 060 this.parser = parser; 061 return this; 062 } 063 064 RequestPath remainder(String remainder) { 065 put("/**", remainder); 066 put("/*", urlDecode(remainder)); 067 return this; 068 } 069 070 /** 071 * Sets a request query parameter value. 072 * 073 * @param name The parameter name. 074 * @param value The parameter value. 075 */ 076 public void put(String name, Object value) { 077 super.put(name, value.toString()); 078 } 079 080 /** 081 * Returns the specified path parameter converted to a String. 082 * 083 * @param name The path variable name. 084 * @return The parameter value. 085 * @throws BadRequest Thrown if input could not be parsed. 086 * @throws InternalServerError Thrown if any other exception occurs. 087 */ 088 public String getString(String name) throws BadRequest, InternalServerError { 089 return getInner(parser, null, name, null, req.getBeanSession().string()); 090 } 091 092 /** 093 * Returns the specified path parameter converted to an integer. 094 * 095 * @param name The path variable name. 096 * @return The parameter value. 097 * @throws BadRequest Thrown if input could not be parsed. 098 * @throws InternalServerError Thrown if any other exception occurs. 099 */ 100 public int getInt(String name) throws BadRequest, InternalServerError { 101 return getInner(parser, null, name, null, getClassMeta(int.class)); 102 } 103 104 /** 105 * Returns the specified path parameter converted to a boolean. 106 * 107 * @param name The path variable name. 108 * @return The parameter value. 109 * @throws BadRequest Thrown if input could not be parsed. 110 * @throws InternalServerError Thrown if any other exception occurs. 111 */ 112 public boolean getBoolean(String name) throws BadRequest, InternalServerError { 113 return getInner(null, null, name, null, getClassMeta(boolean.class)); 114 } 115 116 /** 117 * Returns the specified path parameter value converted to a POJO using the {@link HttpPartParser} registered with the resource. 118 * 119 * <h5 class='section'>Examples:</h5> 120 * <p class='bcode w800'> 121 * <jc>// Parse into an integer.</jc> 122 * <jk>int</jk> myparam = path.get(<js>"myparam"</js>, <jk>int</jk>.<jk>class</jk>); 123 * 124 * <jc>// Parse into an int array.</jc> 125 * <jk>int</jk>[] myparam = path.get(<js>"myparam"</js>, <jk>int</jk>[].<jk>class</jk>); 126 127 * <jc>// Parse into a bean.</jc> 128 * MyBean myparam = path.get(<js>"myparam"</js>, MyBean.<jk>class</jk>); 129 * 130 * <jc>// Parse into a linked-list of objects.</jc> 131 * List myparam = path.get(<js>"myparam"</js>, LinkedList.<jk>class</jk>); 132 * 133 * <jc>// Parse into a map of object keys/values.</jc> 134 * Map myparam = path.get(<js>"myparam"</js>, TreeMap.<jk>class</jk>); 135 * </p> 136 * 137 * <ul class='seealso'> 138 * <li class='jf'>{@link RestContext#REST_partParser} 139 * </ul> 140 * 141 * @param name The attribute name. 142 * @param type The class type to convert the attribute value to. 143 * @param <T> The class type to convert the attribute value to. 144 * @return The attribute value converted to the specified class type. 145 * @throws BadRequest Thrown if input could not be parsed. 146 * @throws InternalServerError Thrown if any other exception occurs. 147 */ 148 public <T> T get(String name, Class<T> type) throws BadRequest, InternalServerError { 149 return getInner(null, null, name, null, this.<T>getClassMeta(type)); 150 } 151 152 /** 153 * Same as {@link #get(String, Class)} but allows you to override the part parser. 154 * 155 * @param parser 156 * The parser to use for parsing the string value. 157 * <br>If <jk>null</jk>, uses the part parser defined on the resource/method. 158 * @param schema 159 * The schema object that defines the format of the input. 160 * <br>If <jk>null</jk>, defaults to the schema defined on the parser. 161 * <br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}. 162 * <br>Only used if parser is schema-aware (e.g. {@link OpenApiParser}). 163 * @param name The attribute name. 164 * @param type The class type to convert the attribute value to. 165 * @param <T> The class type to convert the attribute value to. 166 * @return The attribute value converted to the specified class type. 167 * @throws BadRequest Thrown if input could not be parsed or fails schema validation. 168 * @throws InternalServerError Thrown if any other exception occurs. 169 */ 170 public <T> T get(HttpPartParser parser, HttpPartSchema schema, String name, Class<T> type) throws BadRequest, InternalServerError { 171 return getInner(parser, schema, name, null, this.<T>getClassMeta(type)); 172 } 173 174 /** 175 * Returns the specified query parameter value converted to a POJO using the {@link HttpPartParser} registered with the resource. 176 * 177 * <p> 178 * Similar to {@link #get(String,Class)} but allows for complex collections of POJOs to be created. 179 * 180 * <p> 181 * Use this method if you want to parse into a parameterized <c>Map</c>/<c>Collection</c> object. 182 * 183 * <h5 class='section'>Examples:</h5> 184 * <p class='bcode w800'> 185 * <jc>// Parse into a linked-list of strings.</jc> 186 * List<String> myparam = req.getPathParameter(<js>"myparam"</js>, LinkedList.<jk>class</jk>, String.<jk>class</jk>); 187 * 188 * <jc>// Parse into a linked-list of linked-lists of strings.</jc> 189 * List<List<String>> myparam = req.getPathParameter(<js>"myparam"</js>, LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>); 190 * 191 * <jc>// Parse into a map of string keys/values.</jc> 192 * Map<String,String> myparam = req.getPathParameter(<js>"myparam"</js>, TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>); 193 * 194 * <jc>// Parse into a map containing string keys and values of lists containing beans.</jc> 195 * Map<String,List<MyBean>> myparam = req.getPathParameter(<js>"myparam"</js>, TreeMap.<jk>class</jk>, String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>); 196 * </p> 197 * 198 * <ul class='notes'> 199 * <li> 200 * <c>Collections</c> must be followed by zero or one parameter representing the value type. 201 * <li> 202 * <c>Maps</c> must be followed by zero or two parameters representing the key and value types. 203 * </ul> 204 * 205 * <ul class='seealso'> 206 * <li class='jf'>{@link RestContext#REST_partParser} 207 * </ul> 208 * 209 * @param name The attribute name. 210 * @param type 211 * The type of object to create. 212 * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} 213 * @param args 214 * The type arguments of the class if it's a collection or map. 215 * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} 216 * <br>Ignored if the main type is not a map or collection. 217 * @param <T> The class type to convert the attribute value to. 218 * @return The attribute value converted to the specified class type. 219 * @throws BadRequest Thrown if input could not be parsed. 220 * @throws InternalServerError Thrown if any other exception occurs. 221 */ 222 public <T> T get(String name, Type type, Type...args) throws BadRequest, InternalServerError { 223 return getInner(null, null, name, null, this.<T>getClassMeta(type, args)); 224 } 225 226 /** 227 * Same as {@link #get(String, Type, Type...)} but allows you to override the part parser. 228 * 229 * @param parser 230 * The parser to use for parsing the string value. 231 * <br>If <jk>null</jk>, uses the part parser defined on the resource/method. 232 * @param schema 233 * The schema object that defines the format of the input. 234 * <br>If <jk>null</jk>, defaults to the schema defined on the parser. 235 * <br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}. 236 * <br>Only used if parser is schema-aware (e.g. {@link OpenApiParser}). 237 * @param name The attribute name. 238 * @param type 239 * The type of object to create. 240 * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} 241 * @param args 242 * The type arguments of the class if it's a collection or map. 243 * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} 244 * <br>Ignored if the main type is not a map or collection. 245 * @param <T> The class type to convert the attribute value to. 246 * @return The attribute value converted to the specified class type. 247 * @throws BadRequest Thrown if input could not be parsed or fails schema validation. 248 * @throws InternalServerError Thrown if any other exception occurs. 249 */ 250 public <T> T get(HttpPartParser parser, HttpPartSchema schema, String name, Type type, Type...args) throws BadRequest, InternalServerError { 251 return getInner(parser, schema, name, null, this.<T>getClassMeta(type, args)); 252 } 253 254 /* Workhorse method */ 255 private <T> T getInner(HttpPartParser parser, HttpPartSchema schema, String name, T def, ClassMeta<T> cm) throws BadRequest, InternalServerError { 256 try { 257 if (cm.isMapOrBean() && isOneOf(name, "*", "")) { 258 ObjectMap m = new ObjectMap(); 259 for (Map.Entry<String,String> e : this.entrySet()) { 260 String k = e.getKey(); 261 HttpPartSchema pschema = schema == null ? null : schema.getProperty(k); 262 ClassMeta<?> cm2 = cm.getValueType(); 263 m.put(k, getInner(parser, pschema, k, null, cm2)); 264 } 265 return req.getBeanSession().convertToType(m, cm); 266 } 267 T t = parse(parser, schema, get(name), cm); 268 return (t == null ? def : t); 269 } catch (SchemaValidationException e) { 270 throw new BadRequest(e, "Validation failed on path parameter ''{0}''. ", name); 271 } catch (ParseException e) { 272 throw new BadRequest(e, "Could not parse path parameter ''{0}''.", name) ; 273 } catch (Exception e) { 274 throw new InternalServerError(e, "Could not parse path parameter ''{0}''.", name) ; 275 } 276 } 277 278 /* Workhorse method */ 279 private <T> T parse(HttpPartParser parser, HttpPartSchema schema, String val, ClassMeta<T> cm) throws SchemaValidationException, ParseException { 280 if (parser == null) 281 parser = this.parser; 282 return parser.createPartSession(req.getParserSessionArgs()).parse(HttpPartType.PATH, schema, val, cm); 283 } 284 285 /** 286 * Returns the decoded remainder of the URL following any path pattern matches. 287 * 288 * <p> 289 * The behavior of path remainder is shown below given the path pattern "/foo/*": 290 * <table class='styled'> 291 * <tr> 292 * <th>URL</th> 293 * <th>Path Remainder</th> 294 * </tr> 295 * <tr> 296 * <td><c>/foo</c></td> 297 * <td><jk>null</jk></td> 298 * </tr> 299 * <tr> 300 * <td><c>/foo/</c></td> 301 * <td><js>""</js></td> 302 * </tr> 303 * <tr> 304 * <td><c>/foo//</c></td> 305 * <td><js>"/"</js></td> 306 * </tr> 307 * <tr> 308 * <td><c>/foo///</c></td> 309 * <td><js>"//"</js></td> 310 * </tr> 311 * <tr> 312 * <td><c>/foo/a/b</c></td> 313 * <td><js>"a/b"</js></td> 314 * </tr> 315 * <tr> 316 * <td><c>/foo//a/b/</c></td> 317 * <td><js>"/a/b/"</js></td> 318 * </tr> 319 * <tr> 320 * <td><c>/foo/a%2Fb</c></td> 321 * <td><js>"a/b"</js></td> 322 * </tr> 323 * </table> 324 * 325 * <h5 class='section'>Example:</h5> 326 * <p class='bcode w800'> 327 * <jc>// REST method</jc> 328 * <ja>@RestMethod</ja>(name=<jsf>GET</jsf>,path=<js>"/foo/{bar}/*"</js>) 329 * <jk>public</jk> String doGetById(RequestPathMatch path, <jk>int</jk> bar) { 330 * <jk>return</jk> path.getRemainder(); 331 * } 332 * </p> 333 * 334 * <p> 335 * The remainder can also be retrieved by calling <code>get(<js>"/*"</js>)</code>. 336 * 337 * @return The path remainder string. 338 */ 339 public String getRemainder() { 340 return get("/*"); 341 } 342 343 /** 344 * Same as {@link #getRemainder()} but doesn't decode characters. 345 * 346 * <p> 347 * The undecoded remainder can also be retrieved by calling <code>get(<js>"/**"</js>)</code>. 348 * 349 * @return The un-decoded path remainder. 350 */ 351 public String getRemainderUndecoded() { 352 return get("/**"); 353 } 354 355 //----------------------------------------------------------------------------------------------------------------- 356 // Helper methods 357 //----------------------------------------------------------------------------------------------------------------- 358 359 private <T> ClassMeta<T> getClassMeta(Type type, Type...args) { 360 return req.getBeanSession().getClassMeta(type, args); 361 } 362 363 private <T> ClassMeta<T> getClassMeta(Class<T> type) { 364 return req.getBeanSession().getClassMeta(type); 365 } 366 367 //------------------------------------------------------------------------------------------------------------------ 368 // Static utility methods. 369 //------------------------------------------------------------------------------------------------------------------ 370 371 /** 372 * Utility method that adds path variables to the specified request. 373 */ 374 @SuppressWarnings("unchecked") 375 static HttpServletRequest addPathVars(HttpServletRequest req, Map<String,String> vars) { 376 if (vars != null && ! vars.isEmpty()) { 377 Map<String,String> m = (Map<String,String>)req.getAttribute(REST_PATHVARS_ATTR); 378 if (m == null) { 379 m = new TreeMap<>(); 380 req.setAttribute(REST_PATHVARS_ATTR, m); 381 } 382 m.putAll(vars); 383 } 384 return req; 385 } 386}