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