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 org.apache.juneau.commons.httppart.HttpPartType.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022import static org.apache.juneau.commons.utils.StringUtils.*; 023 024import java.util.*; 025import java.util.stream.*; 026 027import org.apache.juneau.commons.collections.*; 028import org.apache.juneau.commons.lang.*; 029import org.apache.juneau.commons.svl.*; 030import org.apache.juneau.commons.utils.*; 031import org.apache.juneau.http.*; 032import org.apache.juneau.http.HttpPart; 033import org.apache.juneau.http.header.*; 034import org.apache.juneau.http.part.*; 035import org.apache.juneau.http.response.*; 036import org.apache.juneau.marshall.httppart.*; 037import org.apache.juneau.rest.server.*; 038import org.apache.juneau.rest.server.util.*; 039 040import jakarta.servlet.http.Part; 041 042/** 043 * Represents the parsed form-data parameters in an HTTP request. 044 * 045 * <p> 046 * The {@link RequestFormParamList} object is the API for accessing the HTTP request content as form data. 047 * It can be accessed by passing it as a parameter on your REST Java method: 048 * </p> 049 * <p class='bjava'> 050 * <ja>@RestPost</ja>(...) 051 * <jk>public</jk> Object myMethod(RequestFormParamList <jv>formData</jv>) {...} 052 * </p> 053 * 054 * <h5 class='figure'>Example:</h5> 055 * <p class='bjava'> 056 * <ja>@RestPost</ja>(...) 057 * <jk>public</jk> Object myMethod(RequestFormParamList <jv>formData</jv>) { 058 * 059 * <jc>// Get query parameters converted to various types.</jc> 060 * <jk>int</jk> <jv>p1</jv> = <jv>formData</jv>.get(<js>"p1"</js>).asInteger().orElse(0); 061 * String <jv>p2</jv> = <jv>formData</jv>.get(<js>"p2"</js>).orElse(<jk>null</jk>); 062 * UUID <jv>p3</jv> = <jv>formData</jv>.get(<js>"p3"</js>).as(UUID.<jk>class</jk>).orElse(<jk>null</jk>); 063 * } 064 * </p> 065 * 066 * <p> 067 * Note that this object does NOT take GET parameters into account and only returns values found in the content of the request. 068 * </p> 069 * 070 * <p> 071 * Some important methods on this class are: 072 * </p> 073 * <ul class='javatree'> 074 * <li class='jc'>{@link RequestFormParamList} 075 * <ul class='spaced-list'> 076 * <li>Methods for retrieving form data parameters: 077 * <ul class='javatreec'> 078 * <li class='jm'>{@link RequestFormParamList#contains(String) contains(String)} 079 * <li class='jm'>{@link RequestFormParamList#containsAny(String...) containsAny(String...)} 080 * <li class='jm'>{@link RequestFormParamList#get(Class) get(Class)} 081 * <li class='jm'>{@link RequestFormParamList#get(String) get(String)} 082 * <li class='jm'>{@link RequestFormParamList#getAll(String) getAll(String)} 083 * <li class='jm'>{@link RequestFormParamList#getFirst(String) getFirst(String)} 084 * <li class='jm'>{@link RequestFormParamList#getLast(String) getLast(String)} 085 * </ul> 086 * <li>Methods overridding form data parameters: 087 * <ul class='javatreec'> 088 * <li class='jm'>{@link RequestFormParamList#add(HttpPart...) add(HttpPart...)} 089 * <li class='jm'>{@link RequestFormParamList#add(Part) add(Part)} 090 * <li class='jm'>{@link RequestFormParamList#add(String,Object) add(String,Object)} 091 * <li class='jm'>{@link RequestFormParamList#addDefault(List) addDefault(List)} 092 * <li class='jm'>{@link RequestFormParamList#addDefault(HttpPart...) addDefault(HttpPart...)} 093 * <li class='jm'>{@link RequestFormParamList#addDefault(String,String) addDefault(String,String)} 094 * <li class='jm'>{@link RequestFormParamList#remove(String) remove(String)} 095 * <li class='jm'>{@link RequestFormParamList#set(HttpPart...) set(HttpPart...)} 096 * <li class='jm'>{@link RequestFormParamList#set(String,Object) set(String,Object)} 097 * </ul> 098 * <li>Other methods: 099 * <ul class='javatreec'> 100 * <li class='jm'>{@link RequestFormParamList#asQueryString() asQueryString()} 101 * <li class='jm'>{@link RequestFormParamList#copy() copy()} 102 * <li class='jm'>{@link RequestFormParamList#isEmpty() isEmpty()} 103 * </ul> 104 * </ul> 105 * </ul> 106 * 107 * <h5 class='section'>See Also:</h5><ul> 108 * <li class='jc'>{@link RequestFormParam} 109 * <li class='ja'>{@link FormData} 110 * <li class='ja'>{@link HasFormData} 111 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerHttpParts">HTTP parts in REST servers</a> 112 * </ul> 113 */ 114@SuppressWarnings({ 115 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 116 "resource" // Resource management handled externally 117}) 118public class RequestFormParamList extends ArrayList<RequestFormParam> { 119 120 private static final long serialVersionUID = 1L; 121 122 private final transient RestRequest req; 123 private boolean caseSensitive; 124 private transient HttpPartParserSession parser; 125 private final transient VarResolverSession vs; 126 127 /** 128 * Constructor. 129 * 130 * <p> 131 * For a {@code multipart/form-data} request, the effective {@code @Rest(maxInput)} ceiling is enforced 132 * against both the declared {@code Content-Length} (checked before the servlet container parses the body) 133 * and the cumulative size of the parsed parts (checked afterward, as a backstop for a declared length that 134 * was absent, chunked, or simply didn't match) — a request whose multipart body is too large is 135 * rejected with a {@link PayloadTooLarge} (413) response rather than being handed off to the servlet 136 * container's own (often much larger, or unset) multipart limits. 137 * 138 * @param req The request creating this bean. Must not be <jk>null</jk>. 139 * @param caseSensitive Whether case-sensitive name matching is enabled. 140 * @throws Exception Any exception can be thrown. 141 */ 142 143 @SuppressWarnings({ 144 "java:S112", // Generic exception thrown; acceptable for framework/lifecycle methods 145 "java:S3776", // Cognitive complexity acceptable for this specific logic 146 "null" // throws Exception intentional - callback/lifecycle method 147 }) 148 public RequestFormParamList(RestRequest req, boolean caseSensitive) throws Exception { 149 this.req = req; 150 this.caseSensitive = caseSensitive; 151 this.vs = req.getVarResolverSession(); 152 153 Map<String,String[]> m = null; 154 Collection<Part> c = null; 155 156 RequestContent content = req.getContent(); 157 if (content.isLoaded() || ! req.getHeader(ContentType.class).orElse(ContentType.NULL).equalsIgnoreCase("multipart/form-data")) { 158 var listMap = RestUtils.parseQuery(content.getReader()); 159 m = map(); 160 for (var e : listMap.entrySet()) { 161 if (e.getValue() == null) 162 m.put(e.getKey(), null); 163 else 164 m.put(e.getKey(), array(e.getValue(), String.class)); 165 } 166 } else { 167 var maxInput = req.getOpContext().getMaxInput(); 168 checkMultipartLength(maxInput, req.getHttpServletRequest().getContentLengthLong()); 169 c = req.getHttpServletRequest().getParts(); 170 checkMultipartLength(maxInput, c); 171 if (ie(c)) 172 m = req.getHttpServletRequest().getParameterMap(); 173 } 174 175 if (nn(m)) { 176 for (var e : m.entrySet()) { 177 var name = e.getKey(); 178 179 var values = e.getValue(); 180 if (values == null) 181 values = new String[0]; 182 183 // Fix for behavior difference between Tomcat and WAS. 184 // getParameter("foo") on "&foo" in Tomcat returns "". 185 // getParameter("foo") on "&foo" in WAS returns null. 186 if (values.length == 1 && values[0] == null) 187 values[0] = ""; 188 189 if (values.length == 0) 190 values = a((String)null); 191 192 for (var value : values) 193 add(new RequestFormParam(req, name, value)); 194 } 195 } else if (nn(c)) { 196 c.forEach(this::add); 197 } 198 } 199 200 /** 201 * Rejects a {@code multipart/form-data} request outright when its declared {@code Content-Length} already 202 * exceeds {@code maxInput}, before the servlet container is asked to parse (and potentially buffer to disk) 203 * a body the resource has no intention of accepting. 204 * 205 * <p> 206 * Package-private for direct unit-test coverage. 207 * 208 * @param maxInput The effective {@code @Rest(maxInput)} ceiling in bytes. Values {@code <= 0} disable the check. 209 * @param declaredContentLength The request's declared {@code Content-Length}, as returned by 210 * {@code HttpServletRequest.getContentLengthLong()}. Values {@code < 0} (absent/unparsable) are ignored; 211 * the post-parse {@link #checkMultipartLength(long, Collection)} check still applies. 212 * @throws PayloadTooLarge If {@code declaredContentLength} exceeds {@code maxInput}. 213 */ 214 static void checkMultipartLength(long maxInput, long declaredContentLength) { 215 if (maxInput > 0 && declaredContentLength > maxInput) 216 throw new PayloadTooLarge("Multipart request content exceeds the maximum allowed input size of %d bytes.", maxInput); 217 } 218 219 /** 220 * Rejects a {@code multipart/form-data} request once the cumulative size of its parsed parts exceeds 221 * {@code maxInput}, as a backstop for the case where the declared {@code Content-Length} was absent, 222 * chunked, or simply didn't match what the container actually buffered. 223 * 224 * <p> 225 * Package-private for direct unit-test coverage. 226 * 227 * @param maxInput The effective {@code @Rest(maxInput)} ceiling in bytes. Values {@code <= 0} disable the check. 228 * @param parts The parts already parsed by the servlet container. Can be <jk>null</jk> or empty (no-op). 229 * @throws PayloadTooLarge If the cumulative {@link Part#getSize()} across {@code parts} exceeds {@code maxInput}. 230 */ 231 static void checkMultipartLength(long maxInput, Collection<Part> parts) { 232 if (maxInput <= 0 || ie(parts)) 233 return; 234 var total = 0L; 235 for (var p : parts) { 236 total += p.getSize(); 237 if (total > maxInput) 238 throw new PayloadTooLarge("Multipart request content exceeds the maximum allowed input size of %d bytes.", maxInput); 239 } 240 } 241 242 /** 243 * Copy constructor. 244 */ 245 private RequestFormParamList(RequestFormParamList copyFrom) { 246 req = copyFrom.req; 247 caseSensitive = copyFrom.caseSensitive; 248 parser = copyFrom.parser; 249 addAll(copyFrom); 250 vs = copyFrom.vs; 251 } 252 253 /** 254 * Subset constructor. 255 */ 256 private RequestFormParamList(RequestFormParamList copyFrom, String...names) { 257 this.req = copyFrom.req; 258 caseSensitive = copyFrom.caseSensitive; 259 parser = copyFrom.parser; 260 vs = copyFrom.vs; 261 for (var n : names) 262 copyFrom.stream().filter(x -> eq(x.getName(), n)).forEach(this::add); 263 } 264 265 /** 266 * Adds request parameter values. 267 * 268 * <p> 269 * Parameters are added to the end. 270 * <br>Existing parameters with the same name are not changed. 271 * 272 * @param parameters The parameter objects. Must not be <jk>null</jk>. 273 * @return This object. 274 */ 275 public RequestFormParamList add(HttpPart...parameters) { 276 reqnn("parameters", parameters); 277 for (var p : parameters) 278 if (nn(p)) 279 add(p.getName(), p.getValue()); 280 return this; 281 } 282 283 /** 284 * Adds a parameter value. 285 * 286 * <p> 287 * Parameter is added to the end. 288 * <br>Existing parameter with the same name are not changed. 289 * 290 * @param part The parameter part. Must not be <jk>null</jk>. 291 * @return This object. 292 */ 293 public RequestFormParamList add(Part part) { 294 reqnn("part", part); 295 add(new RequestFormParam(req, part).parser(parser)); 296 return this; 297 } 298 299 /** 300 * Adds a parameter value. 301 * 302 * <p> 303 * Parameter is added to the end. 304 * <br>Existing parameter with the same name are not changed. 305 * 306 * @param name The parameter name. Must not be <jk>null</jk>. 307 * @param value The parameter value. 308 * @return This object. 309 */ 310 public RequestFormParamList add(String name, Object value) { 311 reqnn("name", name); 312 add(new RequestFormParam(req, name, s(value)).parser(parser)); 313 return this; 314 } 315 316 /** 317 * Adds default entries to these parameters. 318 * 319 * <p> 320 * Similar to {@link #set(String, Object)} but doesn't override existing values. 321 * 322 * @param pairs 323 * The default entries. 324 * <br>Can be <jk>null</jk> (no-op). 325 * @return This object. 326 */ 327 public RequestFormParamList addDefault(List<? extends HttpPart> pairs) { 328 if (pairs == null) 329 return this; 330 for (var p : pairs) { 331 var name = p.getName(); 332 var l = stream(name); 333 var hasAllBlanks = l.allMatch(x -> Shorts.ie(x.getValue())); // NOAI 334 if (hasAllBlanks) { 335 removeAll(getAll(name)); 336 add(new RequestFormParam(req, name, vs.resolve(p.getValue()))); 337 } 338 } 339 return this; 340 } 341 342 /** 343 * Adds default entries to these parameters. 344 * 345 * <p> 346 * Similar to {@link #set(String, Object)} but doesn't override existing values. 347 * 348 * @param pairs 349 * The default entries. 350 * <br>Can be <jk>null</jk> (no-op). 351 * @return This object. 352 */ 353 public RequestFormParamList addDefault(HttpPart...pairs) { 354 return addDefault(l(pairs)); 355 } 356 357 /** 358 * Adds a default entry to the form data parameters. 359 * 360 * @param name The name. 361 * @param value The value. 362 * @return This object. 363 */ 364 public RequestFormParamList addDefault(String name, String value) { 365 return addDefault(HttpStringPart.of(name, value)); 366 } 367 368 /** 369 * Converts this object to a query string. 370 * 371 * <p> 372 * Returned query string does not start with <js>'?'</js>. 373 * 374 * @return A new query string, or an empty string if this object is empty. 375 */ 376 public String asQueryString() { 377 var sb = new StringBuilder(); 378 for (var e : this) { 379 if (!sb.isEmpty()) 380 sb.append("&"); 381 sb.append(urlEncode(e.getName())).append('=').append(urlEncode(e.getValue())); 382 } 383 return sb.toString(); 384 } 385 386 /** 387 * Sets case sensitivity for names in this list. 388 * 389 * @param value The new value for this setting. 390 * @return This object (for method chaining). 391 */ 392 public RequestFormParamList caseSensitive(boolean value) { 393 caseSensitive = value; 394 return this; 395 } 396 397 /** 398 * Returns <jk>true</jk> if the parameters with the specified names are present. 399 * 400 * @param name The parameter name. Must not be <jk>null</jk>. 401 * @return <jk>true</jk> if the parameters with the specified names are present. 402 */ 403 public boolean contains(String name) { 404 return stream(name).findAny().isPresent(); 405 } 406 407 /** 408 * Returns <jk>true</jk> if the parameter with any of the specified names are present. 409 * 410 * @param names The parameter names. Must not be <jk>null</jk>. 411 * @return <jk>true</jk> if the parameter with any of the specified names are present. 412 */ 413 public boolean containsAny(String...names) { 414 reqnn("names", names); 415 for (var n : names) 416 if (stream(n).findAny().isPresent()) 417 return true; 418 return false; 419 } 420 421 /** 422 * Makes a copy of these parameters. 423 * 424 * @return A new parameters object. 425 */ 426 public RequestFormParamList copy() { 427 return new RequestFormParamList(this); 428 } 429 430 /** 431 * Returns the form data parameter as the specified bean type. 432 * 433 * <p> 434 * Type must have a name specified via the {@link FormData} annotation 435 * and a public constructor that takes in either <c>value</c> or <c>name,value</c> as strings. 436 * 437 * @param <T> The bean type to create. 438 * @param type The bean type to create. 439 * @return The bean, or {@link Optional#empty()} if the parameter is not present. 440 */ 441 public <T> Optional<T> get(Class<T> type) { 442 var cm = req.getMarshallingSession().getClassMeta(type); 443 var name = HttpParts.getName(FORMDATA, cm).orElseThrow(() -> rex("@FormData(name) not found on class %s", cn(type))); 444 return get(name).as(type); 445 } 446 447 /** 448 * Returns the condensed parameter with the specified name. 449 * 450 * <p> 451 * If multiple parameters are present, they will be combined into a single comma-delimited list. 452 * 453 * @param name The parameter name. 454 * @return The parameter, never <jk>null</jk>. 455 */ 456 public RequestFormParam get(String name) { 457 List<RequestFormParam> l = getAll(name); 458 if (l.isEmpty()) 459 return new RequestFormParam(req, name, null).parser(parser); 460 if (l.size() == 1) 461 return l.get(0); 462 var sb = new StringBuilder(128); 463 for (var i = 0; i < l.size(); i++) { 464 if (i > 0) 465 sb.append(", "); 466 sb.append(l.get(i).getValue()); 467 } 468 return new RequestFormParam(req, name, sb.toString()).parser(parser); 469 } 470 471 /** 472 * Returns all the parameters with the specified name. 473 * 474 * @param name The parameter name. 475 * @return The list of all parameters with the specified name, or an empty list if none are found. 476 */ 477 public List<RequestFormParam> getAll(String name) { 478 return stream(name).toList(); 479 } 480 481 /** 482 * Returns the first parameter with the specified name. 483 * 484 * <p> 485 * Note that this method never returns <jk>null</jk> and that {@link RequestFormParam#isPresent()} can be used 486 * to test for the existence of the parameter. 487 * 488 * @param name The parameter name. 489 * @return The parameter. Never <jk>null</jk>. 490 */ 491 public RequestFormParam getFirst(String name) { 492 reqnn("name", name); 493 return stream(name).findFirst().orElseGet(() -> new RequestFormParam(req, name, null).parser(parser)); 494 } 495 496 /** 497 * Returns the last parameter with the specified name. 498 * 499 * <p> 500 * Note that this method never returns <jk>null</jk> and that {@link RequestFormParam#isPresent()} can be used 501 * to test for the existence of the parameter. 502 * 503 * @param name The parameter name. 504 * @return The parameter. Never <jk>null</jk>. 505 */ 506 public RequestFormParam getLast(String name) { 507 reqnn("name", name); 508 var v = Holder.<RequestFormParam>empty(); 509 stream(name).forEach(v::set); 510 return v.orElseGet(() -> new RequestFormParam(req, name, null).parser(parser)); 511 } 512 513 /** 514 * Returns all the unique header names in this list. 515 * @return The list of all unique header names in this list. 516 */ 517 public List<String> getNames() { return stream().map(RequestFormParam::getName).map(x -> caseSensitive ? x : lcr(x)).distinct().toList(); } 518 519 /** 520 * Returns all headers in sorted order. 521 * 522 * @return The stream of all headers in sorted order. 523 */ 524 public Stream<RequestFormParam> getSorted() { 525 Comparator<RequestFormParam> x; 526 if (caseSensitive) 527 x = Comparator.comparing(RequestFormParam::getName); 528 else 529 x = (x1, x2) -> String.CASE_INSENSITIVE_ORDER.compare(x1.getName(), x2.getName()); 530 return stream().sorted(x); 531 } 532 533 /** 534 * Sets the parser to use for part values. 535 * 536 * @param value The new value for this setting. 537 * @return This object. 538 */ 539 public RequestFormParamList parser(HttpPartParserSession value) { 540 parser = value; 541 forEach(x -> x.parser(parser)); 542 return this; 543 } 544 545 /** 546 * Remove parameters. 547 * 548 * @param name The parameter names. Must not be <jk>null</jk>. 549 * @return This object. 550 */ 551 public RequestFormParamList remove(String name) { 552 reqnn("name", name); 553 removeIf(x -> eq(x.getName(), name)); 554 return this; 555 } 556 557 /** 558 * Sets request header values. 559 * 560 * <p> 561 * Parameters are added to the end of the headers. 562 * <br>Any previous parameters with the same name are removed. 563 * 564 * @param parameters The parameters to set. Must not be <jk>null</jk> or contain <jk>null</jk>. 565 * @return This object. 566 */ 567 public RequestFormParamList set(HttpPart...parameters) { 568 reqnn("headers", parameters); 569 for (var p : parameters) 570 remove(p.getName()); 571 for (var p : parameters) 572 add(p); 573 return this; 574 } 575 576 /** 577 * Sets a parameter value. 578 * 579 * <p> 580 * Parameter is added to the end. 581 * <br>Any previous parameters with the same name are removed. 582 * 583 * @param name The parameter name. Must not be <jk>null</jk>. 584 * @param value 585 * The parameter value. 586 * <br>Converted to a string using {@link Object#toString()}. 587 * <br>Can be <jk>null</jk> (the parameter is set with a <jk>null</jk> value). 588 * @return This object. 589 */ 590 public RequestFormParamList set(String name, Object value) { 591 reqnn("name", name); 592 set(new RequestFormParam(req, name, s(value)).parser(parser)); 593 return this; 594 } 595 596 /** 597 * Returns all headers with the specified name. 598 * 599 * @param name The header name. 600 * @return The stream of all headers with matching names. Never <jk>null</jk>. 601 */ 602 public Stream<RequestFormParam> stream(String name) { 603 return stream().filter(x -> eq(x.getName(), name)); 604 } 605 606 /** 607 * Returns a copy of this object but only with the specified param names copied. 608 * 609 * @param names The list to include in the copy. 610 * @return A new list object. 611 */ 612 public RequestFormParamList subset(String...names) { 613 return new RequestFormParamList(this, names); 614 } 615 616 protected FluentMap<String,Object> properties() { 617 // @formatter:off 618 var m = filteredBeanPropertyMap(); 619 for (var n : getNames()) 620 m.a(n, get(n).asString().orElse(null)); 621 return m; 622 // @formatter:on 623 } 624 625 @Override 626 public boolean equals(Object o) { 627 return this == o || (o instanceof RequestFormParamList o2 && super.equals(o2)); 628 } 629 630 @Override 631 public int hashCode() { 632 return super.hashCode(); 633 } 634 635 @Override /* Overridden from Object */ 636 public String toString() { 637 return r(properties()); 638 } 639 640 private boolean eq(String s1, String s2) { 641 return Shorts.eq(! caseSensitive, s1, s2); // NOAI 642 } 643}