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 © 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}