/*******************************************************************************
* Copyright (c) 2012-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.everrest.core.impl;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
import org.everrest.core.ApplicationContext;
import org.everrest.core.ContainerResponseWriter;
import org.everrest.core.GenericContainerResponse;
import org.everrest.core.impl.header.HeaderHelper;
import org.everrest.core.impl.provider.StringEntityProvider;
import org.everrest.core.util.CaselessMultivaluedMap;
import org.everrest.core.util.Tracer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.ext.MessageBodyWriter;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.EventObject;
import java.util.List;
import static javax.ws.rs.HttpMethod.HEAD;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE;
/**
* @author andrew00x
*/
public class ContainerResponse implements GenericContainerResponse {
private static final Logger LOG = LoggerFactory.getLogger(ContainerResponse.class);
/**
* Wrapper for underlying MessageBodyWriter. Need such wrapper to give possibility update HTTP headers but commit them before writing
* the response body. NotifiesOutputStream wraps original OutputStream for the HTTP body and notify OutputListener about any changes,
* e.g. write bytes, flush or close. OutputListener processes events and initiates process of commit HTTP headers after getting the
* first one.
*/
private static class BodyWriter implements MessageBodyWriter<Object> {
private final MessageBodyWriter<Object> delegate;
private final OutputListener writeListener;
BodyWriter(MessageBodyWriter<Object> writer, OutputListener writeListener) {
this.delegate = writer;
this.writeListener = writeListener;
}
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return delegate.isWriteable(type, genericType, annotations, mediaType);
}
@Override
public long getSize(Object t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return delegate.getSize(t, type, genericType, annotations, mediaType);
}
@Override
public void writeTo(Object t,
Class<?> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException {
try {
delegate.writeTo(t, type, genericType, annotations, mediaType, httpHeaders,
new NotifiesOutputStream(entityStream, writeListener));
} catch (Exception e) {
if (Throwables.getCausalChain(e).stream().anyMatch(throwable -> "org.apache.catalina.connector.ClientAbortException".equals(throwable.getClass().getName()))) {
LOG.warn("Client has aborted connection. Response writing omitted.");
} else {
throw e;
}
}
}
}
/**
* Use underlying output stream as data stream. Pass all invocations to the back-end stream and notify OutputListener about changes in
* back-end stream.
*/
private static class NotifiesOutputStream extends FilterOutputStream {
OutputListener writeListener;
NotifiesOutputStream(OutputStream output, OutputListener writeListener) {
super(output);
this.writeListener = writeListener;
}
@Override
public void write(int b) throws IOException {
writeListener.onChange(new EventObject(this));
out.write(b);
}
@Override
public void write(byte[] b) throws IOException {
writeListener.onChange(new EventObject(this));
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
writeListener.onChange(new EventObject(this));
out.write(b, off, len);
}
@Override
public void flush() throws IOException {
writeListener.onChange(new EventObject(this));
out.flush();
}
@Override
public void close() throws IOException {
writeListener.onChange(new EventObject(this));
out.close();
}
}
/** Listen any changes in response output stream, e.g. write, flush, close, */
private interface OutputListener {
void onChange(EventObject event) throws IOException;
}
/** HTTP status. */
private int status;
/** Entity type. */
private Type entityType;
/** Entity. */
private Object entity;
/** HTTP response headers. */
private MultivaluedMap<String, Object> headers;
/** Response entity content-type. */
private MediaType contentType;
/** See {@link Response}, {@link ResponseBuilder}. */
private Response response;
/** See {@link ContainerResponseWriter}. */
private ContainerResponseWriter responseWriter;
/**
* @param responseWriter
* See {@link ContainerResponseWriter}
*/
public ContainerResponse(ContainerResponseWriter responseWriter) {
this.responseWriter = responseWriter;
}
@Override
public void setResponse(Response response) {
this.response = response;
if (response == null) {
status = 0;
entity = null;
entityType = null;
headers = null;
contentType = null;
} else {
status = response.getStatus();
headers = response.getMetadata();
entity = response.getEntity();
if (entity instanceof GenericEntity) {
GenericEntity genericEntity = (GenericEntity)entity;
entity = genericEntity.getEntity();
entityType = genericEntity.getType();
} else if (entity != null) {
entityType = entity.getClass();
}
if (headers != null) {
Object contentTypeHeader = headers.getFirst(CONTENT_TYPE);
if (contentTypeHeader instanceof MediaType) {
contentType = (MediaType)contentTypeHeader;
} else if (contentTypeHeader != null) {
contentType = MediaType.valueOf(HeaderHelper.getHeaderAsString(contentTypeHeader));
} else {
contentType = null;
}
}
}
}
@Override
public Response getResponse() {
return response;
}
@SuppressWarnings("unchecked")
@Override
public void writeResponse() throws IOException {
if (entity == null) {
writeResponseWithoutEntity();
return;
}
ApplicationContext context = ApplicationContext.getCurrent();
MediaType contentType = getContentType();
if (isNullOrWildcard(contentType)) {
List<MediaType> acceptableWriterMediaTypes = context.getProviders().getAcceptableWriterMediaTypes(entity.getClass(), entityType, null);
if (isEmptyOrContainsSingleWildcardMediaType(acceptableWriterMediaTypes)) {
contentType = context.getContainerRequest().getAcceptableMediaTypes().get(0);
} else {
contentType = context.getContainerRequest().getAcceptableMediaType(acceptableWriterMediaTypes);
}
if (isNullOrWildcard(contentType)) {
contentType = APPLICATION_OCTET_STREAM_TYPE;
}
this.contentType = contentType;
getHttpHeaders().putSingle(CONTENT_TYPE, contentType);
}
MessageBodyWriter entityWriter = context.getProviders().getMessageBodyWriter(entity.getClass(), entityType, null, contentType);
if (entityWriter == null) {
String message = String.format("Not found writer for %s and MIME type %s", entity.getClass(), contentType);
if (HEAD.equals(context.getContainerRequest().getMethod())) {
LOG.warn(message);
getHttpHeaders().putSingle(CONTENT_LENGTH, Long.toString(-1));
} else {
LOG.error(message);
setResponse(Response.status(NOT_ACCEPTABLE)
.entity(message)
.type(TEXT_PLAIN)
.build());
entityWriter = new StringEntityProvider();
}
} else {
if (Tracer.isTracingEnabled()) {
Tracer.trace("Matched MessageBodyWriter for type %s, media type %s = (%s)", entity.getClass(), contentType, entityWriter);
}
if (getHttpHeaders().getFirst(CONTENT_LENGTH) == null) {
long contentLength = entityWriter.getSize(entity, entity.getClass(), entityType, null, contentType);
if (contentLength >= 0) {
getHttpHeaders().putSingle(CONTENT_LENGTH, Long.toString(contentLength));
}
}
}
if (context.getContainerRequest().getMethod().equals(HEAD)) {
writeResponseWithoutEntity();
return;
}
OutputListener headersWriter = new OutputListener() {
private boolean done;
@Override
public void onChange(EventObject event) throws IOException {
if (done) {
return;
}
done = true;
responseWriter.writeHeaders(ContainerResponse.this);
}
};
if (Tracer.isTracingEnabled()) {
Tracer.addTraceHeaders(this);
}
responseWriter.writeBody(this, new BodyWriter(entityWriter, headersWriter));
}
private void writeResponseWithoutEntity() throws IOException {
if (Tracer.isTracingEnabled()) {
Tracer.addTraceHeaders(this);
}
responseWriter.writeHeaders(this);
}
private boolean isEmptyOrContainsSingleWildcardMediaType(List<MediaType> acceptableWriterMediaTypes) {
if (acceptableWriterMediaTypes.isEmpty()) {
return true;
}
if (acceptableWriterMediaTypes.size() == 1) {
MediaType mediaType = acceptableWriterMediaTypes.get(0);
if (mediaType.isWildcardType() && mediaType.isWildcardSubtype()) {
return true;
}
}
return false;
}
private boolean isNullOrWildcard(MediaType contentType) {
return contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype();
}
@Override
public MediaType getContentType() {
return contentType;
}
@Override
public Type getEntityType() {
return entityType;
}
@Override
public Object getEntity() {
return entity;
}
@Override
public MultivaluedMap<String, Object> getHttpHeaders() {
if (headers == null) {
headers = new CaselessMultivaluedMap<>();
}
return headers;
}
@Override
public int getStatus() {
return status;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("Status", status)
.add("Content type", contentType)
.add("Entity type", entityType)
.omitNullValues()
.toString();
}
}