001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.httppart;
018
019import static org.apache.juneau.common.utils.IOUtils.*;
020import static org.apache.juneau.common.utils.StringUtils.*;
021import static org.apache.juneau.common.utils.Utils.*;
022
023import java.io.*;
024import java.lang.reflect.*;
025import java.util.*;
026
027import org.apache.juneau.*;
028import org.apache.juneau.collections.*;
029import org.apache.juneau.common.utils.*;
030import org.apache.juneau.encoders.*;
031import org.apache.juneau.http.header.*;
032import org.apache.juneau.http.response.*;
033import org.apache.juneau.httppart.*;
034import org.apache.juneau.internal.*;
035import org.apache.juneau.marshaller.*;
036import org.apache.juneau.parser.*;
037import org.apache.juneau.rest.*;
038import org.apache.juneau.rest.util.*;
039
040import jakarta.servlet.*;
041
042/**
043 * Contains the content of the HTTP request.
044 *
045 * <p>
046 *    The {@link RequestContent} object is the API for accessing the content of an HTTP request.
047 *    It can be accessed by passing it as a parameter on your REST Java method:
048 * </p>
049 * <p class='bjava'>
050 *    <ja>@RestPost</ja>(...)
051 *    <jk>public</jk> Object myMethod(RequestContent <jv>content</jv>) {...}
052 * </p>
053 *
054 * <h5 class='figure'>Example:</h5>
055 * <p class='bjava'>
056 *    <ja>@RestPost</ja>(...)
057 *    <jk>public void</jk> doPost(RequestContent <jv>content</jv>) {
058 *       <jc>// Convert content to a linked list of Person objects.</jc>
059 *       List&lt;Person&gt; <jv>list</jv> = <jv>content</jv>.as(LinkedList.<jk>class</jk>, Person.<jk>class</jk>);
060 *       ...
061 *    }
062 * </p>
063 *
064 * <p>
065 *    Some important methods on this class are:
066 * </p>
067 * <ul class='javatree'>
068 *    <li class='jc'>{@link RequestContent}
069 *    <ul class='spaced-list'>
070 *       <li>Methods for accessing the raw contents of the request content:
071 *       <ul class='javatreec'>
072 *          <li class='jm'>{@link RequestContent#asBytes() asBytes()}
073 *          <li class='jm'>{@link RequestContent#asHex() asHex()}
074 *          <li class='jm'>{@link RequestContent#asSpacedHex() asSpacedHex()}
075 *          <li class='jm'>{@link RequestContent#asString() asString()}
076 *          <li class='jm'>{@link RequestContent#getInputStream() getInputStream()}
077 *          <li class='jm'>{@link RequestContent#getReader() getReader()}
078 *       </ul>
079 *       <li>Methods for parsing the contents of the request content:
080 *       <ul class='javatreec'>
081 *          <li class='jm'>{@link RequestContent#as(Class) as(Class)}
082 *          <li class='jm'>{@link RequestContent#as(Type, Type...) as(Type, Type...)}
083 *          <li class='jm'>{@link RequestContent#setSchema(HttpPartSchema) setSchema(HttpPartSchema)}
084 *       </ul>
085 *       <li>Other methods:
086 *       <ul class='javatreec'>
087 *          <li class='jm'>{@link RequestContent#cache() cache()}
088 *          <li class='jm'>{@link RequestContent#getParserMatch() getParserMatch()}
089 *       </ul>
090 *    </ul>
091 * </ul>
092 *
093 * <h5 class='section'>See Also:</h5><ul>
094 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HttpParts">HTTP Parts</a>
095 * </ul>
096 */
097@SuppressWarnings("unchecked")
098public class RequestContent {
099
100   private byte[] content;
101   private final RestRequest req;
102   private EncoderSet encoders;
103   private Encoder encoder;
104   private ParserSet parsers;
105   private long maxInput;
106   private int contentLength;
107   private MediaType mediaType;
108   private Parser parser;
109   private HttpPartSchema schema;
110
111   /**
112    * Constructor.
113    *
114    * @param req The request creating this bean.
115    */
116   public RequestContent(RestRequest req) {
117      this.req = req;
118   }
119
120   /**
121    * Sets the encoders to use for decoding this content.
122    *
123    * @param value The new value for this setting.
124    * @return This object.
125    */
126   public RequestContent encoders(EncoderSet value) {
127      this.encoders = value;
128      return this;
129   }
130
131   /**
132    * Sets the parsers to use for parsing this content.
133    *
134    * @param value The new value for this setting.
135    * @return This object.
136    */
137   public RequestContent parsers(ParserSet value) {
138      this.parsers = value;
139      return this;
140   }
141
142   /**
143    * Sets the schema for this content.
144    *
145    * @param schema The new schema for this content.
146    * @return This object.
147    */
148   public RequestContent setSchema(HttpPartSchema schema) {
149      this.schema = schema;
150      return this;
151   }
152
153   /**
154    * Sets the max input value for this content.
155    *
156    * @param value The new value for this setting.
157    * @return This object.
158    */
159   public RequestContent maxInput(long value) {
160      this.maxInput = value;
161      return this;
162   }
163
164   /**
165    * Sets the media type of this content.
166    *
167    * @param value The new value for this setting.
168    * @return This object.
169    */
170   public RequestContent mediaType(MediaType value) {
171      this.mediaType = value;
172      return this;
173   }
174
175   /**
176    * Sets the parser to use for this content.
177    *
178    * @param value The new value for this setting.
179    * @return This object.
180    */
181   public RequestContent parser(Parser value) {
182      this.parser = value;
183      return this;
184   }
185
186   /**
187    * Sets the contents of this content.
188    *
189    * @param value The new value for this setting.
190    * @return This object.
191    */
192   public RequestContent content(byte[] value) {
193      this.content = value;
194      return this;
195   }
196
197   boolean isLoaded() {
198      return content != null;
199   }
200
201   /**
202    * Reads the input from the HTTP request parsed into a POJO.
203    *
204    * <p>
205    * The parser used is determined by the matching <c>Content-Type</c> header on the request.
206    *
207    * <p>
208    * If type is <jk>null</jk> or <code>Object.<jk>class</jk></code>, then the actual type will be determined
209    * automatically based on the following input:
210    * <table class='styled'>
211    *    <tr><th>Type</th><th>JSON input</th><th>XML input</th><th>Return type</th></tr>
212    *    <tr>
213    *       <td>object</td>
214    *       <td><js>"{...}"</js></td>
215    *       <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>
216    *       <td>{@link JsonMap}</td>
217    *    </tr>
218    *    <tr>
219    *       <td>array</td>
220    *       <td><js>"[...]"</js></td>
221    *       <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>
222    *       <td>{@link JsonList}</td>
223    *    </tr>
224    *    <tr>
225    *       <td>string</td>
226    *       <td><js>"'...'"</js></td>
227    *       <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>
228    *       <td>{@link String}</td>
229    *    </tr>
230    *    <tr>
231    *       <td>number</td>
232    *       <td><c>123</c></td>
233    *       <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>
234    *       <td>{@link Number}</td>
235    *    </tr>
236    *    <tr>
237    *       <td>boolean</td>
238    *       <td><jk>true</jk></td>
239    *       <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>
240    *       <td>{@link Boolean}</td>
241    *    </tr>
242    *    <tr>
243    *       <td>null</td>
244    *       <td><jk>null</jk> or blank</td>
245    *       <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>
246    *       <td><jk>null</jk></td>
247    *    </tr>
248    * </table>
249    *
250    * <p>
251    * Refer to <a class="doclink" href="https://juneau.apache.org/docs/topics/PojoCategories">POJO Categories</a> for a complete definition of supported POJOs.
252    *
253    * <h5 class='section'>Examples:</h5>
254    * <p class='bjava'>
255    *    <jc>// Parse into an integer.</jc>
256    *    <jk>int</jk> <jv>content1</jv> = <jv>req</jv>.getContent().as(<jk>int</jk>.<jk>class</jk>);
257    *
258    *    <jc>// Parse into an int array.</jc>
259    *    <jk>int</jk>[] <jv>content2</jv> = <jv>req</jv>.getContent().as(<jk>int</jk>[].<jk>class</jk>);
260
261    *    <jc>// Parse into a bean.</jc>
262    *    MyBean <jv>content3</jv> = <jv>req</jv>.getContent().as(MyBean.<jk>class</jk>);
263    *
264    *    <jc>// Parse into a linked-list of objects.</jc>
265    *    List <jv>content4</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>);
266    *
267    *    <jc>// Parse into a map of object keys/values.</jc>
268    *    Map <jv>content5</jv> = <jv>req</jv>.getContent().as(TreeMap.<jk>class</jk>);
269    * </p>
270    *
271    * <h5 class='section'>Notes:</h5><ul>
272    *    <li class='note'>
273    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
274    * </ul>
275    *
276    * @param type The class type to instantiate.
277    * @param <T> The class type to instantiate.
278    * @return The input parsed to a POJO.
279    * @throws BadRequest Thrown if input could not be parsed or fails schema validation.
280    * @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers.
281    * @throws InternalServerError Thrown if an {@link IOException} occurs.
282    */
283   public <T> T as(Class<T> type) throws BadRequest, UnsupportedMediaType, InternalServerError {
284      return getInner(getClassMeta(type));
285   }
286
287   /**
288    * Reads the input from the HTTP request parsed into a POJO.
289    *
290    * <p>
291    * This is similar to {@link #as(Class)} but allows for complex collections of POJOs to be created.
292    *
293    * <h5 class='section'>Examples:</h5>
294    * <p class='bjava'>
295    *    <jc>// Parse into a linked-list of strings.</jc>
296    *    List&lt;String&gt; <jv>content1</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>, String.<jk>class</jk>);
297    *
298    *    <jc>// Parse into a linked-list of linked-lists of strings.</jc>
299    *    List&lt;List&lt;String&gt;&gt; <jv>content2</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>);
300    *
301    *    <jc>// Parse into a map of string keys/values.</jc>
302    *    Map&lt;String,String&gt; <jv>content3</jv> = <jv>req</jv>.getContent().as(TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>);
303    *
304    *    <jc>// Parse into a map containing string keys and values of lists containing beans.</jc>
305    *    Map&lt;String,List&lt;MyBean&gt;&gt; <jv>content4</jv> = <jv>req</jv>.getContent().as(TreeMap.<jk>class</jk>, String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
306    * </p>
307    *
308    * <h5 class='section'>Notes:</h5><ul>
309    *    <li class='note'>
310    *       <c>Collections</c> must be followed by zero or one parameter representing the value type.
311    *    <li class='note'>
312    *       <c>Maps</c> must be followed by zero or two parameters representing the key and value types.
313    *    <li class='note'>
314    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
315    * </ul>
316    *
317    * @param type
318    *    The type of object to create.
319    *    <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
320    * @param args
321    *    The type arguments of the class if it's a collection or map.
322    *    <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
323    *    <br>Ignored if the main type is not a map or collection.
324    * @param <T> The class type to instantiate.
325    * @return The input parsed to a POJO.
326    * @throws BadRequest Thrown if input could not be parsed or fails schema validation.
327    * @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers.
328    * @throws InternalServerError Thrown if an {@link IOException} occurs.
329    */
330   public <T> T as(Type type, Type...args) throws BadRequest, UnsupportedMediaType, InternalServerError {
331      return getInner(this.<T>getClassMeta(type, args));
332   }
333
334   /**
335    * Returns the HTTP content content as a plain string.
336    *
337    * <h5 class='section'>Notes:</h5><ul>
338    *    <li class='note'>
339    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
340    * </ul>
341    *
342    * @return The incoming input from the connection as a plain string.
343    * @throws IOException If a problem occurred trying to read from the reader.
344    */
345   public String asString() throws IOException {
346      cache();
347      return new String(content, UTF8);
348   }
349
350   /**
351    * Returns the HTTP content content as a plain string.
352    *
353    * <h5 class='section'>Notes:</h5><ul>
354    *    <li class='note'>
355    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
356    * </ul>
357    *
358    * @return The incoming input from the connection as a plain string.
359    * @throws IOException If a problem occurred trying to read from the reader.
360    */
361   public byte[] asBytes() throws IOException {
362      cache();
363      return content;
364   }
365
366   /**
367    * Returns the HTTP content content as a simple hexadecimal character string.
368    *
369    * <h5 class='section'>Example:</h5>
370    * <p class='bcode'>
371    *    0123456789ABCDEF
372    * </p>
373    *
374    * @return The incoming input from the connection as a plain string.
375    * @throws IOException If a problem occurred trying to read from the reader.
376    */
377   public String asHex() throws IOException {
378      cache();
379      return toHex(content);
380   }
381
382   /**
383    * Returns the HTTP content content as a simple space-delimited hexadecimal character string.
384    *
385    * <h5 class='section'>Example:</h5>
386    * <p class='bcode'>
387    *    01 23 45 67 89 AB CD EF
388    * </p>
389    *
390    * @return The incoming input from the connection as a plain string.
391    * @throws IOException If a problem occurred trying to read from the reader.
392    */
393   public String asSpacedHex() throws IOException {
394      cache();
395      return toSpacedHex(content);
396   }
397
398   /**
399    * Returns the HTTP content content as a {@link Reader}.
400    *
401    * <h5 class='section'>Notes:</h5><ul>
402    *    <li class='note'>
403    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
404    *    <li class='note'>
405    *       Automatically handles GZipped input streams.
406    * </ul>
407    *
408    * @return The content contents as a reader.
409    * @throws IOException Thrown by underlying stream.
410    */
411   public BufferedReader getReader() throws IOException {
412      Reader r = getUnbufferedReader();
413      if (r instanceof BufferedReader)
414         return (BufferedReader)r;
415      int len = req.getHttpServletRequest().getContentLength();
416      int buffSize = len <= 0 ? 8192 : Math.max(len, 8192);
417      return new BufferedReader(r, buffSize);
418   }
419
420   /**
421    * Same as {@link #getReader()}, but doesn't encapsulate the result in a {@link BufferedReader};
422    *
423    * @return An unbuffered reader.
424    * @throws IOException Thrown by underlying stream.
425    */
426   protected Reader getUnbufferedReader() throws IOException {
427      if (content != null)
428         return new CharSequenceReader(new String(content, UTF8));
429      return new InputStreamReader(getInputStream(), req.getCharset());
430   }
431
432   /**
433    * Returns the HTTP content content as an {@link InputStream}.
434    *
435    * @return The negotiated input stream.
436    * @throws IOException If any error occurred while trying to get the input stream or wrap it in the GZIP wrapper.
437    */
438   public ServletInputStream getInputStream() throws IOException {
439
440      if (content != null)
441         return new BoundedServletInputStream(content);
442
443      Encoder enc = getEncoder();
444
445      InputStream is = req.getHttpServletRequest().getInputStream();
446
447      if (enc == null)
448         return new BoundedServletInputStream(is, maxInput);
449
450      return new BoundedServletInputStream(enc.getInputStream(is), maxInput);
451   }
452
453   /**
454    * Returns the parser and media type matching the request <c>Content-Type</c> header.
455    *
456    * @return
457    *    The parser matching the request <c>Content-Type</c> header, or {@link Optional#empty()} if no matching parser was
458    *    found.
459    *    Includes the matching media type.
460    */
461   public Optional<ParserMatch> getParserMatch() {
462      if (mediaType != null && parser != null)
463         return Utils.opt(new ParserMatch(mediaType, parser));
464      MediaType mt = getMediaType();
465      return Utils.opt(mt).map(x -> parsers.getParserMatch(x));
466   }
467
468   private MediaType getMediaType() {
469      if (mediaType != null)
470         return mediaType;
471      Optional<ContentType> ct = req.getHeader(ContentType.class);
472      if (!ct.isPresent() && content != null)
473         return MediaType.UON;
474      return ct.isPresent() ? ct.get().asMediaType().orElse(null) : null;
475   }
476
477   private <T> T getInner(ClassMeta<T> cm) throws BadRequest, UnsupportedMediaType, InternalServerError {
478      try {
479         return parse(cm);
480      } catch (UnsupportedMediaType e) {
481         throw e;
482      } catch (SchemaValidationException e) {
483         throw new BadRequest("Validation failed on request content. " + e.getLocalizedMessage());
484      } catch (ParseException e) {
485         throw new BadRequest(e, "Could not convert request content content to class type ''{0}''.", cm);
486      } catch (IOException e) {
487         throw new InternalServerError(e, "I/O exception occurred while parsing request content.");
488      } catch (Exception e) {
489         throw new InternalServerError(e, "Exception occurred while parsing request content.");
490      }
491   }
492
493   /* Workhorse method */
494   private <T> T parse(ClassMeta<T> cm) throws SchemaValidationException, ParseException, UnsupportedMediaType, IOException {
495
496      if (cm.isReader())
497         return (T)getReader();
498
499      if (cm.isInputStream())
500         return (T)getInputStream();
501
502      Optional<TimeZone> timeZone = req.getTimeZone();
503      Locale locale = req.getLocale();
504      ParserMatch pm = getParserMatch().orElse(null);
505
506      if (schema == null)
507         schema = HttpPartSchema.DEFAULT;
508
509      if (pm != null) {
510         Parser p = pm.getParser();
511         MediaType mediaType = pm.getMediaType();
512         ParserSession session = p
513            .createSession()
514            .properties(req.getAttributes().asMap())
515            .javaMethod(req.getOpContext().getJavaMethod())
516            .locale(locale)
517            .timeZone(timeZone.orElse(null))
518            .mediaType(mediaType)
519            .apply(ReaderParser.Builder.class, x -> x.streamCharset(req.getCharset()))
520            .schema(schema)
521            .debug(req.isDebug() ? true : null)
522            .outer(req.getContext().getResource())
523            .build();
524
525         try (Closeable in = session.isReaderParser() ? getUnbufferedReader() : getInputStream()) {
526            T o = session.parse(in, cm);
527            if (schema != null)
528               schema.validateOutput(o, cm.getBeanContext());
529            return o;
530         }
531      }
532
533      if (cm.hasReaderMutater())
534         return cm.getReaderMutater().mutate(getReader());
535
536      if (cm.hasInputStreamMutater())
537         return cm.getInputStreamMutater().mutate(getInputStream());
538
539      MediaType mt = getMediaType();
540
541      if ((Utils.isEmpty(Utils.s(mt)) || mt.toString().startsWith("text/plain")) && cm.hasStringMutater())
542         return cm.getStringMutater().mutate(asString());
543
544      Optional<ContentType> ct = req.getHeader(ContentType.class);
545      throw new UnsupportedMediaType(
546         "Unsupported media-type in request header ''Content-Type'': ''{0}''\n\tSupported media-types: {1}",
547         ct.isPresent() ? ct.get().asMediaType().orElse(null) : "not-specified", Json5.of(req.getOpContext().getParsers().getSupportedMediaTypes())
548      );
549   }
550
551   private Encoder getEncoder() throws UnsupportedMediaType {
552      if (encoder == null) {
553         String ce = req.getHeaderParam("content-encoding").orElse(null);
554         if (isNotEmpty(ce)) {
555            ce = ce.trim();
556            encoder = encoders.getEncoder(ce);
557            if (encoder == null)
558               throw new UnsupportedMediaType(
559                  "Unsupported encoding in request header ''Content-Encoding'': ''{0}''\n\tSupported codings: {1}",
560                  req.getHeaderParam("content-encoding").orElse(null), Json5.of(encoders.getSupportedEncodings())
561               );
562         }
563
564         if (encoder != null)
565            contentLength = -1;
566      }
567      // Note that if this is the identity encoder, we want to return null
568      // so that we don't needlessly wrap the input stream.
569      if (encoder == IdentityEncoder.INSTANCE)
570         return null;
571      return encoder;
572   }
573
574   /**
575    * Returns the content length of the content.
576    *
577    * @return The content length of the content in bytes.
578    */
579   public int getContentLength() {
580      return contentLength == 0 ? req.getHttpServletRequest().getContentLength() : contentLength;
581   }
582
583   /**
584    * Caches the content in memory for reuse.
585    *
586    * @return This object.
587    * @throws IOException If error occurs while reading stream.
588    */
589   public RequestContent cache() throws IOException {
590      if (content == null)
591         content = readBytes(getInputStream());
592      return this;
593   }
594
595   //-----------------------------------------------------------------------------------------------------------------
596   // Helper methods
597   //-----------------------------------------------------------------------------------------------------------------
598
599   private <T> ClassMeta<T> getClassMeta(Type type, Type...args) {
600      return req.getBeanSession().getClassMeta(type, args);
601   }
602
603   private <T> ClassMeta<T> getClassMeta(Class<T> type) {
604      return req.getBeanSession().getClassMeta(type);
605   }
606}