KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > opensymphony > oscache > web > filter > CacheFilter


1 /*
2  * Copyright (c) 2002-2003 by OpenSymphony
3  * All rights reserved.
4  */

5 package com.opensymphony.oscache.web.filter;
6
7 import com.opensymphony.oscache.base.Cache;
8 import com.opensymphony.oscache.base.EntryRefreshPolicy;
9 import com.opensymphony.oscache.base.NeedsRefreshException;
10 import com.opensymphony.oscache.web.ServletCacheAdministrator;
11
12 import org.apache.commons.logging.Log;
13 import org.apache.commons.logging.LogFactory;
14
15 import java.io.IOException JavaDoc;
16
17 import javax.servlet.*;
18 import javax.servlet.http.HttpServletRequest JavaDoc;
19 import javax.servlet.http.HttpServletResponse JavaDoc;
20 import javax.servlet.jsp.PageContext JavaDoc;
21
22 /**
23  * CacheFilter is a filter that allows for server-side caching of post-processed servlet content.<p>
24  *
25  * It also gives great programatic control over refreshing, flushing and updating the cache.<p>
26  *
27  * @author <a HREF="mailto:sergek@lokitech.com">Serge Knystautas</a>
28  * @author <a HREF="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
29  * @author <a HREF="mailto:ltorunski@t-online.de">Lars Torunski</a>
30  * @version $Revision: 1.12 $
31  */

32 public class CacheFilter implements Filter, ICacheKeyProvider, ICacheGroupsProvider {
33     // Header
34
public static final String JavaDoc HEADER_LAST_MODIFIED = "Last-Modified";
35     public static final String JavaDoc HEADER_CONTENT_TYPE = "Content-Type";
36     public static final String JavaDoc HEADER_CONTENT_ENCODING = "Content-Encoding";
37     public static final String JavaDoc HEADER_EXPIRES = "Expires";
38     public static final String JavaDoc HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
39     public static final String JavaDoc HEADER_CACHE_CONTROL = "Cache-Control";
40     public static final String JavaDoc HEADER_ACCEPT_ENCODING = "Accept-Encoding";
41
42     // Fragment parameter
43
public static final int FRAGMENT_AUTODETECT = -1;
44     public static final int FRAGMENT_NO = 0;
45     public static final int FRAGMENT_YES = 1;
46     
47     // No cache parameter
48
public static final int NOCACHE_OFF = 0;
49     public static final int NOCACHE_SESSION_ID_IN_URL = 1;
50     
51     // Last Modified parameter
52
public static final long LAST_MODIFIED_OFF = 0;
53     public static final long LAST_MODIFIED_ON = 1;
54     public static final long LAST_MODIFIED_INITIAL = -1;
55     
56     // Expires parameter
57
public static final long EXPIRES_OFF = 0;
58     public static final long EXPIRES_ON = 1;
59     public static final long EXPIRES_TIME = -1;
60     
61     // Cache Control
62
public static final long MAX_AGE_NO_INIT = Long.MIN_VALUE;
63     public static final long MAX_AGE_TIME = Long.MAX_VALUE;
64
65     // request attribute to avoid reentrance
66
private final static String JavaDoc REQUEST_FILTERED = "__oscache_filtered";
67
68     // the policy for the expires header
69
private EntryRefreshPolicy expiresRefreshPolicy;
70     
71     // the logger
72
private final Log log = LogFactory.getLog(this.getClass());
73
74     // filter variables
75
private FilterConfig config;
76     private ServletCacheAdministrator admin = null;
77     private int cacheScope = PageContext.APPLICATION_SCOPE; // filter scope - default is APPLICATION
78
private int fragment = FRAGMENT_AUTODETECT; // defines if this filter handles fragments of a page - default is auto detect
79
private int time = 60 * 60; // time before cache should be refreshed - default one hour (in seconds)
80
private String JavaDoc cron = null; // A cron expression that determines when this cached content will expire - default is null
81
private int nocache = NOCACHE_OFF; // defines special no cache option for the requests - default is off
82
private long lastModified = LAST_MODIFIED_INITIAL; // defines if the last-modified-header will be sent - default is intial setting
83
private long expires = EXPIRES_ON; // defines if the expires-header will be sent - default is on
84
private long cacheControlMaxAge = -60; // defines which max-age in Cache-Control to be set - default is 60 seconds for max-age
85
private ICacheKeyProvider cacheKeyProvider = this; // the provider of the cache key - default is the CacheFilter itselfs
86
private ICacheGroupsProvider cacheGroupsProvider = this; // the provider of the cache groups - default is the CacheFilter itselfs
87

88     /**
89      * Filter clean-up
90      */

91     public void destroy() {
92         //Not much to do...
93
}
94
95     /**
96      * The doFilter call caches the response by wrapping the <code>HttpServletResponse</code>
97      * object so that the output stream can be caught. This works by splitting off the output
98      * stream into two with the {@link SplitServletOutputStream} class. One stream gets written
99      * out to the response as normal, the other is fed into a byte array inside a {@link ResponseContent}
100      * object.
101      *
102      * @param request The servlet request
103      * @param response The servlet response
104      * @param chain The filter chain
105      * @throws ServletException IOException
106      */

107     public void doFilter(ServletRequest JavaDoc request, ServletResponse JavaDoc response, FilterChain chain) throws ServletException, IOException JavaDoc {
108         if (log.isInfoEnabled()) {
109             log.info("<cache>: filter in scope " + cacheScope);
110         }
111
112         // avoid reentrance (CACHE-128) and check if request is cacheable
113
if (isFilteredBefore(request) || !isCacheable(request)) {
114             chain.doFilter(request, response);
115             return;
116         }
117         request.setAttribute(REQUEST_FILTERED, Boolean.TRUE);
118
119         HttpServletRequest JavaDoc httpRequest = (HttpServletRequest JavaDoc) request;
120
121         // checks if the response will be a fragment of a page
122
boolean fragmentRequest = isFragment(httpRequest);
123
124         // avoid useless session creation for application scope pages (CACHE-129)
125
Cache cache;
126         if (cacheScope == PageContext.SESSION_SCOPE) {
127             cache = admin.getSessionScopeCache(httpRequest.getSession(true));
128         } else {
129             cache = admin.getAppScopeCache(config.getServletContext());
130         }
131
132         // generate the cache entry key
133
String JavaDoc key = cacheKeyProvider.createCacheKey(httpRequest, admin, cache);
134
135         try {
136             ResponseContent respContent = (ResponseContent) cache.getFromCache(key, time, cron);
137
138             if (log.isInfoEnabled()) {
139                 log.info("<cache>: Using cached entry for " + key);
140             }
141
142             boolean acceptsGZip = false;
143             if ((!fragmentRequest) && (lastModified != LAST_MODIFIED_OFF)) {
144                 long clientLastModified = httpRequest.getDateHeader(HEADER_IF_MODIFIED_SINCE); // will return -1 if no header...
145

146                 // only reply with SC_NOT_MODIFIED
147
// if the client has already the newest page and the response isn't a fragment in a page
148
if ((clientLastModified != -1) && (clientLastModified >= respContent.getLastModified())) {
149                     ((HttpServletResponse JavaDoc) response).setStatus(HttpServletResponse.SC_NOT_MODIFIED);
150                     return;
151                 }
152                 
153                 acceptsGZip = respContent.isContentGZiped() && acceptsGZipEncoding(httpRequest);
154             }
155
156             respContent.writeTo(response, fragmentRequest, acceptsGZip);
157             // acceptsGZip is used for performance reasons above; use the following line for CACHE-49
158
// respContent.writeTo(response, fragmentRequest, acceptsGZipEncoding(httpRequest));
159
} catch (NeedsRefreshException nre) {
160             boolean updateSucceeded = false;
161
162             try {
163                 if (log.isInfoEnabled()) {
164                     log.info("<cache>: New cache entry, cache stale or cache scope flushed for " + key);
165                 }
166
167                 CacheHttpServletResponseWrapper cacheResponse = new CacheHttpServletResponseWrapper((HttpServletResponse JavaDoc) response, fragmentRequest, time * 1000L, lastModified, expires, cacheControlMaxAge);
168                 chain.doFilter(request, cacheResponse);
169                 cacheResponse.flushBuffer();
170
171                 // Only cache if the response is cacheable
172
if (isCacheable(cacheResponse)) {
173                     // get the cache groups of the content
174
String JavaDoc[] groups = cacheGroupsProvider.createCacheGroups(httpRequest, admin, cache);
175                     // Store as the cache content the result of the response
176
cache.putInCache(key, cacheResponse.getContent(), groups, expiresRefreshPolicy, null);
177                     updateSucceeded = true;
178                 }
179             } finally {
180                 if (!updateSucceeded) {
181                     cache.cancelUpdate(key);
182                 }
183             }
184         }
185     }
186
187     /**
188      * Initialize the filter. This retrieves a {@link ServletCacheAdministrator}
189      * instance and configures the filter based on any initialization parameters.<p>
190      * The supported initialization parameters are:
191      * <ul>
192      *
193      * <li><b>time</b> - the default time (in seconds) to cache content for. The default
194      * value is 3600 seconds (one hour).</li>
195      *
196      * <li><b>scope</b> - the default scope to cache content in. Acceptable values
197      * are <code>application</code> (default), <code>session</code>, <code>request</code> and
198      * <code>page</code>.</li>
199      *
200      * <li><b>fragment</b> - defines if this filter handles fragments of a page. Acceptable values
201      * are <code>auto</code> (default) for auto detect, <code>no</code> and <code>yes</code>.</li>
202      *
203      * <li><b>nocache</b> - defines which objects shouldn't be cached. Acceptable values
204      * are <code>off</code> (default) and <code>sessionIdInURL</code> if the session id is
205      * contained in the URL.</li>
206      *
207      * <li><b>lastModified</b> - defines if the last modified header will be sent in the response. Acceptable values are
208      * <code>off</code> for don't sending the header, even it is set in the filter chain,
209      * <code>on</code> for sending it if it is set in the filter chain and
210      * <code>inital</code> (default) the last modified information will be set based on the current time and changes are allowed.</li>
211      *
212      * <li><b>expires</b> - defines if the expires header will be sent in the response. Acceptable values are
213      * <code>off</code> for don't sending the header, even it is set in the filter chain,
214      * <code>on</code> (default) for sending it if it is set in the filter chain and
215      * <code>time</code> the expires information will be intialized based on the time parameter and creation time of the content.</li>
216      *
217      * <li><b>max-age</b> - defines the cache control response header max-age. Acceptable values are
218      * <code>no init</code> for don't initializing the max-age cache control,
219      * <code>time</code> the max-age information will be based on the time parameter and creation time of the content (expiration timestamp minus current timestamp), and</li>
220      * <code>[positive integer]</code> value constant in seconds to be set in every response, the default value is 60.</li>
221      *
222      * <li><b>ICacheKeyProvider</b> - Class implementing the interface <code>ICacheKeyProvider</code>.
223      * A developer can implement a method which provides cache keys based on the request,
224      * the servlect cache administrator and cache.</li>
225      *
226      * <li><b>ICacheGroupsProvider</b> - Class implementing the interface <code>ICacheGroupsProvider</code>.
227      * A developer can implement a method which provides cache groups based on the request,
228      * the servlect cache administrator and cache.</li>
229      *
230      * <li><b>EntryRefreshPolicy</b> - Class implementing the interface <code>EntryRefreshPolicy</code>.
231      * A developer can implement a class which provides a different custom cache invalidation policy for a specific cache entry.
232      * If not specified, the default policy is timed entry expiry as specified with the <b>time</b> parameter described above.
233      * </li>
234      *
235      * @param filterConfig The filter configuration
236      */

237     public void init(FilterConfig filterConfig) {
238         //Get whatever settings we want...
239
config = filterConfig;
240         admin = ServletCacheAdministrator.getInstance(config.getServletContext());
241
242         // filter parameter time
243
try {
244             time = Integer.parseInt(config.getInitParameter("time"));
245         } catch (Exception JavaDoc e) {
246             log.info("Could not get init parameter 'time', defaulting to one hour.");
247         }
248         
249         // setting the refresh period for this cache filter
250
expiresRefreshPolicy = new ExpiresRefreshPolicy(time);
251
252         // filter parameter scope
253
try {
254             String JavaDoc scopeString = config.getInitParameter("scope");
255
256             if (scopeString.equals("session")) {
257                 cacheScope = PageContext.SESSION_SCOPE;
258             } else if (scopeString.equals("application")) {
259                 cacheScope = PageContext.APPLICATION_SCOPE;
260             } else if (scopeString.equals("request")) {
261                 cacheScope = PageContext.REQUEST_SCOPE;
262             } else if (scopeString.equals("page")) {
263                 cacheScope = PageContext.PAGE_SCOPE;
264             }
265         } catch (Exception JavaDoc e) {
266             log.info("Could not get init parameter 'scope', defaulting to 'application'.");
267         }
268
269         // filter parameter cron
270
cron = config.getInitParameter("cron");
271
272         // filter parameter fragment
273
try {
274             String JavaDoc fragmentString = config.getInitParameter("fragment");
275             
276             if (fragmentString.equals("no")) {
277                 fragment = FRAGMENT_NO;
278             } else if (fragmentString.equals("yes")) {
279                 fragment = FRAGMENT_YES;
280             } else if (fragmentString.equalsIgnoreCase("auto")) {
281                 fragment = FRAGMENT_AUTODETECT;
282             }
283         } catch (Exception JavaDoc e) {
284             log.info("Could not get init parameter 'fragment', defaulting to 'auto detect'.");
285         }
286         
287         // filter parameter nocache
288
try {
289             String JavaDoc nocacheString = config.getInitParameter("nocache");
290             
291             if (nocacheString.equals("off")) {
292                 nocache = NOCACHE_OFF;
293             } else if (nocacheString.equalsIgnoreCase("sessionIdInURL")) {
294                 nocache = NOCACHE_SESSION_ID_IN_URL;
295             }
296         } catch (Exception JavaDoc e) {
297             log.info("Could not get init parameter 'nocache', defaulting to 'off'.");
298         }
299
300         // filter parameter last modified
301
try {
302             String JavaDoc lastModifiedString = config.getInitParameter("lastModified");
303             
304             if (lastModifiedString.equals("off")) {
305                 lastModified = LAST_MODIFIED_OFF;
306             } else if (lastModifiedString.equals("on")) {
307                 lastModified = LAST_MODIFIED_ON;
308             } else if (lastModifiedString.equalsIgnoreCase("initial")) {
309                 lastModified = LAST_MODIFIED_INITIAL;
310             }
311         } catch (Exception JavaDoc e) {
312             log.info("Could not get init parameter 'lastModified', defaulting to 'initial'.");
313         }
314         
315         // filter parameter expires
316
try {
317             String JavaDoc expiresString = config.getInitParameter("expires");
318             
319             if (expiresString.equals("off")) {
320                 expires = EXPIRES_OFF;
321             } else if (expiresString.equals("on")) {
322                 expires = EXPIRES_ON;
323             } else if (expiresString.equalsIgnoreCase("time")) {
324                 expires = EXPIRES_TIME;
325             }
326         } catch (Exception JavaDoc e) {
327             log.info("Could not get init parameter 'expires', defaulting to 'on'.");
328         }
329
330         // filter parameter Cache-Control
331
try {
332             String JavaDoc cacheControlMaxAgeString = config.getInitParameter("max-age");
333             
334             if (cacheControlMaxAgeString.equals("no init")) {
335                 cacheControlMaxAge = MAX_AGE_NO_INIT;
336             } else if (cacheControlMaxAgeString.equals("time")) {
337                 cacheControlMaxAge = MAX_AGE_TIME;
338             } else {
339                 cacheControlMaxAge = Long.parseLong(cacheControlMaxAgeString);
340                 if (cacheControlMaxAge >= 0) {
341                     // declare the cache control as a constant
342
cacheControlMaxAge = - cacheControlMaxAge;
343                 } else {
344                     log.warn("Init parameter 'max-age' must be at least a positive integer, defaulting to 'time'. ");
345                     cacheControlMaxAge = 60;
346                 }
347             }
348         } catch (Exception JavaDoc e) {
349             log.info("Could not get init parameter 'max-age', defaulting to 'time'.");
350         }
351
352         // filter parameter ICacheKeyProvider
353
ICacheKeyProvider cacheKeyProvider = (ICacheKeyProvider)instantiateFromInitParam("ICacheKeyProvider", ICacheKeyProvider.class, this.getClass().getName());
354         if (cacheKeyProvider != null) {
355             this.cacheKeyProvider = cacheKeyProvider;
356         }
357
358         // filter parameter ICacheGroupsProvider
359
ICacheGroupsProvider cacheGroupsProvider = (ICacheGroupsProvider)instantiateFromInitParam("ICacheGroupsProvider", ICacheGroupsProvider.class, this.getClass().getName());
360         if (cacheGroupsProvider != null) {
361             this.cacheGroupsProvider = cacheGroupsProvider;
362         }
363         
364         // filter parameter EntryRefreshPolicy
365
EntryRefreshPolicy expiresRefreshPolicy = (EntryRefreshPolicy)instantiateFromInitParam("EntryRefreshPolicy", EntryRefreshPolicy.class, this.expiresRefreshPolicy.getClass().getName());
366         if (expiresRefreshPolicy != null) {
367             this.expiresRefreshPolicy = expiresRefreshPolicy;
368         }
369     }
370
371     private Object JavaDoc instantiateFromInitParam(String JavaDoc classInitParam, Class JavaDoc interfaceClass, String JavaDoc defaultClassName) {
372         String JavaDoc className = config.getInitParameter(classInitParam);
373         if (className == null) {
374             log.info("Could not get init parameter '" + classInitParam + "', defaulting to " + defaultClassName + ".");
375             return null;
376         } else {
377             try {
378                 Class JavaDoc clazz = Class.forName(className);
379                 if (!interfaceClass.isAssignableFrom(clazz)) {
380                     log.error("Specified class '" + className + "' does not implement" + interfaceClass.getName() + ". Using default " + defaultClassName + ".");
381                     return null;
382                 } else {
383                     return clazz.newInstance();
384                 }
385             } catch (ClassNotFoundException JavaDoc e) {
386                 log.error("Class '" + className + "' not found. Defaulting to " + defaultClassName + ".", e);
387             } catch (InstantiationException JavaDoc e) {
388                 log.error("Class '" + className + "' could not be instantiated because it is not a concrete class. Using default class " + defaultClassName + ".", e);
389             } catch (IllegalAccessException JavaDoc e) {
390                 log.error("Class '"+ className+ "' could not be instantiated because it is not public. Using default class " + defaultClassName + ".", e);
391             }
392             return null;
393         }
394     }
395     
396     /**
397      * @see com.opensymphony.oscache.web.filter.ICacheKeyProvider#createCacheKey(javax.servlet.http.HttpServletRequest, ServletCacheAdministrator, Cache)
398      */

399     public String JavaDoc createCacheKey(HttpServletRequest JavaDoc httpRequest, ServletCacheAdministrator scAdmin, Cache cache) {
400         return scAdmin.generateEntryKey(null, httpRequest, cacheScope);
401     }
402
403     /**
404      * @see com.opensymphony.oscache.web.filter.ICacheGroupsProvider#createCacheGroups(javax.servlet.http.HttpServletRequest, ServletCacheAdministrator, Cache)
405      */

406     public String JavaDoc[] createCacheGroups(HttpServletRequest JavaDoc httpRequest, ServletCacheAdministrator scAdmin, Cache cache) {
407         return null;
408     }
409
410     /**
411      * Checks if the request is a fragment in a page.
412      *
413      * According to Java Servlet API 2.2 (8.2.1 Dispatching Requests, Included
414      * Request Parameters), when a servlet is being used from within an include,
415      * the attribute <code>javax.servlet.include.request_uri</code> is set.
416      * According to Java Servlet API 2.3 this is excepted for servlets obtained
417      * by using the getNamedDispatcher method.
418      *
419      * @param request the to be handled request
420      * @return true if the request is a fragment in a page
421      */

422     protected boolean isFragment(HttpServletRequest JavaDoc request) {
423         if (fragment == FRAGMENT_AUTODETECT) {
424             return request.getAttribute("javax.servlet.include.request_uri") != null;
425         } else {
426             return (fragment == FRAGMENT_NO) ? false : true;
427         }
428     }
429
430     /**
431      * Checks if the request was filtered before, so
432      * guarantees to be executed once per request. You
433      * can override this methods to define a more specific
434      * behaviour.
435      *
436      * @param request checks if the request was filtered before.
437      * @return true if it is the first execution
438      */

439     protected boolean isFilteredBefore(ServletRequest JavaDoc request) {
440         return request.getAttribute(REQUEST_FILTERED) != null;
441     }
442
443     /**
444      * isCacheable is a method allowing a subclass to decide if a request is
445      * cachable or not.
446      *
447      * @param request The servlet request
448      * @return Returns a boolean indicating if the request can be cached or not.
449      */

450     protected boolean isCacheable(ServletRequest JavaDoc request) {
451         // TODO implement CACHE-137 and CACHE-141 here
452
boolean cachable = request instanceof HttpServletRequest JavaDoc;
453
454         if (cachable) {
455             HttpServletRequest JavaDoc requestHttp = (HttpServletRequest JavaDoc) request;
456             if (nocache == NOCACHE_SESSION_ID_IN_URL) { // don't cache requests if session id is in the URL
457
cachable = !requestHttp.isRequestedSessionIdFromURL();
458             }
459         }
460
461         if (log.isDebugEnabled()) {
462             log.debug("<cache>: the request " + ((cachable) ? "is" : "is not") + " cachable.");
463         }
464         
465         return cachable;
466     }
467     
468     /**
469      * isCacheable is a method allowing subclass to decide if a response is
470      * cachable or not.
471      *
472      * @param cacheResponse The HTTP servlet response
473      * @return Returns a boolean indicating if the response can be cached or not.
474      */

475     protected boolean isCacheable(CacheHttpServletResponseWrapper cacheResponse) {
476         // TODO implement CACHE-137 and CACHE-141 here
477
// Only cache if the response was 200
478
boolean cachable = cacheResponse.getStatus() == HttpServletResponse.SC_OK;
479
480         if (log.isDebugEnabled()) {
481             log.debug("<cache>: the response " + ((cachable) ? "is" : "is not") + " cachable.");
482         }
483         
484         return cachable;
485     }
486
487     /**
488      * Check if the client browser support gzip compression.
489      *
490      * @param request the http request
491      * @return true if client browser supports GZIP
492      */

493     protected boolean acceptsGZipEncoding(HttpServletRequest JavaDoc request) {
494         String JavaDoc acceptEncoding = request.getHeader(HEADER_ACCEPT_ENCODING);
495         return (acceptEncoding != null) && (acceptEncoding.indexOf("gzip") != -1);
496     }
497
498 }
499
Popular Tags