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 }