org.apache.juneau.rest.RestResponse 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;
import static org.apache.juneau.internal.StringUtils.*;
import java.io.*;
import java.nio.charset.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.juneau.*;
import org.apache.juneau.encoders.*;
import org.apache.juneau.http.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.httppart.bean.*;
import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.exception.*;
import org.apache.juneau.rest.util.*;
import org.apache.juneau.serializer.*;
/**
* Represents an HTTP response for a REST resource.
*
*
* Essentially an extended {@link HttpServletResponse} with some special convenience methods that allow you to easily
* output POJOs as responses.
*
*
* Since this class extends {@link HttpServletResponse}, developers are free to use these convenience methods, or
* revert to using lower level methods like any other servlet response.
*
*
Example:
*
* @RestMethod (name=GET )
* public void doGet(RestRequest req, RestResponse res) {
* res.setOutput("Simple string response" );
* }
*
*
* See Also:
*
* - {@doc juneau-rest-server.RestMethod.RestResponse}
*
*/
public final class RestResponse extends HttpServletResponseWrapper {
private final RestRequest request;
private RestJavaMethod restJavaMethod;
private Object output; // The POJO being sent to the output.
private boolean isNullOutput; // The output is null (as opposed to not being set at all)
private RequestProperties properties; // Response properties
private ServletOutputStream sos;
private FinishableServletOutputStream os;
private FinishablePrintWriter w;
private HtmlDocBuilder htmlDocBuilder;
private ResponseBeanMeta responseMeta;
/**
* Constructor.
*/
RestResponse(RestContext context, RestRequest req, HttpServletResponse res) throws BadRequest {
super(res);
this.request = req;
for (Map.Entry e : context.getDefaultResponseHeaders().entrySet())
setHeader(e.getKey(), asString(e.getValue()));
try {
String passThroughHeaders = req.getHeader("x-response-headers");
if (passThroughHeaders != null) {
HttpPartParser p = context.getPartParser();
ObjectMap m = p.createPartSession(req.getParserSessionArgs()).parse(HttpPartType.HEADER, null, passThroughHeaders, context.getBeanContext().getClassMeta(ObjectMap.class));
for (Map.Entry e : m.entrySet())
setHeader(e.getKey(), e.getValue().toString());
}
} catch (Exception e1) {
throw new BadRequest(e1, "Invalid format for header 'x-response-headers'. Must be in URL-encoded format.");
}
}
/*
* Called from RestServlet after a match has been made but before the guard or method invocation.
*/
final void init(RestJavaMethod rjm, RequestProperties properties) throws NotAcceptable {
this.restJavaMethod = rjm;
this.properties = properties;
// Find acceptable charset
String h = request.getHeader("accept-charset");
String charset = null;
if (h == null)
charset = rjm.defaultCharset;
else for (MediaTypeRange r : MediaTypeRange.parse(h)) {
if (r.getQValue() > 0) {
MediaType mt = r.getMediaType();
if (mt.getType().equals("*"))
charset = rjm.defaultCharset;
else if (Charset.isSupported(mt.getType()))
charset = mt.getType();
if (charset != null)
break;
}
}
if (charset == null)
throw new NotAcceptable("No supported charsets in header ''Accept-Charset'': ''{0}''", request.getHeader("Accept-Charset"));
super.setCharacterEncoding(charset);
this.responseMeta = rjm.responseMeta;
}
/**
* Gets the serializer group for the response.
*
* See Also:
*
* - {@doc juneau-rest-server.Serializers}
*
*
* @return The serializer group for the response.
*/
public SerializerGroup getSerializers() {
return restJavaMethod == null ? SerializerGroup.EMPTY : restJavaMethod.serializers;
}
/**
* Returns the media types that are valid for Accept
headers on the request.
*
* @return The set of media types registered in the parser group of this request.
*/
public List getSupportedMediaTypes() {
return restJavaMethod == null ? Collections.emptyList() : restJavaMethod.supportedAcceptTypes;
}
/**
* Returns the codings that are valid for Accept-Encoding
and Content-Encoding
headers on
* the request.
*
* @return The set of media types registered in the parser group of this request.
* @throws RestServletException
*/
public List getSupportedEncodings() throws RestServletException {
return restJavaMethod == null ? Collections.emptyList() : restJavaMethod.encoders.getSupportedEncodings();
}
/**
* Sets the HTTP output on the response.
*
*
* The object type can be anything allowed by the registered response handlers.
*
*
* Calling this method is functionally equivalent to returning the object in the REST Java method.
*
*
Example:
*
* @RestMethod (..., path="/example2/{personId}" )
* public void doGet2(RestResponse res, @Path UUID personId) {
* Person p = getPersonById(personId);
* res.setOutput(p);
* }
*
*
* Notes:
*
* -
* Calling this method with a
null value is NOT the same as not calling this method at all.
*
A null output value means we want to serialize null as a response (e.g. as a JSON null
).
*
Not calling this method or returning a value means you're handing the response yourself via the underlying stream or writer.
*
This distinction affects the {@link #hasOutput()} method behavior.
*
*
* See Also:
*
* - {@link RestContext#REST_responseHandlers}
*
- {@doc juneau-rest-server.RestMethod.MethodReturnTypes}
*
*
* @param output The output to serialize to the connection.
* @return This object (for method chaining).
*/
public RestResponse setOutput(Object output) {
this.output = output;
this.isNullOutput = output == null;
return this;
}
/**
* Returns a programmatic interface for setting properties for the HTML doc view.
*
*
* This is the programmatic equivalent to the {@link RestMethod#htmldoc() @RestMethod(htmldoc)} annotation.
*
*
Example:
*
* // Declarative approach.
* @RestMethod (
* htmldoc=@HtmlDoc (
* header={
* "<p>This is my REST interface</p>"
* },
* aside={
* "<p>Custom aside content</p>"
* }
* )
* )
* public Object doGet(RestResponse res) {
*
* // Equivalent programmatic approach.
* res.getHtmlDocBuilder()
* .header("<p>This is my REST interface</p>" )
* .aside("<p>Custom aside content</p>" );
* }
*
*
* See Also:
*
* - {@link RestMethod#htmldoc()}
*
- {@doc juneau-rest-server.HtmlDocAnnotation}
*
*
* @return A new programmatic interface for setting properties for the HTML doc view.
*/
public HtmlDocBuilder getHtmlDocBuilder() {
if (htmlDocBuilder == null)
htmlDocBuilder = new HtmlDocBuilder(properties);
return htmlDocBuilder;
}
/**
* Retrieve the properties active for this request.
*
*
* This contains all resource and method level properties from the following:
*
* - {@link RestResource#properties()}
*
- {@link RestMethod#properties()}
*
- {@link RestContextBuilder#set(String, Object)}
*
*
*
* The returned object is modifiable and allows you to override session-level properties before
* they get passed to the serializers.
*
However, properties are open-ended, and can be used for any purpose.
*
*
Example:
*
* @RestMethod (
* properties={
* @Property (name=SERIALIZER_sortMaps , value="false" )
* }
* )
* public Map doGet(RestResponse res, @Query ("sortMaps" ) Boolean sortMaps) {
*
* // Override value if specified through query parameter.
* if (sortMaps != null )
* res.getProperties().put(SERIALIZER_sortMaps , sortMaps);
*
* return getMyMap ();
* }
*
*
* See Also:
*
* - {@link #prop(String, Object)}
*
- {@doc juneau-rest-server.Properties}
*
*
* @return The properties active for this request.
*/
public RequestProperties getProperties() {
return properties;
}
/**
* Shortcut for calling getProperties().append(name, value);
fluently.
*
* @param name The property name.
* @param value The property value.
* @return This object (for method chaining).
*/
public RestResponse prop(String name, Object value) {
this.properties.append(name, value);
return this;
}
/**
* Shortcut method that allows you to use var-args to simplify setting array output.
*
* Example:
*
* // Instead of...
* response.setOutput(new Object[]{x,y,z});
*
* // ...call this...
* response.setOutput(x,y,z);
*
*
* @param output The output to serialize to the connection.
* @return This object (for method chaining).
*/
public RestResponse setOutputs(Object...output) {
this.output = output;
return this;
}
/**
* Returns the output that was set by calling {@link #setOutput(Object)}.
*
* @return The output object.
*/
public Object getOutput() {
return output;
}
/**
* Returns true if this response has any output associated with it.
*
* @return true if {@link #setOutput(Object)} has been called, even if the value passed was null .
*/
public boolean hasOutput() {
return output != null || isNullOutput;
}
/**
* Sets the output to a plain-text message regardless of the content type.
*
* @param text The output text to send.
* @return This object (for method chaining).
* @throws IOException If a problem occurred trying to write to the writer.
*/
public RestResponse sendPlainText(String text) throws IOException {
setContentType("text/plain");
getNegotiatedWriter().write(text);
return this;
}
/**
* Equivalent to {@link HttpServletResponse#getOutputStream()}, except wraps the output stream if an {@link Encoder}
* was found that matched the Accept-Encoding
header.
*
* @return A negotiated output stream.
* @throws NotAcceptable If unsupported Accept-Encoding value specified.
* @throws IOException
*/
public FinishableServletOutputStream getNegotiatedOutputStream() throws NotAcceptable, IOException {
if (os == null) {
Encoder encoder = null;
EncoderGroup encoders = restJavaMethod == null ? EncoderGroup.DEFAULT : restJavaMethod.encoders;
String ae = request.getHeader("Accept-Encoding");
if (! (ae == null || ae.isEmpty())) {
EncoderMatch match = encoders.getEncoderMatch(ae);
if (match == null) {
// Identity should always match unless "identity;q=0" or "*;q=0" is specified.
if (ae.matches(".*(identity|\\*)\\s*;\\s*q\\s*=\\s*(0(?!\\.)|0\\.0).*")) {
throw new NotAcceptable(
"Unsupported encoding in request header ''Accept-Encoding'': ''{0}''\n\tSupported codings: {1}",
ae, encoders.getSupportedEncodings()
);
}
} else {
encoder = match.getEncoder();
String encoding = match.getEncoding().toString();
// Some clients don't recognize identity as an encoding, so don't set it.
if (! encoding.equals("identity"))
setHeader("content-encoding", encoding);
}
}
@SuppressWarnings("resource")
ServletOutputStream sos = getOutputStream();
os = new FinishableServletOutputStream(encoder == null ? sos : encoder.getOutputStream(sos));
}
return os;
}
@Override /* ServletResponse */
public ServletOutputStream getOutputStream() throws IOException {
if (sos == null)
sos = super.getOutputStream();
return sos;
}
/**
* Returns true if {@link #getOutputStream()} has been called.
*
* @return true if {@link #getOutputStream()} has been called.
*/
public boolean getOutputStreamCalled() {
return sos != null;
}
/**
* Returns the writer to the response body.
*
*
* This methods bypasses any specified encoders and returns a regular unbuffered writer.
* Use the {@link #getNegotiatedWriter()} method if you want to use the matched encoder (if any).
*/
@Override /* ServletResponse */
public PrintWriter getWriter() throws IOException {
return getWriter(true);
}
/**
* Convenience method meant to be used when rendering directly to a browser with no buffering.
*
*
* Sets the header "x-content-type-options=nosniff" so that output is rendered immediately on IE and Chrome
* without any buffering for content-type sniffing.
*
*
* This can be useful if you want to render a streaming 'console' on a web page.
*
* @param contentType The value to set as the Content-Type
on the response.
* @return The raw writer.
* @throws IOException
*/
public PrintWriter getDirectWriter(String contentType) throws IOException {
setContentType(contentType);
setHeader("x-content-type-options", "nosniff");
return getWriter();
}
/**
* Equivalent to {@link HttpServletResponse#getWriter()}, except wraps the output stream if an {@link Encoder} was
* found that matched the Accept-Encoding
header and sets the Content-Encoding
* header to the appropriate value.
*
* @return The negotiated writer.
* @throws NotAcceptable If unsupported charset in request header Accept-Charset.
* @throws IOException
*/
public FinishablePrintWriter getNegotiatedWriter() throws NotAcceptable, IOException {
return getWriter(false);
}
@SuppressWarnings("resource")
private FinishablePrintWriter getWriter(boolean raw) throws NotAcceptable, IOException {
if (w != null)
return w;
// If plain text requested, override it now.
if (request.isPlainText())
setHeader("Content-Type", "text/plain");
try {
OutputStream out = (raw ? getOutputStream() : getNegotiatedOutputStream());
w = new FinishablePrintWriter(out, getCharacterEncoding());
return w;
} catch (UnsupportedEncodingException e) {
String ce = getCharacterEncoding();
setCharacterEncoding("UTF-8");
throw new NotAcceptable("Unsupported charset in request header ''Accept-Charset'': ''{0}''", ce);
}
}
/**
* Returns the Content-Type
header stripped of the charset attribute if present.
*
* @return The media-type
portion of the Content-Type
header.
*/
public MediaType getMediaType() {
return MediaType.forString(getContentType());
}
/**
* Redirects to the specified URI.
*
*
* Relative URIs are always interpreted as relative to the context root.
* This is similar to how WAS handles redirect requests, and is different from how Tomcat handles redirect requests.
*/
@Override /* ServletResponse */
public void sendRedirect(String uri) throws IOException {
char c = (uri.length() > 0 ? uri.charAt(0) : 0);
if (c != '/' && uri.indexOf("://") == -1)
uri = request.getContextPath() + '/' + uri;
super.sendRedirect(uri);
}
@Override /* ServletResponse */
public void setHeader(String name, String value) {
// Jetty doesn't set the content type correctly if set through this method.
// Tomcat/WAS does.
if (name.equalsIgnoreCase("Content-Type"))
super.setContentType(value);
else
super.setHeader(name, value);
}
/**
* Same as {@link #setHeader(String, String)} but header is defined as a response part
*
* @param h Header to set.
* @throws SchemaValidationException
* @throws SerializeException
*/
public void setHeader(HttpPart h) throws SchemaValidationException, SerializeException {
setHeader(h.getName(), h.asString());
}
/**
* Returns the metadata about this response.
*
* @return
* The metadata about this response.
* Never null .
*/
public ResponseBeanMeta getResponseMeta() {
return responseMeta;
}
/**
* Sets metadata about this response.
*
* @param rbm The metadata about this response.
* @return This object (for method chaining).
*/
public RestResponse setResponseMeta(ResponseBeanMeta rbm) {
this.responseMeta = rbm;
return this;
}
/**
* Returns true if this response object is of the specified type.
*
* @param c The type to check against.
* @return true if this response object is of the specified type.
*/
public boolean isOutputType(Class> c) {
return c.isInstance(output);
}
/**
* Returns this value cast to the specified class.
*
* @param c The class to cast to.
* @return This value cast to the specified class.
*/
@SuppressWarnings("unchecked")
public T getOutput(Class c) {
return (T)output;
}
@Override /* ServletResponse */
public void flushBuffer() throws IOException {
if (w != null)
w.flush();
if (os != null)
os.flush();
super.flushBuffer();
}
}