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