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