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}