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