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; 014 015import static javax.servlet.http.HttpServletResponse.*; 016import static org.apache.juneau.internal.IOUtils.*; 017import static org.apache.juneau.internal.StringUtils.*; 018 019import java.io.*; 020import java.lang.reflect.*; 021import java.util.*; 022 023import javax.servlet.*; 024 025import org.apache.juneau.*; 026import org.apache.juneau.encoders.*; 027import org.apache.juneau.http.*; 028import org.apache.juneau.internal.*; 029import org.apache.juneau.parser.*; 030 031/** 032 * Contains the body of the HTTP request. 033 * 034 * <h5 class='section'>See Also:</h5> 035 * <ul> 036 * <li class='link'><a class="doclink" href="../../../../overview-summary.html#juneau-rest-server.RequestBody">Overview > juneau-rest-server > RequestBody</a> 037 * </ul> 038 */ 039@SuppressWarnings("unchecked") 040public class RequestBody { 041 042 private byte[] body; 043 private final RestRequest req; 044 private EncoderGroup encoders; 045 private Encoder encoder; 046 private ParserGroup parsers; 047 private long maxInput; 048 private RequestHeaders headers; 049 private BeanSession beanSession; 050 private int contentLength = 0; 051 052 RequestBody(RestRequest req) { 053 this.req = req; 054 } 055 056 RequestBody encoders(EncoderGroup encoders) { 057 this.encoders = encoders; 058 return this; 059 } 060 061 RequestBody parsers(ParserGroup parsers) { 062 this.parsers = parsers; 063 return this; 064 } 065 066 RequestBody headers(RequestHeaders headers) { 067 this.headers = headers; 068 return this; 069 } 070 071 RequestBody maxInput(long maxInput) { 072 this.maxInput = maxInput; 073 return this; 074 } 075 076 RequestBody beanSession(BeanSession beanSession) { 077 this.beanSession = beanSession; 078 return this; 079 } 080 081 RequestBody load(byte[] body) { 082 this.body = body; 083 return this; 084 } 085 086 boolean isLoaded() { 087 return body != null; 088 } 089 090 /** 091 * Reads the input from the HTTP request parsed into a POJO. 092 * 093 * <p> 094 * The parser used is determined by the matching <code>Content-Type</code> header on the request. 095 * 096 * <p> 097 * If type is <jk>null</jk> or <code>Object.<jk>class</jk></code>, then the actual type will be determined 098 * automatically based on the following input: 099 * <table class='styled'> 100 * <tr><th>Type</th><th>JSON input</th><th>XML input</th><th>Return type</th></tr> 101 * <tr> 102 * <td>object</td> 103 * <td><js>"{...}"</js></td> 104 * <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> 105 * <td>{@link ObjectMap}</td> 106 * </tr> 107 * <tr> 108 * <td>array</td> 109 * <td><js>"[...]"</js></td> 110 * <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> 111 * <td>{@link ObjectList}</td> 112 * </tr> 113 * <tr> 114 * <td>string</td> 115 * <td><js>"'...'"</js></td> 116 * <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> 117 * <td>{@link String}</td> 118 * </tr> 119 * <tr> 120 * <td>number</td> 121 * <td><code>123</code></td> 122 * <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> 123 * <td>{@link Number}</td> 124 * </tr> 125 * <tr> 126 * <td>boolean</td> 127 * <td><jk>true</jk></td> 128 * <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> 129 * <td>{@link Boolean}</td> 130 * </tr> 131 * <tr> 132 * <td>null</td> 133 * <td><jk>null</jk> or blank</td> 134 * <td><code><xt><null/></xt></code> or blank<br><code><xt><x</xt> <xa>type</xa>=<xs>'null'</xs><xt>/></xt></code></td> 135 * <td><jk>null</jk></td> 136 * </tr> 137 * </table> 138 * 139 * <p> 140 * Refer to <a class="doclink" href="../../../../overview-summary.html#juneau-marshall.PojoCategories">POJO Categories</a> for 141 * a complete definition of supported POJOs. 142 * 143 * <h5 class='section'>Examples:</h5> 144 * <p class='bcode'> 145 * <jc>// Parse into an integer.</jc> 146 * <jk>int</jk> body = req.getBody().asType(<jk>int</jk>.<jk>class</jk>); 147 * 148 * <jc>// Parse into an int array.</jc> 149 * <jk>int</jk>[] body = req.getBody().asType(<jk>int</jk>[].<jk>class</jk>); 150 151 * <jc>// Parse into a bean.</jc> 152 * MyBean body = req.getBody().asType(MyBean.<jk>class</jk>); 153 * 154 * <jc>// Parse into a linked-list of objects.</jc> 155 * List body = req.getBody().asType(LinkedList.<jk>class</jk>); 156 * 157 * <jc>// Parse into a map of object keys/values.</jc> 158 * Map body = req.getBody().asType(TreeMap.<jk>class</jk>); 159 * </p> 160 * 161 * <h5 class='section'>Notes:</h5> 162 * <ul class='spaced-list'> 163 * <li> 164 * If {@code allowHeaderParams} init parameter is true, then first looks for {@code &body=xxx} in the URL query string. 165 * </ul> 166 * 167 * @param type The class type to instantiate. 168 * @param <T> The class type to instantiate. 169 * @return The input parsed to a POJO. 170 * @throws IOException If a problem occurred trying to read from the reader. 171 * @throws ParseException 172 * If the input contains a syntax error or is malformed for the requested {@code Accept} header or is not valid 173 * for the specified type. 174 */ 175 public <T> T asType(Class<T> type) throws IOException, ParseException { 176 return parse(beanSession.getClassMeta(type)); 177 } 178 179 /** 180 * Reads the input from the HTTP request parsed into a POJO. 181 * 182 * <p> 183 * This is similar to {@link #asType(Class)} but allows for complex collections of POJOs to be created. 184 * 185 * <h5 class='section'>Examples:</h5> 186 * <p class='bcode'> 187 * <jc>// Parse into a linked-list of strings.</jc> 188 * List<String> body = req.getBody().asType(LinkedList.<jk>class</jk>, String.<jk>class</jk>); 189 * 190 * <jc>// Parse into a linked-list of linked-lists of strings.</jc> 191 * List<List<String>> body = req.getBody().asType(LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>); 192 * 193 * <jc>// Parse into a map of string keys/values.</jc> 194 * Map<String,String> body = req.getBody().asType(TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>); 195 * 196 * <jc>// Parse into a map containing string keys and values of lists containing beans.</jc> 197 * Map<String,List<MyBean>> body = req.getBody().asType(TreeMap.<jk>class</jk>, String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>); 198 * </p> 199 * 200 * <h5 class='section'>Notes:</h5> 201 * <ul class='spaced-list'> 202 * <li> 203 * <code>Collections</code> must be followed by zero or one parameter representing the value type. 204 * <li> 205 * <code>Maps</code> must be followed by zero or two parameters representing the key and value types. 206 * <li> 207 * If {@code allowHeaderParams} init parameter is true, then first looks for {@code &body=xxx} in the URL query string. 208 * </ul> 209 * 210 * @param type 211 * The type of object to create. 212 * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} 213 * @param args 214 * The type arguments of the class if it's a collection or map. 215 * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} 216 * <br>Ignored if the main type is not a map or collection. 217 * @param <T> The class type to instantiate. 218 * @return The input parsed to a POJO. 219 */ 220 public <T> T asType(Type type, Type...args) { 221 return (T)parse(beanSession.getClassMeta(type, args)); 222 } 223 224 /** 225 * Returns the HTTP body content as a plain string. 226 * 227 * <h5 class='section'>Notes:</h5> 228 * <ul class='spaced-list'> 229 * <li> 230 * If {@code allowHeaderParams} init parameter is true, then first looks for {@code &body=xxx} in the URL query string. 231 * </ul> 232 * 233 * @return The incoming input from the connection as a plain string. 234 * @throws IOException If a problem occurred trying to read from the reader. 235 */ 236 public String asString() throws IOException { 237 if (body == null) 238 body = readBytes(getInputStream(), 1024); 239 return new String(body, UTF8); 240 } 241 242 /** 243 * Returns the HTTP body content as a simple hexadecimal character string. 244 * 245 * <h5 class='section'>Example:</h5> 246 * <p class='bcode'> 247 * 0123456789ABCDEF 248 * </p> 249 * 250 * @return The incoming input from the connection as a plain string. 251 * @throws IOException If a problem occurred trying to read from the reader. 252 */ 253 public String asHex() throws IOException { 254 if (body == null) 255 body = readBytes(getInputStream(), 1024); 256 return toHex(body); 257 } 258 259 /** 260 * Returns the HTTP body content as a simple space-delimited hexadecimal character string. 261 * 262 * <h5 class='section'>Example:</h5> 263 * <p class='bcode'> 264 * 01 23 45 67 89 AB CD EF 265 * </p> 266 * 267 * @return The incoming input from the connection as a plain string. 268 * @throws IOException If a problem occurred trying to read from the reader. 269 */ 270 public String asSpacedHex() throws IOException { 271 if (body == null) 272 body = readBytes(getInputStream(), 1024); 273 return toSpacedHex(body); 274 } 275 276 /** 277 * Returns the HTTP body content as a {@link Reader}. 278 * 279 * <h5 class='section'>Notes:</h5> 280 * <ul class='spaced-list'> 281 * <li> 282 * If {@code allowHeaderParams} init parameter is true, then first looks for {@code &body=xxx} in the URL query string. 283 * <li> 284 * Automatically handles GZipped input streams. 285 * </ul> 286 * 287 * @return The body contents as a reader. 288 * @throws IOException 289 */ 290 public BufferedReader getReader() throws IOException { 291 Reader r = getUnbufferedReader(); 292 if (r instanceof BufferedReader) 293 return (BufferedReader)r; 294 int len = req.getContentLength(); 295 int buffSize = len <= 0 ? 8192 : Math.max(len, 8192); 296 return new BufferedReader(r, buffSize); 297 } 298 299 /** 300 * Same as {@link #getReader()}, but doesn't encapsulate the result in a {@link BufferedReader}; 301 * 302 * @return An unbuffered reader. 303 * @throws IOException 304 */ 305 protected Reader getUnbufferedReader() throws IOException { 306 if (body != null) 307 return new CharSequenceReader(new String(body, UTF8)); 308 return new InputStreamReader(getInputStream(), req.getCharacterEncoding()); 309 } 310 311 /** 312 * Returns the HTTP body content as an {@link InputStream}. 313 * 314 * @return The negotiated input stream. 315 * @throws IOException If any error occurred while trying to get the input stream or wrap it in the GZIP wrapper. 316 */ 317 public ServletInputStream getInputStream() throws IOException { 318 319 if (body != null) 320 return new BoundedServletInputStream(body); 321 322 Encoder enc = getEncoder(); 323 324 if (enc == null) 325 return new BoundedServletInputStream(req.getRawInputStream(), maxInput); 326 327 return new BoundedServletInputStream(enc.getInputStream(req.getRawInputStream()), maxInput); 328 } 329 330 /** 331 * Returns the parser and media type matching the request <code>Content-Type</code> header. 332 * 333 * @return 334 * The parser matching the request <code>Content-Type</code> header, or <jk>null</jk> if no matching parser was 335 * found. 336 * Includes the matching media type. 337 */ 338 public ParserMatch getParserMatch() { 339 MediaType mediaType = headers.getContentType(); 340 if (isEmpty(mediaType)) { 341 if (body != null) 342 mediaType = MediaType.UON; 343 else 344 mediaType = MediaType.JSON; 345 } 346 return parsers.getParserMatch(mediaType); 347 } 348 349 /** 350 * Returns the parser matching the request <code>Content-Type</code> header. 351 * 352 * @return 353 * The parser matching the request <code>Content-Type</code> header, or <jk>null</jk> if no matching parser was 354 * found. 355 */ 356 public Parser getParser() { 357 ParserMatch pm = getParserMatch(); 358 return (pm == null ? null : pm.getParser()); 359 } 360 361 /** 362 * Returns the reader parser matching the request <code>Content-Type</code> header. 363 * 364 * @return 365 * The reader parser matching the request <code>Content-Type</code> header, or <jk>null</jk> if no matching 366 * reader parser was found, or the matching parser was an input stream parser. 367 */ 368 public ReaderParser getReaderParser() { 369 Parser p = getParser(); 370 if (p != null && p.isReaderParser()) 371 return (ReaderParser)p; 372 return null; 373 } 374 375 /* Workhorse method */ 376 private <T> T parse(ClassMeta<T> cm) throws RestException { 377 378 try { 379 if (cm.isReader()) 380 return (T)getReader(); 381 382 if (cm.isInputStream()) 383 return (T)getInputStream(); 384 385 TimeZone timeZone = headers.getTimeZone(); 386 Locale locale = req.getLocale(); 387 ParserMatch pm = getParserMatch(); 388 389 if (pm != null) { 390 Parser p = pm.getParser(); 391 MediaType mediaType = pm.getMediaType(); 392 try { 393 req.getProperties().append("mediaType", mediaType).append("characterEncoding", req.getCharacterEncoding()); 394 ParserSession session = p.createSession(new ParserSessionArgs(req.getProperties(), req.getJavaMethod(), locale, timeZone, mediaType, req.getContext().getResource())); 395 try (Closeable in = session.isReaderParser() ? getUnbufferedReader() : getInputStream()) { 396 return session.parse(in, cm); 397 } 398 } catch (ParseException e) { 399 throw new RestException(SC_BAD_REQUEST, 400 "Could not convert request body content to class type ''{0}'' using parser ''{1}''.", 401 cm, p.getClass().getName() 402 ).initCause(e); 403 } 404 } 405 406 throw new RestException(SC_UNSUPPORTED_MEDIA_TYPE, 407 "Unsupported media-type in request header ''Content-Type'': ''{0}''\n\tSupported media-types: {1}", 408 headers.getContentType(), req.getParsers().getSupportedMediaTypes() 409 ); 410 411 } catch (IOException e) { 412 throw new RestException(SC_INTERNAL_SERVER_ERROR, 413 "I/O exception occurred while attempting to handle request ''{0}''.", 414 req.getDescription() 415 ).initCause(e); 416 } 417 } 418 419 private Encoder getEncoder() { 420 if (encoder == null) { 421 String ce = req.getHeader("content-encoding"); 422 if (! isEmpty(ce)) { 423 ce = ce.trim(); 424 encoder = encoders.getEncoder(ce); 425 if (encoder == null) 426 throw new RestException(SC_UNSUPPORTED_MEDIA_TYPE, 427 "Unsupported encoding in request header ''Content-Encoding'': ''{0}''\n\tSupported codings: {1}", 428 req.getHeader("content-encoding"), encoders.getSupportedEncodings() 429 ); 430 } 431 432 if (encoder != null) 433 contentLength = -1; 434 } 435 // Note that if this is the identity encoder, we want to return null 436 // so that we don't needlessly wrap the input stream. 437 if (encoder == IdentityEncoder.INSTANCE) 438 return null; 439 return encoder; 440 } 441 442 /** 443 * Returns the content length of the body. 444 * 445 * @return The content length of the body in bytes. 446 */ 447 public int getContentLength() { 448 return contentLength == 0 ? req.getRawContentLength() : contentLength; 449 } 450 451 /** 452 * ServletInputStream wrapper around a normal input stream. 453 */ 454 static final class BoundedServletInputStream extends ServletInputStream { 455 456 private final InputStream is; 457 private final ServletInputStream sis; 458 private long remain; 459 460 BoundedServletInputStream(InputStream is, long max) { 461 this.is = is; 462 this.sis = null; 463 this.remain = max; 464 } 465 466 BoundedServletInputStream(ServletInputStream sis, long max) { 467 this.sis = sis; 468 this.is = sis; 469 this.remain = max; 470 } 471 472 BoundedServletInputStream(byte[] b) { 473 this(new ByteArrayInputStream(b), Long.MAX_VALUE); 474 } 475 476 @Override /* InputStream */ 477 public final int read() throws IOException { 478 decrement(); 479 return is.read(); 480 } 481 482 @Override /* InputStream */ 483 public int read(byte[] b) throws IOException { 484 return read(b, 0, b.length); 485 } 486 487 @Override /* InputStream */ 488 public int read(final byte[] b, final int off, final int len) throws IOException { 489 long numBytes = Math.min(len, remain); 490 int r = is.read(b, off, (int) numBytes); 491 if (r == -1) 492 return -1; 493 decrement(numBytes); 494 return r; 495 } 496 497 @Override /* InputStream */ 498 public long skip(final long n) throws IOException { 499 long toSkip = Math.min(n, remain); 500 long r = is.skip(toSkip); 501 decrement(r); 502 return r; 503 } 504 505 @Override /* InputStream */ 506 public int available() throws IOException { 507 if (remain <= 0) 508 return 0; 509 return is.available(); 510 } 511 512 @Override /* InputStream */ 513 public synchronized void reset() throws IOException { 514 is.reset(); 515 } 516 517 @Override /* InputStream */ 518 public synchronized void mark(int limit) { 519 is.mark(limit); 520 } 521 522 @Override /* InputStream */ 523 public boolean markSupported() { 524 return is.markSupported(); 525 } 526 527 @Override /* InputStream */ 528 public final void close() throws IOException { 529 is.close(); 530 } 531 532 @Override /* ServletInputStream */ 533 public boolean isFinished() { 534 return sis == null ? false : sis.isFinished(); 535 } 536 537 @Override /* ServletInputStream */ 538 public boolean isReady() { 539 return sis == null ? true : sis.isReady(); 540 } 541 542 @Override /* ServletInputStream */ 543 public void setReadListener(ReadListener arg0) { 544 if (sis != null) 545 sis.setReadListener(arg0); 546 } 547 548 private void decrement() throws IOException { 549 remain--; 550 if (remain < 0) 551 throw new IOException("Input limit exceeded. See @RestResource.maxInput()."); 552 } 553 554 private void decrement(long count) throws IOException { 555 remain -= count; 556 if (remain < 0) 557 throw new IOException("Input limit exceeded. See @RestResource.maxInput()."); 558 } 559 } 560}