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