// Copyright (C) 1999-2001 by Jason Hunter <jhunter_AT_acm_DOT_org>. // All rights reserved. Use of this class is limited. // Please see the LICENSE for more information. package com.oreilly.servlet; import java.io.*; import java.util.*; import javax.servlet.http.Cookie; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * <p>A superclass for HTTP servlets</p> * Use it when the servlet must have its output * cached and automatically resent as appropriate according to the * servlet's getLastModified() method. To take advantage of this class, * a servlet must:<br> * <ul> * <li>Extend <tt>CacheHttpServlet</tt> instead of <tt>HttpServlet</tt> * <li>Implement a <tt>getLastModified(HttpServletRequest)</tt> method as usual * </ul> * This class uses the value returned by <tt>getLastModified()</tt> to manage * an internal cache of the servlet's output. Before handling a request, * this class checks the value of <tt>getLastModified()</tt>, and if the * output cache is at least as current as the servlet's last modified time, * the cached output is sent without calling the servlet's <tt>doGet()</tt> * method. * <p> * In order to be safe, if this class detects that the servlet's query * string, extra path info, or servlet path has changed, the cache is * invalidated and recreated. However, this class does not invalidate * the cache based on differing request headers or cookies; for * servlets that vary their output based on these values (i.e. a session * tracking servlet) this class should probably not be used. * <p> * No caching is performed for POST requests. * <p> * <tt>CacheHttpServletResponse</tt> and <tt>CacheServletOutputStream</tt> * are helper classes to this class and should not be used directly. * <p> * This class has been built against Servlet API 2.2. Using it with previous * Servlet API versions should work; using it with future API versions likely * won't work. * <p> * If you get the error Cannot resolve symbol method getContentType() * Then include library /lib/compile/servlet.jar when compiling the project. * * @author <b>Jason Hunter</b>, Copyright © 1999 * @version 0.92, 00/03/16, added synchronization blocks to make thread safe * @version 0.91, 99/12/28, made support classes package protected * @version 0.90, 99/12/19 */ public abstract class CacheHttpServlet extends HttpServlet { // --------------------------------------------------------------------------- CacheHttpServletResponse cacheResponse; long cacheLastMod = -1; String cacheQueryString = null; String cachePathInfo = null; String cacheServletPath = null; Object lock = new Object(); // --------------------------------------------------------------------------- protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Only do caching for GET requests String method = req.getMethod(); if (!method.equals("GET")) { super.service(req, res); return; } // Check the last modified time for this servlet long servletLastMod = getLastModified(req); // A last modified of -1 means we shouldn't use any cache logic if (servletLastMod == -1) { super.service(req, res); return; } // If the client sent an If-Modified-Since header equal or after the // servlet's last modified time, send a short "Not Modified" status code // Round down to the nearest second since client headers are in seconds if ((servletLastMod / 1000 * 1000) <= req.getDateHeader("If-Modified-Since")) { res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } // Use the existing cache if it's current and valid CacheHttpServletResponse localResponseCopy = null; synchronized (lock) { if (servletLastMod <= cacheLastMod && cacheResponse.isValid() && equal(cacheQueryString, req.getQueryString()) && equal(cachePathInfo, req.getPathInfo()) && equal(cacheServletPath, req.getServletPath())) { localResponseCopy = cacheResponse; } } if (localResponseCopy != null) { localResponseCopy.writeTo(res); return; } // Otherwise make a new cache to capture the response localResponseCopy = new CacheHttpServletResponse(res); super.service(req, localResponseCopy); synchronized (lock) { cacheResponse = localResponseCopy; cacheLastMod = servletLastMod; cacheQueryString = req.getQueryString(); cachePathInfo = req.getPathInfo(); cacheServletPath = req.getServletPath(); } } // --------------------------------------------------------------------------- private static boolean equal(String s1, String s2) { if (s1 == null && s2 == null) { return true; } else if (s1 == null || s2 == null) { return false; } else { return s1.equals(s2); } } } // ----------------------------------------------------------------------------- class CacheHttpServletResponse implements HttpServletResponse { // Store key response variables so they can be set later private int status; private Hashtable headers; private int contentLength; private String contentType; private Locale locale; private Vector cookies; private boolean didError; private boolean didRedirect; private boolean gotStream; private boolean gotWriter; private HttpServletResponse delegate; private CacheServletOutputStream out; private PrintWriter writer; // --------------------------------------------------------------------------- CacheHttpServletResponse(HttpServletResponse res) { delegate = res; try { out = new CacheServletOutputStream(res.getOutputStream()); } catch (IOException e) { System.out.println( "Got IOException constructing cached response: " + e.getMessage()); } internalReset(); } // --------------------------------------------------------------------------- private void internalReset() { status = 200; headers = new Hashtable(); contentLength = -1; contentType = null; locale = null; cookies = new Vector(); didError = false; didRedirect = false; gotStream = false; gotWriter = false; out.getBuffer().reset(); } public boolean isValid() { // We don't cache error pages or redirects return (!didError) && (!didRedirect); } // --------------------------------------------------------------------------- private void internalSetHeader(String name, Object value) { Vector v = new Vector(); v.addElement(value); headers.put(name, v); } // --------------------------------------------------------------------------- private void internalAddHeader(String name, Object value) { Vector v = (Vector) headers.get(name); if (v == null) { v = new Vector(); } v.addElement(value); headers.put(name, v); } // --------------------------------------------------------------------------- public void writeTo(HttpServletResponse res) { // Write status code res.setStatus(status); // Write convenience headers if (contentType != null) res.setContentType(contentType); if (locale != null) res.setLocale(locale); // Write cookies Enumeration cookieenum = cookies.elements(); while (cookieenum.hasMoreElements()) { Cookie c = (Cookie) cookieenum.nextElement(); res.addCookie(c); } // Write standard headers Enumeration headerenum = headers.keys(); while (headerenum.hasMoreElements()) { String name = (String) headerenum.nextElement(); Vector values = (Vector) headers.get(name); // may have multiple values Enumeration enum2 = values.elements(); while (enum2.hasMoreElements()) { Object value = enum2.nextElement(); if (value instanceof String) { res.setHeader(name, (String)value); } if (value instanceof Integer) { res.setIntHeader(name, ((Integer)value).intValue()); } if (value instanceof Long) { res.setDateHeader(name, ((Long)value).longValue()); } } } // Write content length res.setContentLength(out.getBuffer().size()); // Write body try { out.getBuffer().writeTo(res.getOutputStream()); } catch (IOException e) { System.out.println( "Got IOException writing cached response: " + e.getMessage()); } } // --------------------------------------------------------------------------- public String getContentType() { return delegate.getContentType(); } // --------------------------------------------------------------------------- public ServletOutputStream getOutputStream() throws IOException,IllegalStateException { if (gotWriter) { throw new IllegalStateException( "Cannot get output stream after getting writer"); } gotStream = true; return out; } // --------------------------------------------------------------------------- public PrintWriter getWriter() throws UnsupportedEncodingException,IllegalStateException { if (gotStream) { throw new IllegalStateException( "Cannot get writer after getting output stream"); } gotWriter = true; if (writer == null) { OutputStreamWriter w = new OutputStreamWriter(out, getCharacterEncoding()); writer = new PrintWriter(w, true); // autoflush is necessary } return writer; } // --------------------------------------------------------------------------- public void setContentLength(int len) { delegate.setContentLength(len); // No need to save the length; we can calculate it later } // --------------------------------------------------------------------------- public void setContentType(String type) { delegate.setContentType(type); contentType = type; } // --------------------------------------------------------------------------- public String getCharacterEncoding() { return delegate.getCharacterEncoding(); } // --------------------------------------------------------------------------- public void setBufferSize(int size) throws IllegalStateException { delegate.setBufferSize(size); } // --------------------------------------------------------------------------- public int getBufferSize() { return delegate.getBufferSize(); } // --------------------------------------------------------------------------- public void resetBuffer() throws IllegalStateException { delegate.reset(); internalReset(); } // --------------------------------------------------------------------------- public void reset() throws IllegalStateException { delegate.reset(); internalReset(); } // --------------------------------------------------------------------------- /* public void resetBuffer() throws IllegalStateException { delegate.resetBuffer(); contentLength = -1; out.getBuffer().reset(); } */ // --------------------------------------------------------------------------- public boolean isCommitted() { return delegate.isCommitted(); } // --------------------------------------------------------------------------- public void flushBuffer() throws IOException { delegate.flushBuffer(); } // --------------------------------------------------------------------------- public void setLocale(Locale loc) { delegate.setLocale(loc); locale = loc; } // --------------------------------------------------------------------------- public Locale getLocale() { return delegate.getLocale(); } // --------------------------------------------------------------------------- public void addCookie(Cookie cookie) { delegate.addCookie(cookie); cookies.addElement(cookie); } // --------------------------------------------------------------------------- public boolean containsHeader(String name) { return delegate.containsHeader(name); } // --------------------------------------------------------------------------- public void setCharacterEncoding(String enc) { delegate.setCharacterEncoding(enc); } // --------------------------------------------------------------------------- /** @deprecated */ public void setStatus(int sc, String sm) { delegate.setStatus(sc, sm); status = sc; } // --------------------------------------------------------------------------- public void setStatus(int sc) { delegate.setStatus(sc); status = sc; } // --------------------------------------------------------------------------- public void setHeader(String name, String value) { delegate.setHeader(name, value); internalSetHeader(name, value); } // --------------------------------------------------------------------------- public void setIntHeader(String name, int value) { delegate.setIntHeader(name, value); internalSetHeader(name, new Integer(value)); } // --------------------------------------------------------------------------- public void setDateHeader(String name, long date) { delegate.setDateHeader(name, date); internalSetHeader(name, new Long(date)); } // --------------------------------------------------------------------------- public void sendError(int sc, String msg) throws IOException { delegate.sendError(sc, msg); didError = true; } // --------------------------------------------------------------------------- public void sendError(int sc) throws IOException { delegate.sendError(sc); didError = true; } // --------------------------------------------------------------------------- public void sendRedirect(String location) throws IOException { delegate.sendRedirect(location); didRedirect = true; } // --------------------------------------------------------------------------- public String encodeURL(String url) { return delegate.encodeURL(url); } // --------------------------------------------------------------------------- public String encodeRedirectURL(String url) { return delegate.encodeRedirectURL(url); } // --------------------------------------------------------------------------- public void addHeader(String name, String value) { internalAddHeader(name, value); } // --------------------------------------------------------------------------- public void addIntHeader(String name, int value) { internalAddHeader(name, new Integer(value)); } // --------------------------------------------------------------------------- public void addDateHeader(String name, long value) { internalAddHeader(name, new Long(value)); } // --------------------------------------------------------------------------- /** @deprecated */ public String encodeUrl(String url) { return this.encodeURL(url); } // --------------------------------------------------------------------------- /** @deprecated */ public String encodeRedirectUrl(String url) { return this.encodeRedirectURL(url); } } // ----------------------------------------------------------------------------- class CacheServletOutputStream extends ServletOutputStream { ServletOutputStream dlgate; ByteArrayOutputStream cache; CacheServletOutputStream(ServletOutputStream out) { dlgate = out; cache = new ByteArrayOutputStream(4096); } public ByteArrayOutputStream getBuffer() { return cache; } public void write(int b) throws IOException { dlgate.write(b); cache.write(b); } public void write(byte b[]) throws IOException { dlgate.write(b); cache.write(b); } public void write(byte buf[], int offset, int len) throws IOException { dlgate.write(buf, offset, len); cache.write(buf, offset, len); } } // CacheServletOutputStream