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