org.apache.wicket.request.resource.AbstractResource.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.request.resource.AbstractResource.java

Source

/*
 * 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.wicket.request.resource;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

import javax.servlet.http.HttpServletResponse;

import org.apache.wicket.Application;
import org.apache.wicket.MetaDataKey;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.request.HttpHeaderCollection;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.request.resource.caching.IResourceCachingStrategy;
import org.apache.wicket.request.resource.caching.IStaticCacheableResource;
import org.apache.wicket.util.io.Streams;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Classes;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.time.Duration;
import org.apache.wicket.util.time.Time;

/**
 * Convenience resource implementation. The subclass must implement
 * {@link #newResourceResponse(org.apache.wicket.request.resource.IResource.Attributes)} method.
 * 
 * @author Matej Knopp
 * @author Tobias Soloschenko
 */
public abstract class AbstractResource implements IResource {
    private static final long serialVersionUID = 1L;

    /** header values that are managed internally and must not be set directly */
    public static final Set<String> INTERNAL_HEADERS;

    /** The meta data key of the content range start byte **/
    public static final MetaDataKey<Long> CONTENT_RANGE_STARTBYTE = new MetaDataKey<Long>() {
        private static final long serialVersionUID = 1L;
    };

    /** The meta data key of the content range end byte **/
    public static final MetaDataKey<Long> CONTENT_RANGE_ENDBYTE = new MetaDataKey<Long>() {
        private static final long serialVersionUID = 1L;
    };

    public static final String CONTENT_DISPOSITION_HEADER_NAME = "content-disposition";

    /**
     * All available content range types. The type name represents the name used in header
     * information.
     */
    public enum ContentRangeType {
        BYTES("bytes"), NONE("none");

        private final String typeName;

        ContentRangeType(String typeName) {
            this.typeName = typeName;
        }

        public String getTypeName() {
            return typeName;
        }
    }

    static {
        INTERNAL_HEADERS = new HashSet<>();
        INTERNAL_HEADERS.add("server");
        INTERNAL_HEADERS.add("date");
        INTERNAL_HEADERS.add("expires");
        INTERNAL_HEADERS.add("last-modified");
        INTERNAL_HEADERS.add("content-type");
        INTERNAL_HEADERS.add("content-length");
        INTERNAL_HEADERS.add(CONTENT_DISPOSITION_HEADER_NAME);
        INTERNAL_HEADERS.add("transfer-encoding");
        INTERNAL_HEADERS.add("connection");
        INTERNAL_HEADERS.add("content-range");
        INTERNAL_HEADERS.add("accept-range");
    }

    /**
     * Construct.
     */
    public AbstractResource() {
    }

    /**
     * Override this method to return a {@link ResourceResponse} for the request.
     * 
     * @param attributes
     *            request attributes
     * @return resource data instance
     */
    protected abstract ResourceResponse newResourceResponse(Attributes attributes);

    /**
     * Represents data used to configure response and write resource data.
     * 
     * @author Matej Knopp
     */
    public static class ResourceResponse {
        private Integer errorCode;
        private Integer statusCode;
        private String errorMessage;
        private String fileName = null;
        private ContentDisposition contentDisposition = ContentDisposition.INLINE;
        private String contentType = null;
        private String contentRange = null;
        private ContentRangeType contentRangeType = null;
        private String textEncoding;
        private long contentLength = -1;
        private Time lastModified = null;
        private WriteCallback writeCallback;
        private Duration cacheDuration;
        private WebResponse.CacheScope cacheScope;
        private final HttpHeaderCollection headers;

        /**
         * Construct.
         */
        public ResourceResponse() {
            // disallow caching for public caches. this behavior is similar to wicket 1.4:
            // setting it to [PUBLIC] seems to be sexy but could potentially cache confidential
            // data on public proxies for users migrating to 1.5
            cacheScope = WebResponse.CacheScope.PRIVATE;

            // collection of directly set response headers
            headers = new HttpHeaderCollection();
        }

        /**
         * Sets the error code for resource. If there is an error code set the data will not be
         * rendered and the code will be sent to client.
         * 
         * @param errorCode
         *            error code
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setError(Integer errorCode) {
            setError(errorCode, null);
            return this;
        }

        /**
         * Sets the error code and message for resource. If there is an error code set the data will
         * not be rendered and the code and message will be sent to client.
         * 
         * @param errorCode
         *            error code
         * @param errorMessage
         *            error message
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setError(Integer errorCode, String errorMessage) {
            this.errorCode = errorCode;
            this.errorMessage = errorMessage;
            return this;
        }

        /**
         * @return error code or <code>null</code>
         */
        public Integer getErrorCode() {
            return errorCode;
        }

        /**
         * Sets the status code for resource.
         *
         * @param statusCode
         *            status code
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setStatusCode(Integer statusCode) {
            this.statusCode = statusCode;
            return this;
        }

        /**
         * @return status code or <code>null</code>
         */
        public Integer getStatusCode() {
            return statusCode;
        }

        /**
         * @return error message or <code>null</code>
         */
        public String getErrorMessage() {
            return errorMessage;
        }

        /**
         * Sets the file name of the resource.
         * 
         * @param fileName
         *            file name
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setFileName(String fileName) {
            this.fileName = fileName;
            return this;
        }

        /**
         * @return resource file name
         */
        public String getFileName() {
            return fileName;
        }

        /**
         * Determines whether the resource will be inline or an attachment.
         * 
         * @see ContentDisposition
         * 
         * @param contentDisposition
         *            content disposition (attachment or inline)
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setContentDisposition(ContentDisposition contentDisposition) {
            Args.notNull(contentDisposition, "contentDisposition");
            this.contentDisposition = contentDisposition;
            return this;
        }

        /**
         * @return whether the resource is inline or attachment
         */
        public ContentDisposition getContentDisposition() {
            return contentDisposition;
        }

        /**
         * Sets the content type for the resource. If no content type is set it will be determined
         * by the extension.
         * 
         * @param contentType
         *            content type (also known as mime type)
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setContentType(String contentType) {
            this.contentType = contentType;
            return this;
        }

        /**
         * @return resource content type
         */
        public String getContentType() {
            if (contentType == null && fileName != null) {
                contentType = Application.get().getMimeType(fileName);
            }
            return contentType;
        }

        /**
         * Gets the content range of the resource. If no content range is set the client assumes the
         * whole content.
         *
         * @return the content range
         */
        public String getContentRange() {
            return contentRange;
        }

        /**
         * Sets the content range of the resource. If no content range is set the client assumes the
         * whole content. Please note that if the content range is set, the content length, the
         * status code and the accept range must be set right, too.
         *
         * @param contentRange
         *            the content range
         */
        public void setContentRange(String contentRange) {
            this.contentRange = contentRange;
        }

        /**
         * If the resource accepts ranges
         *
         * @return the type of range (e.g. bytes)
         */
        public ContentRangeType getAcceptRange() {
            return contentRangeType;
        }

        /**
         * Sets the accept range header (e.g. bytes)
         *
         * @param contentRangeType
         *            the content range header information
         */
        public void setAcceptRange(ContentRangeType contentRangeType) {
            this.contentRangeType = contentRangeType;
        }

        /**
         * Sets the text encoding for the resource. This setting must only used if the resource
         * response represents text.
         * 
         * @param textEncoding
         *            character encoding of text body
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setTextEncoding(String textEncoding) {
            this.textEncoding = textEncoding;
            return this;
        }

        /**
         * @return text encoding for resource
         */
        protected String getTextEncoding() {
            return textEncoding;
        }

        /**
         * Sets the content length (in bytes) of the data. Content length is optional but it's
         * recommended to set it so that the browser can show download progress.
         * 
         * @param contentLength
         *            length of response body
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setContentLength(long contentLength) {
            this.contentLength = contentLength;
            return this;
        }

        /**
         * @return content length (in bytes)
         */
        public long getContentLength() {
            return contentLength;
        }

        /**
         * Sets the last modified data of the resource. Even though this method is optional it is
         * recommended to set the date. If the date is set properly Wicket can check the
         * <code>If-Modified-Since</code> to determine if the actual data really needs to be sent
         * to client.
         * 
         * @param lastModified
         *            last modification timestamp
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setLastModified(Time lastModified) {
            this.lastModified = lastModified;
            return this;
        }

        /**
         * @return last modification timestamp
         */
        public Time getLastModified() {
            return lastModified;
        }

        /**
         * Check to determine if the resource data needs to be written. This method checks the
         * <code>If-Modified-Since</code> request header and compares it to lastModified property.
         * In order for this method to work {@link #setLastModified(Time)} has to be called first.
         * 
         * @param attributes
         *            request attributes
         * @return <code>true</code> if the resource data does need to be written,
         *         <code>false</code> otherwise.
         */
        public boolean dataNeedsToBeWritten(Attributes attributes) {
            WebRequest request = (WebRequest) attributes.getRequest();
            Time ifModifiedSince = request.getIfModifiedSinceHeader();

            if (cacheDuration != Duration.NONE && ifModifiedSince != null && lastModified != null) {
                // [Last-Modified] headers have a maximum precision of one second
                // so we have to truncate the milliseconds part for a proper compare.
                // that's stupid, since changes within one second will not be reliably
                // detected by the client ... any hint or clarification to improve this
                // situation will be appreciated...
                Time roundedLastModified = Time.millis(lastModified.getMilliseconds() / 1000 * 1000);

                return ifModifiedSince.before(roundedLastModified);
            } else {
                return true;
            }
        }

        /**
         * Disables caching.
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse disableCaching() {
            return setCacheDuration(Duration.NONE);
        }

        /**
         * Sets caching to maximum available duration.
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setCacheDurationToMaximum() {
            cacheDuration = WebResponse.MAX_CACHE_DURATION;
            return this;
        }

        /**
         * Controls how long this response may be cached.
         * 
         * @param duration
         *            caching duration in seconds
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setCacheDuration(Duration duration) {
            cacheDuration = Args.notNull(duration, "duration");
            return this;
        }

        /**
         * Returns how long this resource may be cached for.
         * <p/>
         * The special value Duration.NONE means caching is disabled.
         * 
         * @return duration for caching
         * 
         * @see org.apache.wicket.settings.ResourceSettings#setDefaultCacheDuration(org.apache.wicket.util.time.Duration)
         * @see org.apache.wicket.settings.ResourceSettings#getDefaultCacheDuration()
         */
        public Duration getCacheDuration() {
            Duration duration = cacheDuration;
            if (duration == null && Application.exists()) {
                duration = Application.get().getResourceSettings().getDefaultCacheDuration();
            }

            return duration;
        }

        /**
         * returns what kind of caches are allowed to cache the resource response
         * <p/>
         * resources are only cached at all if caching is enabled by setting a cache duration.
         * 
         * @return cache scope
         * 
         * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#getCacheDuration()
         * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#setCacheDuration(org.apache.wicket.util.time.Duration)
         * @see org.apache.wicket.request.http.WebResponse.CacheScope
         */
        public WebResponse.CacheScope getCacheScope() {
            return cacheScope;
        }

        /**
         * controls what kind of caches are allowed to cache the response
         * <p/>
         * resources are only cached at all if caching is enabled by setting a cache duration.
         * 
         * @param scope
         *            scope for caching
         * 
         * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#getCacheDuration()
         * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#setCacheDuration(org.apache.wicket.util.time.Duration)
         * @see org.apache.wicket.request.http.WebResponse.CacheScope
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setCacheScope(WebResponse.CacheScope scope) {
            cacheScope = Args.notNull(scope, "scope");
            return this;
        }

        /**
         * Sets the {@link WriteCallback}. The callback is responsible for generating the response
         * data.
         * <p>
         * It is necessary to set the {@link WriteCallback} if
         * {@link #dataNeedsToBeWritten(org.apache.wicket.request.resource.IResource.Attributes)}
         * returns <code>true</code> and {@link #setError(Integer)} has not been called.
         * 
         * @param writeCallback
         *            write callback
         *
         * @return {@code this}, for chaining.
         */
        public ResourceResponse setWriteCallback(final WriteCallback writeCallback) {
            Args.notNull(writeCallback, "writeCallback");
            this.writeCallback = writeCallback;
            return this;
        }

        /**
         * @return write callback.
         */
        public WriteCallback getWriteCallback() {
            return writeCallback;
        }

        /**
         * get custom headers
         * 
         * @return collection of the response headers
         */
        public HttpHeaderCollection getHeaders() {
            return headers;
        }
    }

    /**
     * Configure the web response header for client cache control.
     * 
     * @param data
     *            resource data
     * @param attributes
     *            request attributes
     */
    protected void configureCache(final ResourceResponse data, final Attributes attributes) {
        Response response = attributes.getResponse();

        if (response instanceof WebResponse) {
            Duration duration = data.getCacheDuration();
            WebResponse webResponse = (WebResponse) response;
            if (duration.compareTo(Duration.NONE) > 0) {
                webResponse.enableCaching(duration, data.getCacheScope());
            } else {
                webResponse.disableCaching();
            }
        }
    }

    protected IResourceCachingStrategy getCachingStrategy() {
        return Application.get().getResourceSettings().getCachingStrategy();
    }

    /**
     * 
     * @see org.apache.wicket.request.resource.IResource#respond(org.apache.wicket.request.resource.IResource.Attributes)
     */
    @Override
    public void respond(final Attributes attributes) {
        // Sets the request attributes
        setRequestMetaData(attributes);

        // Get a "new" ResourceResponse to write a response
        ResourceResponse data = newResourceResponse(attributes);

        // is resource supposed to be cached?
        if (this instanceof IStaticCacheableResource) {
            final IStaticCacheableResource cacheable = (IStaticCacheableResource) this;

            // is caching enabled?
            if (cacheable.isCachingEnabled()) {
                // apply caching strategy to response
                getCachingStrategy().decorateResponse(data, cacheable);
            }
        }
        // set response header
        setResponseHeaders(data, attributes);

        if (!data.dataNeedsToBeWritten(attributes) || data.getErrorCode() != null
                || needsBody(data.getStatusCode()) == false) {
            return;
        }

        if (data.getWriteCallback() == null) {
            throw new IllegalStateException("ResourceResponse#setWriteCallback() must be set.");
        }

        try {
            data.getWriteCallback().writeData(attributes);
        } catch (IOException iox) {
            throw new WicketRuntimeException(iox);
        }
    }

    /**
     * Decides whether a response body should be written back to the client depending on the set
     * status code
     *
     * @param statusCode
     *            the status code set by the application
     * @return {@code true} if the status code allows response body, {@code false} - otherwise
     */
    private boolean needsBody(Integer statusCode) {
        return statusCode == null || (statusCode < 300 && statusCode != HttpServletResponse.SC_NO_CONTENT
                && statusCode != HttpServletResponse.SC_RESET_CONTENT);
    }

    /**
     * check if header is directly modifyable
     * 
     * @param name
     *            header name
     * 
     * @throws IllegalArgumentException
     *             if access is forbidden
     */
    private void checkHeaderAccess(String name) {
        name = Args.notEmpty(name.trim().toLowerCase(Locale.ROOT), "name");

        if (INTERNAL_HEADERS.contains(name)) {
            throw new IllegalArgumentException("you are not allowed to directly access header [" + name + "], "
                    + "use one of the other specialized methods of " + Classes.simpleName(getClass())
                    + " to get or modify its value");
        }
    }

    /**
     * Reads the plain request header information and applies enriched information as meta data to
     * the current request. Those information are available for the whole request cycle.
     *
     * @param attributes
     *            the attributes to get the plain request header information
     */
    protected void setRequestMetaData(Attributes attributes) {
        Request request = attributes.getRequest();
        if (request instanceof WebRequest) {
            WebRequest webRequest = (WebRequest) request;

            setRequestRangeMetaData(webRequest);
        }
    }

    protected void setRequestRangeMetaData(WebRequest webRequest) {
        String rangeHeader = webRequest.getHeader("range");

        // The content range header is only be calculated if a range is given
        if (!Strings.isEmpty(rangeHeader) && rangeHeader.contains(ContentRangeType.BYTES.getTypeName())) {
            // fixing white spaces
            rangeHeader = rangeHeader.replaceAll(" ", "");

            String range = rangeHeader.substring(rangeHeader.indexOf('=') + 1, rangeHeader.length());

            // support only the first range (WICKET-5995)
            final int idxOfComma = range.indexOf(',');
            String firstRange = idxOfComma > -1 ? range.substring(0, idxOfComma) : range;

            String[] rangeParts = Strings.split(firstRange, '-');

            String startByteString = rangeParts[0];
            String endByteString = rangeParts[1];

            long startbyte = !Strings.isEmpty(startByteString) ? Long.parseLong(startByteString) : 0;
            long endbyte = !Strings.isEmpty(endByteString) ? Long.parseLong(endByteString) : -1;

            // Make the content range information available for the whole request cycle
            RequestCycle requestCycle = RequestCycle.get();
            requestCycle.setMetaData(CONTENT_RANGE_STARTBYTE, startbyte);
            requestCycle.setMetaData(CONTENT_RANGE_ENDBYTE, endbyte);
        }
    }

    /**
     * Sets the response header of resource response to the response received from the attributes
     *
     * @param resourceResponse
     *            the resource response to get the header fields from
     * @param attributes
     *            the attributes to get the response from to which the header information are going
     *            to be applied
     */
    protected void setResponseHeaders(final ResourceResponse resourceResponse, final Attributes attributes) {
        Response response = attributes.getResponse();
        if (response instanceof WebResponse) {
            WebResponse webResponse = (WebResponse) response;

            // 1. Last Modified
            Time lastModified = resourceResponse.getLastModified();
            if (lastModified != null) {
                webResponse.setLastModifiedTime(lastModified);
            }

            // 2. Caching
            configureCache(resourceResponse, attributes);

            if (resourceResponse.getErrorCode() != null) {
                webResponse.sendError(resourceResponse.getErrorCode(), resourceResponse.getErrorMessage());
                return;
            }

            if (resourceResponse.getStatusCode() != null) {
                webResponse.setStatus(resourceResponse.getStatusCode());
            }

            if (!resourceResponse.dataNeedsToBeWritten(attributes)) {
                webResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                return;
            }

            // 3. Content Disposition
            String fileName = resourceResponse.getFileName();
            ContentDisposition disposition = resourceResponse.getContentDisposition();
            if (ContentDisposition.ATTACHMENT == disposition) {
                webResponse.setAttachmentHeader(fileName);
            } else if (ContentDisposition.INLINE == disposition) {
                webResponse.setInlineHeader(fileName);
            }

            // 4. Mime Type (+ encoding)
            String mimeType = resourceResponse.getContentType();
            if (mimeType != null) {
                final String encoding = resourceResponse.getTextEncoding();

                if (encoding == null) {
                    webResponse.setContentType(mimeType);
                } else {
                    webResponse.setContentType(mimeType + "; charset=" + encoding);
                }
            }

            // 5. Accept Range
            ContentRangeType acceptRange = resourceResponse.getAcceptRange();
            if (acceptRange != null) {
                webResponse.setAcceptRange(acceptRange.getTypeName());
            }

            long contentLength = resourceResponse.getContentLength();
            boolean contentRangeApplied = false;

            // 6. Content Range
            // for more information take a look here:
            // http://stackoverflow.com/questions/8293687/sample-http-range-request-session
            // if the content range header has been set directly
            // to the resource response use it otherwise calculate it
            String contentRange = resourceResponse.getContentRange();
            if (contentRange != null) {
                webResponse.setContentRange(contentRange);
            } else {
                // content length has to be set otherwise the content range header can not be
                // calculated - accept range must be set to bytes - others are not supported at the
                // moment
                if (contentLength != -1 && ContentRangeType.BYTES.equals(acceptRange)) {
                    contentRangeApplied = setResponseContentRangeHeaderFields(webResponse, attributes,
                            contentLength);
                }
            }

            // 7. Content Length
            if (contentLength != -1 && !contentRangeApplied) {
                webResponse.setContentLength(contentLength);
            }

            // add custom headers and values
            final HttpHeaderCollection headers = resourceResponse.getHeaders();

            for (String name : headers.getHeaderNames()) {
                checkHeaderAccess(name);

                for (String value : headers.getHeaderValues(name)) {
                    webResponse.addHeader(name, value);
                }
            }
        }
    }

    /**
     * Sets the content range header fields to the given web response
     *
     * @param webResponse
     *            the web response to apply the content range information to
     * @param attributes
     *            the attributes to get the request from
     * @param contentLength
     *            the content length of the response
     * @return if the content range header information has been applied
     */
    protected boolean setResponseContentRangeHeaderFields(WebResponse webResponse, Attributes attributes,
            long contentLength) {
        boolean contentRangeApplied = false;
        if (attributes.getRequest() instanceof WebRequest) {
            Long startbyte = RequestCycle.get().getMetaData(CONTENT_RANGE_STARTBYTE);
            Long endbyte = RequestCycle.get().getMetaData(CONTENT_RANGE_ENDBYTE);

            if (startbyte != null && endbyte != null) {
                // if end byte hasn't been set
                if (endbyte == -1) {
                    endbyte = contentLength - 1;
                }

                // Change the status code to 206 partial content
                webResponse.setStatus(206);
                // currently only bytes are supported.
                webResponse.setContentRange(ContentRangeType.BYTES.getTypeName() + " " + startbyte + '-' + endbyte
                        + '/' + contentLength);
                // WARNING - DO NOT SET THE CONTENT LENGTH, even if it is calculated right - 
                // SAFARI / CHROME are causing issues otherwise!
                // webResponse.setContentLength((endbyte - startbyte) + 1);

                // content range has been applied do not set the content length again!
                contentRangeApplied = true;
            }
        }
        return contentRangeApplied;
    }

    /**
     * Callback invoked when resource data needs to be written to response. Subclass needs to
     * implement the {@link #writeData(org.apache.wicket.request.resource.IResource.Attributes)}
     * method.
     * 
     * @author Matej Knopp
     */
    public abstract static class WriteCallback {
        /**
         * Write the resource data to response.
         * 
         * @param attributes
         *            request attributes
         */
        public abstract void writeData(Attributes attributes) throws IOException;

        /**
         * Convenience method to write an {@link InputStream} to response.
         * 
         * @param attributes
         *            request attributes
         * @param stream
         *            input stream
         */
        protected final void writeStream(Attributes attributes, InputStream stream) throws IOException {
            final Response response = attributes.getResponse();
            Streams.copy(stream, response.getOutputStream());
        }
    }
}