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>&lt;object&gt;</xt>...<xt>&lt;/object&gt;</xt></code><br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'object'</xs><xt>&gt;</xt>...<xt>&lt;/x&gt;</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>&lt;array&gt;</xt>...<xt>&lt;/array&gt;</xt></code><br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'array'</xs><xt>&gt;</xt>...<xt>&lt;/x&gt;</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>&lt;string&gt;</xt>...<xt>&lt;/string&gt;</xt></code><br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'string'</xs><xt>&gt;</xt>...<xt>&lt;/x&gt;</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>&lt;number&gt;</xt>123<xt>&lt;/number&gt;</xt></code><br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'number'</xs><xt>&gt;</xt>...<xt>&lt;/x&gt;</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>&lt;boolean&gt;</xt>true<xt>&lt;/boolean&gt;</xt></code><br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'boolean'</xs><xt>&gt;</xt>...<xt>&lt;/x&gt;</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>&lt;null/&gt;</xt></code> or blank<br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'null'</xs><xt>/&gt;</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&lt;String&gt; 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&lt;List&lt;String&gt;&gt; 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&lt;String,String&gt; 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&lt;String,List&lt;MyBean&gt;&gt; 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}