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.http.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>&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>
110    *       <td>{@link ObjectMap}</td>
111    *    </tr>
112    *    <tr>
113    *       <td>array</td>
114    *       <td><js>"[...]"</js></td>
115    *       <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>
116    *       <td>{@link ObjectList}</td>
117    *    </tr>
118    *    <tr>
119    *       <td>string</td>
120    *       <td><js>"'...'"</js></td>
121    *       <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>
122    *       <td>{@link String}</td>
123    *    </tr>
124    *    <tr>
125    *       <td>number</td>
126    *       <td><c>123</c></td>
127    *       <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>
128    *       <td>{@link Number}</td>
129    *    </tr>
130    *    <tr>
131    *       <td>boolean</td>
132    *       <td><jk>true</jk></td>
133    *       <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>
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>&lt;null/&gt;</xt></code> or blank<br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'null'</xs><xt>/&gt;</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&lt;String&gt; 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&lt;List&lt;String&gt;&gt; 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&lt;String,String&gt; 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&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>);
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}