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