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.httppart;
018
019import static java.util.stream.Collectors.toList;
020import static org.apache.juneau.common.utils.StringUtils.*;
021import static org.apache.juneau.common.utils.Utils.*;
022import static org.apache.juneau.httppart.HttpPartType.*;
023import static org.apache.juneau.internal.ClassUtils.*;
024
025import java.util.*;
026import java.util.stream.*;
027
028import org.apache.http.*;
029import org.apache.juneau.*;
030import org.apache.juneau.collections.*;
031import org.apache.juneau.common.utils.*;
032import org.apache.juneau.http.*;
033import org.apache.juneau.http.part.*;
034import org.apache.juneau.httppart.*;
035import org.apache.juneau.rest.*;
036import org.apache.juneau.rest.util.*;
037import org.apache.juneau.svl.*;
038
039/**
040 * Represents the path parameters in an HTTP request.
041 *
042 *  <p>
043 *    The {@link RequestPathParams} object is the API for accessing the matched variables
044 *    and remainder on the URL path.
045 * </p>
046 * <p class='bjava'>
047 *    <ja>@RestPost</ja>(...)
048 *    <jk>public</jk> Object myMethod(RequestPathParams <jv>path</jv>) {...}
049 * </p>
050 *
051 * <h5 class='figure'>Example:</h5>
052 * <p class='bjava'>
053 *    <ja>@RestPost</ja>(..., path=<js>"/{foo}/{bar}/{baz}/*"</js>)
054 *    <jk>public void</jk> doGet(RequestPathParams <jv>path</jv>) {
055 *       <jc>// Example URL:  /123/qux/true/quux</jc>
056 *
057 *       <jk>int</jk> <jv>foo</jv> = <jv>path</jv>.get(<js>"foo"</js>).asInteger().orElse(0);  <jc>// =123</jc>
058 *       String <jv>bar</jv> = <jv>path</jv>.get(<js>"bar"</js>).orElse(<jk>null</jk>);  <jc>// =qux</jc>
059 *       <jk>boolean</jk> <jv>baz</jv> = <jv>path</jv>.get(<js>"baz"</js>).asBoolean().orElse(<jk>false</jk>);  <jc>// =true</jc>
060 *       String <jv>remainder</jv> = <jv>path</jv>.getRemainder();  <jc>// =quux</jc>
061 *    }
062 * </p>
063 *
064 * <p>
065 *    Some important methods on this class are:
066 * </p>
067 * <ul class='javatree'>
068 *    <li class='jc'>{@link RequestPathParams}
069 *    <ul class='spaced-list'>
070 *       <li>Methods for retrieving path parameters:
071 *       <ul class='javatreec'>
072 *          <li class='jm'>{@link RequestPathParams#contains(String) contains(String)}
073 *          <li class='jm'>{@link RequestPathParams#containsAny(String...) containsAny(String...)}
074 *          <li class='jm'>{@link RequestPathParams#get(Class) get(Class)}
075 *          <li class='jm'>{@link RequestPathParams#get(String) get(String)}
076 *          <li class='jm'>{@link RequestPathParams#getAll(String) getAll(String)}
077 *          <li class='jm'>{@link RequestPathParams#getFirst(String) getFirst(String)}
078 *          <li class='jm'>{@link RequestPathParams#getLast(String) getLast(String)}
079 *          <li class='jm'>{@link RequestPathParams#getRemainder() getRemainder()}
080 *          <li class='jm'>{@link RequestPathParams#getRemainderUndecoded() getRemainderUndecoded()}
081 *       </ul>
082 *       <li>Methods overridding path parameters:
083 *       <ul class='javatreec'>
084 *          <li class='jm'>{@link RequestPathParams#add(NameValuePair...) add(NameValuePair...)}
085 *          <li class='jm'>{@link RequestPathParams#add(String,Object) add(String,Object)}
086 *          <li class='jm'>{@link RequestPathParams#addDefault(List) addDefault(List)}
087 *          <li class='jm'>{@link RequestPathParams#addDefault(NameValuePair...) addDefault(NameValuePair...)}
088 *          <li class='jm'>{@link RequestPathParams#remove(String) remove(String)}
089 *          <li class='jm'>{@link RequestPathParams#set(NameValuePair...) set(NameValuePair...)}
090 *          <li class='jm'>{@link RequestPathParams#set(String,Object) set(String,Object)}
091 *       </ul>
092 *       <li>Other methods:
093 *       <ul class='javatreec'>
094 *          <li class='jm'>{@link RequestPathParams#copy() copy()}
095 *          <li class='jm'>{@link RequestPathParams#isEmpty() isEmpty()}
096 *       </ul>
097 *    </ul>
098 * </ul>
099 *
100 * <h5 class='section'>See Also:</h5><ul>
101 *    <li class='jc'>{@link RequestPathParam}
102 *    <li class='ja'>{@link org.apache.juneau.http.annotation.Path}
103 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HttpParts">HTTP Parts</a>
104 * </ul>
105*/
106public class RequestPathParams extends ArrayList<RequestPathParam> {
107
108   private static final long serialVersionUID = 1L;
109
110   private final RestRequest req;
111   private boolean caseSensitive;
112   private HttpPartParserSession parser;
113   private final VarResolverSession vs;
114
115   /**
116    * Constructor.
117    *
118    * @param session The current HTTP request session.
119    * @param req The current HTTP request.
120    * @param caseSensitive Whether case-sensitive name matching is enabled.
121    */
122   public RequestPathParams(RestSession session, RestRequest req, boolean caseSensitive) {
123      this.req = req;
124      this.caseSensitive = caseSensitive;
125      this.vs = req.getVarResolverSession();
126
127      // Add parameters from parent context if any.
128      @SuppressWarnings("unchecked")
129      Map<String,String> parentVars = (Map<String,String>)req.getAttribute("juneau.pathVars").orElse(Collections.emptyMap());
130      for (Map.Entry<String,String> e : parentVars.entrySet())
131         add(e.getKey(), e.getValue());
132
133      UrlPathMatch pm = session.getUrlPathMatch();
134      if (pm != null) {
135         for (Map.Entry<String,String> e : pm.getVars().entrySet())
136            add(e.getKey(), e.getValue());
137         String r = pm.getRemainder();
138         if (r != null) {
139            add("/**", r);
140            add("/*", urlDecode(r));
141         }
142      }
143   }
144
145   /**
146    * Copy constructor.
147    */
148   private RequestPathParams(RequestPathParams copyFrom) {
149      req = copyFrom.req;
150      caseSensitive = copyFrom.caseSensitive;
151      parser = copyFrom.parser;
152      addAll(copyFrom);
153      vs = copyFrom.vs;
154   }
155
156   /**
157    * Subset constructor.
158    */
159   private RequestPathParams(RequestPathParams copyFrom, String...names) {
160      this.req = copyFrom.req;
161      caseSensitive = copyFrom.caseSensitive;
162      parser = copyFrom.parser;
163      vs = copyFrom.vs;
164      for (String n : names)
165         copyFrom.stream().filter(x -> eq(x.getName(), n)).forEach(this::add);
166   }
167
168   /**
169    * Sets the parser to use for part values.
170    *
171    * @param value The new value for this setting.
172    * @return This object.
173    */
174   public RequestPathParams parser(HttpPartParserSession value) {
175      this.parser = value;
176      forEach(x -> x.parser(parser));
177      return this;
178   }
179
180   /**
181    * Sets case sensitivity for names in this list.
182    *
183    * @param value The new value for this setting.
184    * @return This object (for method chaining).
185    */
186   public RequestPathParams caseSensitive(boolean value) {
187      this.caseSensitive = value;
188      return this;
189   }
190
191   //-----------------------------------------------------------------------------------------------------------------
192   // Basic operations.
193   //-----------------------------------------------------------------------------------------------------------------
194
195   /**
196    * Adds default entries to these parameters.
197    *
198    * <p>
199    * Similar to {@link #set(String, Object)} but doesn't override existing values.
200    *
201    * @param pairs
202    *    The default entries.
203    *    <br>Can be <jk>null</jk>.
204    * @return This object.
205    */
206   public RequestPathParams addDefault(List<NameValuePair> pairs) {
207      for (NameValuePair p : pairs) {
208         String name = p.getName();
209         Stream<RequestPathParam> l = stream(name);
210         boolean hasAllBlanks = l.allMatch(x -> Utils.isEmpty(x.getValue()));
211         if (hasAllBlanks) {
212            removeAll(getAll(name));
213            add(new RequestPathParam(req, name, vs.resolve(p.getValue())));
214         }
215      }
216      return this;
217   }
218
219   /**
220    * Adds default entries to these parameters.
221    *
222    * <p>
223    * Similar to {@link #set(String, Object)} but doesn't override existing values.
224    *
225    * @param pairs
226    *    The default entries.
227    *    <br>Can be <jk>null</jk>.
228    * @return This object.
229    */
230   public RequestPathParams addDefault(NameValuePair...pairs) {
231      return addDefault(alist(pairs));
232   }
233
234   /**
235    * Adds a default entry to the query parameters.
236    *
237    * @param name The name.
238    * @param value The value.
239    * @return This object.
240    */
241   public RequestPathParams addDefault(String name, String value) {
242      return addDefault(BasicStringPart.of(name, value));
243   }
244
245   /**
246    * Adds a parameter value.
247    *
248    * <p>
249    * Parameter is added to the end.
250    * <br>Existing parameter with the same name are not changed.
251    *
252    * @param name The parameter name.  Must not be <jk>null</jk>.
253    * @param value The parameter value.
254    * @return This object.
255    */
256   public RequestPathParams add(String name, Object value) {
257      Utils.assertArgNotNull("name", name);
258      add(new RequestPathParam(req, name, Utils.s(value)).parser(parser));
259      return this;
260   }
261
262   /**
263    * Adds request parameter values.
264    *
265    * <p>
266    * Parameters are added to the end.
267    * <br>Existing parameters with the same name are not changed.
268    *
269    * @param parameters The parameter objects.  Must not be <jk>null</jk>.
270    * @return This object.
271    */
272   public RequestPathParams add(NameValuePair...parameters) {
273      Utils.assertArgNotNull("parameters", parameters);
274      for (NameValuePair p : parameters)
275         if (p != null)
276            add(p.getName(), p.getValue());
277      return this;
278   }
279
280   /**
281    * Sets a parameter value.
282    *
283    * <p>
284    * Parameter is added to the end.
285    * <br>Any previous parameters with the same name are removed.
286    *
287    * @param name The parameter name.  Must not be <jk>null</jk>.
288    * @param value
289    *    The parameter value.
290    *    <br>Converted to a string using {@link Object#toString()}.
291    *    <br>Can be <jk>null</jk>.
292    * @return This object.
293    */
294   public RequestPathParams set(String name, Object value) {
295      Utils.assertArgNotNull("name", name);
296      set(new RequestPathParam(req, name, Utils.s(value)).parser(parser));
297      return this;
298   }
299
300
301   /**
302    * Sets request header values.
303    *
304    * <p>
305    * Parameters are added to the end of the headers.
306    * <br>Any previous parameters with the same name are removed.
307    *
308    * @param parameters The parameters to set.  Must not be <jk>null</jk> or contain <jk>null</jk>.
309    * @return This object.
310    */
311   public RequestPathParams set(NameValuePair...parameters) {
312      Utils.assertArgNotNull("headers", parameters);
313      for (NameValuePair p : parameters)
314         remove(p);
315      for (NameValuePair p : parameters)
316         add(p);
317      return this;
318   }
319
320   /**
321    * Remove parameters.
322    *
323    * @param name The parameter name.  Must not be <jk>null</jk>.
324    * @return This object.
325    */
326   public RequestPathParams remove(String name) {
327      Utils.assertArgNotNull("name", name);
328      removeIf(x -> eq(x.getName(), name));
329      return this;
330   }
331
332   /**
333    * Returns a copy of this object but only with the specified param names copied.
334    *
335    * @param names The list to include in the copy.
336    * @return A new list object.
337    */
338   public RequestPathParams subset(String...names) {
339      return new RequestPathParams(this, names);
340   }
341
342   //-----------------------------------------------------------------------------------------------------------------
343   // Convenience getters.
344   //-----------------------------------------------------------------------------------------------------------------
345
346   /**
347    * Returns <jk>true</jk> if the parameters with the specified name is present.
348    *
349    * @param name The parameter name.  Must not be <jk>null</jk>.
350    * @return <jk>true</jk> if the parameters with the specified name is present.
351    */
352   public boolean contains(String name) {
353      Utils.assertArgNotNull("names", name);
354      return stream(name).findAny().isPresent();
355   }
356
357   /**
358    * Returns <jk>true</jk> if the parameter with any of the specified names are present.
359    *
360    * @param names The parameter names.  Must not be <jk>null</jk>.
361    * @return <jk>true</jk> if the parameter with any of the specified names are present.
362    */
363   public boolean containsAny(String...names) {
364      Utils.assertArgNotNull("names", names);
365      for (String n : names)
366         if (stream(n).findAny().isPresent())
367            return true;
368      return false;
369   }
370
371   /**
372    * Returns all the parameters with the specified name.
373    *
374    * @param name The parameter name.
375    * @return The list of all parameters with the specified name, or an empty list if none are found.
376    */
377   public List<RequestPathParam> getAll(String name) {
378      Utils.assertArgNotNull("name", name);
379      return stream(name).collect(toList());
380   }
381
382   /**
383    * Returns all headers with the specified name.
384    *
385    * @param name The header name.
386    * @return The stream of all headers with matching names.  Never <jk>null</jk>.
387    */
388   public Stream<RequestPathParam> stream(String name) {
389      return stream().filter(x -> eq(x.getName(), name));
390   }
391
392   /**
393    * Returns all headers in sorted order.
394    *
395    * @return The stream of all headers in sorted order.
396    */
397   public Stream<RequestPathParam> getSorted() {
398      Comparator<RequestPathParam> x;
399      if (caseSensitive)
400         x = Comparator.comparing(RequestPathParam::getName);
401      else
402         x = (x1,x2) -> String.CASE_INSENSITIVE_ORDER.compare(x1.getName(), x2.getName());
403      return stream().sorted(x);
404   }
405
406   /**
407    * Returns all the unique header names in this list.
408    * @return The list of all unique header names in this list.
409    */
410   public List<String> getNames() {
411      return stream().map(RequestPathParam::getName).map(x -> caseSensitive ? x : x.toLowerCase()).distinct().collect(toList());
412   }
413
414   /**
415    * Returns the first parameter with the specified name.
416    *
417    * <p>
418    * Note that this method never returns <jk>null</jk> and that {@link RequestPathParam#isPresent()} can be used
419    * to test for the existence of the parameter.
420    *
421    * @param name The parameter name.
422    * @return The parameter.  Never <jk>null</jk>.
423    */
424   public RequestPathParam getFirst(String name) {
425      Utils.assertArgNotNull("name", name);
426      return stream(name).findFirst().orElseGet(()->new RequestPathParam(req, name, null).parser(parser));
427   }
428
429   /**
430    * Returns the last parameter with the specified name.
431    *
432    * <p>
433    * Note that this method never returns <jk>null</jk> and that {@link RequestPathParam#isPresent()} can be used
434    * to test for the existence of the parameter.
435    *
436    * @param name The parameter name.
437    * @return The parameter.  Never <jk>null</jk>.
438    */
439   public RequestPathParam getLast(String name) {
440      Utils.assertArgNotNull("name", name);
441      Value<RequestPathParam> v = Value.empty();
442      stream(name).forEach(x -> v.set(x));
443      return v.orElseGet(() -> new RequestPathParam(req, name, null).parser(parser));
444   }
445
446   /**
447    * Returns the last parameter with the specified name.
448    *
449    * <p>
450    * This is equivalent to {@link #getLast(String)}.
451    *
452    * @param name The parameter name.
453    * @return The parameter value, or {@link Optional#empty()} if it doesn't exist.
454    */
455   public RequestPathParam get(String name) {
456      List<RequestPathParam> l = getAll(name);
457      if (l.isEmpty())
458         return new RequestPathParam(req, name, null).parser(parser);
459      if (l.size() == 1)
460         return l.get(0);
461      StringBuilder sb = new StringBuilder(128);
462      for (int i = 0, j = l.size(); i < j; i++) {
463         if (i > 0)
464            sb.append(", ");
465         sb.append(l.get(i).getValue());
466      }
467      return new RequestPathParam(req, name, sb.toString()).parser(parser);
468   }
469
470   /**
471    * Returns the path parameter as the specified bean type.
472    *
473    * <p>
474    * Type must have a name specified via the {@link org.apache.juneau.http.annotation.Path} annotation
475    * and a public constructor that takes in either <c>value</c> or <c>name,value</c> as strings.
476    *
477    * @param <T> The bean type to create.
478    * @param type The bean type to create.
479    * @return The bean, never <jk>null</jk>.
480    */
481   public <T> Optional<T> get(Class<T> type) {
482      ClassMeta<T> cm = req.getBeanSession().getClassMeta(type);
483      String name = HttpParts.getName(PATH, cm).orElseThrow(()->new BasicRuntimeException("@Path(name) not found on class {0}", className(type)));
484      return get(name).as(type);
485   }
486
487   //-----------------------------------------------------------------------------------------------------------------
488   // Other methods
489   //-----------------------------------------------------------------------------------------------------------------
490
491   /**
492    * Makes a copy of these parameters.
493    *
494    * @return A new parameters object.
495    */
496   public RequestPathParams copy() {
497      return new RequestPathParams(this);
498   }
499
500   /**
501    * Returns the decoded remainder of the URL following any path pattern matches.
502    *
503    * <p>
504    * The behavior of path remainder is shown below given the path pattern "/foo/*":
505    * <table class='styled'>
506    *    <tr>
507    *       <th>URL</th>
508    *       <th>Path Remainder</th>
509    *    </tr>
510    *    <tr>
511    *       <td><c>/foo</c></td>
512    *       <td><jk>null</jk></td>
513    *    </tr>
514    *    <tr>
515    *       <td><c>/foo/</c></td>
516    *       <td><js>""</js></td>
517    *    </tr>
518    *    <tr>
519    *       <td><c>/foo//</c></td>
520    *       <td><js>"/"</js></td>
521    *    </tr>
522    *    <tr>
523    *       <td><c>/foo///</c></td>
524    *       <td><js>"//"</js></td>
525    *    </tr>
526    *    <tr>
527    *       <td><c>/foo/a/b</c></td>
528    *       <td><js>"a/b"</js></td>
529    *    </tr>
530    *    <tr>
531    *       <td><c>/foo//a/b/</c></td>
532    *       <td><js>"/a/b/"</js></td>
533    *    </tr>
534    *    <tr>
535    *       <td><c>/foo/a%2Fb</c></td>
536    *       <td><js>"a/b"</js></td>
537    *    </tr>
538    * </table>
539    *
540    * <h5 class='section'>Example:</h5>
541    * <p class='bjava'>
542    *    <jc>// REST method</jc>
543    *    <ja>@RestGet</ja>(<js>"/foo/{bar}/*"</js>)
544    *    <jk>public</jk> String doGetById(RequestPathParams <jv>path</jv>, <jk>int</jk> <jv>bar</jv>) {
545    *       <jk>return</jk> <jv>path</jv>.remainder().orElse(<jk>null</jk>);
546    *    }
547    * </p>
548    *
549    * <p>
550    * The remainder can also be retrieved by calling <code>get(<js>"/**"</js>)</code>.
551    *
552    * @return The path remainder string.
553    */
554   public RequestPathParam getRemainder() {
555      return get("/*");
556
557   }
558
559   /**
560    * Same as {@link #getRemainder()} but doesn't decode characters.
561    *
562    * <p>
563    * The undecoded remainder can also be retrieved by calling <code>get(<js>"/*"</js>)</code>.
564    *
565    * @return The un-decoded path remainder.
566    */
567   public RequestPathParam getRemainderUndecoded() {
568      return get("/**");
569   }
570
571   private boolean eq(String s1, String s2) {
572      if (caseSensitive)
573         return Utils.eq(s1, s2);
574      return Utils.eqic(s1, s2);
575   }
576
577   @Override /* Object */
578   public String toString() {
579      JsonMap m = new JsonMap();
580      for (String n : getNames())
581         m.put(n, get(n).asString().orElse(null));
582      return m.asJson();
583   }
584}