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