This is an automated email from the ASF dual-hosted git repository. jamesbognar pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push: new 098e525 REST refactoring. 098e525 is described below commit 098e525cab84f5fd5c14b78d44cb06f5bc527210 Author: JamesBognar <james.bog...@salesforce.com> AuthorDate: Thu Mar 11 11:29:18 2021 -0500 REST refactoring. --- .../apache/juneau/serializer/SerializerMatch.java | 8 +- .../org/apache/juneau/rest/ResponseProcessor.java | 60 ++--- .../main/java/org/apache/juneau/rest/RestCall.java | 90 +------ .../java/org/apache/juneau/rest/RestContext.java | 33 ++- .../org/apache/juneau/rest/RestContextBuilder.java | 4 +- .../java/org/apache/juneau/rest/RestResponse.java | 228 ++++++++++++----- .../juneau/rest/RrpcRestOperationContext.java | 4 +- .../apache/juneau/rest/args/ResponseBeanArg.java | 2 +- .../juneau/rest/processors/DefaultProcessor.java | 276 --------------------- .../rest/processors/HttpEntityProcessor.java | 14 +- .../rest/processors/HttpResourceProcessor.java | 18 +- .../rest/processors/HttpResponseProcessor.java | 22 +- .../rest/processors/InputStreamProcessor.java | 15 +- ...oProcessor.java => PlainTextPojoProcessor.java} | 29 ++- .../juneau/rest/processors/ReaderProcessor.java | 14 +- .../rest/processors/ResponseBeanProcessor.java | 121 ++++++++- .../rest/processors/SerializedPojoProcessor.java | 110 ++++++++ .../juneau/rest/processors/ThrowableProcessor.java | 13 +- 18 files changed, 550 insertions(+), 511 deletions(-) diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerMatch.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerMatch.java index f82c55b..eabfd20 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerMatch.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerMatch.java @@ -22,7 +22,13 @@ public final class SerializerMatch { private final MediaType mediaType; private final Serializer serializer; - SerializerMatch(MediaType mediaType, Serializer serializer) { + /** + * Constructor. + * + * @param mediaType The media type matched. + * @param serializer The serializer matched. + */ + public SerializerMatch(MediaType mediaType, Serializer serializer) { this.mediaType = mediaType; this.serializer = serializer; } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponseProcessor.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponseProcessor.java index 0fa888e..ccf3011 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponseProcessor.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponseProcessor.java @@ -18,7 +18,6 @@ import javax.servlet.http.*; import org.apache.juneau.http.response.*; import org.apache.juneau.rest.annotation.*; -import org.apache.juneau.rest.processors.*; /** * Defines the interface for processors that convert POJOs to appropriate HTTP responses. @@ -36,16 +35,6 @@ import org.apache.juneau.rest.processors.*; * </ul> * * <p> - * By default, REST resources are registered with the following response processors: - * <ul class='spaced-list'> - * <li class='jc'>{@link ReaderProcessor} - * <li class='jc'>{@link InputStreamProcessor} - * <li class='jc'>{@link HttpResourceProcessor} - * <li class='jc'>{@link HttpEntityProcessor} - * <li class='jc'>{@link DefaultProcessor} - * </ul> - * - * <p> * Response processors can be used to process POJOs that cannot normally be handled through Juneau serializers, or * because it's simply easier to define response processors for special cases. * @@ -55,7 +44,7 @@ import org.apache.juneau.rest.processors.*; * <p class='bcode w800'> * <ja>@Rest</ja>( * path=<js>"/example"</js>, - * responseHandlers=FooHandler.<jk>class</jk> + * responseProcessors=FooProcessor.<jk>class</jk> * ) * <jk>public class</jk> Example <jk>extends</jk> RestServlet { * @@ -64,29 +53,44 @@ import org.apache.juneau.rest.processors.*; * <jk>return new</jk> Foo(<js>"123"</js>); * } * - * <jk>public static class</jk> FooHandler <jk>implements</jk> ResponseHandler { + * <jk>public static class</jk> FooProcessor <jk>implements</jk> ResponseProcessor { * <ja>@Override</ja> - * <jk>public boolean</jk> handle(RestRequest <jv>req</jv>, RestResponse <jv>res</jv>, Object <jv>output</jv>) <jk>throws</jk> IOException, RestException { - * <jk>if</jk> (<jv>output</jv> <jk>instanceof</jk> Foo) { - * Foo <jv>foo</jv> = (Foo)<jv>output</jv>; - * <jc>// Set some headers and body content.</jc> - * <jv>res</jv>.setHeader(<js>"Foo-ID"</js>, <jv>foo</jv>.getId()); - * <jv>res</jv>.getWriter().write(<js>"foo.id="</js> + <jv>foo</jv>.getId()); - * <jk>return true</jk>; <jc>// We handled it.</jc> - * } - * <jk>return false</jk>; <jc>// We didn't handle it.</jc> + * <jk>public int</jk> process(RestCall <jv>call</jv>) { + * + * RestResponse <jv>res</jv> = <jv>call</jv>.getRestResponse(); + * Foo <jv>foo</jv> = <jv>res</jv>.getOutput(Foo.<jk>class</jk>); + * + * <jk>if</jk> (<jv>foo</jv> == <jk>null</jk>) + * <jk>return</jk> <jsf>NEXT</jsf>; <jc>// Let the next processor handle it.</jc> + * + * <jc>// Set some headers and body content.</jc> + * <jv>res</jv>.setHeader(<js>"Foo-ID"</js>, <jv>foo</jv>.getId()); + * <jv>res</jv>.getWriter().write(<js>"foo.id="</js> + <jv>foo</jv>.getId()); + * + * <jk>return</jk> <jsf>FINISHED</jsf>; <jc>// We handled it.</jc> * } * } * } * </p> - * - * <ul class='seealso'> - * <li class='link'>{@doc RestmReturnTypes} - * </ul> */ public interface ResponseProcessor { /** + * Return code indicating to proceed to the next response processor in the chain. + */ + public static final int NEXT = 0; + + /** + * Return code indicating that processing is complete and to exit the chain. + */ + public static final int FINISHED = 1; + + /** + * Return code indicating to restart processing the chain from the beginning. + */ + public static final int RESTART = 2; + + /** * Process this response if possible. * * @param call The HTTP call. @@ -99,10 +103,10 @@ public interface ResponseProcessor { * </ul> * @throws IOException * If low-level exception occurred on output stream. - * Results in a {@link HttpServletResponse#SC_INTERNAL_SERVER_ERROR} error. + * <br>Results in a {@link HttpServletResponse#SC_INTERNAL_SERVER_ERROR} error. * @throws BasicHttpException * If some other exception occurred. - * Can be used to provide an appropriate HTTP response code and message. + * <br>Can be used to provide an appropriate HTTP response code and message. */ int process(RestCall call) throws IOException, BasicHttpException; } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCall.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCall.java index 211d9a3..0fa6d6e 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCall.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCall.java @@ -12,8 +12,6 @@ // *************************************************************************************************************************** package org.apache.juneau.rest; -import static java.util.Optional.*; - import java.io.*; import java.lang.reflect.*; import java.util.*; @@ -22,10 +20,7 @@ import javax.servlet.http.*; import org.apache.http.*; import org.apache.juneau.cp.*; -import org.apache.juneau.http.header.*; import org.apache.juneau.http.response.*; -import org.apache.juneau.httppart.bean.*; -import org.apache.juneau.reflect.*; import org.apache.juneau.rest.logging.*; import org.apache.juneau.rest.util.*; @@ -208,52 +203,6 @@ public class RestCall { } /** - * Adds a header to the response. - * - * <p> - * If the header is a {@link BasicUriHeader}, the URI will be resolved using the {@link RestRequest#getUriResolver()} object. - * - * <p> - * If the header is a {@link SerializedHeader} and the serializer session is not set, it will be set to the one returned by {@link RestRequest#getPartSerializerSession()} before serialization. - * - * @param h The header to add. Can be <jk>null</jk>. - * @return This object (for method chaining). - */ - public RestCall addResponseHeader(Header h) { - - if (h == null) - return this; - - if (h instanceof BasicUriHeader) { - BasicUriHeader x = (BasicUriHeader)h; - addResponseHeader(x.getName(), rreq.getUriResolver().resolve(x.getValue())); - } else if (h instanceof SerializedHeader) { - SerializedHeader x = ((SerializedHeader)h).copyWith(rreq.getPartSerializerSession(), null); - addResponseHeader(x.getName(), rreq.getUriResolver().resolve(x.getValue())); - } else { - addResponseHeader(h.getName(), h.getValue()); - } - - return this; - } - - /** - * Adds a header to the response. - * - * <p> - * A no-op of either the name or value is <jk>null</jk>. - * - * @param name The header name. - * @param value The header value. - * @return This object (for method chaining). - */ - public RestCall addResponseHeader(String name, String value) { - if (name != null && value != null) - res.addHeader(name, value); - return this; - } - - /** * Identifies that an exception occurred during this call. * * @param value The thrown exception. @@ -266,18 +215,6 @@ public class RestCall { } /** - * Sets metadata about the response. - * - * @param value The metadata about the response. - * @return This object (for method chaining). - */ - public RestCall responseMeta(ResponseBeanMeta value) { - if (rres != null) - rres.setResponseMeta(value); - return this; - } - - /** * Sets the output object to serialize as the response of this call. * * @param value The response output POJO. @@ -343,15 +280,6 @@ public class RestCall { } /** - * Returns information about the output POJO. - * - * @return The metadata on the object returned by the REST Java method, or {@link ClassInfo#OBJECT} if nothing was set. - */ - public ClassInfo getOutputInfo() { - return getRestResponse().getOutputInfo().orElse(ClassInfo.OBJECT); - } - - /** * Returns the method context of this call. * * @return The method context of this call. @@ -538,19 +466,21 @@ public class RestCall { } /** - * Returns the output that was set by calling {@link RestResponse#setOutput(Object)}. + * Returns <jk>true</jk> if the response contains output. * * <p> - * If it's empty, then {@link RestResponse#setOutput(Object)} wasn't called. - * <br>If it's not empty but contains an empty, then <c>response.setObject(<jk>null</jk>)</c> was called. - * <br>Otherwise, {@link RestResponse#setOutput(Object)} was called with a non-null value. + * This implies {@link RestResponse#setOutput(Object)} has been called on this object. * - * @return The output object. Never <jk>null</jk>. + * <p> + * Note that this also returns <jk>true</jk> even if {@link RestResponse#setOutput(Object)} was called with a <jk>null</jk> + * value as this means the response contains an output value of <jk>null</jk> as opposed to no value at all. + * + * @return <jk>true</jk> if the response contains output. */ - public Optional<Optional<Object>> getOutput() { + public boolean hasOutput() { if (rres != null) - return rres.getOutput(); - return rres == null ? empty() : rres.getOutput(); + return rres.hasOutput(); + return false; } /** diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java index c45fbb1..3f37ccc 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java @@ -18,6 +18,7 @@ import static org.apache.juneau.http.HttpHeaders.*; import static org.apache.juneau.internal.IOUtils.*; import static org.apache.juneau.internal.StringUtils.*; import static org.apache.juneau.rest.HttpRuntimeException.*; +import static org.apache.juneau.rest.ResponseProcessor.*; import static org.apache.juneau.rest.logging.RestLoggingDetail.*; import static java.util.Collections.*; import static java.util.logging.Level.*; @@ -2293,29 +2294,39 @@ public class RestContext extends BeanContext { * set via {@link RestResponse#setOutput(Object)} into appropriate HTTP responses. * * <p> - * By default, the following response handlers are provided out-of-the-box: + * By default, the following response handlers are provided in the specified order: * <ul> * <li class='jc'>{@link ReaderProcessor} * <li class='jc'>{@link InputStreamProcessor} + * <li class='jc'>{@link ThrowableProcessor} + * <li class='jc'>{@link HttpResponseProcessor} * <li class='jc'>{@link HttpResourceProcessor} * <li class='jc'>{@link HttpEntityProcessor} - * <li class='jc'>{@link DefaultProcessor} + * <li class='jc'>{@link ResponseBeanProcessor} + * <li class='jc'>{@link PlainTextPojoProcessor} + * <li class='jc'>{@link SerializedPojoProcessor} * </ul> * * <h5 class='section'>Example:</h5> * <p class='bcode w800'> - * <jc>// Our custom response processor for MySpecialObject objects. </jc> + * <jc>// Our custom response processor for Foo objects. </jc> * <jk>public class</jk> MyResponseProcessor <jk>implements</jk> ResponseProcessor { * * <ja>@Override</ja> - * <jk>public boolean</jk> process(RestRequest <jv>req</jv>, RestResponse <jv>res</jv>, Object <jv>output</jv>) <jk>throws</jk> IOException, RestException { - * <jk>if</jk> (output <jk>instanceof</jk> MySpecialObject) { + * <jk>public int</jk> process(RestCall <jv>call</jv>) <jk>throws</jk> IOException { + * + * RestResponse <jv>res</jv> = <jv>call</jv>.getRestResponse(); + * Foo <jv>foo</jv> = <jv>res</jv>.getOutput(Foo.<jk>class</jk>); + * + * <jk>if</jk> (<jv>foo</jv> == <jk>null</jk>) + * <jk>return</jk> <jsf>NEXT</jsf>; <jc>// Let the next processor handle it.</jc> + * * <jk>try</jk> (Writer <jv>w</jv> = <jv>res</jv>.getNegotiatedWriter()) { * <jc>//Pipe it to the writer ourselves.</jc> * } - * <jk>return true</jk>; <jc>// We handled it.</jc> - * } - * <jk>return false</jk>; <jc>// We didn't handle it.</jc> + * + * <jk>return</jk> <jsf>FINISHED</jsf>; <jc>// We handled it.</jc> + * } * } * } * @@ -6730,7 +6741,7 @@ public class RestContext extends BeanContext { handleNotFound(call); } - if (call.getOutput().isPresent()) { + if (call.hasOutput()) { // Now serialize the output if there was any. // Some subclasses may write to the OutputStream or Writer directly. processResponse(call); @@ -6785,9 +6796,9 @@ public class RestContext extends BeanContext { int loops = 5; for (int i = 0; i < l.size(); i++) { int j = l.get(i).process(call); - if (j == 1) + if (j == FINISHED) return; - if (j == 2) { + if (j == RESTART) { if (loops-- < 0) throw new InternalServerError("Too many processing loops."); i = -1; // Start over. diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java index 1f13dba..81c225c 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java @@ -133,8 +133,8 @@ public class RestContextBuilder extends BeanContextBuilder implements ServletCon HttpResourceProcessor.class, HttpEntityProcessor.class, ResponseBeanProcessor.class, - PojoProcessor.class, - DefaultProcessor.class + PlainTextPojoProcessor.class, + SerializedPojoProcessor.class ); // Pass-through default values. diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java index 46bdb4e..4e3afd5 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java @@ -29,7 +29,7 @@ import org.apache.juneau.collections.*; import org.apache.juneau.encoders.*; import org.apache.juneau.httppart.*; import org.apache.juneau.httppart.bean.*; -import org.apache.juneau.reflect.*; +import org.apache.juneau.oapi.*; import org.apache.juneau.http.header.*; import org.apache.juneau.http.response.*; import org.apache.juneau.rest.logging.*; @@ -65,11 +65,16 @@ public final class RestResponse { private final RestRequest request; private Optional<Optional<Object>> output = empty(); // The POJO being sent to the output. - private Optional<ClassInfo> outputInfo = empty(); private ServletOutputStream sos; private FinishableServletOutputStream os; private FinishablePrintWriter w; - private ResponseBeanMeta responseMeta; + private ResponseBeanMeta responseBeanMeta; + private RestOperationContext opContext; + private Optional<HttpPartSchema> bodySchema; + private Serializer serializer; + private Optional<SerializerMatch> serializerMatch; + private boolean safeHeaders; + private int maxHeaderLength = 8096; /** * Constructor. @@ -79,8 +84,8 @@ public final class RestResponse { inner = call.getResponse(); request = call.getRestRequest(); - RestOperationContext opContext = call.getRestOperationContext(); - responseMeta = opContext.getResponseMeta(); + opContext = call.getRestOperationContext(); + responseBeanMeta = opContext.getResponseMeta(); RestContext context = call.getContext(); @@ -90,7 +95,7 @@ public final class RestResponse { HttpPartParser p = context.getPartParser(); OMap m = p.createPartSession(request.getParserSessionArgs()).parse(HEADER, null, passThroughHeaders, context.getClassMeta(OMap.class)); for (Map.Entry<String,Object> e : m.entrySet()) - setHeaderSafe(e.getKey(), e.getValue().toString()); + addHeader(e.getKey(), resolveUris(e.getValue())); } } catch (Exception e1) { throw new BadRequest(e1, "Invalid format for header 'x-response-headers'. Must be in URL-encoded format."); @@ -113,9 +118,9 @@ public final class RestResponse { } for (Header e : request.getContext().getDefaultResponseHeaders().getAll()) - setHeaderSafe(e.getName(), stringify(e.getValue())); + addHeader(e.getName(), resolveUris(e.getValue())); for (Header e : opContext.getDefaultResponseHeaders().getAll()) - setHeaderSafe(e.getName(), stringify(e.getValue())); + addHeader(e.getName(), resolveUris(e.getValue())); if (charset == null) throw new NotAcceptable("No supported charsets in header ''Accept-Charset'': ''{0}''", request.getHeader("Accept-Charset").orElse(null)); @@ -176,7 +181,6 @@ public final class RestResponse { */ public RestResponse setOutput(Object output) { this.output = of(ofNullable(output)); - this.outputInfo = ofNullable(ClassInfo.ofc(output)); return this; } @@ -216,12 +220,19 @@ public final class RestResponse { } /** - * Returns metadata about the output object. + * Returns <jk>true</jk> if the response contains output. * - * @return Metadata about the output object. + * <p> + * This implies {@link #setOutput(Object)} has been called on this object. + * + * <p> + * Note that this also returns <jk>true</jk> even if {@link #setOutput(Object)} was called with a <jk>null</jk> + * value as this means the response contains an output value of <jk>null</jk> as opposed to no value at all. + * + * @return <jk>true</jk> if the response contains output. */ - public Optional<ClassInfo> getOutputInfo() { - return outputInfo; + public boolean hasOutput() { + return output.isPresent(); } /** @@ -505,6 +516,9 @@ public final class RestResponse { if (ct != null && ct.getParameter("charset") != null) inner.setCharacterEncoding(ct.getParameter("charset")); } else { + if (safeHeaders) + value = stripInvalidHttpHeaderChars(value); + value = abbreviate(value, maxHeaderLength); inner.setHeader(name, value); } } @@ -520,40 +534,6 @@ public final class RestResponse { } /** - * Same as {@link #setHeader(String, String)} but strips invalid characters from the value if present. - * - * These include CTRL characters, newlines, and non-ISO8859-1 characters. - * Also limits the string length to 1024 characters. - * - * @param name Header name. - * @param value Header value. - */ - public void setHeaderSafe(String name, String value) { - setHeaderSafe(name, value, 1024); - } - - /** - * Same as {@link #setHeader(String, String)} but strips invalid characters from the value if present. - * - * These include CTRL characters, newlines, and non-ISO8859-1 characters. - * - * @param name Header name. - * @param value Header value. - * @param maxLength - * The maximum length of the header value. - * Will be truncated with <js>"..."</js> added if the value exceeds the length. - */ - public void setHeaderSafe(String name, String value, int maxLength) { - - // Jetty doesn't set the content type correctly if set through this method. - // Tomcat/WAS does. - if (name.equalsIgnoreCase("Content-Type")) - inner.setContentType(value); - else - inner.setHeader(name, abbreviate(stripInvalidHttpHeaderChars(value), maxLength)); - } - - /** * Sets a header on the request. * * @param name The header name. @@ -628,6 +608,20 @@ public final class RestResponse { } /** + * Specifies the schema for the response body. + * + * <p> + * Used by schema-aware serializers such as {@link OpenApiSerializer}. Ignored by other serializers. + * + * @param schema The body schema + * @return This object (for method chaining). + */ + public RestResponse bodySchema(HttpPartSchema schema) { + this.bodySchema = ofNullable(schema); + return this; + } + + /** * Same as {@link #setHeader(String, String)} but header is defined as a response part * * @param h Header to set. @@ -635,7 +629,7 @@ public final class RestResponse { * @throws SerializeException Header part could not be serialized. */ public void setHeader(HttpPart h) throws SchemaValidationException, SerializeException { - setHeaderSafe(h.getName(), h.getValue()); + setHeader(h.getName(), h.getValue()); } /** @@ -709,8 +703,8 @@ public final class RestResponse { * The metadata about this response. * <jk>Never <jk>null</jk>. */ - public ResponseBeanMeta getResponseMeta() { - return responseMeta; + public ResponseBeanMeta getResponseBeanMeta() { + return responseBeanMeta; } /** @@ -719,8 +713,8 @@ public final class RestResponse { * @param rbm The metadata about this response. * @return This object (for method chaining). */ - public RestResponse setResponseMeta(ResponseBeanMeta rbm) { - this.responseMeta = rbm; + public RestResponse setResponseBeanMeta(ResponseBeanMeta rbm) { + this.responseBeanMeta = rbm; return this; } @@ -738,11 +732,13 @@ public final class RestResponse { * Returns this value cast to the specified class. * * @param c The class to cast to. - * @return This value cast to the specified class. + * @return This value cast to the specified class, or <jk>null</jk> if the object doesn't exist or isn't the specified type. */ @SuppressWarnings("unchecked") public <T> T getOutput(Class<T> c) { - return (T)getRawOutput(); + if (isOutputType(c)) + return (T)getRawOutput(); + return null; } /** @@ -808,17 +804,61 @@ public final class RestResponse { } /** + * Enabled safe-header mode. + * + * <p> + * When enabled, invalid characters such as CTRL characters will be stripped from header values + * before they get set. + * + * @return This object (for method chaining). + */ + public RestResponse safeHeaders() { + this.safeHeaders = true; + return this; + } + + /** + * Specifies the maximum length for header values. + * + * <p> + * Header values that exceed this length will get truncated. + * + * @param value The new value for this setting. The default is <c>8096</c>. + * @return This object (for method chaining). + */ + public RestResponse maxHeaderLength(int value) { + this.maxHeaderLength = value; + return this; + } + + /** * Adds a response header with the given name and value. * * <p> * This method allows response headers to have multiple values. * + * <p> + * A no-op of either the name or value is <jk>null</jk>. + * + * <p> + * Note that per <a class='doclink' href='https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2'>RFC2616</a>, + * only headers defined as comma-delimited lists [i.e., #(values)] should be defined as multiple message header fields. + * * @param name The header name. - * @param value The header value. + * @param value The header value. * @return This object (for method chaining). */ public RestResponse addHeader(String name, String value) { - inner.addHeader(name, value); + if (name != null && value != null) { + if (name.equalsIgnoreCase("Content-Type")) + setHeader(name, value); + else { + if (safeHeaders) + value = stripInvalidHttpHeaderChars(value); + value = abbreviate(value, maxHeaderLength); + inner.addHeader(name, value); + } + } return this; } @@ -835,7 +875,17 @@ public final class RestResponse { * @return This object (for method chaining). */ public RestResponse setHeader(Header header) { - inner.setHeader(header.getName(), header.getValue()); + if (header == null) { + // Do nothing. + } else if (header instanceof BasicUriHeader) { + BasicUriHeader x = (BasicUriHeader)header; + setHeader(x.getName(), resolveUris(x.getValue())); + } else if (header instanceof SerializedHeader) { + SerializedHeader x = ((SerializedHeader)header).copyWith(request.getPartSerializerSession(), null); + setHeader(x.getName(), resolveUris(x.getValue())); + } else { + setHeader(header.getName(), header.getValue()); + } return this; } @@ -848,14 +898,39 @@ public final class RestResponse { * <p> * Value is added at the end of the headers. * + * <p> + * If the header is a {@link BasicUriHeader}, the URI will be resolved using the {@link RestRequest#getUriResolver()} object. + * + * <p> + * If the header is a {@link SerializedHeader} and the serializer session is not set, it will be set to the one returned by {@link RestRequest#getPartSerializerSession()} before serialization. + * + * <p> + * Note that per <a class='doclink' href='https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2'>RFC2616</a>, + * only headers defined as comma-delimited lists [i.e., #(values)] should be defined as multiple message header fields. + * * @param header The header. * @return This object (for method chaining). */ public RestResponse addHeader(Header header) { - inner.addHeader(header.getName(), header.getValue()); + if (header == null) { + // Do nothing. + } else if (header instanceof BasicUriHeader) { + BasicUriHeader x = (BasicUriHeader)header; + addHeader(x.getName(), resolveUris(x.getValue())); + } else if (header instanceof SerializedHeader) { + SerializedHeader x = ((SerializedHeader)header).copyWith(request.getPartSerializerSession(), null); + addHeader(x.getName(), resolveUris(x.getValue())); + } else { + addHeader(header.getName(), header.getValue()); + } return this; } + private String resolveUris(Object value) { + String s = stringify(value); + return request.getUriResolver().resolve(s); + } + /** * Gets the value of the response header with the given name. * @@ -868,4 +943,41 @@ public final class RestResponse { public String getHeader(String name) { return inner.getHeader(name); } + + + /** + * Returns the matching serializer and media type for this response. + * + * @return The matching serializer, never <jk>null</jk>. + */ + public Optional<SerializerMatch> getSerializerMatch() { + if (serializerMatch != null) + return serializerMatch; + if (serializer != null) { + serializerMatch = of(new SerializerMatch(getMediaType(), serializer)); + } else { + serializerMatch = ofNullable(opContext.getSerializers().getSerializerMatch(request.getHeader("Accept").orElse("*/*"))); + } + return serializerMatch; + } + + /** + * Returns the schema of the response body. + * + * @return The schema of the response body, never <jk>null</jk>. + */ + public Optional<HttpPartSchema> getBodySchema() { + if (bodySchema != null) + return bodySchema; + if (responseBeanMeta != null) + bodySchema = ofNullable(responseBeanMeta.getSchema()); + else { + ResponseBeanMeta rbm = opContext.getResponseBeanMeta(getOutput(Object.class)); + if (rbm != null) + bodySchema = ofNullable(rbm.getSchema()); + else + bodySchema = empty(); + } + return bodySchema; + } } \ No newline at end of file diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RrpcRestOperationContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RrpcRestOperationContext.java index 7a8b45e..b10e738 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RrpcRestOperationContext.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RrpcRestOperationContext.java @@ -17,7 +17,6 @@ import static org.apache.juneau.rest.HttpRuntimeException.*; import java.io.*; import java.lang.reflect.*; -import java.util.*; import javax.servlet.*; @@ -54,8 +53,7 @@ public class RrpcRestOperationContext extends RestOperationContext { super.invoke(call); - Optional<Optional<Object>> x = call.getOutput(); - final Object o = x.isPresent() ? x.get().orElse(null) : null; + final Object o = call.hasOutput() ? call.getRestResponse().getOutput(Object.class) : null; if ("GET".equals(call.getMethod())) { call.output(meta.getMethodsByPath().keySet()); diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/args/ResponseBeanArg.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/args/ResponseBeanArg.java index afc383e..370a2cc 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/args/ResponseBeanArg.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/args/ResponseBeanArg.java @@ -71,7 +71,7 @@ public class ResponseBeanArg implements RestOperationArg { ResponseBeanMeta meta = req.getOpContext().getResponseBeanMeta(o); if (meta == null) meta = ResponseBeanArg.this.meta; - res.setResponseMeta(meta); + res.setResponseBeanMeta(meta); res.setOutput(o); } }); diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/DefaultProcessor.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/DefaultProcessor.java deleted file mode 100644 index 972118e..0000000 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/DefaultProcessor.java +++ /dev/null @@ -1,276 +0,0 @@ -// *************************************************************************************************************************** -// * 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.processors; - -import static org.apache.juneau.internal.StringUtils.*; -import static org.apache.juneau.httppart.HttpPartType.*; -import static org.apache.juneau.internal.IOUtils.*; - -import java.io.*; -import java.lang.reflect.*; -import java.util.*; - -import org.apache.http.*; -import org.apache.juneau.httppart.*; -import org.apache.juneau.httppart.bean.*; -import org.apache.juneau.internal.*; -import org.apache.juneau.rest.*; -import org.apache.juneau.http.header.*; -import org.apache.juneau.http.part.*; -import org.apache.juneau.http.response.*; -import org.apache.juneau.rest.util.FinishablePrintWriter; -import org.apache.juneau.rest.util.FinishableServletOutputStream; -import org.apache.juneau.serializer.*; - -/** - * Response processor for POJOs not handled by other processors. - * - * <p> - * This uses the serializers defined on the response to serialize the POJO. - * - * <p> - * The {@link Serializer} used is based on the <c>Accept</c> header on the request. - * - * <p> - * The <c>Content-Type</c> header is set to the mime-type defined on the selected serializer based on the - * <c>produces</c> value passed in through the constructor. - * - * <ul class='seealso'> - * <li class='link'>{@doc RestmReturnTypes} - * </ul> - */ -public class DefaultProcessor implements ResponseProcessor { - - @SuppressWarnings("resource") - @Override /* ResponseHandler */ - public int process(RestCall call) throws IOException, InternalServerError, NotAcceptable { - RestRequest req = call.getRestRequest(); - RestResponse res = call.getRestResponse(); - SerializerGroup g = res.getOpContext().getSerializers(); - String accept = req.getHeader("Accept").orElse("*/*"); - SerializerMatch sm = g.getSerializerMatch(accept); - HttpPartSchema schema = null; - HttpPartSerializerSession ps = req.getPartSerializerSession(); - - Optional<Optional<Object>> output = res.getOutput(); - Object o = output.isPresent() ? output.get().orElse(null) : null; - - ResponseBeanMeta rm = res.getResponseMeta(); - if (rm == null) - rm = req.getOpContext().getResponseBeanMeta(o); - - if (rm != null) { - - boolean isThrowable = rm.getClassMeta().isChildOf(Throwable.class); - if (isThrowable && o != null) { - Throwable t = (Throwable)o; - res.setHeader(Thrown.of(t)); - if (req.isDebug()) - t.printStackTrace(); - } - - ResponseBeanPropertyMeta stm = rm.getStatusMethod(); - if (stm != null) { - try { - res.setStatus((int)stm.getGetter().invoke(o)); - } catch (Exception e) { - throw new InternalServerError(e, "Could not get status."); - } - } else if (rm.getCode() != 0) { - res.setStatus(rm.getCode()); - } - - for (ResponseBeanPropertyMeta hm : rm.getHeaderMethods()) { - String n = hm.getPartName().orElse(null); - try { - Object ho = hm.getGetter().invoke(o); - HttpPartSchema partSchema = hm.getSchema(); - if ("*".equals(n)) { - for (Object ho2 : iterate(ho)) { - if (ho2 instanceof Map.Entry) { - @SuppressWarnings("rawtypes") - Map.Entry x = (Map.Entry)ho2; - String k = stringify(x.getKey()); - Object v = x.getValue(); - res.setHeader(new HttpPart(k, RESPONSE_HEADER, partSchema.getProperty(k), hm.getSerializerSession().orElse(ps), v)); - } else if (ho2 instanceof SerializedHeader) { - SerializedHeader x = ((SerializedHeader)ho2); - x = x.copyWith(ps, partSchema.getProperty(x.getName())); - res.setHeader(x.getName(), x.getValue()); - } else if (ho2 instanceof SerializedPart) { - SerializedPart x = ((SerializedPart)ho2); - x = x.copyWith(ps, partSchema.getProperty(x.getName())); - res.setHeader(x.getName(), x.getValue()); - } else if (ho2 instanceof BasicUriHeader) { - BasicUriHeader x = (BasicUriHeader)ho2; - res.setHeader(x.getName(), req.getUriResolver().resolve(x.getValue())); - } else if (ho2 instanceof Header) { - Header x = (Header)ho2; - res.setHeader(x.getName(), x.getValue()); - } else if (ho2 instanceof NameValuePair) { - NameValuePair x = (NameValuePair)ho2; - res.setHeader(x.getName(), x.getValue()); - } else { - throw new InternalServerError("Invalid type ''{0}'' for header ''{1}''", ho2 == null ? null : ho2.getClass().getName(), n); - } - } - } else { - if (ho instanceof SerializedHeader) { - SerializedHeader x = ((SerializedHeader)ho).copyWith(ps, schema); - res.setHeader(x.getName(), x.getValue()); - } else if (ho instanceof SerializedPart) { - SerializedPart x = ((SerializedPart)ho).copyWith(ps, schema); - res.setHeader(x.getName(), x.getValue()); - } else if (ho instanceof Header) { - Header x = (Header)ho; - res.setHeader(x.getName(), x.getValue()); - } else if (ho instanceof NameValuePair) { - NameValuePair x = (NameValuePair)ho; - res.setHeader(x.getName(), x.getValue()); - } else { - res.setHeader(new HttpPart(n, RESPONSE_HEADER, hm.getSchema(), hm.getSerializerSession().orElse(ps), ho)); - } - } - } catch (Exception e) { - throw new InternalServerError(e, "Could not set header ''{0}''", n); - } - } - - ResponseBeanPropertyMeta bm = rm.getBodyMethod(); - - if (bm != null) { - Method m = bm.getGetter(); - try { - Class<?>[] pt = m.getParameterTypes(); - if (pt.length == 1) { - Class<?> ptt = pt[0]; - if (ptt == OutputStream.class) - m.invoke(o, res.getOutputStream()); - else if (ptt == Writer.class) - m.invoke(o, res.getWriter()); - return 1; - } - o = m.invoke(o); - } catch (Exception e) { - throw new InternalServerError(e, "Could not get body."); - } - } - - schema = rm.getSchema(); - } - - if (sm != null) { - Serializer s = sm.getSerializer(); - MediaType mediaType = res.getMediaType(); - if (mediaType == null) - mediaType = sm.getMediaType(); - - MediaType responseType = s.getResponseContentType(); - if (responseType == null) - responseType = mediaType; - - res.setContentType(responseType.toString()); - - try { - if (req.isPlainText()) - res.setContentType("text/plain"); - SerializerSession session = s.createSession( - SerializerSessionArgs - .create() - .properties(req.getAttributes().asMap()) - .javaMethod(req.getOpContext().getJavaMethod()) - .locale(req.getLocale()) - .timeZone(req.getTimeZone().orElse(null)) - .mediaType(mediaType) - .streamCharset(res.getCharset()) - .schema(schema) - .debug(req.isDebug() ? true : null) - .uriContext(req.getUriContext()) - .useWhitespace(req.isPlainText() ? true : null) - .resolver(req.getVarResolverSession()) - ); - - for (Map.Entry<String,String> h : session.getResponseHeaders().entrySet()) - res.setHeaderSafe(h.getKey(), h.getValue()); - - if (! session.isWriterSerializer()) { - if (req.isPlainText()) { - FinishablePrintWriter w = res.getNegotiatedWriter(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - session.serialize(o, baos); - w.write(StringUtils.toSpacedHex(baos.toByteArray())); - w.flush(); - w.finish(); - } else { - FinishableServletOutputStream os = res.getNegotiatedOutputStream(); - session.serialize(o, os); - os.flush(); - os.finish(); - } - } else { - FinishablePrintWriter w = res.getNegotiatedWriter(); - session.serialize(o, w); - w.flush(); - w.finish(); - } - } catch (SerializeException e) { - throw new InternalServerError(e); - } - return 1; - } - - // Non-existent Accept or plain/text can just be serialized as-is. - if (o != null && (isEmpty(accept) || accept.startsWith("text/plain") || accept.contains("*/*"))) { - String out = null; - if (isEmpty(res.getContentType())) - res.setContentType("text/plain"); - if (o instanceof InputStream) { - try (OutputStream os = res.getNegotiatedOutputStream()) { - pipe((InputStream)o, os); - } - } else if (o instanceof Reader) { - try (FinishablePrintWriter w = res.getNegotiatedWriter()) { - pipe((Reader)o, w); - w.finish(); - } - } else { - out = req.getBeanSession().getClassMetaForObject(o).toString(o); - FinishablePrintWriter w = res.getNegotiatedWriter(); - w.append(out); - w.flush(); - w.finish(); - } - return 1; - } - - if (o == null) - return 1; - - throw new NotAcceptable( - "Unsupported media-type in request header ''Accept'': ''{0}''\n\tSupported media-types: {1}", - req.getHeader("Accept").orElse(""), g.getSupportedMediaTypes() - ); - } - - private Iterable<?> iterate(Object o) { - if (o == null) - return Collections.emptyList(); - if (o instanceof Map) - return ((Map<?,?>)o).entrySet(); - if (o.getClass().isArray()) - return Arrays.asList((Object[])o); - if (o instanceof Collection) - return (Collection<?>)o; - throw new InternalServerError("Could not iterate over Headers of type ''{0}''", o.getClass().getName()); - } - } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpEntityProcessor.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpEntityProcessor.java index 7ab555a..93f5787 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpEntityProcessor.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpEntityProcessor.java @@ -27,24 +27,24 @@ public final class HttpEntityProcessor implements ResponseProcessor { @Override /* ResponseProcessor */ public int process(RestCall call) throws IOException { - if (! call.getOutputInfo().isChildOf(HttpEntity.class)) - return 0; - RestResponse res = call.getRestResponse(); HttpEntity e = res.getOutput(HttpEntity.class); - call.addResponseHeader(e.getContentType()); - call.addResponseHeader(e.getContentEncoding()); + if (e == null) + return NEXT; + + res.setHeader(e.getContentType()); + res.setHeader(e.getContentEncoding()); long contentLength = e.getContentLength(); if (contentLength >= 0) - call.addResponseHeader(contentLength(contentLength)); + res.setHeader(contentLength(contentLength)); try (OutputStream os = res.getNegotiatedOutputStream()) { e.writeTo(os); os.flush(); } - return 1; + return FINISHED; } } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpResourceProcessor.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpResourceProcessor.java index 9ddffef..a65c4d7 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpResourceProcessor.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpResourceProcessor.java @@ -29,28 +29,28 @@ public final class HttpResourceProcessor implements ResponseProcessor { @Override /* ResponseProcessor */ public int process(RestCall call) throws IOException { - if (! call.getOutputInfo().isChildOf(HttpResource.class)) - return 0; - RestResponse res = call.getRestResponse(); HttpResource r = res.getOutput(HttpResource.class); - call.addResponseHeader(r.getContentType()); - call.addResponseHeader(r.getContentEncoding()); + if (r == null) + return NEXT; + + res.setHeader(r.getContentType()); + res.setHeader(r.getContentEncoding()); long contentLength = r.getContentLength(); if (contentLength >= 0) - call.addResponseHeader(contentLength(contentLength)); - + res.setHeader(contentLength(contentLength)); + List<Header> allHeaders = r.getAllHeaders(); for (int i = 0; i < allHeaders.size() ; i++) // Avoids Iterator creation. - call.addResponseHeader(allHeaders.get(i)); + res.addHeader(allHeaders.get(i)); try (OutputStream os = res.getNegotiatedOutputStream()) { r.writeTo(os); os.flush(); } - return 1; + return FINISHED; } } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpResponseProcessor.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpResponseProcessor.java index 6d8f251..d3a7869 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpResponseProcessor.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/HttpResponseProcessor.java @@ -27,31 +27,31 @@ public final class HttpResponseProcessor implements ResponseProcessor { @Override /* ResponseProcessor */ public int process(RestCall call) throws IOException { - if (! call.getOutputInfo().isChildOf(HttpResponse.class)) - return 0; - RestResponse res = call.getRestResponse(); HttpResponse r = res.getOutput(HttpResponse.class); - call.status(r.getStatusLine().getStatusCode()); + if (r == null) + return NEXT; + + call.status(r.getStatusLine()); HttpEntity e = r.getEntity(); - call.addResponseHeader(e.getContentType()); - call.addResponseHeader(e.getContentEncoding()); + res.setHeader(e.getContentType()); + res.setHeader(e.getContentEncoding()); long contentLength = e.getContentLength(); if (contentLength >= 0) - call.addResponseHeader(contentLength(contentLength)); - + res.setHeader(contentLength(contentLength)); + for (Header h : r.getAllHeaders()) // No iterator involved. - call.addResponseHeader(h); + res.addHeader(h); try (OutputStream os = res.getNegotiatedOutputStream()) { e.writeTo(os); os.flush(); } - - return 1; + + return FINISHED; } } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/InputStreamProcessor.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/InputStreamProcessor.java index 1a93e2f..067b24d 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/InputStreamProcessor.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/InputStreamProcessor.java @@ -33,13 +33,16 @@ public final class InputStreamProcessor implements ResponseProcessor { @Override /* ResponseProcessor */ public int process(RestCall call) throws IOException { - if (! call.getOutputInfo().isChildOf(InputStream.class)) - return 0; - RestResponse res = call.getRestResponse(); - try (InputStream is = res.getOutput(InputStream.class); OutputStream os = res.getNegotiatedOutputStream()) { - pipe(is, os); + InputStream is = res.getOutput(InputStream.class); + + if (is == null) + return NEXT; + + try (InputStream is2 = is; OutputStream os = res.getNegotiatedOutputStream()) { + pipe(is2, os); } - return 1; + + return FINISHED; } } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/PojoProcessor.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/PlainTextPojoProcessor.java similarity index 64% rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/PojoProcessor.java rename to juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/PlainTextPojoProcessor.java index 334b7f6..2e27bcd 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/PojoProcessor.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/PlainTextPojoProcessor.java @@ -12,20 +12,43 @@ // *************************************************************************************************************************** package org.apache.juneau.rest.processors; +import static org.apache.juneau.internal.StringUtils.*; + import java.io.*; import org.apache.juneau.rest.*; +import org.apache.juneau.rest.util.*; +import org.apache.juneau.http.header.*; import org.apache.juneau.http.response.*; /** - * Response handler for plain-old Java objects. + * Response handler for plain-old Java objects when a serializer match is not found and they're asking for plain/text or anything. */ -public final class PojoProcessor implements ResponseProcessor { +public final class PlainTextPojoProcessor implements ResponseProcessor { @Override /* ResponseProcessor */ public int process(RestCall call) throws IOException, NotAcceptable, BasicHttpException { + RestRequest req = call.getRestRequest(); + RestResponse res = call.getRestResponse(); + String accept = req.getHeader("Accept").orElse("*/*"); + + if (res.getSerializerMatch().isPresent()) + return NEXT; + + if (! (isEmpty(accept) || accept.startsWith("text/plain") || accept.contains("*/*"))) + return NEXT; + + Object o = res.getOutput(Object.class); + + if (isEmpty(res.getContentType())) + res.setHeader(ContentType.TEXT_PLAIN); + + FinishablePrintWriter w = res.getNegotiatedWriter(); + w.append(req.getBeanSession().getClassMetaForObject(o).toString(o)); + w.flush(); + w.finish(); - return 0; + return FINISHED; } } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ReaderProcessor.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ReaderProcessor.java index 42a5225..06b756a 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ReaderProcessor.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ReaderProcessor.java @@ -33,14 +33,16 @@ public final class ReaderProcessor implements ResponseProcessor { @Override /* ResponseProcessor */ public int process(RestCall call) throws IOException { - if (! call.getOutputInfo().isChildOf(Reader.class)) - return 0; - RestResponse res = call.getRestResponse(); - try (Reader r = res.getOutput(Reader.class); Writer w = res.getNegotiatedWriter()) { - pipe(r, w); + Reader r = res.getOutput(Reader.class); + + if (r == null) + return NEXT; + + try (Reader r2 = r; Writer w = res.getNegotiatedWriter()) { + pipe(r2, w); } - return 1; + return FINISHED; } } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ResponseBeanProcessor.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ResponseBeanProcessor.java index 9990656..09d0b93 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ResponseBeanProcessor.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ResponseBeanProcessor.java @@ -12,10 +12,21 @@ // *************************************************************************************************************************** package org.apache.juneau.rest.processors; +import static org.apache.juneau.internal.StringUtils.*; + import java.io.*; +import java.lang.reflect.*; +import java.util.*; import org.apache.juneau.rest.*; +import org.apache.http.*; +import org.apache.http.Header; import org.apache.juneau.http.annotation.*; +import org.apache.juneau.http.header.*; +import org.apache.juneau.http.part.*; +import org.apache.juneau.http.response.*; +import org.apache.juneau.httppart.*; +import org.apache.juneau.httppart.bean.*; /** * Response handler for {@link Response @Response}-annotated objects. @@ -25,10 +36,112 @@ public final class ResponseBeanProcessor implements ResponseProcessor { @Override /* ResponseProcessor */ public int process(RestCall call) throws IOException { - if (! call.getOutputInfo().hasAnnotation(Response.class)) - return 0; + RestRequest req = call.getRestRequest(); + RestResponse res = call.getRestResponse(); + Object output = res.getOutput(Object.class); + + if (output == null) + return NEXT; + + if (! (output.getClass().getAnnotation(Response.class) != null || res.getResponseBeanMeta() != null)) + return NEXT; + + ResponseBeanMeta rm = res.getResponseBeanMeta(); + if (rm == null) + rm = req.getOpContext().getResponseBeanMeta(output); + + HttpPartSerializerSession ss = req.getPartSerializerSession(); + + ResponseBeanPropertyMeta stm = rm.getStatusMethod(); + if (stm != null) { + try { + res.setStatus((int)stm.getGetter().invoke(output)); + } catch (Exception e) { + throw new InternalServerError(e, "Could not get status."); + } + } else if (rm.getCode() != 0) { + res.setStatus(rm.getCode()); + } - return 0; + for (ResponseBeanPropertyMeta hm : rm.getHeaderMethods()) { + String n = hm.getPartName().orElse(null); + try { + Object o = hm.getGetter().invoke(output); + HttpPartSchema ps = hm.getSchema(); + if ("*".equals(n)) { + for (Object o2 : iterate(o)) { + Header h = null; + if (o2 instanceof Map.Entry) { + @SuppressWarnings("rawtypes") + Map.Entry x = (Map.Entry)o2; + String k = stringify(x.getKey()); + h = new SerializedHeader(k, x.getValue(), hm.getSerializerSession().orElse(ss), ps.getProperty(k), true); + } else if (o2 instanceof SerializedHeader) { + SerializedHeader x = ((SerializedHeader)o2); + h = x.copyWith(ss, ps.getProperty(x.getName())); + } else if (o2 instanceof SerializedPart) { + SerializedPart x = ((SerializedPart)o2); + h = BasicHeader.of(x.copyWith(ss, ps.getProperty(x.getName()))); + } else if (o2 instanceof Header) { + h = (Header)o2; + } else if (o2 instanceof NameValuePair) { + h = BasicHeader.of((NameValuePair)o2); + } else { + throw new InternalServerError("Invalid type ''{0}'' for header ''{1}''", o2 == null ? null : o2.getClass().getName(), n); + } + res.addHeader(h); + } + } else { + Header h = null; + if (o instanceof SerializedHeader) + h = ((SerializedHeader)o).copyWith(ss, ps); + else if (o instanceof SerializedPart) + h = BasicHeader.of(((SerializedPart)o).copyWith(ss, ps)); + else if (o instanceof Header) + h = (Header)o; + else if (o instanceof NameValuePair) + h = BasicHeader.of((NameValuePair)o); + else + h = new SerializedHeader(n, o, hm.getSerializerSession().orElse(ss), ps, true); + res.addHeader(h); + } + } catch (Exception e) { + throw new InternalServerError(e, "Could not set header ''{0}''", n); + } + } + + ResponseBeanPropertyMeta bm = rm.getBodyMethod(); + + if (bm != null) { + Method m = bm.getGetter(); + try { + Class<?>[] pt = m.getParameterTypes(); + if (pt.length == 1) { + Class<?> ptt = pt[0]; + if (ptt == OutputStream.class) + m.invoke(output, res.getOutputStream()); + else if (ptt == Writer.class) + m.invoke(output, res.getWriter()); + return 1; + } + res.setOutput(m.invoke(output)); + } catch (Exception e) { + throw new InternalServerError(e, "Could not get body."); + } + } + + return NEXT; // Let PojoProcessor serialize it. } -} + private Iterable<?> iterate(Object o) { + if (o == null) + return Collections.emptyList(); + if (o instanceof Map) + return ((Map<?,?>)o).entrySet(); + if (o.getClass().isArray()) + return Arrays.asList((Object[])o); + if (o instanceof Collection) + return (Collection<?>)o; + throw new InternalServerError("Could not iterate over Headers of type ''{0}''", o.getClass().getName()); + } +} diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/SerializedPojoProcessor.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/SerializedPojoProcessor.java new file mode 100644 index 0000000..0a2863b --- /dev/null +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/SerializedPojoProcessor.java @@ -0,0 +1,110 @@ +// *************************************************************************************************************************** +// * 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.processors; + +import java.io.*; +import java.util.*; + +import org.apache.juneau.rest.*; +import org.apache.juneau.rest.util.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.http.header.*; +import org.apache.juneau.http.response.*; +import org.apache.juneau.httppart.*; +import org.apache.juneau.internal.*; + +/** + * Response handler for plain-old Java objects. + */ +public final class SerializedPojoProcessor implements ResponseProcessor { + + @Override /* ResponseProcessor */ + public int process(RestCall call) throws IOException, NotAcceptable, BasicHttpException { + RestRequest req = call.getRestRequest(); + RestResponse res = call.getRestResponse(); + SerializerMatch sm = res.getSerializerMatch().orElse(null); + HttpPartSchema schema = res.getBodySchema().orElse(null); + + Object o = res.getOutput(Object.class); + + if (sm != null) { + try { + Serializer s = sm.getSerializer(); + MediaType mediaType = res.getMediaType(); + if (mediaType == null) + mediaType = sm.getMediaType(); + + MediaType responseType = s.getResponseContentType(); + if (responseType == null) + responseType = mediaType; + + if (req.isPlainText()) + res.setHeader(ContentType.TEXT_PLAIN); + else + res.setHeader(ContentType.of(responseType.toString())); + + SerializerSession session = s.createSession( + SerializerSessionArgs + .create() + .properties(req.getAttributes().asMap()) + .javaMethod(req.getOpContext().getJavaMethod()) + .locale(req.getLocale()) + .timeZone(req.getTimeZone().orElse(null)) + .mediaType(mediaType) + .streamCharset(res.getCharset()) + .schema(schema) + .debug(req.isDebug() ? true : null) + .uriContext(req.getUriContext()) + .useWhitespace(req.isPlainText() ? true : null) + .resolver(req.getVarResolverSession()) + ); + + for (Map.Entry<String,String> h : session.getResponseHeaders().entrySet()) + res.addHeader(h.getKey(), h.getValue()); + + if (! session.isWriterSerializer()) { + if (req.isPlainText()) { + FinishablePrintWriter w = res.getNegotiatedWriter(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + session.serialize(o, baos); + w.write(StringUtils.toSpacedHex(baos.toByteArray())); + w.flush(); + w.finish(); + } else { + FinishableServletOutputStream os = res.getNegotiatedOutputStream(); + session.serialize(o, os); + os.flush(); + os.finish(); + } + } else { + FinishablePrintWriter w = res.getNegotiatedWriter(); + session.serialize(o, w); + w.flush(); + w.finish(); + } + } catch (SerializeException e) { + throw new InternalServerError(e); + } + return FINISHED; + } + + if (o == null) + return FINISHED; + + throw new NotAcceptable( + "Unsupported media-type in request header ''Accept'': ''{0}''\n\tSupported media-types: {1}", + req.getHeader("Accept").orElse(""), res.getOpContext().getSerializers().getSupportedMediaTypes() + ); + } +} + diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ThrowableProcessor.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ThrowableProcessor.java index 235babe..5f74193 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ThrowableProcessor.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/processors/ThrowableProcessor.java @@ -19,7 +19,7 @@ import org.apache.juneau.rest.*; /** * Response handler for {@link Throwable} objects. - * + * * <p> * Adds a <c>Thrown</c> header to the response and returns <c>0</c> so that the processor chain can continue. */ @@ -28,12 +28,15 @@ public final class ThrowableProcessor implements ResponseProcessor { @Override /* ResponseProcessor */ public int process(RestCall call) throws IOException { - if (! call.getOutputInfo().isChildOf(Throwable.class)) - return 0; + RestResponse res = call.getRestResponse(); + Throwable t = res.getOutput(Throwable.class); - call.addResponseHeader(Thrown.of(call.getRestResponse().getOutput(Throwable.class))); + if (t == null) + return NEXT; - return 0; // Continue processing. + res.addHeader(Thrown.of(t)); + + return NEXT; // Continue processing as bean. } }