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