| // *************************************************************************************************************************** |
| // * 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; |
| |
| import static org.apache.juneau.internal.IOUtils.*; |
| import static org.apache.juneau.internal.StringUtils.*; |
| |
| import java.io.*; |
| import java.lang.reflect.*; |
| import java.util.*; |
| |
| import javax.servlet.*; |
| |
| import org.apache.juneau.*; |
| import org.apache.juneau.encoders.*; |
| import org.apache.juneau.http.*; |
| import org.apache.juneau.httppart.*; |
| import org.apache.juneau.internal.*; |
| import org.apache.juneau.parser.*; |
| import org.apache.juneau.http.exception.*; |
| import org.apache.juneau.rest.util.*; |
| |
| /** |
| * Contains the body of the HTTP request. |
| * |
| * <ul class='seealso'> |
| * <li class='link'>{@doc juneau-rest-server.RestMethod.RequestBody} |
| * </ul> |
| */ |
| @SuppressWarnings("unchecked") |
| public class RequestBody { |
| |
| private byte[] body; |
| private final RestRequest req; |
| private EncoderGroup encoders; |
| private Encoder encoder; |
| private ParserGroup parsers; |
| private long maxInput; |
| private RequestHeaders headers; |
| private int contentLength = 0; |
| private MediaType mediaType; |
| private Parser parser; |
| private HttpPartSchema schema; |
| |
| RequestBody(RestRequest req) { |
| this.req = req; |
| } |
| |
| RequestBody encoders(EncoderGroup encoders) { |
| this.encoders = encoders; |
| return this; |
| } |
| |
| RequestBody parsers(ParserGroup parsers) { |
| this.parsers = parsers; |
| return this; |
| } |
| |
| RequestBody schema(HttpPartSchema schema) { |
| this.schema = schema; |
| return this; |
| } |
| |
| RequestBody headers(RequestHeaders headers) { |
| this.headers = headers; |
| return this; |
| } |
| |
| RequestBody maxInput(long maxInput) { |
| this.maxInput = maxInput; |
| return this; |
| } |
| |
| RequestBody load(MediaType mediaType, Parser parser, byte[] body) { |
| this.mediaType = mediaType; |
| this.parser = parser; |
| this.body = body; |
| return this; |
| } |
| |
| boolean isLoaded() { |
| return body != null; |
| } |
| |
| /** |
| * Reads the input from the HTTP request parsed into a POJO. |
| * |
| * <p> |
| * The parser used is determined by the matching <c>Content-Type</c> header on the request. |
| * |
| * <p> |
| * If type is <jk>null</jk> or <code>Object.<jk>class</jk></code>, then the actual type will be determined |
| * automatically based on the following input: |
| * <table class='styled'> |
| * <tr><th>Type</th><th>JSON input</th><th>XML input</th><th>Return type</th></tr> |
| * <tr> |
| * <td>object</td> |
| * <td><js>"{...}"</js></td> |
| * <td><code><xt><object></xt>...<xt></object></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'object'</xs><xt>></xt>...<xt></x></xt></code></td> |
| * <td>{@link ObjectMap}</td> |
| * </tr> |
| * <tr> |
| * <td>array</td> |
| * <td><js>"[...]"</js></td> |
| * <td><code><xt><array></xt>...<xt></array></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'array'</xs><xt>></xt>...<xt></x></xt></code></td> |
| * <td>{@link ObjectList}</td> |
| * </tr> |
| * <tr> |
| * <td>string</td> |
| * <td><js>"'...'"</js></td> |
| * <td><code><xt><string></xt>...<xt></string></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'string'</xs><xt>></xt>...<xt></x></xt></code></td> |
| * <td>{@link String}</td> |
| * </tr> |
| * <tr> |
| * <td>number</td> |
| * <td><c>123</c></td> |
| * <td><code><xt><number></xt>123<xt></number></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'number'</xs><xt>></xt>...<xt></x></xt></code></td> |
| * <td>{@link Number}</td> |
| * </tr> |
| * <tr> |
| * <td>boolean</td> |
| * <td><jk>true</jk></td> |
| * <td><code><xt><boolean></xt>true<xt></boolean></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'boolean'</xs><xt>></xt>...<xt></x></xt></code></td> |
| * <td>{@link Boolean}</td> |
| * </tr> |
| * <tr> |
| * <td>null</td> |
| * <td><jk>null</jk> or blank</td> |
| * <td><code><xt><null/></xt></code> or blank<br><code><xt><x</xt> <xa>type</xa>=<xs>'null'</xs><xt>/></xt></code></td> |
| * <td><jk>null</jk></td> |
| * </tr> |
| * </table> |
| * |
| * <p> |
| * Refer to {@doc PojoCategories} for a complete definition of supported POJOs. |
| * |
| * <h5 class='section'>Examples:</h5> |
| * <p class='bcode w800'> |
| * <jc>// Parse into an integer.</jc> |
| * <jk>int</jk> body = req.getBody().asType(<jk>int</jk>.<jk>class</jk>); |
| * |
| * <jc>// Parse into an int array.</jc> |
| * <jk>int</jk>[] body = req.getBody().asType(<jk>int</jk>[].<jk>class</jk>); |
| |
| * <jc>// Parse into a bean.</jc> |
| * MyBean body = req.getBody().asType(MyBean.<jk>class</jk>); |
| * |
| * <jc>// Parse into a linked-list of objects.</jc> |
| * List body = req.getBody().asType(LinkedList.<jk>class</jk>); |
| * |
| * <jc>// Parse into a map of object keys/values.</jc> |
| * Map body = req.getBody().asType(TreeMap.<jk>class</jk>); |
| * </p> |
| * |
| * <ul class='notes'> |
| * <li> |
| * If {@code allowHeaderParams} init parameter is true, then first looks for {@code &body=xxx} in the URL query string. |
| * </ul> |
| * |
| * @param type The class type to instantiate. |
| * @param <T> 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> T asType(Class<T> type) throws BadRequest, UnsupportedMediaType, InternalServerError { |
| return getInner(getClassMeta(type)); |
| } |
| |
| /** |
| * Reads the input from the HTTP request parsed into a POJO. |
| * |
| * <p> |
| * This is similar to {@link #asType(Class)} but allows for complex collections of POJOs to be created. |
| * |
| * <h5 class='section'>Examples:</h5> |
| * <p class='bcode w800'> |
| * <jc>// Parse into a linked-list of strings.</jc> |
| * List<String> body = req.getBody().asType(LinkedList.<jk>class</jk>, String.<jk>class</jk>); |
| * |
| * <jc>// Parse into a linked-list of linked-lists of strings.</jc> |
| * List<List<String>> body = req.getBody().asType(LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>); |
| * |
| * <jc>// Parse into a map of string keys/values.</jc> |
| * Map<String,String> body = req.getBody().asType(TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>); |
| * |
| * <jc>// Parse into a map containing string keys and values of lists containing beans.</jc> |
| * Map<String,List<MyBean>> body = req.getBody().asType(TreeMap.<jk>class</jk>, String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>); |
| * </p> |
| * |
| * <ul class='notes'> |
| * <li> |
| * <c>Collections</c> must be followed by zero or one parameter representing the value type. |
| * <li> |
| * <c>Maps</c> must be followed by zero or two parameters representing the key and value types. |
| * <li> |
| * If {@code allowHeaderParams} init parameter is true, then first looks for {@code &body=xxx} in the URL query string. |
| * </ul> |
| * |
| * @param type |
| * The type of object to create. |
| * <br>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. |
| * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} |
| * <br>Ignored if the main type is not a map or collection. |
| * @param <T> 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> T asType(Type type, Type...args) throws BadRequest, UnsupportedMediaType, InternalServerError { |
| return getInner(this.<T>getClassMeta(type, args)); |
| } |
| |
| /** |
| * Returns the HTTP body content as a plain string. |
| * |
| * <ul class='notes'> |
| * <li> |
| * If {@code allowHeaderParams} init parameter is true, then first looks for {@code &body=xxx} in the URL query string. |
| * </ul> |
| * |
| * @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 { |
| if (body == null) |
| body = readBytes(getInputStream(), 1024); |
| return new String(body, UTF8); |
| } |
| |
| /** |
| * Returns the HTTP body content as a simple hexadecimal character string. |
| * |
| * <h5 class='section'>Example:</h5> |
| * <p class='bcode w800'> |
| * 0123456789ABCDEF |
| * </p> |
| * |
| * @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 { |
| if (body == null) |
| body = readBytes(getInputStream(), 1024); |
| return toHex(body); |
| } |
| |
| /** |
| * Returns the HTTP body content as a simple space-delimited hexadecimal character string. |
| * |
| * <h5 class='section'>Example:</h5> |
| * <p class='bcode w800'> |
| * 01 23 45 67 89 AB CD EF |
| * </p> |
| * |
| * @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 { |
| if (body == null) |
| body = readBytes(getInputStream(), 1024); |
| return toSpacedHex(body); |
| } |
| |
| /** |
| * Returns the HTTP body content as a {@link Reader}. |
| * |
| * <ul class='notes'> |
| * <li> |
| * If {@code allowHeaderParams} init parameter is true, then first looks for {@code &body=xxx} in the URL query string. |
| * <li> |
| * Automatically handles GZipped input streams. |
| * </ul> |
| * |
| * @return The body 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.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 (body != null) |
| return new CharSequenceReader(new String(body, UTF8)); |
| return new InputStreamReader(getInputStream(), req.getCharacterEncoding()); |
| } |
| |
| /** |
| * Returns the HTTP body 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 (body != null) |
| return new BoundedServletInputStream(body); |
| |
| Encoder enc = getEncoder(); |
| |
| if (enc == null) |
| return new BoundedServletInputStream(req.getRawInputStream(), maxInput); |
| |
| return new BoundedServletInputStream(enc.getInputStream(req.getRawInputStream()), maxInput); |
| } |
| |
| /** |
| * Returns the parser and media type matching the request <c>Content-Type</c> header. |
| * |
| * @return |
| * The parser matching the request <c>Content-Type</c> header, or <jk>null</jk> if no matching parser was |
| * found. |
| * Includes the matching media type. |
| */ |
| public ParserMatch getParserMatch() { |
| if (mediaType != null && parser != null) |
| return new ParserMatch(mediaType, parser); |
| MediaType mt = getMediaType(); |
| return mt == null ? null : parsers.getParserMatch(mt); |
| } |
| |
| private MediaType getMediaType() { |
| if (mediaType != null) |
| return mediaType; |
| MediaType mediaType = headers.getContentType(); |
| if (mediaType == null && body != null) |
| return MediaType.UON; |
| return mediaType; |
| } |
| |
| /** |
| * Returns the parser matching the request <c>Content-Type</c> header. |
| * |
| * @return |
| * The parser matching the request <c>Content-Type</c> header, or <jk>null</jk> if no matching parser was |
| * found. |
| */ |
| public Parser getParser() { |
| ParserMatch pm = getParserMatch(); |
| return (pm == null ? null : pm.getParser()); |
| } |
| |
| /** |
| * Returns the reader parser matching the request <c>Content-Type</c> header. |
| * |
| * @return |
| * The reader parser matching the request <c>Content-Type</c> header, or <jk>null</jk> if no matching |
| * reader parser was found, or the matching parser was an input stream parser. |
| */ |
| public ReaderParser getReaderParser() { |
| Parser p = getParser(); |
| if (p != null && p.isReaderParser()) |
| return (ReaderParser)p; |
| return null; |
| } |
| |
| /** |
| * Returns the input stream parser matching the request <c>Content-Type</c> header. |
| * |
| * @return |
| * The input stream parser matching the request <c>Content-Type</c> header, or <jk>null</jk> if no matching |
| * reader parser was found, or the matching parser was a reader parser. |
| */ |
| public InputStreamParser getInputStreamParser() { |
| Parser p = getParser(); |
| if (p != null && ! p.isReaderParser()) |
| return (InputStreamParser)p; |
| return null; |
| } |
| |
| private <T> T getInner(ClassMeta<T> cm) throws BadRequest, UnsupportedMediaType, InternalServerError { |
| try { |
| return parse(cm); |
| } catch (UnsupportedMediaType e) { |
| throw e; |
| } catch (SchemaValidationException e) { |
| throw new BadRequest("Validation failed on request body. " + e.getLocalizedMessage()); |
| } catch (ParseException e) { |
| throw new BadRequest(e, "Could not convert request body content to class type ''{0}''.", cm); |
| } catch (IOException e) { |
| throw new InternalServerError(e, "I/O exception occurred while parsing request body."); |
| } catch (Exception e) { |
| throw new InternalServerError(e, "Exception occurred while parsing request body."); |
| } |
| } |
| |
| /* Workhorse method */ |
| private <T> T parse(ClassMeta<T> cm) throws SchemaValidationException, ParseException, UnsupportedMediaType, IOException { |
| |
| if (cm.isReader()) |
| return (T)getReader(); |
| |
| if (cm.isInputStream()) |
| return (T)getInputStream(); |
| |
| TimeZone timeZone = headers.getTimeZone(); |
| Locale locale = req.getLocale(); |
| ParserMatch pm = getParserMatch(); |
| |
| if (schema == null) |
| schema = HttpPartSchema.DEFAULT; |
| |
| if (pm != null) { |
| Parser p = pm.getParser(); |
| MediaType mediaType = pm.getMediaType(); |
| ParserSessionArgs pArgs = ParserSessionArgs |
| .create() |
| .properties(req.getAttributes()) |
| .javaMethod(req.getJavaMethod()) |
| .locale(locale) |
| .timeZone(timeZone) |
| .mediaType(mediaType) |
| .streamCharset(req.getCharset()) |
| .schema(schema) |
| .debug(req.isDebug() ? true : null) |
| .outer(req.getContext().getResource()); |
| ParserSession session = p.createSession(pArgs); |
| 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(mt) || mt.toString().startsWith("text/plain")) && cm.hasStringMutater()) |
| return cm.getStringMutater().mutate(asString()); |
| |
| throw new UnsupportedMediaType( |
| "Unsupported media-type in request header ''Content-Type'': ''{0}''\n\tSupported media-types: {1}", |
| headers.getContentType(), req.getParsers().getSupportedMediaTypes() |
| ); |
| } |
| |
| private Encoder getEncoder() throws UnsupportedMediaType { |
| if (encoder == null) { |
| String ce = req.getHeader("content-encoding"); |
| 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.getHeader("content-encoding"), 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 body. |
| * |
| * @return The content length of the body in bytes. |
| */ |
| public int getContentLength() { |
| return contentLength == 0 ? req.getRawContentLength() : contentLength; |
| } |
| |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Helper methods |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| private <T> ClassMeta<T> getClassMeta(Type type, Type...args) { |
| return req.getBeanSession().getClassMeta(type, args); |
| } |
| |
| private <T> ClassMeta<T> getClassMeta(Class<T> type) { |
| return req.getBeanSession().getClassMeta(type); |
| } |
| } |