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