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