View Javadoc

1   /**
2    *  Copyright 2003-2007 Greg Luck
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  
17  package net.sf.ehcache.constructs.web.filter;
18  
19  import net.sf.ehcache.CacheException;
20  import net.sf.ehcache.CacheManager;
21  import net.sf.ehcache.Ehcache;
22  import net.sf.ehcache.Element;
23  import net.sf.ehcache.constructs.blocking.BlockingCache;
24  import net.sf.ehcache.constructs.blocking.LockTimeoutException;
25  import net.sf.ehcache.constructs.web.AlreadyCommittedException;
26  import net.sf.ehcache.constructs.web.AlreadyGzippedException;
27  import net.sf.ehcache.constructs.web.GenericResponseWrapper;
28  import net.sf.ehcache.constructs.web.PageInfo;
29  import net.sf.ehcache.constructs.web.ResponseHeadersNotModifiableException;
30  import net.sf.ehcache.constructs.web.ResponseUtil;
31  import net.sf.ehcache.constructs.web.SerializableCookie;
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  
35  import javax.servlet.FilterChain;
36  import javax.servlet.http.Cookie;
37  import javax.servlet.http.HttpServletRequest;
38  import javax.servlet.http.HttpServletResponse;
39  import java.io.ByteArrayOutputStream;
40  import java.io.IOException;
41  import java.util.Collection;
42  import java.util.Iterator;
43  import java.util.zip.DataFormatException;
44  
45  /**
46   * An abstract CachingFilter.
47   * <p/>
48   * This class should be sub-classed for each page to be cached.
49   * <p/>
50   * The filters must be declared in the web.xml deployment descriptor. Then a mapping from a web resource,
51   * such as a JSP, Servlet or static resouce needs to be defined. Finally, a succession of mappings can be used
52   * to create a filter chain. See SRV.6 of the Servlet 2.3 specification for more details.
53   * <p/>
54   * Care should be taken not to define a filter chain such that the same {@link CachingFilter} class is reentered.
55   * The {@link CachingFilter} uses the {@link net.sf.ehcache.constructs.blocking.BlockingCache}. It blocks until the thread which
56   * did a get which results in a null does a put. If reentry happens a second get happens before the first put. The second
57   * get could wait indefinitely. This situation is monitored and if it happens, an IllegalStateException will be thrown.
58   * @author @author Greg Luck
59   * @version $Id: CachingFilter.java 512 2007-07-10 09:18:45Z gregluck $
60   */
61  public abstract class CachingFilter extends Filter {
62      private static final Log LOG = LogFactory.getLog(CachingFilter.class.getName());
63  
64  
65      /**
66       * The cache holding the web pages. Ensure that all threads for a given cache name are using the same instance of this.
67       */
68      protected BlockingCache blockingCache;
69  
70      /**
71       * Initialises blockingCache to use. The BlockingCache created by this method does not have a lock timeout set.
72       * <p/>
73       * A timeout can be appled using <code>blockingCache.setTimeoutMillis(int timeout)</code> and takes effect immediately
74       * for all new requests
75       *
76       * @throws CacheException The most likely cause is that a cache has not been
77       *                        configured in ehcache's configuration file ehcache.xml for the filter name
78       */
79      public void doInit() throws CacheException {
80          synchronized (this.getClass()) {
81              if (blockingCache == null) {
82                  final String cacheName = getCacheName();
83                  Ehcache cache = getCacheManager().getEhcache(cacheName);
84                  if (!(cache instanceof BlockingCache)) {
85                      //decorate and substitute
86                      BlockingCache newBlockingCache = new BlockingCache(cache);
87                      getCacheManager().replaceCacheWithDecoratedCache(cache, newBlockingCache);
88                  }
89                  blockingCache = (BlockingCache) getCacheManager().getEhcache(getCacheName());
90              }
91          }
92      }
93  
94  
95      /**
96       * Destroys the filter.
97       */
98      protected void doDestroy() {
99          //noop
100     }
101 
102     /**
103      * Performs the filtering for a request.
104      *
105      * @param request
106      * @param response
107      * @param chain
108      * @throws AlreadyGzippedException     if a double gzip is attempted
109      * @throws AlreadyCommittedException   if the response was committed on the way in or the on the way back
110      * @throws FilterNonReentrantException if an attempt is made to reenter this filter in the same request.
111      * @throws LockTimeoutException        if this request is waiting on another that is populating the cache entry
112      *                                     and timeouts while waiting. Only occurs if the BlockingCache has a timeout set.
113      * @throws Exception                   for all other exceptions. They will be caught and logged in
114      *                                     {@link Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)}
115      */
116     protected void doFilter(final HttpServletRequest request, final HttpServletResponse response,
117                             final FilterChain chain)
118             throws AlreadyGzippedException,
119             AlreadyCommittedException,
120             FilterNonReentrantException,
121             LockTimeoutException,
122             Exception {
123         if (response.isCommitted()) {
124             throw new AlreadyCommittedException("Response already committed before doing buildPage.");
125         }
126         logRequestHeaders(request);
127         PageInfo pageInfo = buildPageInfo(request, response, chain);
128 
129         //return on error or redirect code
130         int statusCode = pageInfo.getStatusCode();
131         if (statusCode != HttpServletResponse.SC_OK) {
132             return;
133         }
134 
135         if (response.isCommitted()) {
136             throw new AlreadyCommittedException("Response already committed after doing buildPage"
137                     + "but before writing response from PageInfo.");
138         }
139         writeResponse(request, response, pageInfo);
140     }
141 
142 
143     /**
144      * Build page info either using the cache or building the page directly.
145      * <p/>
146      * Some requests are for page fragments which should never be gzipped, or for
147      * other pages which are not gzipped.
148      */
149     protected PageInfo buildPageInfo(final HttpServletRequest request, final HttpServletResponse response,
150                                      final FilterChain chain) throws Exception {
151         // Look up the cached page
152         final String key = calculateKey(request);
153         PageInfo pageInfo = null;
154         String originalThreadName = Thread.currentThread().getName();
155         try {
156             checkNoReentry(request);
157             Element element = blockingCache.get(key);
158             if (element == null || element.getObjectValue() == null) {
159                 try {
160                     // Page is not cached - build the response, cache it, and send to client
161                     pageInfo = buildPage(request, response, chain);
162                     if (pageInfo.isOk()) {
163                         if (LOG.isTraceEnabled()) {
164                             LOG.trace("PageInfo ok. Adding to cache " + blockingCache.getName() + " with key " + key);
165                         }
166                         blockingCache.put(new Element(key, pageInfo));
167                     } else {
168                         if (LOG.isWarnEnabled()) {
169                             LOG.warn("PageInfo was not ok(200). Putting null into cache " + blockingCache.getName()
170                                     + " with key " + key);
171                         }
172                         blockingCache.put(new Element(key, null));
173                     }
174                 } catch (final Throwable throwable) {
175                     // Must unlock the cache if the above fails. Will be logged at Filter
176                     blockingCache.put(new Element(key, null));
177                     throw new Exception(throwable);
178                 }
179             } else {
180                 pageInfo = (PageInfo) element.getObjectValue();
181             }
182         } catch (LockTimeoutException e) {
183             //do not release the lock, because you never acquired it
184             throw e;
185         } finally {
186             Thread.currentThread().setName(originalThreadName);
187         }
188         return pageInfo;
189     }
190 
191 
192     /**
193      * Builds the PageInfo object by passing the request along the filter chain
194      *
195      * @param request
196      * @param response
197      * @param chain
198      * @return a Serializable value object for the page or page fragment
199      * @throws AlreadyGzippedException if an attempt is made to double gzip the body
200      * @throws Exception
201      */
202     protected PageInfo buildPage(final HttpServletRequest request, final HttpServletResponse response,
203                                  final FilterChain chain) throws AlreadyGzippedException, Exception {
204 
205         // Invoke the next entity in the chain
206         final ByteArrayOutputStream outstr = new ByteArrayOutputStream();
207         final GenericResponseWrapper wrapper = new GenericResponseWrapper(response, outstr);
208         chain.doFilter(request, wrapper);
209         wrapper.flush();
210 
211 
212         // Return the page info
213         return new PageInfo(wrapper.getStatus(), wrapper.getContentType(), wrapper.getHeaders(), wrapper.getCookies(),
214                 outstr.toByteArray(), true);
215     }
216 
217     /**
218      * Writes the response from a PageInfo object.
219      *
220      * @param request
221      * @param response
222      * @param pageInfo
223      * @throws IOException
224      * @throws DataFormatException
225      * @throws ResponseHeadersNotModifiableException
226      *
227      */
228     protected void writeResponse(final HttpServletRequest request, final HttpServletResponse response, final PageInfo pageInfo)
229             throws IOException, DataFormatException, ResponseHeadersNotModifiableException {
230         boolean requestAcceptsGzipEncoding = acceptsGzipEncoding(request);
231 
232         setStatus(response, pageInfo);
233         setHeaders(pageInfo, requestAcceptsGzipEncoding, response);
234         setCookies(pageInfo, response);
235         setContentType(response, pageInfo);
236         writeContent(request, response, pageInfo);
237     }
238 
239     /**
240      * Set the content type
241      *
242      * @param response
243      * @param pageInfo
244      */
245     protected void setContentType(final HttpServletResponse response, final PageInfo pageInfo) {
246         response.setContentType(pageInfo.getContentType());
247     }
248 
249     /**
250      * Set the serializableCookies
251      *
252      * @param pageInfo
253      * @param response
254      */
255     protected void setCookies(final PageInfo pageInfo, final HttpServletResponse response) {
256 
257         final Collection cookies = pageInfo.getSerializableCookies();
258         for (Iterator iterator = cookies.iterator(); iterator.hasNext();) {
259             final Cookie cookie = ((SerializableCookie) iterator.next()).toCookie();
260             response.addCookie(cookie);
261         }
262     }
263 
264     /**
265      * Status code
266      *
267      * @param response
268      * @param pageInfo
269      */
270     protected void setStatus(final HttpServletResponse response, final PageInfo pageInfo) {
271         response.setStatus(pageInfo.getStatusCode());
272     }
273 
274 
275     /**
276      * Set the headers in the response object, excluding the Gzip header
277      *
278      * @param pageInfo
279      * @param requestAcceptsGzipEncoding
280      * @param response
281      */
282     protected void setHeaders(final PageInfo pageInfo,
283                               boolean requestAcceptsGzipEncoding,
284                               final HttpServletResponse response) {
285 
286         final Collection headers = pageInfo.getHeaders();
287         final int header = 0;
288         final int value = 1;
289 
290         for (Iterator iterator = headers.iterator(); iterator.hasNext();) {
291             final String[] headerPair = (String[]) iterator.next();
292             response.addHeader(headerPair[header], headerPair[value]);
293         }
294     }
295 
296 
297     /**
298      * A meaningful name representative of the JSP page being cached.
299      *
300      * @return the name of the cache to use for this filter.
301      */
302     protected abstract String getCacheName();
303 
304 
305     /**
306      * Gets the CacheManager for this CachingFilter. It is therefore up to subclasses what CacheManager to use.
307      * <p/>
308      * This method was introduced in ehcache 1.2.1. Older versions used a singleton CacheManager instance created with
309      * the default factory method.
310      * @return the CacheManager to be used
311      * @since 1.2.1
312      */
313     protected abstract CacheManager getCacheManager();
314 
315 
316     /**
317      * CachingFilter works off a key.
318      * <p/>
319      * The key should be unique. Factors to consider in generating a key are:
320      * <ul>
321      * <li>The various hostnames that a request could come through
322      * <li>Whether additional parameters used for referral tracking e.g. google should be excluded
323      * to maximise cache hits
324      * <li>Additional parameters can be added to any page. The page will still work but will miss the
325      * cache. Consider coding defensively around this issue.
326      * </ul>
327      *
328      * @param httpRequest
329      * @return the key, generally the URL plus request parameters
330      */
331     protected abstract String calculateKey(final HttpServletRequest httpRequest);
332 
333 
334     /**
335      * Writes the response content.
336      * This will be gzipped or non gzipped depending on whether the User Agent accepts
337      * GZIP encoding.
338      * <p/>
339      * If the body is written gzipped a gzip header is added.
340      *
341      * @param response
342      * @param pageInfo
343      * @throws IOException
344      */
345     protected void writeContent(final HttpServletRequest request,
346                                 final HttpServletResponse response, final PageInfo pageInfo)
347             throws IOException, ResponseHeadersNotModifiableException {
348         byte[] body;
349         if (acceptsGzipEncoding(request)) {
350             ResponseUtil.addGzipHeader(response);
351             body = pageInfo.getGzippedBody();
352             if (ResponseUtil.shouldGzippedBodyBeZero(body, request)) {
353                 body = new byte[0];
354             }
355         } else {
356             body = pageInfo.getUngzippedBody();
357         }
358 
359         boolean shouldBodyBeZero = ResponseUtil.shouldBodyBeZero(request, pageInfo.getStatusCode());
360         if (shouldBodyBeZero) {
361             body = new byte[0];
362         }
363 
364         response.setContentLength(body.length);
365         response.getOutputStream().write(body);
366     }
367 
368     /**
369      * Check that this caching filter is not being reentered by the same recursively.
370      * Recursive calls will block indefinitely because the first request has not yet
371      * unblocked the cache.
372      * <p/>
373      * This condition usually indicates an error in filter chaining or RequestDispatcher
374      * dispatching.
375      *
376      * @param httpRequest
377      * @throws FilterNonReentrantException if reentry is detected
378      */
379     protected void checkNoReentry(final HttpServletRequest httpRequest) throws FilterNonReentrantException {
380         Thread thread = Thread.currentThread();
381         String threadName = thread.getName();
382         String filterName = getClass().getName();
383         if (thread.getName().indexOf(" been through " + filterName) != -1) {
384             throw new FilterNonReentrantException("The request thread is attempting to reenter"
385                     + " filter "
386                     + filterName
387                     + ". URL: "
388                     + httpRequest.getRequestURL());
389         }
390         //Instrument thread name
391         thread.setName(thread.getName() + " been through " + filterName);
392         String newThreadName = thread.getName();
393         if (LOG.isDebugEnabled()) {
394             LOG.debug("Thread name changed from " + threadName
395                     + " to " + newThreadName);
396         }
397     }
398 }