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