001// Copyright (C) 1999-2001 by Jason Hunter <jhunter_AT_acm_DOT_org>.
002// All rights reserved.  Use of this class is limited.
003// Please see the LICENSE for more information.
004
005package com.oreilly.servlet;
006
007import java.io.*;
008import java.util.*;
009import javax.servlet.*;
010import javax.servlet.http.*;
011
012/** 
013 * A superclass for HTTP servlets that wish to have their output 
014 * cached and automatically resent as appropriate according to the
015 * servlet's getLastModified() method.  To take advantage of this class, 
016 * a servlet must:
017 * <ul>
018 * <li>Extend <tt>CacheHttpServlet</tt> instead of <tt>HttpServlet</tt>
019 * <li>Implement a <tt>getLastModified(HttpServletRequest)</tt> method as usual
020 * </ul>
021 * This class uses the value returned by <tt>getLastModified()</tt> to manage 
022 * an internal cache of the servlet's output.  Before handling a request,
023 * this class checks the value of <tt>getLastModified()</tt>, and if the 
024 * output cache is at least as current as the servlet's last modified time, 
025 * the cached output is sent without calling the servlet's <tt>doGet()</tt> 
026 * method.
027 * <p>
028 * In order to be safe, if this class detects that the servlet's query 
029 * string, extra path info, or servlet path has changed, the cache is
030 * invalidated and recreated.  However, this class does not invalidate
031 * the cache based on differing request headers or cookies; for 
032 * servlets that vary their output based on these values (i.e. a session 
033 * tracking servlet) this class should probably not be used.
034 * <p>
035 * No caching is performed for POST requests.  
036 * <p>
037 * <tt>CacheHttpServletResponse</tt> and <tt>CacheServletOutputStream</tt>
038 * are helper classes to this class and should not be used directly.
039 * <p>
040 * This class has been built against Servlet API 2.2.  Using it with previous
041 * Servlet API versions should work; using it with future API versions likely
042 * won't work.
043 *
044 * @author <b>Jason Hunter</b>, Copyright &#169; 1999
045 * @version 0.92, 00/03/16, added synchronization blocks to make thread safe
046 * @version 0.91, 99/12/28, made support classes package protected
047 * @version 0.90, 99/12/19
048 */
049
050public abstract class CacheHttpServlet extends HttpServlet {
051
052  CacheHttpServletResponse cacheResponse;
053  long cacheLastMod = -1;
054  String cacheQueryString = null;
055  String cachePathInfo = null;
056  String cacheServletPath = null;
057  Object lock = new Object();
058
059  protected void service(HttpServletRequest req, HttpServletResponse res)
060      throws ServletException, IOException {
061    // Only do caching for GET requests
062    String method = req.getMethod();
063    if (!method.equals("GET")) {
064      super.service(req, res);
065      return;
066    }
067
068    // Check the last modified time for this servlet
069    long servletLastMod = getLastModified(req);
070
071    // A last modified of -1 means we shouldn't use any cache logic
072    if (servletLastMod == -1) {
073      super.service(req, res);
074      return;
075    }
076
077    // If the client sent an If-Modified-Since header equal or after the 
078    // servlet's last modified time, send a short "Not Modified" status code
079    // Round down to the nearest second since client headers are in seconds
080    if ((servletLastMod / 1000 * 1000) <= 
081             req.getDateHeader("If-Modified-Since")) {
082      res.setStatus(res.SC_NOT_MODIFIED);
083      return;
084    }
085
086    // Use the existing cache if it's current and valid
087    CacheHttpServletResponse localResponseCopy = null;
088    synchronized (lock) {
089      if (servletLastMod <= cacheLastMod && 
090               cacheResponse.isValid() &&
091               equal(cacheQueryString, req.getQueryString()) &&
092               equal(cachePathInfo, req.getPathInfo()) &&
093               equal(cacheServletPath, req.getServletPath())) {
094        localResponseCopy = cacheResponse;
095      }
096    }
097    if (localResponseCopy != null) {
098      localResponseCopy.writeTo(res);
099      return;
100    }
101
102    // Otherwise make a new cache to capture the response
103    localResponseCopy = new CacheHttpServletResponse(res);
104    super.service(req, localResponseCopy);
105    synchronized (lock) {
106      cacheResponse = localResponseCopy;
107      cacheLastMod = servletLastMod;
108      cacheQueryString = req.getQueryString();
109      cachePathInfo = req.getPathInfo();
110      cacheServletPath = req.getServletPath();
111    }
112  }
113
114  private boolean equal(String s1, String s2) {
115    if (s1 == null && s2 == null) {
116      return true;
117    }
118    else if (s1 == null || s2 == null) {
119      return false;
120    }
121    else {
122      return s1.equals(s2);
123    }
124  }
125}
126
127class CacheHttpServletResponse implements HttpServletResponse {
128  // Store key response variables so they can be set later
129  private int status;
130  private Hashtable headers;
131  private int contentLength;
132  private String contentType;
133  private Locale locale;
134  private Vector cookies;
135  private boolean didError;
136  private boolean didRedirect;
137  private boolean gotStream;
138  private boolean gotWriter;
139
140  private HttpServletResponse delegate;
141  private CacheServletOutputStream out;
142  private PrintWriter writer;
143
144  CacheHttpServletResponse(HttpServletResponse res) {
145    delegate = res;
146    try {
147      out = new CacheServletOutputStream(res.getOutputStream());
148    }
149    catch (IOException e) {
150      System.out.println(
151        "Got IOException constructing cached response: " + e.getMessage());
152    }
153    internalReset();
154  }
155
156  private void internalReset() {
157    status = 200;
158    headers = new Hashtable();
159    contentLength = -1;
160    contentType = null;
161    locale = null;
162    cookies = new Vector();
163    didError = false;
164    didRedirect = false;
165    gotStream = false;
166    gotWriter = false;
167    out.getBuffer().reset();
168  }
169
170  public boolean isValid() {
171    // We don't cache error pages or redirects
172    return didError != true && didRedirect != true;
173  }
174
175  private void internalSetHeader(String name, Object value) {
176    Vector v = new Vector();
177    v.addElement(value);
178    headers.put(name, v);
179  }
180
181  private void internalAddHeader(String name, Object value) {
182    Vector v = (Vector) headers.get(name);
183    if (v == null) {
184      v = new Vector();
185    }
186    v.addElement(value);
187    headers.put(name, v);
188  }
189
190  public void writeTo(HttpServletResponse res) {
191    // Write status code
192    res.setStatus(status);
193    // Write convenience headers
194    if (contentType != null) res.setContentType(contentType);
195    if (locale != null) res.setLocale(locale);
196    // Write cookies
197    Enumeration myEnum = cookies.elements();
198    while (myEnum.hasMoreElements()) {
199      Cookie c = (Cookie) myEnum.nextElement();
200      res.addCookie(c);
201    }
202    // Write standard headers
203    myEnum = headers.keys();
204    while (myEnum.hasMoreElements()) {
205      String name = (String) myEnum.nextElement();
206      Vector values = (Vector) headers.get(name); // may have multiple values
207      Enumeration myEnum2 = values.elements();
208      while (myEnum2.hasMoreElements()) {
209        Object value = myEnum2.nextElement();
210        if (value instanceof String) {
211          res.setHeader(name, (String)value);
212        }
213        if (value instanceof Integer) {
214          res.setIntHeader(name, ((Integer)value).intValue());
215        }
216        if (value instanceof Long) {
217          res.setDateHeader(name, ((Long)value).longValue());
218        }
219      }
220    }
221    // Write content length
222    res.setContentLength(out.getBuffer().size());
223    // Write body
224    try {
225      out.getBuffer().writeTo(res.getOutputStream());
226    }
227    catch (IOException e) {
228      System.out.println(
229        "Got IOException writing cached response: " + e.getMessage());
230    }
231  }
232
233  public ServletOutputStream getOutputStream() throws IOException {
234    if (gotWriter) {
235      throw new IllegalStateException(
236        "Cannot get output stream after getting writer");
237    }
238    gotStream = true;
239    return out;
240  }
241
242  public PrintWriter getWriter() throws UnsupportedEncodingException {
243    if (gotStream) {
244      throw new IllegalStateException(
245        "Cannot get writer after getting output stream");
246    }
247    gotWriter = true;
248    if (writer == null) {
249      OutputStreamWriter w =
250        new OutputStreamWriter(out, getCharacterEncoding());
251      writer = new PrintWriter(w, true);  // autoflush is necessary
252    }
253    return writer;
254  }
255
256  public void setContentLength(int len) {
257    delegate.setContentLength(len);
258    // No need to save the length; we can calculate it later
259  }
260
261  public void setContentType(String type) {
262    delegate.setContentType(type);
263    contentType = type;
264  }
265
266  public String getCharacterEncoding() {
267    return delegate.getCharacterEncoding();
268  }
269
270  public void setBufferSize(int size) throws IllegalStateException {
271    delegate.setBufferSize(size);
272  }
273
274  public int getBufferSize() {
275    return delegate.getBufferSize();
276  }
277
278  public void reset() throws IllegalStateException {
279    delegate.reset();
280    internalReset();
281  }
282
283  public void resetBuffer() throws IllegalStateException {
284    delegate.resetBuffer();
285    contentLength = -1;
286    out.getBuffer().reset();
287  }
288
289  public boolean isCommitted() { 
290    return delegate.isCommitted();
291  }
292
293  public void flushBuffer() throws IOException { 
294    delegate.flushBuffer();
295  }
296
297  public void setLocale(Locale loc) { 
298    delegate.setLocale(loc);
299    locale = loc;
300  }
301
302  public Locale getLocale() { 
303    return delegate.getLocale();
304  }
305
306  public void addCookie(Cookie cookie) { 
307    delegate.addCookie(cookie);
308    cookies.addElement(cookie);
309  }
310
311  public boolean containsHeader(String name) { 
312    return delegate.containsHeader(name);
313  }
314
315  /** @deprecated */
316  public void setStatus(int sc, String sm) { 
317    delegate.setStatus(sc, sm);
318    status = sc;
319  }
320
321  public void setStatus(int sc) { 
322    delegate.setStatus(sc);
323    status = sc;
324  }
325
326  public void setHeader(String name, String value) { 
327    delegate.setHeader(name, value);
328    internalSetHeader(name, value);
329  }
330
331  public void setIntHeader(String name, int value) { 
332    delegate.setIntHeader(name, value);
333    internalSetHeader(name, new Integer(value));
334  }
335
336  public void setDateHeader(String name, long date) { 
337    delegate.setDateHeader(name, date);
338    internalSetHeader(name, new Long(date));
339  }
340
341  public void sendError(int sc, String msg) throws IOException { 
342    delegate.sendError(sc, msg);
343    didError = true;
344  }
345
346  public void sendError(int sc) throws IOException { 
347    delegate.sendError(sc);
348    didError = true;
349  }
350
351  public void sendRedirect(String location) throws IOException { 
352    delegate.sendRedirect(location);
353    didRedirect = true;
354  }
355
356  public String encodeURL(String url) { 
357    return delegate.encodeURL(url);
358  }
359
360  public String encodeRedirectURL(String url) { 
361    return delegate.encodeRedirectURL(url);
362  }
363
364  public void addHeader(String name, String value) { 
365    internalAddHeader(name, value);
366  }
367
368  public void addIntHeader(String name, int value) { 
369    internalAddHeader(name, new Integer(value));
370  }
371
372  public void addDateHeader(String name, long value) { 
373    internalAddHeader(name, new Long(value));
374  }
375
376  /** @deprecated */
377  public String encodeUrl(String url) { 
378    return this.encodeURL(url);
379  }
380
381  /** @deprecated */
382  public String encodeRedirectUrl(String url) { 
383    return this.encodeRedirectURL(url);
384  }
385}
386
387class CacheServletOutputStream extends ServletOutputStream {
388
389  ServletOutputStream delegate;
390  ByteArrayOutputStream cache;
391
392  CacheServletOutputStream(ServletOutputStream out) {
393    delegate = out;
394    cache = new ByteArrayOutputStream(4096);
395  }
396
397  public ByteArrayOutputStream getBuffer() {
398    return cache;
399  }
400
401  public void write(int b) throws IOException {
402    delegate.write(b);
403    cache.write(b);
404  }
405
406  public void write(byte b[]) throws IOException {
407    delegate.write(b);
408    cache.write(b);
409  }
410
411  public void write(byte buf[], int offset, int len) throws IOException {
412    delegate.write(buf, offset, len);
413    cache.write(buf, offset, len);
414  }
415}