/* * Copyright (c) 2002-2003 by OpenSymphony * All rights reserved. */ package com.opensymphony.oscache.web.filter; import com.opensymphony.oscache.base.Cache; import com.opensymphony.oscache.base.Config; import com.opensymphony.oscache.base.EntryRefreshPolicy; import com.opensymphony.oscache.base.NeedsRefreshException; import com.opensymphony.oscache.web.ServletCacheAdministrator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.IOException; import java.io.InputStream; import java.util.Properties; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.jsp.PageContext; /** * CacheFilter is a filter that allows for server-side caching of post-processed servlet content.

* * It also gives great programatic control over refreshing, flushing and updating the cache.

* * @author Serge Knystautas * @author Mike Cannon-Brookes * @author Lars Torunski * @version $Revision: 362 $ */ public class CacheFilter implements Filter, ICacheKeyProvider, ICacheGroupsProvider { // Header public static final String HEADER_LAST_MODIFIED = "Last-Modified"; public static final String HEADER_CONTENT_TYPE = "Content-Type"; public static final String HEADER_CONTENT_ENCODING = "Content-Encoding"; public static final String HEADER_EXPIRES = "Expires"; public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; public static final String HEADER_CACHE_CONTROL = "Cache-Control"; public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding"; // Fragment parameter public static final int FRAGMENT_AUTODETECT = -1; public static final int FRAGMENT_NO = 0; public static final int FRAGMENT_YES = 1; // No cache parameter public static final int NOCACHE_OFF = 0; public static final int NOCACHE_SESSION_ID_IN_URL = 1; // Last Modified parameter public static final long LAST_MODIFIED_OFF = 0; public static final long LAST_MODIFIED_ON = 1; public static final long LAST_MODIFIED_INITIAL = -1; // Expires parameter public static final long EXPIRES_OFF = 0; public static final long EXPIRES_ON = 1; public static final long EXPIRES_TIME = -1; // Cache Control public static final long MAX_AGE_NO_INIT = Long.MIN_VALUE; public static final long MAX_AGE_TIME = Long.MAX_VALUE; // request attribute to avoid reentrance private final static String REQUEST_FILTERED = "__oscache_filtered"; // the policy for the expires header private EntryRefreshPolicy expiresRefreshPolicy; // the logger private final Log log = LogFactory.getLog(this.getClass()); // filter variables private FilterConfig config; private ServletCacheAdministrator admin = null; private int cacheScope = PageContext.APPLICATION_SCOPE; // filter scope - default is APPLICATION private int fragment = FRAGMENT_AUTODETECT; // defines if this filter handles fragments of a page - default is auto detect private int time = 60 * 60; // time before cache should be refreshed - default one hour (in seconds) private String cron = null; // A cron expression that determines when this cached content will expire - default is null private int nocache = NOCACHE_OFF; // defines special no cache option for the requests - default is off private long lastModified = LAST_MODIFIED_INITIAL; // defines if the last-modified-header will be sent - default is intial setting private long expires = EXPIRES_ON; // defines if the expires-header will be sent - default is on private long cacheControlMaxAge = -60; // defines which max-age in Cache-Control to be set - default is 60 seconds for max-age private ICacheKeyProvider cacheKeyProvider = this; // the provider of the cache key - default is the CacheFilter itselfs private ICacheGroupsProvider cacheGroupsProvider = this; // the provider of the cache groups - default is the CacheFilter itselfs /** * Filter clean-up */ public void destroy() { //Not much to do... } /** * The doFilter call caches the response by wrapping the HttpServletResponse * object so that the output stream can be caught. This works by splitting off the output * stream into two with the {@link SplitServletOutputStream} class. One stream gets written * out to the response as normal, the other is fed into a byte array inside a {@link ResponseContent} * object. * * @param request The servlet request * @param response The servlet response * @param chain The filter chain * @throws ServletException IOException */ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { if (log.isInfoEnabled()) { log.info(": filter in scope " + cacheScope); } // avoid reentrance (CACHE-128) and check if request is cacheable if (isFilteredBefore(request) || !isCacheable(request)) { chain.doFilter(request, response); return; } request.setAttribute(REQUEST_FILTERED, Boolean.TRUE); HttpServletRequest httpRequest = (HttpServletRequest) request; // checks if the response will be a fragment of a page boolean fragmentRequest = isFragment(httpRequest); // avoid useless session creation for application scope pages (CACHE-129) Cache cache; if (cacheScope == PageContext.SESSION_SCOPE) { cache = admin.getSessionScopeCache(httpRequest.getSession(true)); } else { cache = admin.getAppScopeCache(config.getServletContext()); } // generate the cache entry key String key = cacheKeyProvider.createCacheKey(httpRequest, admin, cache); try { ResponseContent respContent = (ResponseContent) cache.getFromCache(key, time, cron); if (log.isInfoEnabled()) { log.info(": Using cached entry for " + key); } boolean acceptsGZip = false; if ((!fragmentRequest) && (lastModified != LAST_MODIFIED_OFF)) { long clientLastModified = httpRequest.getDateHeader(HEADER_IF_MODIFIED_SINCE); // will return -1 if no header... // only reply with SC_NOT_MODIFIED // if the client has already the newest page and the response isn't a fragment in a page if ((clientLastModified != -1) && (clientLastModified >= respContent.getLastModified())) { ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } acceptsGZip = respContent.isContentGZiped() && acceptsGZipEncoding(httpRequest); } respContent.writeTo(response, fragmentRequest, acceptsGZip); // acceptsGZip is used for performance reasons above; use the following line for CACHE-49 // respContent.writeTo(response, fragmentRequest, acceptsGZipEncoding(httpRequest)); } catch (NeedsRefreshException nre) { boolean updateSucceeded = false; try { if (log.isInfoEnabled()) { log.info(": New cache entry, cache stale or cache scope flushed for " + key); } CacheHttpServletResponseWrapper cacheResponse = new CacheHttpServletResponseWrapper((HttpServletResponse) response, fragmentRequest, time * 1000L, lastModified, expires, cacheControlMaxAge); chain.doFilter(request, cacheResponse); cacheResponse.flushBuffer(); // Only cache if the response is cacheable if (isCacheable(cacheResponse)) { // get the cache groups of the content String[] groups = cacheGroupsProvider.createCacheGroups(httpRequest, admin, cache); // Store as the cache content the result of the response cache.putInCache(key, cacheResponse.getContent(), groups, expiresRefreshPolicy, null); updateSucceeded = true; } } finally { if (!updateSucceeded) { cache.cancelUpdate(key); } } } } /** * Initialize the filter. This retrieves a {@link ServletCacheAdministrator} * instance and configures the filter based on any initialization parameters.

* The supported initialization parameters are: *