Java tutorial
/* * 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]; } }