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}