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 org.apache.juneau.common.utils.IOUtils.*; 020import static org.apache.juneau.common.utils.StringUtils.*; 021import static org.apache.juneau.common.utils.Utils.*; 022 023import java.io.*; 024import java.lang.reflect.*; 025import java.util.*; 026 027import org.apache.juneau.*; 028import org.apache.juneau.collections.*; 029import org.apache.juneau.common.utils.*; 030import org.apache.juneau.encoders.*; 031import org.apache.juneau.http.header.*; 032import org.apache.juneau.http.response.*; 033import org.apache.juneau.httppart.*; 034import org.apache.juneau.internal.*; 035import org.apache.juneau.marshaller.*; 036import org.apache.juneau.parser.*; 037import org.apache.juneau.rest.*; 038import org.apache.juneau.rest.util.*; 039 040import jakarta.servlet.*; 041 042/** 043 * Contains the content of the HTTP request. 044 * 045 * <p> 046 * The {@link RequestContent} object is the API for accessing the content of an HTTP request. 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(RequestContent <jv>content</jv>) {...} 052 * </p> 053 * 054 * <h5 class='figure'>Example:</h5> 055 * <p class='bjava'> 056 * <ja>@RestPost</ja>(...) 057 * <jk>public void</jk> doPost(RequestContent <jv>content</jv>) { 058 * <jc>// Convert content to a linked list of Person objects.</jc> 059 * List<Person> <jv>list</jv> = <jv>content</jv>.as(LinkedList.<jk>class</jk>, Person.<jk>class</jk>); 060 * ... 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 RequestContent} 069 * <ul class='spaced-list'> 070 * <li>Methods for accessing the raw contents of the request content: 071 * <ul class='javatreec'> 072 * <li class='jm'>{@link RequestContent#asBytes() asBytes()} 073 * <li class='jm'>{@link RequestContent#asHex() asHex()} 074 * <li class='jm'>{@link RequestContent#asSpacedHex() asSpacedHex()} 075 * <li class='jm'>{@link RequestContent#asString() asString()} 076 * <li class='jm'>{@link RequestContent#getInputStream() getInputStream()} 077 * <li class='jm'>{@link RequestContent#getReader() getReader()} 078 * </ul> 079 * <li>Methods for parsing the contents of the request content: 080 * <ul class='javatreec'> 081 * <li class='jm'>{@link RequestContent#as(Class) as(Class)} 082 * <li class='jm'>{@link RequestContent#as(Type, Type...) as(Type, Type...)} 083 * <li class='jm'>{@link RequestContent#setSchema(HttpPartSchema) setSchema(HttpPartSchema)} 084 * </ul> 085 * <li>Other methods: 086 * <ul class='javatreec'> 087 * <li class='jm'>{@link RequestContent#cache() cache()} 088 * <li class='jm'>{@link RequestContent#getParserMatch() getParserMatch()} 089 * </ul> 090 * </ul> 091 * </ul> 092 * 093 * <h5 class='section'>See Also:</h5><ul> 094 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HttpParts">HTTP Parts</a> 095 * </ul> 096 */ 097@SuppressWarnings("unchecked") 098public class RequestContent { 099 100 private byte[] content; 101 private final RestRequest req; 102 private EncoderSet encoders; 103 private Encoder encoder; 104 private ParserSet parsers; 105 private long maxInput; 106 private int contentLength; 107 private MediaType mediaType; 108 private Parser parser; 109 private HttpPartSchema schema; 110 111 /** 112 * Constructor. 113 * 114 * @param req The request creating this bean. 115 */ 116 public RequestContent(RestRequest req) { 117 this.req = req; 118 } 119 120 /** 121 * Sets the encoders to use for decoding this content. 122 * 123 * @param value The new value for this setting. 124 * @return This object. 125 */ 126 public RequestContent encoders(EncoderSet value) { 127 this.encoders = value; 128 return this; 129 } 130 131 /** 132 * Sets the parsers to use for parsing this content. 133 * 134 * @param value The new value for this setting. 135 * @return This object. 136 */ 137 public RequestContent parsers(ParserSet value) { 138 this.parsers = value; 139 return this; 140 } 141 142 /** 143 * Sets the schema for this content. 144 * 145 * @param schema The new schema for this content. 146 * @return This object. 147 */ 148 public RequestContent setSchema(HttpPartSchema schema) { 149 this.schema = schema; 150 return this; 151 } 152 153 /** 154 * Sets the max input value for this content. 155 * 156 * @param value The new value for this setting. 157 * @return This object. 158 */ 159 public RequestContent maxInput(long value) { 160 this.maxInput = value; 161 return this; 162 } 163 164 /** 165 * Sets the media type of this content. 166 * 167 * @param value The new value for this setting. 168 * @return This object. 169 */ 170 public RequestContent mediaType(MediaType value) { 171 this.mediaType = value; 172 return this; 173 } 174 175 /** 176 * Sets the parser to use for this content. 177 * 178 * @param value The new value for this setting. 179 * @return This object. 180 */ 181 public RequestContent parser(Parser value) { 182 this.parser = value; 183 return this; 184 } 185 186 /** 187 * Sets the contents of this content. 188 * 189 * @param value The new value for this setting. 190 * @return This object. 191 */ 192 public RequestContent content(byte[] value) { 193 this.content = value; 194 return this; 195 } 196 197 boolean isLoaded() { 198 return content != null; 199 } 200 201 /** 202 * Reads the input from the HTTP request parsed into a POJO. 203 * 204 * <p> 205 * The parser used is determined by the matching <c>Content-Type</c> header on the request. 206 * 207 * <p> 208 * If type is <jk>null</jk> or <code>Object.<jk>class</jk></code>, then the actual type will be determined 209 * automatically based on the following input: 210 * <table class='styled'> 211 * <tr><th>Type</th><th>JSON input</th><th>XML input</th><th>Return type</th></tr> 212 * <tr> 213 * <td>object</td> 214 * <td><js>"{...}"</js></td> 215 * <td><code><xt><object></xt>...<xt></object></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'object'</xs><xt>></xt>...<xt></x></xt></code></td> 216 * <td>{@link JsonMap}</td> 217 * </tr> 218 * <tr> 219 * <td>array</td> 220 * <td><js>"[...]"</js></td> 221 * <td><code><xt><array></xt>...<xt></array></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'array'</xs><xt>></xt>...<xt></x></xt></code></td> 222 * <td>{@link JsonList}</td> 223 * </tr> 224 * <tr> 225 * <td>string</td> 226 * <td><js>"'...'"</js></td> 227 * <td><code><xt><string></xt>...<xt></string></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'string'</xs><xt>></xt>...<xt></x></xt></code></td> 228 * <td>{@link String}</td> 229 * </tr> 230 * <tr> 231 * <td>number</td> 232 * <td><c>123</c></td> 233 * <td><code><xt><number></xt>123<xt></number></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'number'</xs><xt>></xt>...<xt></x></xt></code></td> 234 * <td>{@link Number}</td> 235 * </tr> 236 * <tr> 237 * <td>boolean</td> 238 * <td><jk>true</jk></td> 239 * <td><code><xt><boolean></xt>true<xt></boolean></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'boolean'</xs><xt>></xt>...<xt></x></xt></code></td> 240 * <td>{@link Boolean}</td> 241 * </tr> 242 * <tr> 243 * <td>null</td> 244 * <td><jk>null</jk> or blank</td> 245 * <td><code><xt><null/></xt></code> or blank<br><code><xt><x</xt> <xa>type</xa>=<xs>'null'</xs><xt>/></xt></code></td> 246 * <td><jk>null</jk></td> 247 * </tr> 248 * </table> 249 * 250 * <p> 251 * Refer to <a class="doclink" href="https://juneau.apache.org/docs/topics/PojoCategories">POJO Categories</a> for a complete definition of supported POJOs. 252 * 253 * <h5 class='section'>Examples:</h5> 254 * <p class='bjava'> 255 * <jc>// Parse into an integer.</jc> 256 * <jk>int</jk> <jv>content1</jv> = <jv>req</jv>.getContent().as(<jk>int</jk>.<jk>class</jk>); 257 * 258 * <jc>// Parse into an int array.</jc> 259 * <jk>int</jk>[] <jv>content2</jv> = <jv>req</jv>.getContent().as(<jk>int</jk>[].<jk>class</jk>); 260 261 * <jc>// Parse into a bean.</jc> 262 * MyBean <jv>content3</jv> = <jv>req</jv>.getContent().as(MyBean.<jk>class</jk>); 263 * 264 * <jc>// Parse into a linked-list of objects.</jc> 265 * List <jv>content4</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>); 266 * 267 * <jc>// Parse into a map of object keys/values.</jc> 268 * Map <jv>content5</jv> = <jv>req</jv>.getContent().as(TreeMap.<jk>class</jk>); 269 * </p> 270 * 271 * <h5 class='section'>Notes:</h5><ul> 272 * <li class='note'> 273 * If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string. 274 * </ul> 275 * 276 * @param type The class type to instantiate. 277 * @param <T> The class type to instantiate. 278 * @return The input parsed to a POJO. 279 * @throws BadRequest Thrown if input could not be parsed or fails schema validation. 280 * @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers. 281 * @throws InternalServerError Thrown if an {@link IOException} occurs. 282 */ 283 public <T> T as(Class<T> type) throws BadRequest, UnsupportedMediaType, InternalServerError { 284 return getInner(getClassMeta(type)); 285 } 286 287 /** 288 * Reads the input from the HTTP request parsed into a POJO. 289 * 290 * <p> 291 * This is similar to {@link #as(Class)} but allows for complex collections of POJOs to be created. 292 * 293 * <h5 class='section'>Examples:</h5> 294 * <p class='bjava'> 295 * <jc>// Parse into a linked-list of strings.</jc> 296 * List<String> <jv>content1</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>, String.<jk>class</jk>); 297 * 298 * <jc>// Parse into a linked-list of linked-lists of strings.</jc> 299 * List<List<String>> <jv>content2</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>); 300 * 301 * <jc>// Parse into a map of string keys/values.</jc> 302 * Map<String,String> <jv>content3</jv> = <jv>req</jv>.getContent().as(TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>); 303 * 304 * <jc>// Parse into a map containing string keys and values of lists containing beans.</jc> 305 * Map<String,List<MyBean>> <jv>content4</jv> = <jv>req</jv>.getContent().as(TreeMap.<jk>class</jk>, String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>); 306 * </p> 307 * 308 * <h5 class='section'>Notes:</h5><ul> 309 * <li class='note'> 310 * <c>Collections</c> must be followed by zero or one parameter representing the value type. 311 * <li class='note'> 312 * <c>Maps</c> must be followed by zero or two parameters representing the key and value types. 313 * <li class='note'> 314 * If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string. 315 * </ul> 316 * 317 * @param type 318 * The type of object to create. 319 * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} 320 * @param args 321 * The type arguments of the class if it's a collection or map. 322 * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} 323 * <br>Ignored if the main type is not a map or collection. 324 * @param <T> The class type to instantiate. 325 * @return The input parsed to a POJO. 326 * @throws BadRequest Thrown if input could not be parsed or fails schema validation. 327 * @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers. 328 * @throws InternalServerError Thrown if an {@link IOException} occurs. 329 */ 330 public <T> T as(Type type, Type...args) throws BadRequest, UnsupportedMediaType, InternalServerError { 331 return getInner(this.<T>getClassMeta(type, args)); 332 } 333 334 /** 335 * Returns the HTTP content content as a plain string. 336 * 337 * <h5 class='section'>Notes:</h5><ul> 338 * <li class='note'> 339 * If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string. 340 * </ul> 341 * 342 * @return The incoming input from the connection as a plain string. 343 * @throws IOException If a problem occurred trying to read from the reader. 344 */ 345 public String asString() throws IOException { 346 cache(); 347 return new String(content, UTF8); 348 } 349 350 /** 351 * Returns the HTTP content content as a plain string. 352 * 353 * <h5 class='section'>Notes:</h5><ul> 354 * <li class='note'> 355 * If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string. 356 * </ul> 357 * 358 * @return The incoming input from the connection as a plain string. 359 * @throws IOException If a problem occurred trying to read from the reader. 360 */ 361 public byte[] asBytes() throws IOException { 362 cache(); 363 return content; 364 } 365 366 /** 367 * Returns the HTTP content content as a simple hexadecimal character string. 368 * 369 * <h5 class='section'>Example:</h5> 370 * <p class='bcode'> 371 * 0123456789ABCDEF 372 * </p> 373 * 374 * @return The incoming input from the connection as a plain string. 375 * @throws IOException If a problem occurred trying to read from the reader. 376 */ 377 public String asHex() throws IOException { 378 cache(); 379 return toHex(content); 380 } 381 382 /** 383 * Returns the HTTP content content as a simple space-delimited hexadecimal character string. 384 * 385 * <h5 class='section'>Example:</h5> 386 * <p class='bcode'> 387 * 01 23 45 67 89 AB CD EF 388 * </p> 389 * 390 * @return The incoming input from the connection as a plain string. 391 * @throws IOException If a problem occurred trying to read from the reader. 392 */ 393 public String asSpacedHex() throws IOException { 394 cache(); 395 return toSpacedHex(content); 396 } 397 398 /** 399 * Returns the HTTP content content as a {@link Reader}. 400 * 401 * <h5 class='section'>Notes:</h5><ul> 402 * <li class='note'> 403 * If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string. 404 * <li class='note'> 405 * Automatically handles GZipped input streams. 406 * </ul> 407 * 408 * @return The content contents as a reader. 409 * @throws IOException Thrown by underlying stream. 410 */ 411 public BufferedReader getReader() throws IOException { 412 Reader r = getUnbufferedReader(); 413 if (r instanceof BufferedReader) 414 return (BufferedReader)r; 415 int len = req.getHttpServletRequest().getContentLength(); 416 int buffSize = len <= 0 ? 8192 : Math.max(len, 8192); 417 return new BufferedReader(r, buffSize); 418 } 419 420 /** 421 * Same as {@link #getReader()}, but doesn't encapsulate the result in a {@link BufferedReader}; 422 * 423 * @return An unbuffered reader. 424 * @throws IOException Thrown by underlying stream. 425 */ 426 protected Reader getUnbufferedReader() throws IOException { 427 if (content != null) 428 return new CharSequenceReader(new String(content, UTF8)); 429 return new InputStreamReader(getInputStream(), req.getCharset()); 430 } 431 432 /** 433 * Returns the HTTP content content as an {@link InputStream}. 434 * 435 * @return The negotiated input stream. 436 * @throws IOException If any error occurred while trying to get the input stream or wrap it in the GZIP wrapper. 437 */ 438 public ServletInputStream getInputStream() throws IOException { 439 440 if (content != null) 441 return new BoundedServletInputStream(content); 442 443 Encoder enc = getEncoder(); 444 445 InputStream is = req.getHttpServletRequest().getInputStream(); 446 447 if (enc == null) 448 return new BoundedServletInputStream(is, maxInput); 449 450 return new BoundedServletInputStream(enc.getInputStream(is), maxInput); 451 } 452 453 /** 454 * Returns the parser and media type matching the request <c>Content-Type</c> header. 455 * 456 * @return 457 * The parser matching the request <c>Content-Type</c> header, or {@link Optional#empty()} if no matching parser was 458 * found. 459 * Includes the matching media type. 460 */ 461 public Optional<ParserMatch> getParserMatch() { 462 if (mediaType != null && parser != null) 463 return Utils.opt(new ParserMatch(mediaType, parser)); 464 MediaType mt = getMediaType(); 465 return Utils.opt(mt).map(x -> parsers.getParserMatch(x)); 466 } 467 468 private MediaType getMediaType() { 469 if (mediaType != null) 470 return mediaType; 471 Optional<ContentType> ct = req.getHeader(ContentType.class); 472 if (!ct.isPresent() && content != null) 473 return MediaType.UON; 474 return ct.isPresent() ? ct.get().asMediaType().orElse(null) : null; 475 } 476 477 private <T> T getInner(ClassMeta<T> cm) throws BadRequest, UnsupportedMediaType, InternalServerError { 478 try { 479 return parse(cm); 480 } catch (UnsupportedMediaType e) { 481 throw e; 482 } catch (SchemaValidationException e) { 483 throw new BadRequest("Validation failed on request content. " + e.getLocalizedMessage()); 484 } catch (ParseException e) { 485 throw new BadRequest(e, "Could not convert request content content to class type ''{0}''.", cm); 486 } catch (IOException e) { 487 throw new InternalServerError(e, "I/O exception occurred while parsing request content."); 488 } catch (Exception e) { 489 throw new InternalServerError(e, "Exception occurred while parsing request content."); 490 } 491 } 492 493 /* Workhorse method */ 494 private <T> T parse(ClassMeta<T> cm) throws SchemaValidationException, ParseException, UnsupportedMediaType, IOException { 495 496 if (cm.isReader()) 497 return (T)getReader(); 498 499 if (cm.isInputStream()) 500 return (T)getInputStream(); 501 502 Optional<TimeZone> timeZone = req.getTimeZone(); 503 Locale locale = req.getLocale(); 504 ParserMatch pm = getParserMatch().orElse(null); 505 506 if (schema == null) 507 schema = HttpPartSchema.DEFAULT; 508 509 if (pm != null) { 510 Parser p = pm.getParser(); 511 MediaType mediaType = pm.getMediaType(); 512 ParserSession session = p 513 .createSession() 514 .properties(req.getAttributes().asMap()) 515 .javaMethod(req.getOpContext().getJavaMethod()) 516 .locale(locale) 517 .timeZone(timeZone.orElse(null)) 518 .mediaType(mediaType) 519 .apply(ReaderParser.Builder.class, x -> x.streamCharset(req.getCharset())) 520 .schema(schema) 521 .debug(req.isDebug() ? true : null) 522 .outer(req.getContext().getResource()) 523 .build(); 524 525 try (Closeable in = session.isReaderParser() ? getUnbufferedReader() : getInputStream()) { 526 T o = session.parse(in, cm); 527 if (schema != null) 528 schema.validateOutput(o, cm.getBeanContext()); 529 return o; 530 } 531 } 532 533 if (cm.hasReaderMutater()) 534 return cm.getReaderMutater().mutate(getReader()); 535 536 if (cm.hasInputStreamMutater()) 537 return cm.getInputStreamMutater().mutate(getInputStream()); 538 539 MediaType mt = getMediaType(); 540 541 if ((Utils.isEmpty(Utils.s(mt)) || mt.toString().startsWith("text/plain")) && cm.hasStringMutater()) 542 return cm.getStringMutater().mutate(asString()); 543 544 Optional<ContentType> ct = req.getHeader(ContentType.class); 545 throw new UnsupportedMediaType( 546 "Unsupported media-type in request header ''Content-Type'': ''{0}''\n\tSupported media-types: {1}", 547 ct.isPresent() ? ct.get().asMediaType().orElse(null) : "not-specified", Json5.of(req.getOpContext().getParsers().getSupportedMediaTypes()) 548 ); 549 } 550 551 private Encoder getEncoder() throws UnsupportedMediaType { 552 if (encoder == null) { 553 String ce = req.getHeaderParam("content-encoding").orElse(null); 554 if (isNotEmpty(ce)) { 555 ce = ce.trim(); 556 encoder = encoders.getEncoder(ce); 557 if (encoder == null) 558 throw new UnsupportedMediaType( 559 "Unsupported encoding in request header ''Content-Encoding'': ''{0}''\n\tSupported codings: {1}", 560 req.getHeaderParam("content-encoding").orElse(null), Json5.of(encoders.getSupportedEncodings()) 561 ); 562 } 563 564 if (encoder != null) 565 contentLength = -1; 566 } 567 // Note that if this is the identity encoder, we want to return null 568 // so that we don't needlessly wrap the input stream. 569 if (encoder == IdentityEncoder.INSTANCE) 570 return null; 571 return encoder; 572 } 573 574 /** 575 * Returns the content length of the content. 576 * 577 * @return The content length of the content in bytes. 578 */ 579 public int getContentLength() { 580 return contentLength == 0 ? req.getHttpServletRequest().getContentLength() : contentLength; 581 } 582 583 /** 584 * Caches the content in memory for reuse. 585 * 586 * @return This object. 587 * @throws IOException If error occurs while reading stream. 588 */ 589 public RequestContent cache() throws IOException { 590 if (content == null) 591 content = readBytes(getInputStream()); 592 return this; 593 } 594 595 //----------------------------------------------------------------------------------------------------------------- 596 // Helper methods 597 //----------------------------------------------------------------------------------------------------------------- 598 599 private <T> ClassMeta<T> getClassMeta(Type type, Type...args) { 600 return req.getBeanSession().getClassMeta(type, args); 601 } 602 603 private <T> ClassMeta<T> getClassMeta(Class<T> type) { 604 return req.getBeanSession().getClassMeta(type); 605 } 606}