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