Clover coverage report -
Coverage timestamp: Mo Mrz 6 2006 19:30:45 CET
file stats: LOC: 466   Methods: 11
NCLOC: 249   Classes: 1
 
 Source file Conditionals Statements Methods TOTAL
CacheFilter.java 0% 0% 0% 0%
coverage
 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;
 16   
 17    import javax.servlet.*;
 18    import javax.servlet.http.HttpServletRequest;
 19    import javax.servlet.http.HttpServletResponse;
 20    import javax.servlet.jsp.PageContext;
 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.11 $
 31    */
 32    public class CacheFilter implements Filter, ICacheKeyProvider, ICacheGroupsProvider {
 33    // Header
 34    public static final String HEADER_LAST_MODIFIED = "Last-Modified";
 35    public static final String HEADER_CONTENT_TYPE = "Content-Type";
 36    public static final String HEADER_CONTENT_ENCODING = "Content-Encoding";
 37    public static final String HEADER_EXPIRES = "Expires";
 38    public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
 39    public static final String HEADER_CACHE_CONTROL = "Cache-control";
 40    public static final String 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    // request attribute to avoid reentrance
 62    private final static String REQUEST_FILTERED = "__oscache_filtered";
 63   
 64    // the policy for the expires header
 65    private EntryRefreshPolicy expiresRefreshPolicy;
 66   
 67    // the logger
 68    private final Log log = LogFactory.getLog(this.getClass());
 69   
 70    // filter variables
 71    private FilterConfig config;
 72    private ServletCacheAdministrator admin = null;
 73    private int cacheScope = PageContext.APPLICATION_SCOPE; // filter scope - default is APPLICATION
 74    private int fragment = FRAGMENT_AUTODETECT; // defines if this filter handles fragments of a page - default is auto detect
 75    private int time = 60 * 60; // time before cache should be refreshed - default one hour (in seconds)
 76    private String cron = null; // A cron expression that determines when this cached content will expire - default is null
 77    private int nocache = NOCACHE_OFF; // defines special no cache option for the requests - default is off
 78    private long lastModified = LAST_MODIFIED_INITIAL; // defines if the last-modified-header will be sent - default is intial setting
 79    private long expires = EXPIRES_ON; // defines if the expires-header will be sent - default is on
 80    private ICacheKeyProvider cacheKeyProvider = this; // the provider of the cache key - default is the CacheFilter itselfs
 81    private ICacheGroupsProvider cacheGroupsProvider = this; // the provider of the cache groups - default is the CacheFilter itselfs
 82   
 83    /**
 84    * Filter clean-up
 85    */
 86  0 public void destroy() {
 87    //Not much to do...
 88    }
 89   
 90    /**
 91    * The doFilter call caches the response by wrapping the <code>HttpServletResponse</code>
 92    * object so that the output stream can be caught. This works by splitting off the output
 93    * stream into two with the {@link SplitServletOutputStream} class. One stream gets written
 94    * out to the response as normal, the other is fed into a byte array inside a {@link ResponseContent}
 95    * object.
 96    *
 97    * @param request The servlet request
 98    * @param response The servlet response
 99    * @param chain The filter chain
 100    * @throws ServletException IOException
 101    */
 102  0 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
 103  0 if (log.isInfoEnabled()) {
 104  0 log.info("<cache>: filter in scope " + cacheScope);
 105    }
 106   
 107    // avoid reentrance (CACHE-128) and check if request is cacheable
 108  0 if (isFilteredBefore(request) || !isCacheable(request)) {
 109  0 chain.doFilter(request, response);
 110  0 return;
 111    }
 112  0 request.setAttribute(REQUEST_FILTERED, Boolean.TRUE);
 113   
 114  0 HttpServletRequest httpRequest = (HttpServletRequest) request;
 115   
 116    // checks if the response will be a fragment of a page
 117  0 boolean fragmentRequest = isFragment(httpRequest);
 118   
 119    // avoid useless session creation for application scope pages (CACHE-129)
 120  0 Cache cache;
 121  0 if (cacheScope == PageContext.SESSION_SCOPE) {
 122  0 cache = admin.getSessionScopeCache(httpRequest.getSession(true));
 123    } else {
 124  0 cache = admin.getAppScopeCache(config.getServletContext());
 125    }
 126   
 127    // generate the cache entry key
 128  0 String key = cacheKeyProvider.createCacheKey(httpRequest, admin, cache);
 129   
 130  0 try {
 131  0 ResponseContent respContent = (ResponseContent) cache.getFromCache(key, time, cron);
 132   
 133  0 if (log.isInfoEnabled()) {
 134  0 log.info("<cache>: Using cached entry for " + key);
 135    }
 136   
 137  0 boolean acceptsGZip = false;
 138  0 if ((!fragmentRequest) && (lastModified != LAST_MODIFIED_OFF)) {
 139  0 long clientLastModified = httpRequest.getDateHeader(HEADER_IF_MODIFIED_SINCE); // will return -1 if no header...
 140   
 141    // only reply with SC_NOT_MODIFIED
 142    // if the client has already the newest page and the response isn't a fragment in a page
 143  0 if ((clientLastModified != -1) && (clientLastModified >= respContent.getLastModified())) {
 144  0 ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_NOT_MODIFIED);
 145  0 return;
 146    }
 147   
 148  0 acceptsGZip = respContent.isContentGZiped() && acceptsGZipEncoding(httpRequest);
 149    }
 150   
 151  0 respContent.writeTo(response, fragmentRequest, acceptsGZip);
 152    // acceptsGZip is used for performance reasons above; use the following line for CACHE-49
 153    // respContent.writeTo(response, fragmentRequest, acceptsGZipEncoding(httpRequest));
 154    } catch (NeedsRefreshException nre) {
 155  0 boolean updateSucceeded = false;
 156   
 157  0 try {
 158  0 if (log.isInfoEnabled()) {
 159  0 log.info("<cache>: New cache entry, cache stale or cache scope flushed for " + key);
 160    }
 161   
 162  0 CacheHttpServletResponseWrapper cacheResponse = new CacheHttpServletResponseWrapper((HttpServletResponse) response, fragmentRequest, time * 1000L, lastModified, expires);
 163  0 chain.doFilter(request, cacheResponse);
 164  0 cacheResponse.flushBuffer();
 165   
 166    // Only cache if the response is cacheable
 167  0 if (isCacheable(cacheResponse)) {
 168    // get the cache groups of the content
 169  0 String[] groups = cacheGroupsProvider.createCacheGroups(httpRequest, admin, cache);
 170    // Store as the cache content the result of the response
 171  0 cache.putInCache(key, cacheResponse.getContent(), groups, expiresRefreshPolicy, null);
 172  0 updateSucceeded = true;
 173    }
 174    } finally {
 175  0 if (!updateSucceeded) {
 176  0 cache.cancelUpdate(key);
 177    }
 178    }
 179    }
 180    }
 181   
 182    /**
 183    * Initialize the filter. This retrieves a {@link ServletCacheAdministrator}
 184    * instance and configures the filter based on any initialization parameters.<p>
 185    * The supported initialization parameters are:
 186    * <ul>
 187    *
 188    * <li><b>time</b> - the default time (in seconds) to cache content for. The default
 189    * value is 3600 seconds (one hour).</li>
 190    *
 191    * <li><b>scope</b> - the default scope to cache content in. Acceptable values
 192    * are <code>application</code> (default), <code>session</code>, <code>request</code> and
 193    * <code>page</code>.</li>
 194    *
 195    * <li><b>fragment</b> - defines if this filter handles fragments of a page. Acceptable values
 196    * are <code>auto</code> (default) for auto detect, <code>no</code> and <code>yes</code>.</li>
 197    *
 198    * <li><b>nocache</b> - defines which objects shouldn't be cached. Acceptable values
 199    * are <code>off</code> (default) and <code>sessionIdInURL</code> if the session id is
 200    * contained in the URL.</li>
 201    *
 202    * <li><b>lastModified</b> - defines if the last modified header will be sent in the response. Acceptable values are
 203    * <code>off</code> for don't sending the header, even it is set in the filter chain,
 204    * <code>on</code> for sending it if it is set in the filter chain and
 205    * <code>inital</code> (default) the last modified information will be set based on the current time and changes are allowed.</li>
 206    *
 207    * <li><b>expires</b> - defines if the expires 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> (default) for sending it if it is set in the filter chain and
 210    * <code>time</code> the expires information will be intialized based on the time parameter and creation time of the content.</li>
 211    *
 212    * <li><b>ICacheKeyProvider</b> - Class implementing the interface <code>ICacheKeyProvider</code>.
 213    * A developer can implement a method which provides cache keys based on the request,
 214    * the servlect cache administrator and cache.</li>
 215    *
 216    * <li><b>ICacheGroupsProvider</b> - Class implementing the interface <code>ICacheGroupsProvider</code>.
 217    * A developer can implement a method which provides cache groups based on the request,
 218    * the servlect cache administrator and cache.</li>
 219    *
 220    * <li><b>EntryRefreshPolicy</b> - Class implementing the interface <code>EntryRefreshPolicy</code>.
 221    * A developer can implement a class which provides a different custom cache invalidation policy for a specific cache entry.
 222    * If not specified, the default policy is timed entry expiry as specified with the <b>time</b> parameter described above.
 223    * </li>
 224    *
 225    * @param filterConfig The filter configuration
 226    */
 227  0 public void init(FilterConfig filterConfig) {
 228    //Get whatever settings we want...
 229  0 config = filterConfig;
 230  0 admin = ServletCacheAdministrator.getInstance(config.getServletContext());
 231   
 232    // filter parameter time
 233  0 try {
 234  0 time = Integer.parseInt(config.getInitParameter("time"));
 235    } catch (Exception e) {
 236  0 log.info("Could not get init parameter 'time', defaulting to one hour.");
 237    }
 238   
 239    // setting the refresh period for this cache filter
 240  0 expiresRefreshPolicy = new ExpiresRefreshPolicy(time);
 241   
 242    // filter parameter scope
 243  0 try {
 244  0 String scopeString = config.getInitParameter("scope");
 245   
 246  0 if (scopeString.equals("session")) {
 247  0 cacheScope = PageContext.SESSION_SCOPE;
 248  0 } else if (scopeString.equals("application")) {
 249  0 cacheScope = PageContext.APPLICATION_SCOPE;
 250  0 } else if (scopeString.equals("request")) {
 251  0 cacheScope = PageContext.REQUEST_SCOPE;
 252  0 } else if (scopeString.equals("page")) {
 253  0 cacheScope = PageContext.PAGE_SCOPE;
 254    }
 255    } catch (Exception e) {
 256  0 log.info("Could not get init parameter 'scope', defaulting to 'application'.");
 257    }
 258   
 259    // filter parameter cron
 260  0 cron = config.getInitParameter("cron");
 261   
 262    // filter parameter fragment
 263  0 try {
 264  0 String fragmentString = config.getInitParameter("fragment");
 265   
 266  0 if (fragmentString.equals("no")) {
 267  0 fragment = FRAGMENT_NO;
 268  0 } else if (fragmentString.equals("yes")) {
 269  0 fragment = FRAGMENT_YES;
 270  0 } else if (fragmentString.equalsIgnoreCase("auto")) {
 271  0 fragment = FRAGMENT_AUTODETECT;
 272    }
 273    } catch (Exception e) {
 274  0 log.info("Could not get init parameter 'fragment', defaulting to 'auto detect'.");
 275    }
 276   
 277    // filter parameter nocache
 278  0 try {
 279  0 String nocacheString = config.getInitParameter("nocache");
 280   
 281  0 if (nocacheString.equals("off")) {
 282  0 nocache = NOCACHE_OFF;
 283  0 } else if (nocacheString.equalsIgnoreCase("sessionIdInURL")) {
 284  0 nocache = NOCACHE_SESSION_ID_IN_URL;
 285    }
 286    } catch (Exception e) {
 287  0 log.info("Could not get init parameter 'nocache', defaulting to 'off'.");
 288    }
 289   
 290    // filter parameter last modified
 291  0 try {
 292  0 String lastModifiedString = config.getInitParameter("lastModified");
 293   
 294  0 if (lastModifiedString.equals("off")) {
 295  0 lastModified = LAST_MODIFIED_OFF;
 296  0 } else if (lastModifiedString.equals("on")) {
 297  0 lastModified = LAST_MODIFIED_ON;
 298  0 } else if (lastModifiedString.equalsIgnoreCase("initial")) {
 299  0 lastModified = LAST_MODIFIED_INITIAL;
 300    }
 301    } catch (Exception e) {
 302  0 log.info("Could not get init parameter 'lastModified', defaulting to 'initial'.");
 303    }
 304   
 305    // filter parameter expires
 306  0 try {
 307  0 String expiresString = config.getInitParameter("expires");
 308   
 309  0 if (expiresString.equals("off")) {
 310  0 expires = EXPIRES_OFF;
 311  0 } else if (expiresString.equals("on")) {
 312  0 expires = EXPIRES_ON;
 313  0 } else if (expiresString.equalsIgnoreCase("time")) {
 314  0 expires = EXPIRES_TIME;
 315    }
 316    } catch (Exception e) {
 317  0 log.info("Could not get init parameter 'expires', defaulting to 'on'.");
 318    }
 319   
 320    // filter parameter ICacheKeyProvider
 321  0 ICacheKeyProvider cacheKeyProvider = (ICacheKeyProvider)instantiateFromInitParam("ICacheKeyProvider", ICacheKeyProvider.class, this.getClass().getName());
 322  0 if (cacheKeyProvider != null) {
 323  0 this.cacheKeyProvider = cacheKeyProvider;
 324    }
 325   
 326    // filter parameter ICacheGroupsProvider
 327  0 ICacheGroupsProvider cacheGroupsProvider = (ICacheGroupsProvider)instantiateFromInitParam("ICacheGroupsProvider", ICacheGroupsProvider.class, this.getClass().getName());
 328  0 if (cacheGroupsProvider != null) {
 329  0 this.cacheGroupsProvider = cacheGroupsProvider;
 330    }
 331   
 332    // filter parameter EntryRefreshPolicy
 333  0 EntryRefreshPolicy expiresRefreshPolicy = (EntryRefreshPolicy)instantiateFromInitParam("EntryRefreshPolicy", EntryRefreshPolicy.class, this.expiresRefreshPolicy.getClass().getName());
 334  0 if (expiresRefreshPolicy != null) {
 335  0 this.expiresRefreshPolicy = expiresRefreshPolicy;
 336    }
 337    }
 338   
 339  0 private Object instantiateFromInitParam(String classInitParam, Class interfaceClass, String defaultClassName) {
 340  0 String className = config.getInitParameter(classInitParam);
 341  0 if (className == null) {
 342  0 log.info("Could not get init parameter '" + classInitParam + "', defaulting to " + defaultClassName + ".");
 343  0 return null;
 344    } else {
 345  0 try {
 346  0 Class clazz = Class.forName(className);
 347  0 if (!interfaceClass.isAssignableFrom(clazz)) {
 348  0 log.error("Specified class '" + className + "' does not implement" + interfaceClass.getName() + ". Using default " + defaultClassName + ".");
 349  0 return null;
 350    } else {
 351  0 return clazz.newInstance();
 352    }
 353    } catch (ClassNotFoundException e) {
 354  0 log.error("Class '" + className + "' not found. Defaulting to " + defaultClassName + ".", e);
 355    } catch (InstantiationException e) {
 356  0 log.error("Class '" + className + "' could not be instantiated because it is not a concrete class. Using default class " + defaultClassName + ".", e);
 357    } catch (IllegalAccessException e) {
 358  0 log.error("Class '"+ className+ "' could not be instantiated because it is not public. Using default class " + defaultClassName + ".", e);
 359    }
 360  0 return null;
 361    }
 362    }
 363   
 364    /**
 365    * @see com.opensymphony.oscache.web.filter.ICacheKeyProvider#createCacheKey(javax.servlet.http.HttpServletRequest, ServletCacheAdministrator, Cache)
 366    */
 367  0 public String createCacheKey(HttpServletRequest httpRequest, ServletCacheAdministrator scAdmin, Cache cache) {
 368  0 return scAdmin.generateEntryKey(null, httpRequest, cacheScope);
 369    }
 370   
 371    /**
 372    * @see com.opensymphony.oscache.web.filter.ICacheGroupsProvider#createCacheGroups(javax.servlet.http.HttpServletRequest, ServletCacheAdministrator, Cache)
 373    */
 374  0 public String[] createCacheGroups(HttpServletRequest httpRequest, ServletCacheAdministrator scAdmin, Cache cache) {
 375  0 return null;
 376    }
 377   
 378    /**
 379    * Checks if the request is a fragment in a page.
 380    *
 381    * According to Java Servlet API 2.2 (8.2.1 Dispatching Requests, Included
 382    * Request Parameters), when a servlet is being used from within an include,
 383    * the attribute <code>javax.servlet.include.request_uri</code> is set.
 384    * According to Java Servlet API 2.3 this is excepted for servlets obtained
 385    * by using the getNamedDispatcher method.
 386    *
 387    * @param request the to be handled request
 388    * @return true if the request is a fragment in a page
 389    */
 390  0 protected boolean isFragment(HttpServletRequest request) {
 391  0 if (fragment == FRAGMENT_AUTODETECT) {
 392  0 return request.getAttribute("javax.servlet.include.request_uri") != null;
 393    } else {
 394  0 return (fragment == FRAGMENT_NO) ? false : true;
 395    }
 396    }
 397   
 398    /**
 399    * Checks if the request was filtered before, so
 400    * guarantees to be executed once per request. You
 401    * can override this methods to define a more specific
 402    * behaviour.
 403    *
 404    * @param request checks if the request was filtered before.
 405    * @return true if it is the first execution
 406    */
 407  0 protected boolean isFilteredBefore(ServletRequest request) {
 408  0 return request.getAttribute(REQUEST_FILTERED) != null;
 409    }
 410   
 411    /**
 412    * isCacheable is a method allowing a subclass to decide if a request is
 413    * cachable or not.
 414    *
 415    * @param request The servlet request
 416    * @return Returns a boolean indicating if the request can be cached or not.
 417    */
 418  0 protected boolean isCacheable(ServletRequest request) {
 419    // TODO implement CACHE-137 and CACHE-141 here
 420  0 boolean cachable = request instanceof HttpServletRequest;
 421   
 422  0 if (cachable) {
 423  0 HttpServletRequest requestHttp = (HttpServletRequest) request;
 424  0 if (nocache == NOCACHE_SESSION_ID_IN_URL) { // don't cache requests if session id is in the URL
 425  0 cachable = !requestHttp.isRequestedSessionIdFromURL();
 426    }
 427    }
 428   
 429  0 if (log.isDebugEnabled()) {
 430  0 log.debug("<cache>: the request " + ((cachable) ? "is" : "is not") + " cachable.");
 431    }
 432   
 433  0 return cachable;
 434    }
 435   
 436    /**
 437    * isCacheable is a method allowing subclass to decide if a response is
 438    * cachable or not.
 439    *
 440    * @param cacheResponse The HTTP servlet response
 441    * @return Returns a boolean indicating if the response can be cached or not.
 442    */
 443  0 protected boolean isCacheable(CacheHttpServletResponseWrapper cacheResponse) {
 444    // TODO implement CACHE-137 and CACHE-141 here
 445    // Only cache if the response was 200
 446  0 boolean cachable = cacheResponse.getStatus() == HttpServletResponse.SC_OK;
 447   
 448  0 if (log.isDebugEnabled()) {
 449  0 log.debug("<cache>: the response " + ((cachable) ? "is" : "is not") + " cachable.");
 450    }
 451   
 452  0 return cachable;
 453    }
 454   
 455    /**
 456    * Check if the client browser support gzip compression.
 457    *
 458    * @param request the http request
 459    * @return true if client browser supports GZIP
 460    */
 461  0 protected boolean acceptsGZipEncoding(HttpServletRequest request) {
 462  0 String acceptEncoding = request.getHeader(HEADER_ACCEPT_ENCODING);
 463  0 return (acceptEncoding != null) && (acceptEncoding.indexOf("gzip") != -1);
 464    }
 465   
 466    }