ch.entwine.weblounge.common.impl.request.Http11ProtocolHandler.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.common.impl.request.Http11ProtocolHandler.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.common.impl.request;

import ch.entwine.weblounge.common.Times;

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

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketException;
import java.util.StringTokenizer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * The <code>Http11ProtocolHandler</code> analyzes HTTP 1.1 request headers and
 * decides what response to generate. It includes support for the following
 * features as defined in RFC2616:
 * <ul>
 * <li>external cache control using Last-Modified, ETag and Expires headers
 * <li>support for conditional requests using If-Modified-Since, If-None-Match
 * If-Unmodified-Since and If-Match headers
 * <li>support for partial requests using Rage and If-Range headers
 * <li>generates the following replies based on the request headers:
 * <ul>
 * <li>200 OK replies
 * <li>206 Partial Content
 * <li>304 Not Modified
 * <li>405 Method Not Allowed
 * <li>412 Precondition Failed
 * <li>416 Requested Range Not Satisfiable
 * <li>500 Internal Server Error
 * </ul>
 * </ul>
 * 
 * @see ftp://ftp.rfc-editor.org/in-notes/rfc2616.txt
 */
public final class Http11ProtocolHandler implements Times, Http11Constants {

    /** Logging facility */
    private static final Logger log = LoggerFactory.getLogger(Http11ProtocolHandler.class);

    /**
     * This response type indicates a "501 Internal Server Error" response
     * required
     **/
    public static final int RESPONSE_INTERNAL_SERVER_ERROR = 0;

    /** This response type indicates a "200 OK" response required */
    public static final int RESPONSE_OK = 1;

    /** This response type indicates a "206 Partial Content" response required */
    public static final int RESPONSE_PARTIAL_CONTENT = 2;

    /** This response type indicates a "304 Not Modified" response required */
    public static final int RESPONSE_NOT_MODIFIED = 3;

    /**
     * This response type indicates a "412 Precondition Failed" response required
     **/
    public static final int RESPONSE_PRECONDITION_FAILED = 4;

    /**
     * This response type indicates a "416 Requested Range Not Satisfiable"
     * response required
     **/
    public static final int RESPONSE_REQUESTED_RANGE_NOT_SATISFIABLE = 5;

    /**
     * This response type indicates a "405 Method Not Allowed" response required
     */
    public static final int RESPONSE_METHOD_NOT_ALLOWED = 6;

    /** unknown response, just in case... */
    public static final int RESPONSE_UNKNOWN = 7;

    /** statistics constant for the number of analyzed requests */
    public static final int STATS_ANALYZED = 0;

    /** statistics constant for the number of response headers generated */
    public static final int STATS_HEADER_GENERATED = 1;

    /** statistics constant for the number of response bodies generated */
    public static final int STATS_BODY_GENERATED = 2;

    /** statistics constant for the numer of bytes written */
    public static final int STATS_BYTES_WRITTEN = 3;

    /** the number of errors while writing the response */
    public static final int STATS_ERRORS = 4;

    /** calculated statisical values */
    public static final int STATS_BYTES_PER_RESPONSE = 20;

    /** the number of general statistics counters */
    protected static final int STATS_NOF_COUNTERS = 5;

    /** the maximum number of response counters */
    protected static final int STATS_NOF_RESPONSE = 8;

    /** the size if the temporary buffer */
    private static final int BUFFER_SIZE = 8 * 1024;

    /** protocol handler statistics */
    protected static long[] stats = new long[STATS_NOF_COUNTERS];

    /** per response code header statistics */
    protected static long[] headerStats = new long[STATS_NOF_RESPONSE];

    /** per response code body statistics */
    protected static long[] bodyStats = new long[STATS_NOF_RESPONSE];

    /** holds a temporary buffer for data copying */
    private static final ThreadLocal<byte[]> buffer = new ThreadLocal<byte[]>();

    /**
     * This class is not intended to be instantiated.
     */
    private Http11ProtocolHandler() {
        // Nothing to be done
    }

    /**
     * Method isError.
     * 
     * @param type
     * @return <code>true</code> if the responsetype is an error
     */
    public static boolean isError(Http11ResponseType type) {
        return type.type != RESPONSE_OK && type.type != RESPONSE_PARTIAL_CONTENT;
    }

    /**
     * Method analyzeRequest.
     * 
     * @param req
     * @param modified
     * @param expires
     * @param size
     * @return Http11ResponseType
     */
    public static Http11ResponseType analyzeRequest(HttpServletRequest req, long modified, long expires,
            long size) {

        /* adjust the statistics */
        ++stats[STATS_ANALYZED];

        /* the response type */
        Http11ResponseType type = new Http11ResponseType(RESPONSE_INTERNAL_SERVER_ERROR, modified, expires);
        type.size = size;

        /* calculate the etag */
        String eTag = Http11Utils.calcETag(modified);

        /* decode the conditional headers */
        long ifModifiedSince = -1;
        try {
            ifModifiedSince = req.getDateHeader(HEADER_IF_MODIFIED_SINCE);
        } catch (IllegalArgumentException e) {
            log.debug("Client provided malformed '{}' header: {}", HEADER_IF_MODIFIED_SINCE,
                    req.getDateHeader(HEADER_IF_MODIFIED_SINCE));
        }
        String ifNoneMatch = req.getHeader(HEADER_IF_NONE_MATCH);

        long ifUnmodifiedSince = -1;
        try {
            ifUnmodifiedSince = req.getDateHeader(HEADER_IF_UNMODIFIED_SINCE);
        } catch (IllegalArgumentException e) {
            log.debug("Client provided malformed '{}' header: {}", HEADER_IF_UNMODIFIED_SINCE,
                    req.getDateHeader(HEADER_IF_UNMODIFIED_SINCE));
        }

        String ifMatch = req.getHeader(HEADER_IF_MATCH);
        String method = req.getMethod();
        type.headerOnly = method.equals(METHOD_HEAD);
        boolean reqGetHead = method.equals(METHOD_GET) || type.headerOnly;
        boolean ifNoneMatchMatch = matchETag(eTag, ifNoneMatch);

        /* method */
        if (!reqGetHead && !method.equals(METHOD_POST)) {
            type.type = RESPONSE_METHOD_NOT_ALLOWED;
            return type;
        }

        /* check e-tag */
        if (ifNoneMatch != null && ifNoneMatchMatch && reqGetHead) {
            type.type = RESPONSE_NOT_MODIFIED;
            return type;
        }

        /* check not modified */
        if (ifNoneMatch == null && ifModifiedSince != -1 && modified < ifModifiedSince + MS_PER_SECOND) {
            type.type = RESPONSE_NOT_MODIFIED;
            return type;
        }

        /* precondition check failed */
        if (ifNoneMatch != null && ifNoneMatchMatch && !reqGetHead) {
            log.error("412 PCF: Method={}, If-None-Match={}, match={}",
                    new Object[] { req.getMethod(), ifNoneMatch, ifNoneMatchMatch });
            log.info("If-None-Match header only supported in GET or HEAD requests.");
            type.type = RESPONSE_PRECONDITION_FAILED;
            type.err = "If-None-Match header only supported in GET or HEAD requests.";
            return type;
        }
        if (ifUnmodifiedSince != -1 && modified > ifUnmodifiedSince) {
            log.error("412 PCF: modified={} > ifUnmodifiedSince={}", modified, ifUnmodifiedSince);
            log.info("If-Unmodified-Since precondition check failed.");
            type.type = RESPONSE_PRECONDITION_FAILED;
            type.err = "If-Unmodified-Since precondition check failed.";
            return type;
        }
        if (ifMatch != null && !matchETag(eTag, ifMatch)) {
            log.error("412 PCF: !matchETag({}, {})", eTag, ifMatch);
            log.info("If-match precondition check failed.");
            type.type = RESPONSE_PRECONDITION_FAILED;
            type.err = "If-match precondition check failed.";
            return type;
        }

        /* decode the range headers */
        if (size >= 0) {
            // PENDING: handle ranges
        }

        /* return the result */
        type.type = RESPONSE_OK;
        return type;
    }

    /**
     * Method matchETag.
     * 
     * @param eTag
     * @param eTagList
     * @return boolean
     */
    protected static boolean matchETag(String eTag, String eTagList) {
        if (eTagList == null || eTag == null)
            return false;
        String s = null;
        StringTokenizer t = new StringTokenizer(eTagList, ",");
        while (t.hasMoreTokens()) {
            s = t.nextToken().trim();
            if ("*".equals(s) || s.equals(eTag))
                return true;
        }
        return false;
    }

    /**
     * Method generateResponse.
     * 
     * @param resp
     * @param type
     * @param buf
     * @return boolean
     * @throws IOException
     *           if generating the response fails
     */
    public static boolean generateResponse(HttpServletResponse resp, Http11ResponseType type, byte[] buf)
            throws IOException {

        return generateResponse(resp, type, new ByteArrayInputStream(buf));
    }

    /**
     * Method generateResponse.
     * 
     * @param resp
     * @param type
     * @param is
     * @return boolean
     * @throws IOException
     *           if generating the response fails
     */
    public static boolean generateResponse(HttpServletResponse resp, Http11ResponseType type, InputStream is)
            throws IOException {

        /* first generate the response headers */
        generateHeaders(resp, type);

        /* adjust the statistics */
        ++stats[STATS_BODY_GENERATED];
        incResponseStats(type.type, bodyStats);

        /* generate the response body */
        try {
            if (resp.isCommitted())
                log.warn("Response is already committed!");
            switch (type.type) {
            case RESPONSE_OK:
                if (!type.isHeaderOnly() && is != null) {
                    resp.setBufferSize(BUFFER_SIZE);
                    OutputStream os = null;
                    try {
                        os = resp.getOutputStream();
                        IOUtils.copy(is, os);
                    } catch (IOException e) {
                        if (RequestUtils.isCausedByClient(e))
                            return true;
                    } finally {
                        IOUtils.closeQuietly(os);
                    }
                }
                break;

            case RESPONSE_PARTIAL_CONTENT:
                if (type.from < 0 || type.to < 0 || type.from > type.to || type.to > type.size) {
                    resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                            "Invalid partial content parameters");
                    log.warn("Invalid partial content parameters");
                } else if (!type.isHeaderOnly() && is != null) {
                    resp.setBufferSize(BUFFER_SIZE);
                    OutputStream os = resp.getOutputStream();
                    if (is.skip(type.from) != type.from) {
                        resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                                "Premature end of input stream");
                        log.warn("Premature end of input stream");
                        break;
                    }
                    try {

                        /* get the temporary buffer for this thread */
                        byte[] tmp = buffer.get();
                        if (tmp == null) {
                            tmp = new byte[BUFFER_SIZE];
                            buffer.set(tmp);
                        }

                        int read = type.to - type.from;
                        int copy = read;
                        int write = 0;

                        read = is.read(tmp);
                        while (copy > 0 && read >= 0) {
                            copy -= read;
                            write = copy > 0 ? read : read + copy;
                            os.write(tmp, 0, write);
                            stats[STATS_BYTES_WRITTEN] += write;
                            read = is.read(tmp);
                        }
                        if (copy > 0) {
                            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                                    "Premature end of input stream");
                            log.warn("Premature end of input stream");
                            break;
                        }
                        os.flush();
                        os.close();
                    } catch (SocketException e) {
                        log.debug("Request cancelled by client");
                    }
                }
                break;

            case RESPONSE_NOT_MODIFIED:
                /* NOTE: we MUST NOT return any content (RFC 2616)!!! */
                break;

            case RESPONSE_PRECONDITION_FAILED:
                if (type.err == null)
                    resp.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
                else
                    resp.sendError(HttpServletResponse.SC_PRECONDITION_FAILED, type.err);
                break;

            case RESPONSE_REQUESTED_RANGE_NOT_SATISFIABLE:
                if (type.err == null)
                    resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                else
                    resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, type.err);
                break;

            case RESPONSE_METHOD_NOT_ALLOWED:
                if (type.err == null)
                    resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
                else
                    resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, type.err);
                break;

            case RESPONSE_INTERNAL_SERVER_ERROR:
            default:
                if (type.err == null)
                    resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                else
                    resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, type.err);
            }
        } catch (IOException e) {
            if (e instanceof EOFException) {
                log.debug("Request canceled by client");
                return true;
            }
            ++stats[STATS_ERRORS];
            String message = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
            Throwable cause = e.getCause() != null ? e.getCause() : e;
            log.warn("I/O exception while sending response: {}", message, cause);
            throw e;
        }

        return true;
    }

    /**
     * Method generateHeaders.
     * 
     * @param resp
     * @param type
     */
    public static void generateHeaders(HttpServletResponse resp, Http11ResponseType type) {

        /* generate headers only once! */
        if (type.headers)
            return;
        type.headers = true;

        /* adjust the statistics */
        ++stats[STATS_HEADER_GENERATED];
        incResponseStats(type.type, headerStats);

        /* set the date header */
        resp.setDateHeader(HEADER_DATE, type.time);

        /* check expires */
        if (type.expires > type.time + MS_PER_YEAR) {
            type.expires = type.time + MS_PER_YEAR;
            log.warn("Expiration date too far in the future. Adjusting.");
        }

        /* set the standard headers and status code */
        switch (type.type) {
        case RESPONSE_PARTIAL_CONTENT:
            if (type.expires > type.time)
                resp.setDateHeader(HEADER_EXPIRES, type.expires);
            if (type.modified > 0) {
                resp.setHeader(HEADER_ETAG, Http11Utils.calcETag(type.modified));
                resp.setDateHeader(HEADER_LAST_MODIFIED, type.modified);
            }
            if (type.size < 0 || type.from < 0 || type.to < 0 || type.from > type.to || type.to > type.size) {
                resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                break;
            }
            resp.setContentLength((int) type.size);
            resp.setHeader(HEADER_CONTENT_RANGE, "bytes " + type.from + "-" + type.to + "/" + type.size);
            resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            break;

        case RESPONSE_OK:
            if (type.expires > type.time) {
                resp.setDateHeader(HEADER_EXPIRES, type.expires);
            } else if (type.expires == 0) {
                resp.setHeader(HEADER_CACHE_CONTROL, "no-cache");
                resp.setHeader(HEADER_PRAGMA, "no-cache");
                resp.setDateHeader(HEADER_EXPIRES, 0);
            }
            if (type.modified > 0) {
                resp.setHeader(HEADER_ETAG, Http11Utils.calcETag(type.modified));
                resp.setDateHeader(HEADER_LAST_MODIFIED, type.modified);
            }
            if (type.size >= 0)
                resp.setContentLength((int) type.size);
            resp.setStatus(HttpServletResponse.SC_OK);
            break;

        case RESPONSE_NOT_MODIFIED:
            if (type.expires > type.time)
                resp.setDateHeader(HEADER_EXPIRES, type.expires);
            if (type.modified > 0)
                resp.setHeader(HEADER_ETAG, Http11Utils.calcETag(type.modified));
            resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            break;

        case RESPONSE_METHOD_NOT_ALLOWED:
            resp.setHeader(HEADER_ALLOW, "GET, POST, HEAD");
            resp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
            break;

        case RESPONSE_PRECONDITION_FAILED:
            if (type.expires > type.time)
                resp.setDateHeader(HEADER_EXPIRES, type.expires);
            if (type.modified > 0) {
                resp.setHeader(HEADER_ETAG, Http11Utils.calcETag(type.modified));
                resp.setDateHeader(HEADER_LAST_MODIFIED, type.modified);
            }
            resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
            break;

        case RESPONSE_REQUESTED_RANGE_NOT_SATISFIABLE:
            if (type.expires > type.time)
                resp.setDateHeader(HEADER_EXPIRES, type.expires);
            if (type.modified > 0) {
                resp.setHeader(HEADER_ETAG, Http11Utils.calcETag(type.modified));
                resp.setDateHeader(HEADER_LAST_MODIFIED, type.modified);
            }
            if (type.size >= 0)
                resp.setHeader(HEADER_CONTENT_RANGE, "*/" + type.size);
            break;

        case RESPONSE_INTERNAL_SERVER_ERROR:
        default:
            resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Method incResponseStatistics.
     * 
     * @param type
     * @param stats
     */
    protected static void incResponseStats(int type, long[] stats) {
        if (type < 0 || type >= stats.length)
            ++stats[RESPONSE_UNKNOWN];
        ++stats[type];
    }

    /**
     * Method getStatistics.
     * 
     * @param value
     * @return
     */
    public static long getStatistics(int value) {
        if (value >= 0 && value < stats.length)
            return stats[value];
        switch (value) {
        case STATS_BYTES_PER_RESPONSE:
            return (stats[STATS_BODY_GENERATED] > 0) ? stats[STATS_BYTES_WRITTEN] / stats[STATS_BODY_GENERATED] : 0;
        default:
            return -1;
        }
    }

    /**
     * Method getHeaderStatistics.
     * 
     * @param value
     * @return
     */
    public static long getHeaderStatistics(int value) {
        return getResponseStats(value, headerStats);
    }

    /**
     * Method getBodyStatistics.
     * 
     * @param value
     * @return
     */
    public static long getBodyStatistics(int value) {
        return getResponseStats(value, bodyStats);
    }

    /**
     * Method getRealStats.
     * 
     * @param value
     * @param values
     * @return
     */
    protected static long getResponseStats(int value, long[] values) {
        if (value < 0 || value >= values.length)
            return -1;
        return values[value];
    }

    /**
     * Resets the statistical values.
     */
    public static void resetStatistics() {
        stats = new long[STATS_NOF_COUNTERS];
        headerStats = new long[STATS_NOF_RESPONSE];
        bodyStats = new long[STATS_NOF_RESPONSE];
    }
}