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.commons.utils.IoUtils.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021import static org.apache.juneau.commons.utils.ThrowableUtils.*;
022import static org.apache.juneau.commons.utils.Utils.*;
023
024import java.io.*;
025import java.lang.reflect.*;
026import java.util.*;
027
028import org.apache.juneau.*;
029import org.apache.juneau.collections.*;
030import org.apache.juneau.commons.io.*;
031import org.apache.juneau.encoders.*;
032import org.apache.juneau.http.header.*;
033import org.apache.juneau.http.response.*;
034import org.apache.juneau.httppart.*;
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", "resource" })
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    * Reads the input from the HTTP request parsed into a POJO.
122    *
123    * <p>
124    * The parser used is determined by the matching <c>Content-Type</c> header on the request.
125    *
126    * <p>
127    * If type is <jk>null</jk> or <code>Object.<jk>class</jk></code>, then the actual type will be determined
128    * automatically based on the following input:
129    * <table class='styled'>
130    *    <tr><th>Type</th><th>JSON input</th><th>XML input</th><th>Return type</th></tr>
131    *    <tr>
132    *       <td>object</td>
133    *       <td><js>"{...}"</js></td>
134    *       <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>
135    *       <td>{@link JsonMap}</td>
136    *    </tr>
137    *    <tr>
138    *       <td>array</td>
139    *       <td><js>"[...]"</js></td>
140    *       <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>
141    *       <td>{@link JsonList}</td>
142    *    </tr>
143    *    <tr>
144    *       <td>string</td>
145    *       <td><js>"'...'"</js></td>
146    *       <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>
147    *       <td>{@link String}</td>
148    *    </tr>
149    *    <tr>
150    *       <td>number</td>
151    *       <td><c>123</c></td>
152    *       <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>
153    *       <td>{@link Number}</td>
154    *    </tr>
155    *    <tr>
156    *       <td>boolean</td>
157    *       <td><jk>true</jk></td>
158    *       <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>
159    *       <td>{@link Boolean}</td>
160    *    </tr>
161    *    <tr>
162    *       <td>null</td>
163    *       <td><jk>null</jk> or blank</td>
164    *       <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>
165    *       <td><jk>null</jk></td>
166    *    </tr>
167    * </table>
168    *
169    * <p>
170    * Refer to <a class="doclink" href="https://juneau.apache.org/docs/topics/PojoCategories">POJO Categories</a> for a complete definition of supported POJOs.
171    *
172    * <h5 class='section'>Examples:</h5>
173    * <p class='bjava'>
174    *    <jc>// Parse into an integer.</jc>
175    *    <jk>int</jk> <jv>content1</jv> = <jv>req</jv>.getContent().as(<jk>int</jk>.<jk>class</jk>);
176    *
177    *    <jc>// Parse into an int array.</jc>
178    *    <jk>int</jk>[] <jv>content2</jv> = <jv>req</jv>.getContent().as(<jk>int</jk>[].<jk>class</jk>);
179   
180    *    <jc>// Parse into a bean.</jc>
181    *    MyBean <jv>content3</jv> = <jv>req</jv>.getContent().as(MyBean.<jk>class</jk>);
182    *
183    *    <jc>// Parse into a linked-list of objects.</jc>
184    *    List <jv>content4</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>);
185    *
186    *    <jc>// Parse into a map of object keys/values.</jc>
187    *    Map <jv>content5</jv> = <jv>req</jv>.getContent().as(TreeMap.<jk>class</jk>);
188    * </p>
189    *
190    * <h5 class='section'>Notes:</h5><ul>
191    *    <li class='note'>
192    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
193    * </ul>
194    *
195    * @param type The class type to instantiate.
196    * @param <T> The class type to instantiate.
197    * @return The input parsed to a POJO.
198    * @throws BadRequest Thrown if input could not be parsed or fails schema validation.
199    * @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers.
200    * @throws InternalServerError Thrown if an {@link IOException} occurs.
201    */
202   public <T> T as(Class<T> type) throws BadRequest, UnsupportedMediaType, InternalServerError {
203      return getInner(getClassMeta(type));
204   }
205
206   /**
207    * Reads the input from the HTTP request parsed into a POJO.
208    *
209    * <p>
210    * This is similar to {@link #as(Class)} but allows for complex collections of POJOs to be created.
211    *
212    * <h5 class='section'>Examples:</h5>
213    * <p class='bjava'>
214    *    <jc>// Parse into a linked-list of strings.</jc>
215    *    List&lt;String&gt; <jv>content1</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>, String.<jk>class</jk>);
216    *
217    *    <jc>// Parse into a linked-list of linked-lists of strings.</jc>
218    *    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>);
219    *
220    *    <jc>// Parse into a map of string keys/values.</jc>
221    *    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>);
222    *
223    *    <jc>// Parse into a map containing string keys and values of lists containing beans.</jc>
224    *    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>);
225    * </p>
226    *
227    * <h5 class='section'>Notes:</h5><ul>
228    *    <li class='note'>
229    *       <c>Collections</c> must be followed by zero or one parameter representing the value type.
230    *    <li class='note'>
231    *       <c>Maps</c> must be followed by zero or two parameters representing the key and value types.
232    *    <li class='note'>
233    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
234    * </ul>
235    *
236    * @param type
237    *    The type of object to create.
238    *    <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
239    * @param args
240    *    The type arguments of the class if it's a collection or map.
241    *    <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
242    *    <br>Ignored if the main type is not a map or collection.
243    * @param <T> The class type to instantiate.
244    * @return The input parsed to a POJO.
245    * @throws BadRequest Thrown if input could not be parsed or fails schema validation.
246    * @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers.
247    * @throws InternalServerError Thrown if an {@link IOException} occurs.
248    */
249   public <T> T as(Type type, Type...args) throws BadRequest, UnsupportedMediaType, InternalServerError {
250      return getInner(this.<T>getClassMeta(type, args));
251   }
252
253   /**
254    * Returns the HTTP content content as a plain string.
255    *
256    * <h5 class='section'>Notes:</h5><ul>
257    *    <li class='note'>
258    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
259    * </ul>
260    *
261    * @return The incoming input from the connection as a plain string.
262    * @throws IOException If a problem occurred trying to read from the reader.
263    */
264   public byte[] asBytes() throws IOException {
265      cache();
266      return content;
267   }
268
269   /**
270    * Returns the HTTP content content as a simple hexadecimal character string.
271    *
272    * <h5 class='section'>Example:</h5>
273    * <p class='bcode'>
274    *    0123456789ABCDEF
275    * </p>
276    *
277    * @return The incoming input from the connection as a plain string.
278    * @throws IOException If a problem occurred trying to read from the reader.
279    */
280   public String asHex() throws IOException {
281      cache();
282      return toHex(content);
283   }
284
285   /**
286    * Returns the HTTP content content as a simple space-delimited hexadecimal character string.
287    *
288    * <h5 class='section'>Example:</h5>
289    * <p class='bcode'>
290    *    01 23 45 67 89 AB CD EF
291    * </p>
292    *
293    * @return The incoming input from the connection as a plain string.
294    * @throws IOException If a problem occurred trying to read from the reader.
295    */
296   public String asSpacedHex() throws IOException {
297      cache();
298      return toSpacedHex(content);
299   }
300
301   /**
302    * Returns the HTTP content content as a plain string.
303    *
304    * <h5 class='section'>Notes:</h5><ul>
305    *    <li class='note'>
306    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
307    * </ul>
308    *
309    * @return The incoming input from the connection as a plain string.
310    * @throws IOException If a problem occurred trying to read from the reader.
311    */
312   public String asString() throws IOException {
313      cache();
314      return new String(content, UTF8);
315   }
316
317   /**
318    * Caches the content in memory for reuse.
319    *
320    * @return This object.
321    * @throws IOException If error occurs while reading stream.
322    */
323   public RequestContent cache() throws IOException {
324      if (content == null)
325         content = readBytes(getInputStream());
326      return this;
327   }
328
329   /**
330    * Sets the contents of this content.
331    *
332    * @param value The new value for this setting.
333    * @return This object.
334    */
335   public RequestContent content(byte[] value) {
336      content = value;
337      return this;
338   }
339
340   /**
341    * Sets the encoders to use for decoding this content.
342    *
343    * @param value The new value for this setting.
344    * @return This object.
345    */
346   public RequestContent encoders(EncoderSet value) {
347      encoders = value;
348      return this;
349   }
350
351   /**
352    * Returns the content length of the content.
353    *
354    * @return The content length of the content in bytes.
355    */
356   public int getContentLength() { return contentLength == 0 ? req.getHttpServletRequest().getContentLength() : contentLength; }
357
358   /**
359    * Returns the HTTP content content as an {@link InputStream}.
360    *
361    * @return The negotiated input stream.
362    * @throws IOException If any error occurred while trying to get the input stream or wrap it in the GZIP wrapper.
363    */
364   public ServletInputStream getInputStream() throws IOException {
365
366      if (nn(content))
367         return new BoundedServletInputStream(content);
368
369      var enc = getEncoder();
370
371      var is = req.getHttpServletRequest().getInputStream();
372
373      if (enc == null)
374         return new BoundedServletInputStream(is, maxInput);
375
376      return new BoundedServletInputStream(enc.getInputStream(is), maxInput);
377   }
378
379   /**
380    * Returns the parser and media type matching the request <c>Content-Type</c> header.
381    *
382    * @return
383    *    The parser matching the request <c>Content-Type</c> header, or {@link Optional#empty()} if no matching parser was
384    *    found.
385    *    Includes the matching media type.
386    */
387   public Optional<ParserMatch> getParserMatch() {
388      if (nn(mediaType) && nn(parser))
389         return opt(new ParserMatch(mediaType, parser));
390      var mt = getMediaType();
391      return opt(mt).map(x -> parsers.getParserMatch(x));
392   }
393
394   /**
395    * Returns the HTTP content content as a {@link Reader}.
396    *
397    * <h5 class='section'>Notes:</h5><ul>
398    *    <li class='note'>
399    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
400    *    <li class='note'>
401    *       Automatically handles GZipped input streams.
402    * </ul>
403    *
404    * @return The content contents as a reader.
405    * @throws IOException Thrown by underlying stream.
406    */
407   public BufferedReader getReader() throws IOException {
408      var r = getUnbufferedReader();
409      if (r instanceof BufferedReader r2)
410         return r2;
411      int len = req.getHttpServletRequest().getContentLength();
412      int buffSize = len <= 0 ? 8192 : Math.max(len, 8192);
413      return new BufferedReader(r, buffSize);
414   }
415
416   /**
417    * Sets the max input value for this content.
418    *
419    * @param value The new value for this setting.
420    * @return This object.
421    */
422   public RequestContent maxInput(long value) {
423      maxInput = value;
424      return this;
425   }
426
427   /**
428    * Sets the media type of this content.
429    *
430    * @param value The new value for this setting.
431    * @return This object.
432    */
433   public RequestContent mediaType(MediaType value) {
434      mediaType = value;
435      return this;
436   }
437
438   /**
439    * Sets the parser to use for this content.
440    *
441    * @param value The new value for this setting.
442    * @return This object.
443    */
444   public RequestContent parser(Parser value) {
445      parser = value;
446      return this;
447   }
448
449   /**
450    * Sets the parsers to use for parsing this content.
451    *
452    * @param value The new value for this setting.
453    * @return This object.
454    */
455   public RequestContent parsers(ParserSet value) {
456      parsers = value;
457      return this;
458   }
459
460   /**
461    * Sets the schema for this content.
462    *
463    * @param schema The new schema for this content.
464    * @return This object.
465    */
466   public RequestContent setSchema(HttpPartSchema schema) {
467      this.schema = schema;
468      return this;
469   }
470
471   private <T> ClassMeta<T> getClassMeta(Class<T> type) {
472      return req.getBeanSession().getClassMeta(type);
473   }
474
475   private <T> ClassMeta<T> getClassMeta(Type type, Type...args) {
476      return req.getBeanSession().getClassMeta(type, args);
477   }
478
479   private Encoder getEncoder() throws UnsupportedMediaType {
480      if (encoder == null) {
481         var ce = req.getHeaderParam("content-encoding").orElse(null);
482         if (ne(ce)) {
483            ce = ce.trim();
484            encoder = encoders.getEncoder(ce);
485            if (encoder == null)
486               throw new UnsupportedMediaType("Unsupported encoding in request header ''Content-Encoding'': ''{0}''\n\tSupported codings: {1}",
487                  req.getHeaderParam("content-encoding").orElse(null), Json5.of(encoders.getSupportedEncodings()));
488         }
489
490         if (nn(encoder))
491            contentLength = -1;
492      }
493      // Note that if this is the identity encoder, we want to return null
494      // so that we don't needlessly wrap the input stream.
495      if (encoder == IdentityEncoder.INSTANCE)
496         return null;
497      return encoder;
498   }
499
500   private <T> T getInner(ClassMeta<T> cm) throws BadRequest, UnsupportedMediaType, InternalServerError {
501      try {
502         return parse(cm);
503      } catch (UnsupportedMediaType e) {
504         throw e;
505      } catch (SchemaValidationException e) {
506         throw new BadRequest("Validation failed on request content. " + lm(e));
507      } catch (ParseException e) {
508         throw new BadRequest(e, "Could not convert request content content to class type ''{0}''.", cm);
509      } catch (IOException e) {
510         throw new InternalServerError(e, "I/O exception occurred while parsing request content.");
511      } catch (Exception e) {
512         throw new InternalServerError(e, "Exception occurred while parsing request content.");
513      }
514   }
515
516   private MediaType getMediaType() {
517      if (nn(mediaType))
518         return mediaType;
519      var ct = req.getHeader(ContentType.class);
520      if (! ct.isPresent() && nn(content))
521         return MediaType.UON;
522      return ct.isPresent() ? ct.get().asMediaType().orElse(null) : null;
523   }
524
525   /* Workhorse method */
526   private <T> T parse(ClassMeta<T> cm) throws SchemaValidationException, ParseException, UnsupportedMediaType, IOException {
527
528      if (cm.isReader())
529         return (T)getReader();
530
531      if (cm.isInputStream())
532         return (T)getInputStream();
533
534      var timeZone = req.getTimeZone();
535      var locale = req.getLocale();
536      var pm = getParserMatch().orElse(null);
537
538      if (schema == null)
539         schema = HttpPartSchema.DEFAULT;
540
541      if (nn(pm)) {
542         var p = pm.getParser();
543         var mediaType = pm.getMediaType();
544         // @formatter:off
545         var session = p
546            .createSession()
547            .properties(req.getAttributes().asMap())
548            .javaMethod(req.getOpContext().getJavaMethod())
549            .locale(locale)
550            .timeZone(timeZone.orElse(null))
551            .mediaType(mediaType)
552            .apply(ReaderParser.Builder.class, x -> x.streamCharset(req.getCharset()))
553            .schema(schema)
554            .debug(req.isDebug() ? true : null)
555            .outer(req.getContext().getResource())
556            .build();
557         // @formatter:on
558
559         try (Closeable in = session.isReaderParser() ? getUnbufferedReader() : getInputStream()) {
560            var o = session.parse(in, cm);
561            if (nn(schema))
562               schema.validateOutput(o, cm.getBeanContext());
563            return o;
564         }
565      }
566
567      if (cm.hasReaderMutater())
568         return cm.getReaderMutater().mutate(getReader());
569
570      if (cm.hasInputStreamMutater())
571         return cm.getInputStreamMutater().mutate(getInputStream());
572
573      var mt = getMediaType();
574
575      if ((isEmpty(s(mt)) || mt.toString().startsWith("text/plain")) && cm.hasStringMutater())
576         return cm.getStringMutater().mutate(asString());
577
578      var ct = req.getHeader(ContentType.class);
579      throw new UnsupportedMediaType("Unsupported media-type in request header ''Content-Type'': ''{0}''\n\tSupported media-types: {1}",
580         ct.isPresent() ? ct.get().asMediaType().orElse(null) : "not-specified", Json5.of(req.getOpContext().getParsers().getSupportedMediaTypes()));
581   }
582
583   /**
584    * Same as {@link #getReader()}, but doesn't encapsulate the result in a {@link BufferedReader};
585    *
586    * @return An unbuffered reader.
587    * @throws IOException Thrown by underlying stream.
588    */
589   protected Reader getUnbufferedReader() throws IOException {
590      if (nn(content))
591         return new CharSequenceReader(new String(content, UTF8));
592      return new InputStreamReader(getInputStream(), req.getCharset());
593   }
594
595   boolean isLoaded() { return nn(content); }
596}