com.sun.jersey.spi.container.ContainerResponse Maven / Gradle / Ivy
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2010-2013 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* http://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.sun.jersey.spi.container;
import com.sun.jersey.api.MessageException;
import com.sun.jersey.core.header.OutBoundHeaders;
import com.sun.jersey.api.Responses;
import com.sun.jersey.api.container.MappableContainerException;
import com.sun.jersey.api.core.HttpResponseContext;
import com.sun.jersey.api.core.TraceInformation;
import com.sun.jersey.core.reflection.ReflectionHelper;
import com.sun.jersey.core.spi.factory.ResponseImpl;
import com.sun.jersey.server.impl.uri.rules.HttpMethodRule;
import com.sun.jersey.spi.MessageBodyWorkers;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.Response.StatusType;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.RuntimeDelegate;
import javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate;
/**
* An out-bound HTTP response to be processed by the web application.
*
* Containers instantiate, or inherit, and provide an instance to the
* {@link WebApplication}.
*
* @author Paul Sandoz (paul.sandoz at oracle.com)
*/
public class ContainerResponse implements HttpResponseContext {
private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0];
private static final Logger LOGGER = Logger.getLogger(ContainerResponse.class.getName());
private static final RuntimeDelegate rd = RuntimeDelegate.getInstance();
private final WebApplication wa;
private ContainerRequest request;
private ContainerResponseWriter responseWriter;
private Response response;
private Throwable mappedThrowable;
private StatusType statusType;
private MultivaluedMap headers;
private Object originalEntity;
private Object entity;
private Type entityType;
private boolean isCommitted;
private CommittingOutputStream out;
private Annotation[] annotations = EMPTY_ANNOTATIONS;
private final class CommittingOutputStream extends OutputStream {
private final long size;
private OutputStream o;
CommittingOutputStream(long size) {
this.size = size;
}
@Override
public void write(byte b[]) throws IOException {
commitWrite();
o.write(b);
}
@Override
public void write(byte b[], int off, int len) throws IOException {
commitWrite();
o.write(b, off, len);
}
public void write(int b) throws IOException {
commitWrite();
o.write(b);
}
@Override
public void flush() throws IOException {
commitWrite();
o.flush();
}
@Override
public void close() throws IOException {
commitClose();
o.close();
}
private void commitWrite() throws IOException {
if (!isCommitted) {
if (getStatus() == 204)
setStatus(200);
isCommitted = true;
o = responseWriter.writeStatusAndHeaders(size, ContainerResponse.this);
}
}
private void commitClose() throws IOException {
if (!isCommitted) {
isCommitted = true;
o = responseWriter.writeStatusAndHeaders(-1, ContainerResponse.this);
}
}
}
/**
* Instantiate a new ContainerResponse.
*
* @param wa the web application.
* @param request the container request associated with this response.
* @param responseWriter the response writer
*/
public ContainerResponse(
WebApplication wa,
ContainerRequest request,
ContainerResponseWriter responseWriter) {
this.wa = wa;
this.request = request;
this.responseWriter = responseWriter;
this.statusType = Status.NO_CONTENT;
}
/*package */ ContainerResponse(
ContainerResponse acr) {
this.wa = acr.wa;
}
// ContainerResponse
/**
* Convert a header value, represented as a general object, to the
* string value.
*
* This method defers to {@link RuntimeDelegate#createHeaderDelegate} to
* obtain a {@link HeaderDelegate} to convert the value to a string. If
* a {@link HeaderDelegate} is not found then the toString
* is utilized.
*
* Containers may use this method to convert the header values obtained
* from the {@link #getHttpHeaders}
*
* @param headerValue the header value as an object
* @return the string value
*/
public static String getHeaderValue(Object headerValue) {
HeaderDelegate hp = rd.createHeaderDelegate(headerValue.getClass());
return (hp != null) ? hp.toString(headerValue) : headerValue.toString();
}
/**
* Write the response.
*
* The status and headers will be written by calling the method
* {@link ContainerResponseWriter#writeStatusAndHeaders} on the provided
* {@link ContainerResponseWriter} instance. The {@link OutputStream}
* returned from that method call is used to write the entity (if any)
* to that {@link OutputStream}. An appropriate {@link MessageBodyWriter}
* will be found to write the entity.
*
* @throws WebApplicationException if {@link MessageBodyWriter} cannot be
* found for the entity with a 500 (Internal Server error) response.
* @throws java.io.IOException if there is an error writing the entity
*/
public void write() throws IOException {
if (isCommitted)
return;
if (request.isTracingEnabled()) {
configureTrace(responseWriter);
}
if (entity == null) {
isCommitted = true;
responseWriter.writeStatusAndHeaders(-1, this);
responseWriter.finish();
return;
}
if (!getHttpHeaders().containsKey(HttpHeaders.VARY)) {
final String varyHeader = (String)request.getProperties().get(ContainerRequest.VARY_HEADER);
if (varyHeader != null) {
getHttpHeaders().add(HttpHeaders.VARY, varyHeader);
}
}
MediaType contentType = getMediaType();
if (contentType == null) {
contentType = getMessageBodyWorkers().getMessageBodyWriterMediaType(
entity.getClass(),
entityType,
annotations,
request.getAcceptableMediaTypes());
if (contentType == null ||
contentType.isWildcardType() || contentType.isWildcardSubtype())
contentType = MediaType.APPLICATION_OCTET_STREAM_TYPE;
getHttpHeaders().putSingle(HttpHeaders.CONTENT_TYPE, contentType);
}
final MessageBodyWriter writer = getMessageBodyWorkers().getMessageBodyWriter(
entity.getClass(), entityType,
annotations, contentType);
if (writer == null) {
String message = "A message body writer for Java class " + entity.getClass().getName() +
", and Java type " + entityType +
", and MIME media type " + contentType + " was not found.\n";
Map> m = getMessageBodyWorkers().
getWriters(contentType);
LOGGER.severe(message + "The registered message body writers compatible with the MIME media type are:\n" +
getMessageBodyWorkers().writersToString(m));
if (request.getMethod().equals("HEAD")) {
writeHttpHead(-1);
} else {
throw new WebApplicationException(new MessageException(message), 500);
}
} else {
final long size = writer.getSize(entity, entity.getClass(), entityType, annotations, contentType);
if (request.getMethod().equals("HEAD")) {
writeHttpHead(size);
} else {
if (request.isTracingEnabled()) {
request.trace(String.format("matched message body writer: %s, \"%s\" -> %s",
ReflectionHelper.objectToString(entity),
contentType,
ReflectionHelper.objectToString(writer)));
}
if (out == null) {
out = new CommittingOutputStream(size);
}
writer.writeTo(entity, entity.getClass(), entityType, annotations, contentType, getHttpHeaders(), out);
if (!isCommitted) {
isCommitted = true;
responseWriter.writeStatusAndHeaders(-1, this);
}
}
}
responseWriter.finish();
}
/**
* Handle HTTP HEAD method (headers, ..) during writing a response.
*
* @param size size of a response entity.
* @throws IOException if an error occurred when writing out the status and headers or obtaining the output stream.
*/
private void writeHttpHead(final long size) throws IOException {
if (size != -1) {
getHttpHeaders().putSingle(HttpHeaders.CONTENT_LENGTH, Long.toString(size));
}
isCommitted = true;
responseWriter.writeStatusAndHeaders(size, this);
// Close the InputStream if the entity is an instance of this type.
if (entity instanceof InputStream) {
((InputStream)entity).close();
}
}
private void configureTrace(final ContainerResponseWriter crw) {
final TraceInformation ti = (TraceInformation)request.getProperties().
get(TraceInformation.class.getName());
setContainerResponseWriter(new ContainerResponseWriter() {
public OutputStream writeStatusAndHeaders(long contentLength,
ContainerResponse response) throws IOException {
ti.addTraceHeaders();
return crw.writeStatusAndHeaders(contentLength, response);
}
public void finish() throws IOException {
crw.finish();
}
});
}
/**
* Reset the response to 204 (No content) with no headers.
*/
public void reset() {
setResponse(Responses.noContent().build());
}
/**
* Get the container request.
*
* @return the container request.
*/
public ContainerRequest getContainerRequest() {
return request;
}
/**
* Set the container request.
*
* @param request the container request.
*/
public void setContainerRequest(ContainerRequest request) {
this.request = request;
}
/**
* Get the container response writer.
*
* @return the container response writer
*/
public ContainerResponseWriter getContainerResponseWriter() {
return responseWriter;
}
/**
* Set the container response writer.
*
* @param responseWriter the container response writer
*/
public void setContainerResponseWriter(ContainerResponseWriter responseWriter) {
this.responseWriter = responseWriter;
}
/**
* Get the message body workers.
*
* @return the message body workers.
*/
public MessageBodyWorkers getMessageBodyWorkers() {
return wa.getMessageBodyWorkers();
}
/**
* Map the cause of a mappable container exception to a response.
*
* If the cause cannot be mapped and then that cause is re-thrown
* if a runtime exception otherwise the mappable container exception is
* re-thrown.
*
* @param e the mappable container exception whose cause will be mapped to
* a response.
*/
public void mapMappableContainerException(MappableContainerException e) {
Throwable cause = e.getCause();
if (cause instanceof WebApplicationException) {
mapWebApplicationException((WebApplicationException)cause);
} else if (!mapException(cause)) {
if (cause instanceof RuntimeException) {
LOGGER.log(Level.SEVERE, "The RuntimeException could not be mapped to a response, " +
"re-throwing to the HTTP container", cause);
throw (RuntimeException)cause;
} else {
LOGGER.log(Level.SEVERE, "The exception contained within " +
"MappableContainerException could not be mapped to a response, " +
"re-throwing to the HTTP container", cause);
throw e;
}
}
}
/**
* Map a web application exception to a response.
*
* @param e the web application exception.
*/
public void mapWebApplicationException(WebApplicationException e) {
if (e.getResponse().getEntity() != null) {
wa.getResponseListener().onError(
Thread.currentThread().getId(),
e
);
onException(e, e.getResponse(), false);
} else {
if (!mapException(e)) {
onException(e, e.getResponse(), false);
}
}
}
/**
* Map an exception to a response.
*
* @param e the exception.
* @return true if the exception was mapped, otherwise false.
*/
public boolean mapException(Throwable e) {
ExceptionMapper em = wa.getExceptionMapperContext().find(e.getClass());
if (em == null) {
wa.getResponseListener().onError(
Thread.currentThread().getId(),
e
);
return false;
}
wa.getResponseListener().onMappedException(
Thread.currentThread().getId(),
e,
em
);
if (request.isTracingEnabled()) {
request.trace(String.format("matched exception mapper: %s -> %s",
ReflectionHelper.objectToString(e),
ReflectionHelper.objectToString(em)));
}
try {
Response r = em.toResponse(e);
if (r == null)
r = Response.noContent().build();
onException(e, r, true);
} catch (MappableContainerException ex) {
// If the exception mapper throws a MappableContainerException then
// rethrow it to the HTTP container
throw ex;
} catch (RuntimeException ex) {
LOGGER.severe("Exception mapper " + em +
" for Throwable " + e +
" threw a RuntimeException when " +
"attempting to obtain the response");
Response r = Response.serverError().build();
onException(ex, r, false);
}
return true;
}
private void onException(Throwable e, Response r, boolean mapped) {
if (request.isTracingEnabled()) {
Response.Status s = Response.Status.fromStatusCode(r.getStatus());
if (s != null) {
request.trace(String.format("mapped exception to response: %s -> %d (%s)",
ReflectionHelper.objectToString(e),
r.getStatus(),
s.getReasonPhrase()));
} else {
request.trace(String.format("mapped exception to response: %s -> %d",
ReflectionHelper.objectToString(e),
r.getStatus()));
}
}
if (!mapped && r.getStatus() >= 500) {
logException(e, r, Level.SEVERE);
} else if (LOGGER.isLoggable(Level.FINE)) {
logException(e, r, Level.FINE);
}
setResponse(r);
this.mappedThrowable = e;
if (getEntity() != null &&
getHttpHeaders().getFirst(HttpHeaders.CONTENT_TYPE) == null) {
Object m = request.getProperties().get(HttpMethodRule.CONTENT_TYPE_PROPERTY);
if (m != null) {
request.getProperties().remove(HttpMethodRule.CONTENT_TYPE_PROPERTY);
getHttpHeaders().putSingle(HttpHeaders.CONTENT_TYPE, m);
}
}
}
private void logException(Throwable e, Response r, Level l) {
Response.Status s = Response.Status.fromStatusCode(r.getStatus());
if (s != null) {
LOGGER.log(l,
"Mapped exception to response: " + r.getStatus() + " (" + s.getReasonPhrase() + ")",
e);
} else {
LOGGER.log(l,
"Mapped exception to response: " + r.getStatus(),
e);
}
}
// HttpResponseContext
@Override
public Response getResponse() {
if (response == null) {
setResponse(null);
}
return response;
}
@Override
public void setResponse(Response response) {
this.isCommitted = false;
this.out = null;
this.response = response = (response != null) ? response : Responses.noContent().build();
this.mappedThrowable = null;
if (response instanceof ResponseImpl) {
final ResponseImpl responseImpl = (ResponseImpl)response;
setStatusType(responseImpl.getStatusType());
setHeaders(response.getMetadata());
setEntity(responseImpl.getEntity(), responseImpl.getEntityType());
} else {
setStatus(response.getStatus());
setHeaders(response.getMetadata());
setEntity(response.getEntity());
}
}
@Override
public boolean isResponseSet() {
return response != null;
}
@Override
public Throwable getMappedThrowable() {
return mappedThrowable;
}
@Override
public StatusType getStatusType() {
return statusType;
}
@Override
public void setStatusType(StatusType statusType) {
this.statusType = statusType;
}
@Override
public int getStatus() {
return statusType.getStatusCode();
}
@Override
public void setStatus(int status) {
this.statusType = ResponseImpl.toStatusType(status);
}
@Override
public Object getEntity() {
return entity;
}
@Override
public Type getEntityType() {
return entityType;
}
@Override
public Object getOriginalEntity() {
return originalEntity;
}
@Override
public void setEntity(Object entity) {
setEntity(entity, (entity == null) ? null : entity.getClass());
}
public void setEntity(Object entity, Type entityType) {
this.originalEntity = this.entity = entity;
this.entityType = entityType;
if (this.entity instanceof GenericEntity) {
final GenericEntity ge = (GenericEntity)this.entity;
this.entity = ge.getEntity();
this.entityType = ge.getType();
}
}
@Override
public Annotation[] getAnnotations() {
return annotations;
}
@Override
public void setAnnotations(Annotation[] annotations) {
this.annotations = (annotations != null) ? annotations : EMPTY_ANNOTATIONS;
}
@Override
public MultivaluedMap getHttpHeaders() {
if (headers == null)
headers = new OutBoundHeaders();
return headers;
}
@Override
public MediaType getMediaType() {
final Object mediaTypeHeader = getHttpHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
if (mediaTypeHeader instanceof MediaType) {
return (MediaType)mediaTypeHeader;
} else if (mediaTypeHeader != null) {
return MediaType.valueOf(mediaTypeHeader.toString());
}
return null;
}
@Override
public OutputStream getOutputStream() throws IOException {
if (out == null)
out = new CommittingOutputStream(-1);
return out;
}
@Override
public boolean isCommitted() {
return isCommitted;
}
private void setHeaders(MultivaluedMap headers) {
this.headers = headers;
Object location = headers.getFirst(HttpHeaders.LOCATION);
if (location != null) {
if (location instanceof URI) {
final URI locationUri = (URI)location;
if (!locationUri.isAbsolute()) {
final URI base = (statusType.getStatusCode() == Status.CREATED.getStatusCode())
? request.getAbsolutePath()
: request.getBaseUri();
location = UriBuilder.fromUri(base).
path(locationUri.getRawPath()).
replaceQuery(locationUri.getRawQuery()).
fragment(locationUri.getRawFragment()).
build();
}
headers.putSingle(HttpHeaders.LOCATION, location);
}
}
}
}