/******************************************************************************* * 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.wink.server.internal.handlers; import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.List; import java.util.Properties; import java.util.Map.Entry; import javax.activation.CommandMap; import javax.activation.DataContentHandler; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; 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.ext.MessageBodyWriter; import javax.ws.rs.ext.Providers; import javax.ws.rs.ext.RuntimeDelegate; import javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate; import org.apache.wink.common.http.HttpStatus; import org.apache.wink.common.internal.MultivaluedMapImpl; import org.apache.wink.common.internal.i18n.Messages; import org.apache.wink.common.internal.runtime.RuntimeContextTLS; import org.apache.wink.common.utils.ProviderUtils; import org.apache.wink.common.utils.ProviderUtils.PROVIDER_EXCEPTION_ORIGINATOR; import org.apache.wink.server.handlers.AbstractHandler; import org.apache.wink.server.handlers.MessageContext; import org.apache.wink.server.internal.contexts.RequestImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FlushResultHandler extends AbstractHandler { private static final Logger logger = LoggerFactory.getLogger(FlushResultHandler.class); private static final RuntimeDelegate runtimeDelegate = RuntimeDelegate.getInstance(); @SuppressWarnings("unchecked") public void handleResponse(MessageContext context) throws Throwable { // assert status code is valid int statusCode = context.getResponseStatusCode(); if (statusCode < 0) { logger.trace("Status code was not set. Nothing to do."); //$NON-NLS-1$ return; } // assert response is not committed final HttpServletResponse httpResponse = context.getAttribute(HttpServletResponse.class); if (httpResponse.isCommitted()) { logger.trace("The response is already committed. Nothing to do."); //$NON-NLS-1$ return; } // set the status code logger.trace("Response status code set to: {}", statusCode); //$NON-NLS-1$ httpResponse.setStatus(statusCode); // get the entity Object entity = context.getResponseEntity(); boolean isOriginalEntityResponseObj = false; // extract the entity and headers from the response (if it is a // response) MultivaluedMap<String, Object> httpHeaders = null; if (entity instanceof Response) { Response response = (Response)entity; entity = response.getEntity(); httpHeaders = response.getMetadata(); isOriginalEntityResponseObj = true; } // prepare the entity to write, its class and generic type Type genericType = null; Annotation[] declaredAnnotations = null; SearchResult searchResult = context.getAttribute(SearchResult.class); if (searchResult != null && searchResult.isFound()) { Method reflectionMethod = searchResult.getMethod().getMetadata().getReflectionMethod(); genericType = reflectionMethod.getGenericReturnType(); declaredAnnotations = reflectionMethod.getDeclaredAnnotations(); } else { declaredAnnotations = new Annotation[0]; } Class<?> rawType = null; if (entity instanceof GenericEntity) { GenericEntity<?> genericEntity = (GenericEntity<?>)entity; entity = genericEntity.getEntity(); rawType = genericEntity.getRawType(); genericType = genericEntity.getType(); } else { rawType = (entity != null ? entity.getClass() : null); if (isOriginalEntityResponseObj) { genericType = rawType; } else { genericType = (genericType != null ? genericType : rawType); } } if (httpHeaders == null) { httpHeaders = new MultivaluedMapImpl<String, Object>(); } // we're done if the actual entity is null if (entity == null) { logger.trace("No entity so writing only the headers"); //$NON-NLS-1$ flushHeaders(httpResponse, httpHeaders); return; } // we have an entity, get the response media type // the actual writing will take places in the flush MediaType responseMediaType = context.getResponseMediaType(); // get the provider to write the entity Providers providers = context.getProviders(); MessageBodyWriter<Object> messageBodyWriter = (MessageBodyWriter<Object>)providers.getMessageBodyWriter(rawType, genericType, declaredAnnotations, responseMediaType); // use the provider to write the entity if (messageBodyWriter != null) { if (logger.isTraceEnabled()) { logger.trace("Serialization using provider {}", messageBodyWriter.getClass().getName()); //$NON-NLS-1$ } final MultivaluedMap<String, Object> headers = httpHeaders; long size; try { size = messageBodyWriter.getSize(entity, rawType, genericType, declaredAnnotations, responseMediaType); } catch (RuntimeException e) { ProviderUtils.logUserProviderException(e, messageBodyWriter, PROVIDER_EXCEPTION_ORIGINATOR.getSize, new Object[] {entity, rawType, genericType, declaredAnnotations, responseMediaType}, context); throw e; } if (logger.isTraceEnabled()) { logger.trace("{}@{}.getSize({}, {}, {}, {}, {}) returned {}", new Object[] { //$NON-NLS-1$ messageBodyWriter.getClass().getName(), Integer.toHexString(System.identityHashCode(messageBodyWriter)), Integer.toHexString(System.identityHashCode(entity)), rawType, genericType, declaredAnnotations, responseMediaType, size}); } if (size >= 0) { headers.putSingle(HttpHeaders.CONTENT_LENGTH, String.valueOf(size)); } FlushHeadersOutputStream outputStream = new FlushHeadersOutputStream(httpResponse, headers, responseMediaType); if (logger.isTraceEnabled()) { logger.trace("{}@{}.writeTo({}, {}, {}, {}, {}, {}, {}) being called", new Object[] { //$NON-NLS-1$ messageBodyWriter.getClass().getName(), Integer.toHexString(System.identityHashCode(messageBodyWriter)), Integer.toHexString(System.identityHashCode(entity)), rawType, genericType, declaredAnnotations, responseMediaType, httpHeaders, outputStream}); } try { messageBodyWriter.writeTo(entity, rawType, genericType, declaredAnnotations, responseMediaType, httpHeaders, outputStream); } catch (RuntimeException e) { ProviderUtils.logUserProviderException(e, messageBodyWriter, ProviderUtils.PROVIDER_EXCEPTION_ORIGINATOR.writeTo, new Object[] {entity, rawType, genericType, declaredAnnotations, responseMediaType, httpHeaders, outputStream}, context); throw e; } logger.trace("Flushing headers if not written"); //$NON-NLS-1$ outputStream.flushHeaders(); return; } else { logger.trace("Could not find a writer for {} and {}. Try to find JAF DataSourceProvider", //$NON-NLS-1$ entity.getClass().getName(), responseMediaType); } DataContentHandler dataContentHandler = null; // Write Entity with ASF DataContentHandler // try to find a data handler using JavaBeans Activation Framework, if // found use DataSourceProvider dataContentHandler = CommandMap .getDefaultCommandMap() .createDataContentHandler(responseMediaType.getType() + "/" + responseMediaType.getSubtype()); //$NON-NLS-1$ if (dataContentHandler == null) { if (logger.isErrorEnabled()) { logger.error(Messages.getMessage("noWriterOrDataSourceProvider", entity.getClass() //$NON-NLS-1$ .getName(), responseMediaType)); } // WINK-379 - From the spec : // If no methods support one of the acceptable response entity body media types // an implementation MUST generate a WebApplicationException with a not acceptable // response (HTTP 406 status) and no entity throw new WebApplicationException(HttpStatus.NOT_ACCEPTABLE.getCode()); } if (logger.isTraceEnabled()) { logger .trace("Serialization using data content handler {}", dataContentHandler.getClass() //$NON-NLS-1$ .getName()); } FlushHeadersOutputStream outputStream = new FlushHeadersOutputStream(httpResponse, httpHeaders, responseMediaType); if (logger.isTraceEnabled()) { logger.trace("{}@{}.writeTo({}, {}, {}) being called", new Object[] { //$NON-NLS-1$ dataContentHandler.getClass().getName(), Integer.toHexString(System.identityHashCode(dataContentHandler)), entity, responseMediaType.toString(), outputStream}); } dataContentHandler .writeTo(entity, responseMediaType.getType() + "/" + responseMediaType.getSubtype(), outputStream); //$NON-NLS-1$ logger.trace("Flushing headers if not written"); //$NON-NLS-1$ outputStream.flushHeaders(); } @SuppressWarnings("unchecked") private static void flushHeaders(final HttpServletResponse httpResponse, final MultivaluedMap<String, Object> headers) { if (headers.get(HttpHeaders.VARY) == null) { RequestImpl.VaryHeader varyHeader = RuntimeContextTLS.getRuntimeContext().getAttribute(RequestImpl.VaryHeader.class); if (varyHeader != null) { logger.trace("Vary header automatically set by a call to RequestImpl"); //$NON-NLS-1$ headers.putSingle(HttpHeaders.VARY, varyHeader.getVaryHeaderValue()); } } logger.trace("Flushing headers: {}", headers); //$NON-NLS-1$ for (Entry<String, List<Object>> entry : headers.entrySet()) { String key = entry.getKey(); List<Object> values = entry.getValue(); for (Object val : values) { if (val != null) { HeaderDelegate<Object> headerDelegate = (HeaderDelegate<Object>)runtimeDelegate .createHeaderDelegate(val.getClass()); String header = headerDelegate != null ? headerDelegate.toString(val) : val.toString(); httpResponse.addHeader(key, header); } } } } private static class FlushHeadersOutputStream extends OutputStream { // The Proxy over OutputStream is provided here in order to write the // headers that a provider MAY have added to the response, before it // actually started writing to stream. private boolean writeStarted; final private HttpServletResponse httpResponse; final private ServletOutputStream outputStream; final private MultivaluedMap<String, Object> headers; final private MediaType responseMediaType; public FlushHeadersOutputStream(HttpServletResponse httpResponse, MultivaluedMap<String, Object> headers, MediaType responseMediaType) throws IOException { this.writeStarted = false; this.httpResponse = httpResponse; this.outputStream = httpResponse.getOutputStream(); this.headers = headers; this.responseMediaType = responseMediaType; } @Override public void write(int b) throws IOException { flushHeaders(); outputStream.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { flushHeaders(); outputStream.write(b, off, len); } @Override public void write(byte[] b) throws IOException { flushHeaders(); outputStream.write(b); } @Override public void flush() throws IOException { flushHeaders(); outputStream.flush(); } @Override public void close() throws IOException { flushHeaders(); outputStream.close(); } private void flushHeaders() { if (!writeStarted) { if (httpResponse.getContentType() == null) { logger.trace("Set response Content-Type to: {} ", responseMediaType); //$NON-NLS-1$ httpResponse.setContentType(responseMediaType.toString()); } FlushResultHandler.flushHeaders(httpResponse, headers); writeStarted = true; } } } public void init(Properties props) { } }