ch.entwine.weblounge.cache.impl.CacheableHttpServletResponse.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.cache.impl.CacheableHttpServletResponse.java

Source

/*
 *  Weblounge: Web Content Management System
 *  Copyright (c) 2003 - 2011 The Weblounge Team
 *  http://entwinemedia.com/weblounge
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software Foundation
 *  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

package ch.entwine.weblounge.cache.impl;

import ch.entwine.weblounge.cache.StreamFilter;
import ch.entwine.weblounge.cache.impl.filter.FilterWriter;
import ch.entwine.weblounge.common.impl.request.CachedOutputStream;
import ch.entwine.weblounge.common.impl.request.RequestUtils;
import ch.entwine.weblounge.common.request.CacheHandle;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.TeeOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

/**
 * Implementation of a <code>HttpServletResponseWrapper</code> that allows for
 * response caching by installing a custom version of an output stream which
 * works like the <code>tee</code> command in un*x systems. Like this, the
 * output can be written to the response cache <i>and</i> to the client at the
 * same time.
 */
class CacheableHttpServletResponse extends HttpServletResponseWrapper {

    /** The logging facility */
    private static final Logger logger = LoggerFactory.getLogger(CacheableHttpServletResponse.class);

    /**
     * Holds the special tee writer that copies the output to the network and to
     * the cache.
     */
    private PrintWriter out = null;

    /** The cache transaction for this response */
    private CacheTransaction tx = null;

    /** The format used for date headers */
    private DateFormat format = null;

    /** The content type */
    private String contentType = null;

    /** Whether the getOuputStream has already been called */
    private boolean osCalled = false;

    /** Default encoding */
    private static final String DEFAULT_ENCODING = "utf-8";

    /**
     * Creates a <code>CacheableHttpServletResponse</code> that is writing any
     * content to the wrapped response as well as to the cached output stream,
     * given a preceding call to {@link #startTransaction(CacheTransaction)}.
     * 
     * @param tx
     *          the cached transaction represented by this cacheable response
     */
    CacheableHttpServletResponse(HttpServletResponse response) {
        super(response);
    }

    /**
     * Starts a cache transaction.
     * 
     * @param handle
     *          the cache handle
     * @param filter
     *          the stream filter
     * @return the transaction
     */
    public CacheTransaction startTransaction(CacheHandle handle, StreamFilter filter) {
        tx = new CacheTransaction(handle, filter);
        return tx;
    }

    /**
     * Returns the modified writer that enables the <code>CacheManager</cache>
     * to copy the response to the cache.
     * 
     * @return a PrintWriter object that can return character data to the client
     * @throws IOException
     *           if the writer could not be allocated
     * @see javax.servlet.ServletResponse#getWriter()
     * @see ch.entwine.weblounge.OldCacheManager.cache.CacheManager
     */
    @Override
    public PrintWriter getWriter() throws IOException {
        // Check whether there's already a writer allocated
        if (out != null)
            return out;

        // Check whether getOutputStream() has already been called
        if (osCalled)
            throw new IllegalStateException("An output stream has already been allocated");

        // Get the character encoding
        String encoding = getCharacterEncoding();
        if (encoding == null) {
            encoding = DEFAULT_ENCODING;
            setCharacterEncoding(encoding);
        }

        // Allocate a new writer. If there is a transaction, the output is written
        // to both the original response and the cache output stream.
        try {
            if (tx == null || tx.getFilter() == null)
                out = new PrintWriter(new OutputStreamWriter(super.getOutputStream(), encoding));
            else
                out = new PrintWriter(new BufferedWriter(new FilterWriter(
                        new OutputStreamWriter(new TeeOutputStream(super.getOutputStream(), tx.getOutputStream()),
                                encoding),
                        tx.getFilter(), contentType)));
        } catch (UnsupportedEncodingException e) {
            throw new IOException(e.getMessage());
        }

        // Check whether the new writer is usable
        if (out == null)
            throw new IOException("Unable to allocate writer");

        // Return the new writer
        return out;
    }

    /**
     * @see javax.servlet.ServletResponseWrapper#getOutputStream()
     */
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (out != null)
            throw new IllegalStateException("A writer has already been allocated");
        osCalled = true;
        return tx != null ? tx.getOutputStream() : super.getOutputStream();
    }

    /**
     * Signals the end of a cache entry.
     * 
     * @param hnd
     *          the handle to end
     */
    void endEntry(CacheHandle hnd) {
        if (out != null)
            out.flush();
    }

    /**
     * Signals that the page display is finished and flushes the buffer.
     * 
     * @return the cached transaction for this page
     */
    CacheTransaction endOutput() {
        try {
            if (out != null) {
                out.flush();
                out.close();
                out = null;
            } else if (tx != null) {
                tx.getOutputStream().flush();
                tx.getOutputStream().close();
                CachedOutputStream cacheOs = tx.getOutputStream();
                OutputStream clientOs = super.getOutputStream();
                ByteArrayInputStream cacheIs = new ByteArrayInputStream(cacheOs.getContent());
                IOUtils.copy(cacheIs, clientOs);
                clientOs.flush();
                clientOs.close();
            } else {
                super.getOutputStream().flush();
                super.getOutputStream().close();
            }
        } catch (IOException e) {
            if (RequestUtils.isCausedByClient(e))
                logger.debug("Can't write cached response back to client: " + e.getMessage());
            else
                logger.error("Unknown error while writing cached response back to client", e);
        }
        return tx;
    }

    /**
     * Invalidate the output. Tells the cache writer to stop adding output to the
     * cache.
     */
    void invalidate() {
        if (tx != null)
            tx.invalidate();
    }

    /**
     * Returns <code>true</code> if the response has been invalidated.
     * 
     * @return <code>true</code> if the response has been invalidated
     */
    public boolean isValid() {
        return tx != null ? tx.isValid() : false;
    }

    /**
     * Returns the active cache transaction.
     * 
     * @return the transaction
     */
    public CacheTransaction getTransaction() {
        return tx;
    }

    /**
     * @see javax.servlet.ServletResponse#setContentType(String)
     */
    @Override
    public void setContentType(String type) {
        super.setContentType(type);
        contentType = type;
        if (tx != null) {
            tx.getHeaders().setHeader("Content-Type", contentType);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see javax.servlet.ServletResponseWrapper#flushBuffer()
     */
    @Override
    public void flushBuffer() throws IOException {
        if (isCommitted())
            return;

        // Make sure to flush any print writer so its content is
        // written to the cached output stream
        if (tx != null && out != null)
            out.flush();

        // Finally initiate writing the content back to the client
        super.flushBuffer();
    }

    /**
     * @see javax.servlet.http.HttpServletResponse#addHeader(java.lang.String,
     *      java.lang.String)
     */
    @Override
    public void addHeader(String name, String value) {
        super.addHeader(name, value);
        if (tx != null)
            tx.getHeaders().addHeader(name, value);
    }

    /**
     * @see javax.servlet.http.HttpServletResponse#setHeader(java.lang.String,
     *      java.lang.String)
     */
    @Override
    public void setHeader(String name, String value) {
        super.setHeader(name, value);
        if (tx != null)
            tx.getHeaders().setHeader(name, value);
    }

    /**
     * @see javax.servlet.http.HttpServletResponse#addDateHeader(java.lang.String,
     *      long)
     */
    @Override
    public void addDateHeader(String name, long date) {
        super.addDateHeader(name, date);
        if (tx != null)
            tx.getHeaders().addHeader(name, formatDate(date));
    }

    /**
     * @see javax.servlet.http.HttpServletResponse#addIntHeader(java.lang.String,
     *      int)
     */
    @Override
    public void addIntHeader(String name, int value) {
        super.addIntHeader(name, value);
        tx.getHeaders().addHeader(name, Integer.toString(value));
    }

    /**
     * @see javax.servlet.http.HttpServletResponse#setDateHeader(java.lang.String,
     *      long)
     */
    @Override
    public void setDateHeader(String name, long date) {
        super.setDateHeader(name, date);
        if (tx != null)
            tx.getHeaders().setHeader(name, formatDate(date));
    }

    /**
     * @see javax.servlet.http.HttpServletResponse#setIntHeader(java.lang.String,
     *      int)
     */
    @Override
    public void setIntHeader(String name, int value) {
        super.setIntHeader(name, value);
        if (tx != null)
            tx.getHeaders().setHeader(name, Integer.toString(value));
    }

    /**
     * Format the date for an HTTP header. The resulting date will match the
     * following example:
     * 
     * <pre>
     * EEE, dd MMM yyyy HH:mm:ss 'GMT'
     * </pre>
     * 
     * @param date
     *          the date to format
     * @return the formatted date
     */
    private String formatDate(long date) {
        if (format == null) {
            format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
            format.setTimeZone(TimeZone.getTimeZone("GMT"));
        }
        return format.format(new Date(date));
    }

}