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.server.httppart;
018
019import static org.apache.juneau.commons.utils.IoUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022import static org.apache.juneau.commons.utils.ThrowableUtils.*;
023
024import java.io.*;
025import java.lang.reflect.*;
026import java.util.*;
027
028import org.apache.juneau.commons.conversion.*;
029import org.apache.juneau.commons.http.*;
030import org.apache.juneau.commons.httppart.*;
031import org.apache.juneau.commons.io.*;
032import org.apache.juneau.http.header.*;
033import org.apache.juneau.http.response.*;
034import org.apache.juneau.marshall.*;
035import org.apache.juneau.marshall.collections.*;
036import org.apache.juneau.marshall.encoders.*;
037import org.apache.juneau.marshall.httppart.*;
038import org.apache.juneau.marshall.marshaller.*;
039import org.apache.juneau.marshall.parser.*;
040import org.apache.juneau.rest.server.*;
041import org.apache.juneau.rest.server.util.*;
042
043import jakarta.servlet.*;
044
045/**
046 * Contains the content of the HTTP request.
047 *
048 * <p>
049 *    The {@link RequestContent} object is the API for accessing the content of an HTTP request.
050 *    It can be accessed by passing it as a parameter on your REST Java method:
051 * </p>
052 * <p class='bjava'>
053 *    <ja>@RestPost</ja>(...)
054 *    <jk>public</jk> Object myMethod(RequestContent <jv>content</jv>) {...}
055 * </p>
056 *
057 * <h5 class='figure'>Example:</h5>
058 * <p class='bjava'>
059 *    <ja>@RestPost</ja>(...)
060 *    <jk>public void</jk> doPost(RequestContent <jv>content</jv>) {
061 *       <jc>// Convert content to a linked list of Person objects.</jc>
062 *       List&lt;Person&gt; <jv>list</jv> = <jv>content</jv>.as(LinkedList.<jk>class</jk>, Person.<jk>class</jk>);
063 *       ...
064 *    }
065 * </p>
066 *
067 * <p>
068 *    Some important methods on this class are:
069 * </p>
070 * <ul class='javatree'>
071 *    <li class='jc'>{@link RequestContent}
072 *    <ul class='spaced-list'>
073 *       <li>Methods for accessing the raw contents of the request content:
074 *       <ul class='javatreec'>
075 *          <li class='jm'>{@link RequestContent#asBytes() asBytes()}
076 *          <li class='jm'>{@link RequestContent#asHex() asHex()}
077 *          <li class='jm'>{@link RequestContent#asSpacedHex() asSpacedHex()}
078 *          <li class='jm'>{@link RequestContent#asString() asString()}
079 *          <li class='jm'>{@link RequestContent#getInputStream() getInputStream()}
080 *          <li class='jm'>{@link RequestContent#getReader() getReader()}
081 *       </ul>
082 *       <li>Methods for parsing the contents of the request content:
083 *       <ul class='javatreec'>
084 *          <li class='jm'>{@link RequestContent#as(Class) as(Class)}
085 *          <li class='jm'>{@link RequestContent#as(Type, Type...) as(Type, Type...)}
086 *          <li class='jm'>{@link RequestContent#setSchema(HttpPartSchema) setSchema(HttpPartSchema)}
087 *       </ul>
088 *       <li>Other methods:
089 *       <ul class='javatreec'>
090 *          <li class='jm'>{@link RequestContent#cache() cache()}
091 *          <li class='jm'>{@link RequestContent#getParserMatch() getParserMatch()}
092 *       </ul>
093 *    </ul>
094 * </ul>
095 *
096 * <h5 class='section'>See Also:</h5><ul>
097 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerHttpParts">HTTP parts in REST servers</a>
098 * </ul>
099 */
100@SuppressWarnings({
101   "resource", // Resource management handled externally
102   "unchecked" // Type erasure requires unchecked casts in content parsing
103})
104public class RequestContent {
105
106   private byte[] content;
107   private final RestRequest req;
108   private EncoderSet encoders;
109   private Encoder encoder;
110   @SuppressWarnings({
111      "java:S1450" // Field backs the public fluent parsers(ParserSet) setter; the value is set externally and consumed later in getParserMatch(), so it cannot be a local variable.
112   })
113   private ParserSet parsers;
114   private long maxInput;
115   private int contentLength;
116   private MediaType mediaType;
117   private Parser parser;
118   private HttpPartSchema schema;
119
120   /**
121    * Constructor.
122    *
123    * @param req The request creating this bean.
124    */
125   public RequestContent(RestRequest req) {
126      this.req = req;
127   }
128
129   /**
130    * Reads the input from the HTTP request parsed into a POJO.
131    *
132    * <p>
133    * The parser used is determined by the matching <c>Content-Type</c> header on the request.
134    *
135    * <p>
136    * If type is <jk>null</jk> or <code>Object.<jk>class</jk></code>, then the actual type will be determined
137    * automatically based on the following input:
138    * <table class='styled'>
139    *    <tr><th>Type</th><th>JSON input</th><th>XML input</th><th>Return type</th></tr>
140    *    <tr>
141    *       <td>object</td>
142    *       <td><js>"{...}"</js></td>
143    *       <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>
144    *       <td>{@link JsonMap}</td>
145    *    </tr>
146    *    <tr>
147    *       <td>array</td>
148    *       <td><js>"[...]"</js></td>
149    *       <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>
150    *       <td>{@link JsonList}</td>
151    *    </tr>
152    *    <tr>
153    *       <td>string</td>
154    *       <td><js>"'...'"</js></td>
155    *       <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>
156    *       <td>{@link String}</td>
157    *    </tr>
158    *    <tr>
159    *       <td>number</td>
160    *       <td><c>123</c></td>
161    *       <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>
162    *       <td>{@link Number}</td>
163    *    </tr>
164    *    <tr>
165    *       <td>boolean</td>
166    *       <td><jk>true</jk></td>
167    *       <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>
168    *       <td>{@link Boolean}</td>
169    *    </tr>
170    *    <tr>
171    *       <td>null</td>
172    *       <td><jk>null</jk> or blank</td>
173    *       <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>
174    *       <td><jk>null</jk></td>
175    *    </tr>
176    * </table>
177    *
178    * <p>
179    * Refer to <a class="doclink" href="https://juneau.apache.org/docs/topics/PojoCategories">POJO Categories</a> for a complete definition of supported POJOs.
180    *
181    * <h5 class='section'>Examples:</h5>
182    * <p class='bjava'>
183    *    <jc>// Parse into an integer.</jc>
184    *    <jk>int</jk> <jv>content1</jv> = <jv>req</jv>.getContent().as(<jk>int</jk>.<jk>class</jk>);
185    *
186    *    <jc>// Parse into an int array.</jc>
187    *    <jk>int</jk>[] <jv>content2</jv> = <jv>req</jv>.getContent().as(<jk>int</jk>[].<jk>class</jk>);
188    * 
189    *    <jc>// Parse into a bean.</jc>
190    *    MyBean <jv>content3</jv> = <jv>req</jv>.getContent().as(MyBean.<jk>class</jk>);
191    *
192    *    <jc>// Parse into a linked-list of objects.</jc>
193    *    List <jv>content4</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>);
194    *
195    *    <jc>// Parse into a map of object keys/values.</jc>
196    *    Map <jv>content5</jv> = <jv>req</jv>.getContent().as(TreeMap.<jk>class</jk>);
197    * </p>
198    *
199    * <h5 class='section'>Notes:</h5><ul>
200    *    <li class='note'>
201    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
202    * </ul>
203    *
204    * @param type
205    *    The class type to instantiate.
206    *    <br>Can be <jk>null</jk> or <code>Object.<jk>class</jk></code> to auto-detect the type based on the input.
207    * @param <T> The class type to instantiate.
208    * @return The input parsed to a POJO.
209    * @throws BadRequest Thrown if input could not be parsed or fails schema validation.
210    * @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers.
211    * @throws InternalServerError Thrown if an {@link IOException} occurs.
212    */
213   public <T> T as(Class<T> type) throws BadRequest, UnsupportedMediaType, InternalServerError {
214      return getInner(getClassMeta(type));
215   }
216
217   /**
218    * Reads the input from the HTTP request parsed into a POJO.
219    *
220    * <p>
221    * This is similar to {@link #as(Class)} but allows for complex collections of POJOs to be created.
222    *
223    * <h5 class='section'>Examples:</h5>
224    * <p class='bjava'>
225    *    <jc>// Parse into a linked-list of strings.</jc>
226    *    List&lt;String&gt; <jv>content1</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>, String.<jk>class</jk>);
227    *
228    *    <jc>// Parse into a linked-list of linked-lists of strings.</jc>
229    *    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>);
230    *
231    *    <jc>// Parse into a map of string keys/values.</jc>
232    *    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>);
233    *
234    *    <jc>// Parse into a map containing string keys and values of lists containing beans.</jc>
235    *    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>);
236    * </p>
237    *
238    * <h5 class='section'>Notes:</h5><ul>
239    *    <li class='note'>
240    *       <c>Collections</c> must be followed by zero or one parameter representing the value type.
241    *    <li class='note'>
242    *       <c>Maps</c> must be followed by zero or two parameters representing the key and value types.
243    *    <li class='note'>
244    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
245    * </ul>
246    *
247    * @param type
248    *    The type of object to create.
249    *    <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
250    * @param args
251    *    The type arguments of the class if it's a collection or map.
252    *    <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
253    *    <br>Ignored if the main type is not a map or collection.
254    * @param <T> The class type to instantiate.
255    * @return The input parsed to a POJO.
256    * @throws BadRequest Thrown if input could not be parsed or fails schema validation.
257    * @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers.
258    * @throws InternalServerError Thrown if an {@link IOException} occurs.
259    */
260   public <T> T as(Type type, Type...args) throws BadRequest, UnsupportedMediaType, InternalServerError {
261      return getInner(this.<T>getClassMeta(type, args));
262   }
263
264   /**
265    * Returns the HTTP content as a byte array.
266    *
267    * <h5 class='section'>Notes:</h5><ul>
268    *    <li class='note'>
269    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
270    * </ul>
271    *
272    * @return The incoming input from the connection as a byte array.
273    * @throws IOException If a problem occurred trying to read from the reader.
274    */
275   public byte[] asBytes() throws IOException {
276      cache();
277      return content;
278   }
279
280   /**
281    * Returns the HTTP content content as a simple hexadecimal character string.
282    *
283    * <h5 class='section'>Example:</h5>
284    * <p class='bcode'>
285    *    0123456789ABCDEF
286    * </p>
287    *
288    * @return The incoming input from the connection as a plain string.
289    * @throws IOException If a problem occurred trying to read from the reader.
290    */
291   public String asHex() throws IOException {
292      cache();
293      return toHex(content);
294   }
295
296   /**
297    * Returns the HTTP content content as a simple space-delimited hexadecimal character string.
298    *
299    * <h5 class='section'>Example:</h5>
300    * <p class='bcode'>
301    *    01 23 45 67 89 AB CD EF
302    * </p>
303    *
304    * @return The incoming input from the connection as a plain string.
305    * @throws IOException If a problem occurred trying to read from the reader.
306    */
307   public String asSpacedHex() throws IOException {
308      cache();
309      return toSpacedHex(content);
310   }
311
312   /**
313    * Returns the HTTP content content as a plain string.
314    *
315    * <h5 class='section'>Notes:</h5><ul>
316    *    <li class='note'>
317    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
318    * </ul>
319    *
320    * @return The incoming input from the connection as a plain string.
321    * @throws IOException If a problem occurred trying to read from the reader.
322    */
323   public String asString() throws IOException {
324      cache();
325      return new String(content, UTF8);
326   }
327
328   /**
329    * Caches the content in memory for reuse.
330    *
331    * @return This object.
332    * @throws IOException If error occurs while reading stream.
333    */
334   public RequestContent cache() throws IOException {
335      if (content == null)
336         content = readBytes(getInputStream());
337      return this;
338   }
339
340   /**
341    * Sets the contents of this content.
342    *
343    * @param value The new value for this setting.
344    * @return This object.
345    */
346   public RequestContent content(byte[] value) {
347      content = value;
348      return this;
349   }
350
351   /**
352    * Sets the encoders to use for decoding this content.
353    *
354    * @param value The new value for this setting.
355    * @return This object.
356    */
357   public RequestContent encoders(EncoderSet value) {
358      encoders = value;
359      return this;
360   }
361
362   /**
363    * Returns the content length of the content.
364    *
365    * @return The content length of the content in bytes.
366    */
367   public int getContentLength() { return contentLength == 0 ? req.getHttpServletRequest().getContentLength() : contentLength; }
368
369   /**
370    * Returns the HTTP content content as an {@link InputStream}.
371    *
372    * @return The negotiated input stream.
373    * @throws IOException If any error occurred while trying to get the input stream or wrap it in the GZIP wrapper.
374    */
375   public ServletInputStream getInputStream() throws IOException {
376
377      if (nn(content))
378         return new BoundedServletInputStream(content);
379
380      var enc = getEncoder();
381
382      var is = req.getHttpServletRequest().getInputStream();
383
384      if (enc == null)
385         return new BoundedServletInputStream(is, maxInput);
386
387      return new BoundedServletInputStream(enc.getInputStream(is), maxInput);
388   }
389
390   /**
391    * Returns the parser and media type matching the request <c>Content-Type</c> header.
392    *
393    * @return
394    *    The parser matching the request <c>Content-Type</c> header, or {@link Optional#empty()} if no matching parser was
395    *    found.
396    *    Includes the matching media type.
397    */
398   public Optional<ParserMatch> getParserMatch() {
399      if (nn(mediaType) && nn(parser))
400         return o(new ParserMatch(mediaType, parser));
401      var mt = getMediaType();
402      return o(mt).flatMap(parsers::getParserMatch);
403   }
404
405   /**
406    * Returns the HTTP content content as a {@link Reader}.
407    *
408    * <h5 class='section'>Notes:</h5><ul>
409    *    <li class='note'>
410    *       If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
411    *    <li class='note'>
412    *       Automatically handles GZipped input streams.
413    *    <li class='note'>
414    *       The initial buffer is sized off the request's declared {@code Content-Length}, but never past the
415    *       effective {@code @Rest(maxInput)} ceiling &mdash; a client can send whatever length it likes in that
416    *       header, regardless of how much (or little) content actually follows.
417    * </ul>
418    *
419    * @return The content contents as a reader.
420    * @throws IOException Thrown by underlying stream.
421    */
422   public BufferedReader getReader() throws IOException {
423      var r = getUnbufferedReader();
424      if (r instanceof BufferedReader r2)
425         return r2;
426      int len = req.getHttpServletRequest().getContentLength();
427      return new BufferedReader(r, computeReaderBufferSize(len, maxInput));
428   }
429
430   /**
431    * Computes the initial char-buffer size for {@link #getReader()}.
432    *
433    * <p>
434    * Package-private (rather than folded directly into {@link #getReader()}) so the sizing decision can be
435    * exercised directly in unit tests independent of a live servlet request.
436    *
437    * @param declaredContentLength The request's declared {@code Content-Length}, as returned by
438    *    {@code HttpServletRequest.getContentLength()}. May be {@code <= 0} if absent/unparsable.
439    * @param maxInput The effective {@code @Rest(maxInput)} ceiling in bytes. Values {@code <= 0} are
440    *    treated as "no additional ceiling beyond {@code Integer.MAX_VALUE}".
441    * @return The buffer size to use, at least the historical {@code 8192}-byte default and never past
442    *    {@code min(maxInput, Integer.MAX_VALUE)}.
443    */
444   static int computeReaderBufferSize(int declaredContentLength, long maxInput) {
445      long ceiling = maxInput > 0 ? maxInput : Integer.MAX_VALUE;
446      long buffSize = declaredContentLength <= 0 ? 8192 : Math.min(Math.max(declaredContentLength, 8192), ceiling);
447      return (int)Math.min(buffSize, Integer.MAX_VALUE);
448   }
449
450   /**
451    * Sets the max input value for this content.
452    *
453    * @param value The new value for this setting.
454    * @return This object.
455    */
456   public RequestContent maxInput(long value) {
457      maxInput = value;
458      return this;
459   }
460
461   /**
462    * Sets the media type of this content.
463    *
464    * @param value The new value for this setting.
465    * @return This object.
466    */
467   public RequestContent mediaType(MediaType value) {
468      mediaType = value;
469      return this;
470   }
471
472   /**
473    * Sets the parser to use for this content.
474    *
475    * @param value The new value for this setting.
476    * @return This object.
477    */
478   public RequestContent parser(Parser value) {
479      parser = value;
480      return this;
481   }
482
483   /**
484    * Sets the parsers to use for parsing this content.
485    *
486    * @param value The new value for this setting.
487    * @return This object.
488    */
489   public RequestContent parsers(ParserSet value) {
490      parsers = value;
491      return this;
492   }
493
494   /**
495    * Sets the schema for this content.
496    *
497    * @param schema The new schema for this content.
498    * @return This object.
499    */
500   public RequestContent setSchema(HttpPartSchema schema) {
501      this.schema = schema;
502      return this;
503   }
504
505   private <T> ClassMeta<T> getClassMeta(Class<T> type) {
506      return req.getMarshallingSession().getClassMeta(type);
507   }
508
509   private <T> ClassMeta<T> getClassMeta(Type type, Type...args) {
510      return req.getMarshallingSession().getClassMeta(type, args);
511   }
512
513   private Encoder getEncoder() throws UnsupportedMediaType {
514      if (encoder == null) {
515         var ce = req.getHeaderParam("content-encoding").orElse(null);
516         if (ine(ce)) {
517            ce = ce.trim();
518            encoder = encoders.getEncoder(ce);
519            if (encoder == null)
520               throw new UnsupportedMediaType("Unsupported encoding in request header 'Content-Encoding': '%s'\n\tSupported codings: %s",
521                  req.getHeaderParam("content-encoding").orElse(null), Json5.of(encoders.getSupportedEncodings()));
522         }
523
524         if (nn(encoder))
525            contentLength = -1;
526      }
527      // Note that if this is the identity encoder, we want to return null
528      // so that we don't needlessly wrap the input stream.
529      if (encoder == IdentityEncoder.INSTANCE)
530         return null;
531      return encoder;
532   }
533
534   private <T> T getInner(ClassMeta<T> cm) throws BadRequest, UnsupportedMediaType, InternalServerError {
535      try {
536         return parse(cm);
537      } catch (UnsupportedMediaType e) {
538         throw e;
539      } catch (SchemaValidationException e) {
540         throw new BadRequest("Validation failed on request content. " + localizedMessage(e));
541      } catch (ParseException e) {
542         throw new BadRequest(e, "Could not convert request content content to class type '%s'.", cm);
543      } catch (IOException e) {
544         throw new InternalServerError(e, "I/O exception occurred while parsing request content.");
545      } catch (Exception e) {
546         throw new InternalServerError(e, "Exception occurred while parsing request content.");
547      }
548   }
549
550   private MediaType getMediaType() {
551      if (nn(mediaType))
552         return mediaType;
553      var ct = req.getHeader(ContentType.class);
554      if (! ct.isPresent() && nn(content))
555         return MediaType.UON;
556      return ct.isPresent() ? ct.get().asMediaType().orElse(null) : null;
557   }
558
559   /* Workhorse method */
560   @SuppressWarnings({
561      "java:S3776" // Cognitive complexity acceptable for content parsing/schema validation
562   })
563   private <T> T parse(ClassMeta<T> cm) throws SchemaValidationException, ParseException, UnsupportedMediaType, IOException {
564
565      if (cm.isReader())
566         return (T)getReader();
567
568      if (cm.isInputStream())
569         return (T)getInputStream();
570
571      var timeZone = req.getTimeZone();
572      var locale = req.getLocale();
573      var pm = getParserMatch().orElse(null);
574
575      if (schema == null)
576         schema = HttpPartSchema.DEFAULT;
577
578      if (nn(pm)) {
579         var p = pm.getParser();
580         var mediaType2 = pm.getMediaType();
581         // @formatter:off
582         var session = p
583            .createSession()
584            .properties(req.getParserSessionPropertyMap())
585            .javaMethod(req.getOpContext().getJavaMethod())
586            .locale(locale)
587            .timeZone(timeZone.orElse(null))
588            .mediaType(mediaType2)
589            .apply(ReaderParser.Builder.class, x -> x.streamCharset(req.getCharset()))
590            .schema(schema)
591            .debug(req.isDebugMarshalling() ? true : null)
592            .build();
593         // @formatter:on
594
595         try (Closeable in = session.isReaderParser() ? getUnbufferedReader() : getInputStream()) {
596            var o = session.read(in, cm);
597            drainQuietly(in);
598            if (nn(schema))
599               schema.validateOutput(o);
600            return o;
601         }
602      }
603
604      if (cm.hasReaderMutater())
605         return convertUnparsed(getReader(), cm);
606
607      if (cm.hasInputStreamMutater())
608         return convertUnparsed(getInputStream(), cm);
609
610      var mt = getMediaType();
611
612      if ((isEmpty(s(mt)) || mt.toString().startsWith("text/plain")) && cm.hasStringMutater())
613         return BasicConverter.INSTANCE.to(asString(), cm.inner());
614
615      throw unsupportedMediaType(null);
616   }
617
618   // Converts the unparsed content using the type's own Reader/InputStream constructor or factory method.
619   //
620   // When the request named a Content-Type that matched no parser, a conversion failure means the content was in a
621   // media type nobody here understands (for example JSON5 into the strict-JSON JsonMap(Reader) constructor), so it is
622   // reported as 415 rather than as a 500.  Without a Content-Type the failure propagates as before.
623   private <T> T convertUnparsed(Object in, ClassMeta<T> cm) throws UnsupportedMediaType {
624      try {
625         return BasicConverter.INSTANCE.to(in, cm.inner());
626      } catch (RuntimeException e) {
627         if (isEmpty(s(req.getHeader(ContentType.class).flatMap(ContentType::asMediaType).orElse(null))))
628            throw e;
629         throw unsupportedMediaType(e);
630      }
631   }
632
633   private UnsupportedMediaType unsupportedMediaType(Throwable cause) {
634      var ct = req.getHeader(ContentType.class);
635      return new UnsupportedMediaType(cause, "Unsupported media-type in request header 'Content-Type': '%s'\n\tSupported media-types: %s",
636         ct.isPresent() ? ct.get().asMediaType().orElse(null) : "not-specified", Json5.of(req.getOpContext().getParsers().getSupportedMediaTypes()));
637   }
638
639   // Reads and discards whatever remains of the request entity after a parser has returned.
640   //
641   // A parser stops as soon as the top-level value is complete, leaving the end of the entity unread: for a
642   // Transfer-Encoding: chunked request, at least the zero-length terminating chunk whenever the client flushes it
643   // separately from the content.  If that is still unread when the response completes, the container finds
644   // unconsumed request content and aborts the connection.  By then the response is usually committed, so no
645   // Connection: close can be added, and a client reusing the pooled keep-alive connection fails its next request
646   // with an immediate EOF.  Draining here (blocking until end-of-stream) keeps the connection reusable.
647   //
648   // Best effort: the stream is still bounded by @Rest(maxInput), and any failure (limit exceeded, client gone)
649   // just falls back to the container's own unconsumed-content handling.
650   /**
651    * Same as {@link #getReader()}, but doesn't encapsulate the result in a {@link BufferedReader};
652    *
653    * @return An unbuffered reader.
654    * @throws IOException Thrown by underlying stream.
655    */
656   protected Reader getUnbufferedReader() throws IOException {
657      if (nn(content))
658         return new CharSequenceReader(new String(content, UTF8));
659      return new InputStreamReader(getInputStream(), req.getCharset());
660   }
661
662   boolean isLoaded() { return nn(content); }
663}