![JAR search and dependency download from the Maven repository](/logo.png)
org.apache.juneau.rest.httppart.RequestContent Maven / Gradle / Ivy
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
// * with the License. You may obtain a copy of the License at *
// * *
// * http://www.apache.org/licenses/LICENSE-2.0 *
// * *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
// * specific language governing permissions and limitations under the License. *
// ***************************************************************************************************************************
package org.apache.juneau.rest.httppart;
import static org.apache.juneau.common.internal.IOUtils.*;
import static org.apache.juneau.common.internal.StringUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import jakarta.servlet.*;
import org.apache.juneau.*;
import org.apache.juneau.collections.*;
import org.apache.juneau.encoders.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.marshaller.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.http.header.*;
import org.apache.juneau.http.response.*;
import org.apache.juneau.rest.*;
import org.apache.juneau.rest.util.*;
/**
* Contains the content of the HTTP request.
*
*
* The {@link RequestContent} object is the API for accessing the content of an HTTP request.
* It can be accessed by passing it as a parameter on your REST Java method:
*
*
* @RestPost (...)
* public Object myMethod(RequestContent content ) {...}
*
*
* Example:
*
* @RestPost (...)
* public void doPost(RequestContent content ) {
* // Convert content to a linked list of Person objects.
* List<Person> list = content .as(LinkedList.class , Person.class );
* ...
* }
*
*
*
* Some important methods on this class are:
*
*
* - {@link RequestContent}
*
* - Methods for accessing the raw contents of the request content:
*
* - {@link RequestContent#asBytes() asBytes()}
*
- {@link RequestContent#asHex() asHex()}
*
- {@link RequestContent#asSpacedHex() asSpacedHex()}
*
- {@link RequestContent#asString() asString()}
*
- {@link RequestContent#getInputStream() getInputStream()}
*
- {@link RequestContent#getReader() getReader()}
*
* - Methods for parsing the contents of the request content:
*
* - {@link RequestContent#as(Class) as(Class)}
*
- {@link RequestContent#as(Type, Type...) as(Type, Type...)}
*
- {@link RequestContent#setSchema(HttpPartSchema) setSchema(HttpPartSchema)}
*
* - Other methods:
*
* - {@link RequestContent#cache() cache()}
*
- {@link RequestContent#getParserMatch() getParserMatch()}
*
*
*
*
* See Also:
* - HTTP Parts
*
*/
@SuppressWarnings("unchecked")
public class RequestContent {
private byte[] content;
private final RestRequest req;
private EncoderSet encoders;
private Encoder encoder;
private ParserSet parsers;
private long maxInput;
private int contentLength = 0;
private MediaType mediaType;
private Parser parser;
private HttpPartSchema schema;
/**
* Constructor.
*
* @param req The request creating this bean.
*/
public RequestContent(RestRequest req) {
this.req = req;
}
/**
* Sets the encoders to use for decoding this content.
*
* @param value The new value for this setting.
* @return This object.
*/
public RequestContent encoders(EncoderSet value) {
this.encoders = value;
return this;
}
/**
* Sets the parsers to use for parsing this content.
*
* @param value The new value for this setting.
* @return This object.
*/
public RequestContent parsers(ParserSet value) {
this.parsers = value;
return this;
}
/**
* Sets the schema for this content.
*
* @param schema The new schema for this content.
* @return This object.
*/
public RequestContent setSchema(HttpPartSchema schema) {
this.schema = schema;
return this;
}
/**
* Sets the max input value for this content.
*
* @param value The new value for this setting.
* @return This object.
*/
public RequestContent maxInput(long value) {
this.maxInput = value;
return this;
}
/**
* Sets the media type of this content.
*
* @param value The new value for this setting.
* @return This object.
*/
public RequestContent mediaType(MediaType value) {
this.mediaType = value;
return this;
}
/**
* Sets the parser to use for this content.
*
* @param value The new value for this setting.
* @return This object.
*/
public RequestContent parser(Parser value) {
this.parser = value;
return this;
}
/**
* Sets the contents of this content.
*
* @param value The new value for this setting.
* @return This object.
*/
public RequestContent content(byte[] value) {
this.content = value;
return this;
}
boolean isLoaded() {
return content != null;
}
/**
* Reads the input from the HTTP request parsed into a POJO.
*
*
* The parser used is determined by the matching Content-Type header on the request.
*
*
* If type is null or Object.class
, then the actual type will be determined
* automatically based on the following input:
*
* Type JSON input XML input Return type
*
* object
* "{...}"
* <object> ...</object>
<x type ='object' > ...</x>
* {@link JsonMap}
*
*
* array
* "[...]"
* <array> ...</array>
<x type ='array' > ...</x>
* {@link JsonList}
*
*
* string
* "'...'"
* <string> ...</string>
<x type ='string' > ...</x>
* {@link String}
*
*
* number
* 123
* <number> 123</number>
<x type ='number' > ...</x>
* {@link Number}
*
*
* boolean
* true
* <boolean> true</boolean>
<x type ='boolean' > ...</x>
* {@link Boolean}
*
*
* null
* null or blank
* <null/>
or blank
<x type ='null' />
* null
*
*
*
*
* Refer to POJO Categories for a complete definition of supported POJOs.
*
*
Examples:
*
* // Parse into an integer.
* int content1 = req .getContent().as(int .class );
*
* // Parse into an int array.
* int [] content2 = req .getContent().as(int [].class );
* // Parse into a bean.
* MyBean content3 = req .getContent().as(MyBean.class );
*
* // Parse into a linked-list of objects.
* List content4 = req .getContent().as(LinkedList.class );
*
* // Parse into a map of object keys/values.
* Map content5 = req .getContent().as(TreeMap.class );
*
*
* Notes:
* -
* If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
*
*
* @param type The class type to instantiate.
* @param The class type to instantiate.
* @return The input parsed to a POJO.
* @throws BadRequest Thrown if input could not be parsed or fails schema validation.
* @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers.
* @throws InternalServerError Thrown if an {@link IOException} occurs.
*/
public T as(Class type) throws BadRequest, UnsupportedMediaType, InternalServerError {
return getInner(getClassMeta(type));
}
/**
* Reads the input from the HTTP request parsed into a POJO.
*
*
* This is similar to {@link #as(Class)} but allows for complex collections of POJOs to be created.
*
*
Examples:
*
* // Parse into a linked-list of strings.
* List<String> content1 = req .getContent().as(LinkedList.class , String.class );
*
* // Parse into a linked-list of linked-lists of strings.
* List<List<String>> content2 = req .getContent().as(LinkedList.class , LinkedList.class , String.class );
*
* // Parse into a map of string keys/values.
* Map<String,String> content3 = req .getContent().as(TreeMap.class , String.class , String.class );
*
* // Parse into a map containing string keys and values of lists containing beans.
* Map<String,List<MyBean>> content4 = req .getContent().as(TreeMap.class , String.class , List.class , MyBean.class );
*
*
* Notes:
* -
*
Collections must be followed by zero or one parameter representing the value type.
* -
*
Maps must be followed by zero or two parameters representing the key and value types.
* -
* If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
*
*
* @param type
* The type of object to create.
*
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
* @param args
* The type arguments of the class if it's a collection or map.
*
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
*
Ignored if the main type is not a map or collection.
* @param The class type to instantiate.
* @return The input parsed to a POJO.
* @throws BadRequest Thrown if input could not be parsed or fails schema validation.
* @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers.
* @throws InternalServerError Thrown if an {@link IOException} occurs.
*/
public T as(Type type, Type...args) throws BadRequest, UnsupportedMediaType, InternalServerError {
return getInner(this.getClassMeta(type, args));
}
/**
* Returns the HTTP content content as a plain string.
*
* Notes:
* -
* If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
*
*
* @return The incoming input from the connection as a plain string.
* @throws IOException If a problem occurred trying to read from the reader.
*/
public String asString() throws IOException {
cache();
return new String(content, UTF8);
}
/**
* Returns the HTTP content content as a plain string.
*
* Notes:
* -
* If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
*
*
* @return The incoming input from the connection as a plain string.
* @throws IOException If a problem occurred trying to read from the reader.
*/
public byte[] asBytes() throws IOException {
cache();
return content;
}
/**
* Returns the HTTP content content as a simple hexadecimal character string.
*
* Example:
*
* 0123456789ABCDEF
*
*
* @return The incoming input from the connection as a plain string.
* @throws IOException If a problem occurred trying to read from the reader.
*/
public String asHex() throws IOException {
cache();
return toHex(content);
}
/**
* Returns the HTTP content content as a simple space-delimited hexadecimal character string.
*
* Example:
*
* 01 23 45 67 89 AB CD EF
*
*
* @return The incoming input from the connection as a plain string.
* @throws IOException If a problem occurred trying to read from the reader.
*/
public String asSpacedHex() throws IOException {
cache();
return toSpacedHex(content);
}
/**
* Returns the HTTP content content as a {@link Reader}.
*
* Notes:
* -
* If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string.
*
-
* Automatically handles GZipped input streams.
*
*
* @return The content contents as a reader.
* @throws IOException Thrown by underlying stream.
*/
public BufferedReader getReader() throws IOException {
Reader r = getUnbufferedReader();
if (r instanceof BufferedReader)
return (BufferedReader)r;
int len = req.getHttpServletRequest().getContentLength();
int buffSize = len <= 0 ? 8192 : Math.max(len, 8192);
return new BufferedReader(r, buffSize);
}
/**
* Same as {@link #getReader()}, but doesn't encapsulate the result in a {@link BufferedReader};
*
* @return An unbuffered reader.
* @throws IOException Thrown by underlying stream.
*/
protected Reader getUnbufferedReader() throws IOException {
if (content != null)
return new CharSequenceReader(new String(content, UTF8));
return new InputStreamReader(getInputStream(), req.getCharset());
}
/**
* Returns the HTTP content content as an {@link InputStream}.
*
* @return The negotiated input stream.
* @throws IOException If any error occurred while trying to get the input stream or wrap it in the GZIP wrapper.
*/
public ServletInputStream getInputStream() throws IOException {
if (content != null)
return new BoundedServletInputStream(content);
Encoder enc = getEncoder();
InputStream is = req.getHttpServletRequest().getInputStream();
if (enc == null)
return new BoundedServletInputStream(is, maxInput);
return new BoundedServletInputStream(enc.getInputStream(is), maxInput);
}
/**
* Returns the parser and media type matching the request Content-Type header.
*
* @return
* The parser matching the request Content-Type header, or {@link Optional#empty()} if no matching parser was
* found.
* Includes the matching media type.
*/
public Optional getParserMatch() {
if (mediaType != null && parser != null)
return optional(new ParserMatch(mediaType, parser));
MediaType mt = getMediaType();
return optional(mt).map(x -> parsers.getParserMatch(x));
}
private MediaType getMediaType() {
if (mediaType != null)
return mediaType;
Optional ct = req.getHeader(ContentType.class);
if (!ct.isPresent() && content != null)
return MediaType.UON;
return ct.isPresent() ? ct.get().asMediaType().orElse(null) : null;
}
private T getInner(ClassMeta cm) throws BadRequest, UnsupportedMediaType, InternalServerError {
try {
return parse(cm);
} catch (UnsupportedMediaType e) {
throw e;
} catch (SchemaValidationException e) {
throw new BadRequest("Validation failed on request content. " + e.getLocalizedMessage());
} catch (ParseException e) {
throw new BadRequest(e, "Could not convert request content content to class type ''{0}''.", cm);
} catch (IOException e) {
throw new InternalServerError(e, "I/O exception occurred while parsing request content.");
} catch (Exception e) {
throw new InternalServerError(e, "Exception occurred while parsing request content.");
}
}
/* Workhorse method */
private T parse(ClassMeta cm) throws SchemaValidationException, ParseException, UnsupportedMediaType, IOException {
if (cm.isReader())
return (T)getReader();
if (cm.isInputStream())
return (T)getInputStream();
Optional timeZone = req.getTimeZone();
Locale locale = req.getLocale();
ParserMatch pm = getParserMatch().orElse(null);
if (schema == null)
schema = HttpPartSchema.DEFAULT;
if (pm != null) {
Parser p = pm.getParser();
MediaType mediaType = pm.getMediaType();
ParserSession session = p
.createSession()
.properties(req.getAttributes().asMap())
.javaMethod(req.getOpContext().getJavaMethod())
.locale(locale)
.timeZone(timeZone.orElse(null))
.mediaType(mediaType)
.apply(ReaderParser.Builder.class, x -> x.streamCharset(req.getCharset()))
.schema(schema)
.debug(req.isDebug() ? true : null)
.outer(req.getContext().getResource())
.build();
try (Closeable in = session.isReaderParser() ? getUnbufferedReader() : getInputStream()) {
T o = session.parse(in, cm);
if (schema != null)
schema.validateOutput(o, cm.getBeanContext());
return o;
}
}
if (cm.hasReaderMutater())
return cm.getReaderMutater().mutate(getReader());
if (cm.hasInputStreamMutater())
return cm.getInputStreamMutater().mutate(getInputStream());
MediaType mt = getMediaType();
if ((isEmpty(stringify(mt)) || mt.toString().startsWith("text/plain")) && cm.hasStringMutater())
return cm.getStringMutater().mutate(asString());
Optional ct = req.getHeader(ContentType.class);
throw new UnsupportedMediaType(
"Unsupported media-type in request header ''Content-Type'': ''{0}''\n\tSupported media-types: {1}",
ct.isPresent() ? ct.get().asMediaType().orElse(null) : "not-specified", Json5.of(req.getOpContext().getParsers().getSupportedMediaTypes())
);
}
private Encoder getEncoder() throws UnsupportedMediaType {
if (encoder == null) {
String ce = req.getHeaderParam("content-encoding").orElse(null);
if (isNotEmpty(ce)) {
ce = ce.trim();
encoder = encoders.getEncoder(ce);
if (encoder == null)
throw new UnsupportedMediaType(
"Unsupported encoding in request header ''Content-Encoding'': ''{0}''\n\tSupported codings: {1}",
req.getHeaderParam("content-encoding").orElse(null), Json5.of(encoders.getSupportedEncodings())
);
}
if (encoder != null)
contentLength = -1;
}
// Note that if this is the identity encoder, we want to return null
// so that we don't needlessly wrap the input stream.
if (encoder == IdentityEncoder.INSTANCE)
return null;
return encoder;
}
/**
* Returns the content length of the content.
*
* @return The content length of the content in bytes.
*/
public int getContentLength() {
return contentLength == 0 ? req.getHttpServletRequest().getContentLength() : contentLength;
}
/**
* Caches the content in memory for reuse.
*
* @return This object.
* @throws IOException If error occurs while reading stream.
*/
public RequestContent cache() throws IOException {
if (content == null)
content = readBytes(getInputStream());
return this;
}
//-----------------------------------------------------------------------------------------------------------------
// Helper methods
//-----------------------------------------------------------------------------------------------------------------
private ClassMeta getClassMeta(Type type, Type...args) {
return req.getBeanSession().getClassMeta(type, args);
}
private ClassMeta getClassMeta(Class type) {
return req.getBeanSession().getClassMeta(type);
}
}