001 package org.rakeshv.filters;
002
003 import java.io.BufferedInputStream;
004 import java.io.File;
005 import java.io.FileInputStream;
006 import java.io.IOException;
007 import java.util.Calendar;
008 import java.util.Date;
009 import java.util.Enumeration;
010 import java.util.HashMap;
011 import java.util.Iterator;
012 import java.util.Locale;
013 import java.util.Map;
014 import java.util.Timer;
015 import java.util.TimerTask;
016 import java.util.TreeMap;
017
018 import javax.servlet.Filter;
019 import javax.servlet.FilterChain;
020 import javax.servlet.FilterConfig;
021 import javax.servlet.ServletException;
022 import javax.servlet.ServletRequest;
023 import javax.servlet.ServletResponse;
024 import javax.servlet.http.HttpServletRequest;
025 import javax.servlet.http.HttpServletResponse;
026
027 import org.rakeshv.io.FileUtilities;
028 import org.rakeshv.utils.StringUtilities;
029
030 /**
031 * A <code>Filter</code> class that is used to cache the responses
032 * generated by the server. This filter caches the output generated
033 * to a request to file. Subsequent requests to the server with
034 * identical request parameters will be returned the cached contents
035 * by this filter.
036 *
037 * <p>The behaviour of the filter should be configured in the
038 * application's <code>web.xml</code>. The following parameters
039 * may be configured:</p>
040 * </ol>
041 * <li><b>baseDirectory</b> - The base directory under which the
042 * filter will cache files. The filter attempts to organise the
043 * directory tree following the same tree for the servlets that this
044 * fileter is applied to. If not specified, the filter will
045 * attempt to use a <code>/tmp/org.rakeshv/filters/cache</code>
046 * directory as the base directory.
047 * <li><b>cleanBaseDirectory</b> - A boolean to indicate
048 * whether the base directory is to be cleaned on application startup.
049 * This is used to control whether old cached files are to be
050 * removed or reused on application start. Specify values of
051 * <code>true</code> or </code>false</code>.
052 * <li><b>timeToLive</b> - The time in seconds until which cached
053 * files are to saved. The filter uses a <code>TimerTask
054 * Thread</code> to maintain the cache. Omit this paramter or specify
055 * <code>-1</code> to disable automatic removal of cached files.
056 * Note that this setting refers to individual cache files.
057 * <li><b>purgeInterval</b> - The interval in seconds at which the
058 * entire cache is to purged. The filter uses a <code>TimerTask
059 * Thread</code> to purge all the cached files at the specified
060 * interval. Omit or specify a value of <code>-1</code> to disable
061 * automatic puring of the entire cache.
062 * <li><b>purgeTime</b> - The fixed time of day at which the entire
063 * cache will be purged. The filter uses a <code>TimerTask Thread
064 * </code> to purge the cached files at the specified time of day.
065 * Omit to disable automatic purging of the entire cache. Do not
066 * specify in combination with <b>purgeInterval</b>. If both are
067 * specified, then <b>purgeInterval</b> setting takes precedence.
068 * This setting should be specified in <code>hh:mm:ss</code> format.
069 * The hour component must be specified in 24 hour format.
070 * </ol>
071 *
072 * <p>The following shows ways in which you can enable the filters
073 * in your web application configuration file (WEB-INF/web.xml):</p>
074 *
075 * <pre>
076 * <filter>
077 * <filter-name>cachingFilter</filter-name>
078 * <display-name>Caching Filter</display-name>
079 * <description>A servlet filter for caching to file responses to requests.</description>
080 * <filter-class>org.rakeshv.filters.CachingFilter</filter-class>
081 * <init-param>
082 * <param-name>baseDirectory</param-name>
083 * <param-value>/tmp/myapp/cache</param-value>
084 * </init-param>
085 * <init-param>
086 * <param-name>cleanBaseDirectory</param-name>
087 * <param-value>false</param-value>
088 * </init-param>
089 * <init-param>
090 * <param-name>timeToLive</param-name>
091 * <param-value>10800</param-value>
092 * </init-param>
093 * <init-param>
094 * <param-name>purgeTime</param-name>
095 * <param-value>01:00:00</param-value>
096 * </init-param>
097 * </filter>
098 *
099 * <filter-mapping>
100 * <filter-name>cachingFilter</filter-name>
101 * <servlet-name>myServlet</servlet-name>
102 * </filter-mapping>
103 *
104 * <filter-mapping>
105 * <filter-name>cachingFilter</filter-name>
106 * <url-pattern>/docs/*</url-pattern>
107 * </filter-mapping>
108 * </pre>
109 *
110 * <p>© Copyright 2005, Rakesh Vidyadharan</p>
111 * @author Rakesh Vidyadharan 11<sup><small>th</small></sup> September 2005
112 *
113 * @version $Id: CachingFilter.java,v 1.20 2005/10/21 17:26:02 rakesh Exp $
114 */
115 public class CachingFilter extends FilterAdapter
116 {
117 /**
118 * The default path used for {@link #baseDirectory}.
119 */
120 private static final String BASE_DIRECTORY =
121 "/tmp/org.rakeshv/filters/cache";
122
123 /**
124 * A <code>Map</code> in which all caches for the various paths are
125 * stored. This map contains maps for each path that is stored in
126 * it.
127 */
128 private static final HashMap cachedPaths = new HashMap( 1024 );
129
130 /**
131 * The default file separator value. Cached for convenience from the
132 * <code>file.separator</code> system property.
133 */
134 public static final String SEPARATOR =
135 System.getProperties().getProperty( "file.separator" );
136
137 /**
138 * The value to indicate that the <code>purgeTime</code>
139 * setting should not be considered.
140 */
141 public static final String NO_PURGE_TIME = "-1";
142
143 /**
144 * The base directory under which cache files will be stored.
145 */
146 private File baseDirectory;
147
148 /**
149 * The attributes associated with the cache.
150 */
151 private Attributes attributes;
152
153 /**
154 * A flag used to indicate if the cache may be safely used. This is
155 * disabled if errors were encountered while attempting to set up the
156 * {@link #baseDirectory} tree.
157 */
158 private boolean enableCache;
159
160 /**
161 * Default constructor. Does nothing special.
162 */
163 public CachingFilter()
164 {
165 super();
166 attributes = new Attributes();
167 enableCache = true;
168 }
169
170 /**
171 * Initialise the filter. Processes the <init-param> values
172 * specified in <code>web.xml</code>.
173 */
174 public void init( FilterConfig filterConfig ) throws ServletException
175 {
176 setFilterConfig( filterConfig );
177 attributes.initCleanBaseDirectory();
178 initBaseDirectory();
179 attributes.initTimeToLive();
180 attributes.initPurgeInterval();
181 attributes.initPurgeTime();
182 initTimer();
183 }
184
185 /**
186 * Hash the request parameters and do a lookup in the {@link
187 * #cachedPaths} to see if a cached file exists for the parameters.
188 * If yes, then return the contents of the cached file. If not
189 * buffer the output from the <code>response</code> from the next
190 * component in the chain, and then cache the contents to a file
191 * under the {@link #baseDirectory}.
192 *
193 * @see #processRequestParameters
194 * @see #fetchFileCache
195 * @see #fetchFileProperties
196 * @see #fetchCacheFile
197 * @see #sendResponse
198 * @see #chainResponse
199 * @throws IOException - If exceptions are encountered while
200 * applying the filter.
201 * @throws ServletException - If exceptions are encountered while
202 * interacting with the request or response objects.
203 */
204 public void doFilter( ServletRequest request,
205 ServletResponse response, FilterChain chain )
206 throws IOException, ServletException
207 {
208 if ( ! enableCache )
209 {
210 chain.doFilter( request, response );
211 }
212 else
213 {
214 HttpServletRequest servletRequest = (HttpServletRequest) request;
215 String path = servletRequest.getRequestURI();
216
217 Map requestParameters = processRequestParameters( servletRequest );
218 Map fileCache = fetchFileCache( path );
219 FileProperties fileProperties =
220 fetchFileProperties( fileCache, requestParameters, path );
221
222 File file = fetchCacheFile( fileProperties );
223 if ( file.exists() )
224 {
225 sendResponse( fileProperties, response );
226 }
227 else
228 {
229 chainResponse( fileProperties, file, requestParameters,
230 fileCache, request, response, chain );
231 }
232 }
233 }
234
235 /**
236 * Initialise the base directory used for caching the files.
237 * Attempts to create the base directory if it does not exist.
238 * Prints an error message to <code>System.err</code> if the attempt
239 * to create the base directory fails.
240 *
241 * @see #purgeCache
242 */
243 private void initBaseDirectory()
244 {
245 if ( filterConfig.getInitParameter( "baseDirectory" ) != null )
246 {
247 baseDirectory =
248 new File( filterConfig.getInitParameter( "baseDirectory" ) );
249 }
250 else
251 {
252 baseDirectory = new File( CachingFilter.BASE_DIRECTORY );
253 }
254
255 if ( baseDirectory.exists() )
256 {
257 if ( ! baseDirectory.isDirectory() )
258 {
259 System.err.println( new Date() + " SEVERE " +
260 getClass().getName() +
261 ".initCacheDirectory. baseDirectory " +
262 baseDirectory.toString() +
263 " is not a directory. Disabling cache." );
264 enableCache = false;
265 }
266 else if ( attributes.cleanBaseDirectory )
267 {
268 System.out.println( new Date() + " INFO " +
269 getClass().getName() +
270 ".initCacheDirectory. baseDirectory " +
271 baseDirectory.toString() +
272 " exists. Cleaning baseDirectory." );
273 purgeCache( baseDirectory );
274 }
275 }
276 else
277 {
278 try
279 {
280 baseDirectory.mkdirs();
281 }
282 catch ( Throwable t )
283 {
284 System.err.println( new Date() + " SEVERE " +
285 getClass().getName() +
286 ".initCacheDirectory. Error creating baseDirectory " +
287 baseDirectory.toString() + ". Disabling cache. " +
288 StringUtilities.stackTrace( t ) );
289 enableCache = false;
290 }
291 }
292 }
293
294 /**
295 * Create a <code>Timer</code> that runs the {@link PurgeTask} at
296 * the interval specified by either {@link Attributes#purgeInterval}
297 * or {@link Attributes#purgeTime}. If both are configured then
298 * the {@link Attributes#purgeInterval} setting takes precedence.
299 */
300 private void initTimer()
301 {
302 if ( attributes.purgeInterval != Attributes.NO_PURGE_INTERVAL )
303 {
304 long interval = attributes.purgeInterval * 1000;
305 long delay = interval;
306 System.out.println( new Date() + " INFO " +
307 getClass().getName() +
308 ".initTimer. Scheduling timer to run at " +
309 interval + " milliseconds after " +
310 delay + " milliseconds." );
311 ( new Timer( true ) ).scheduleAtFixedRate(
312 new PurgeTask(), delay, interval );
313 }
314 else if ( attributes.purgeTime.hour !=
315 Integer.parseInt( CachingFilter.NO_PURGE_TIME ) )
316 {
317 long interval = 24 * 60 * 60 * 1000;
318 Calendar calendar = Calendar.getInstance();
319 calendar.set( Calendar.HOUR_OF_DAY, attributes.purgeTime.hour );
320 calendar.set( Calendar.MINUTE, attributes.purgeTime.minute );
321 calendar.set( Calendar.SECOND, attributes.purgeTime.second );
322 if ( calendar.getTimeInMillis() < System.currentTimeMillis() )
323 {
324 calendar.add( Calendar.DAY_OF_YEAR, 1 );
325 }
326 System.out.println( new Date() + " INFO " +
327 getClass().getName() +
328 ".initTimer. Scheduling timer to run at " +
329 calendar.getTime() + " repeated every day." );
330 ( new Timer( true ) ).scheduleAtFixedRate(
331 new PurgeTask(), calendar.getTime(), interval );
332 }
333 }
334
335 /**
336 * Create a sorted map that contains all the request parameters and
337 * their values. Sorting is essential to ensure that the order in
338 * which the request parameters are specified does not lead to
339 * duplicated cache files.
340 *
341 * @param request The HTTP request from which the request parameters
342 * and their values are to be retrieved.
343 */
344 private Map processRequestParameters( ServletRequest request )
345 {
346 TreeMap requestParameters = new TreeMap();
347
348 for ( Iterator iterator =
349 request.getParameterMap().entrySet().iterator();
350 iterator.hasNext(); )
351 {
352 Map.Entry entry = (Map.Entry) iterator.next();
353 StringBuffer buffer = new StringBuffer();
354 String[] values = (String[]) entry.getValue();
355 for ( int i = 0; i < values.length; ++i )
356 {
357 buffer.append( values[i] ).append( "__" );
358 }
359 requestParameters.put( (String) entry.getKey(), buffer.toString() );
360 }
361
362 Enumeration e = ( (HttpServletRequest) request ).getHeaderNames();
363 while ( e.hasMoreElements() )
364 {
365 String header = (String) e.nextElement();
366 if ( header.equalsIgnoreCase( "Accept-Encoding" ) ||
367 header.equalsIgnoreCase( "TE" ) )
368 {
369 String encoding =
370 ( (HttpServletRequest ) request ).getHeader( header );
371 if ( encoding.indexOf( "gzip" ) != -1 ||
372 encoding.indexOf( "zip" ) != -1 ||
373 encoding.indexOf( "deflate" ) != -1 ||
374 encoding.equals( "*" ) )
375 {
376 requestParameters.put( header, encoding );
377 }
378 }
379 }
380
381 return requestParameters;
382 }
383
384 /**
385 * Fetch the <code>Map</code> that stores the request parameters and
386 * the associated file names from {@link #cachedPaths}. If it does
387 * not exist, then a new <code>Map</code> is created and added to
388 * the {@link #cachedPaths}.
389 *
390 * @param path The URI path for which the cache is to be retrieved.
391 * @return Map - The map that stores the key-value pairs.
392 */
393 private Map fetchFileCache( String path )
394 {
395 Map fileCache = (Map) cachedPaths.get( path );
396 if ( fileCache == null )
397 {
398 fileCache = new HashMap( 128 );
399 cachedPaths.put( path, fileCache );
400 }
401
402 return fileCache;
403 }
404
405 /**
406 * Fetch the file name that for the cache file that represents the
407 * current request. If it does not exist in the
408 * <code>fileCache</code>, create the new file name.
409 *
410 * @param fileCache The <code>Map</code> in which the file names are
411 * stored for a given <code>requestParameters</code>.
412 * @param requestParameters A <code>Map</code> that contains key-value
413 * mappings of all the request parameters.
414 * @param path The URI path component with which the file name will be
415 * associated.
416 * @return FileProperties - The data structure that holds
417 * metadata about the file that was cached.
418 */
419 private FileProperties fetchFileProperties( Map fileCache,
420 Map requestParameters, String path )
421 {
422 FileProperties fileProperties =
423 (FileProperties) fileCache.get( requestParameters );
424
425 if ( fileProperties == null )
426 {
427 StringBuffer fileName = new StringBuffer( 128 );
428 fileName.append( baseDirectory );
429 fileName.append( CachingFilter.SEPARATOR );
430 fileName.append( path );
431 fileName.append( CachingFilter.SEPARATOR );
432
433 for ( Iterator iterator = requestParameters.entrySet().iterator();
434 iterator.hasNext(); )
435 {
436 Map.Entry entry = (Map.Entry) iterator.next();
437 String key = (String) entry.getKey();
438 if ( key.equalsIgnoreCase( "Accept-Encoding" ) )
439 {
440 fileName.append( (String) entry.getValue() );
441 }
442 else
443 {
444 fileName.append( key );
445 }
446 fileName.append( CachingFilter.SEPARATOR );
447 }
448 fileName.append( requestParameters.hashCode() );
449 fileProperties = new FileProperties( fileName.toString() );
450 }
451
452 return fileProperties;
453 }
454
455 /**
456 * Fetch a file handle to the cache file represented by <code>fileName
457 * </code>. If the file exists, and if the {@link
458 * Attributes#timeToLive} was specified, delete the file if it is
459 * older than <code>timeToLive</code>. Return a valid file handle
460 * after processing all the rules.
461 *
462 * @param fileProperties The data structure that contains information
463 * about the file that should be represented.
464 * @return File The file handle to use.
465 * @throws IOException If errors are encountered while operating on
466 * the file.
467 */
468 private File fetchCacheFile( FileProperties fileProperties )
469 {
470 File file = new File( fileProperties.getFileName() );
471 if ( file.exists() &&
472 attributes.timeToLive != Attributes.NO_TIME_TO_LIVE )
473 {
474 long lastModified = file.lastModified();
475 if ( ( file.lastModified() + ( attributes.timeToLive * 1000 ) ) <
476 System.currentTimeMillis() )
477 {
478 file.delete();
479 }
480 }
481
482 return file;
483 }
484
485 /**
486 * Write the contents of the <code>fileName</code> to the <code>
487 * HTTP Response</code> specified.
488 *
489 * @see CachingFilter.FileProperties#setHeadersFromFields
490 * @param fileProperties The data structure that contains information
491 * about the file whose contents are to be written to the
492 * <code>response</code>.
493 * @param response The HTTP Response to which the cached contents
494 * are to be written.
495 */
496 private void sendResponse( FileProperties fileProperties,
497 ServletResponse response ) throws IOException
498 {
499 fileProperties.setHeadersFromFields( response );
500
501 int bufferSize = 8192;
502 BufferedInputStream bis = new BufferedInputStream(
503 new FileInputStream( fileProperties.getFileName() ),
504 bufferSize );
505
506 byte[] buffer = new byte[bufferSize];
507 int length = 0;
508 while ( ( length = bis.read( buffer, 0, buffer.length ) ) != -1 )
509 {
510 response.getOutputStream().write( buffer, 0, length );
511 }
512
513 bis.close();
514 response.getOutputStream().flush();
515 }
516
517 /**
518 * Create a {@link CachingResponseWrapper} and chain this filter to
519 * the next component in the chain. Create a temporary file and
520 * initialise the {@link CachingResponseWrapper} with it. After the
521 * response has been finished, rename the temporary file to the
522 * prorper <code>file</code> to avoid multiple threads with the same
523 * response from corrupting the file.
524 *
525 * @see #createTemporaryFile
526 * @param fileProperties The data structure that stores all the
527 * meta data about the file to be cached. The data structure
528 * will be populated from the <code>request</code> after the
529 * request has been processed by the components down the chain.
530 * @param file The <code>File</code> to which the cached response is
531 * to be ultimately saved.
532 * @param requestParameters A <code>Map</code> of the HTTP request
533 * parameters and their values.
534 * @param fileCache The <code>Map</code> that stores the association
535 * between the <code>requestParameters</code> and the cache
536 * <code>fileName</code>.
537 * @param request The HTTP Request object that represents the entire
538 * HTTP request that is to be processed.
539 * @param response The HTTP Response object to which the response is
540 * to be sent.
541 * @param chain The <code>chain</code> to which the processing of the
542 * actual response is delegated to.
543 * @throws ServletException If errors are encountered up the chain.
544 * @throws IOException If errors are encountered while writing to the
545 * cache file.
546 */
547 private void chainResponse( FileProperties fileProperties,
548 File file, Map requestParameters, Map fileCache,
549 ServletRequest request, ServletResponse response,
550 FilterChain chain ) throws ServletException, IOException
551 {
552 File tempFile = createTemporaryFile( fileProperties.getFileName() );
553
554 CachingResponseWrapper wrappedResponse =
555 new CachingResponseWrapper( (HttpServletResponse) response,
556 tempFile, fileProperties );
557 chain.doFilter( request, wrappedResponse );
558 wrappedResponse.closeCacheFile();
559
560 if ( fileProperties.getValidResponse() )
561 {
562 tempFile.renameTo( file );
563 fileCache.put( requestParameters, fileProperties );
564 }
565 else
566 {
567 tempFile.delete();
568 }
569 }
570
571 /**
572 * Create a temporary file based on the <code>fileName</code>
573 * specified. This will ensure that only a complete valid cached
574 * file is actually available at the location specified by
575 * <code>fileName</code>. Also creates any directories that may be
576 * required to store the file.
577 *
578 * @param fileName The name of the destination file.
579 * @return File - The temporary file to which the response should be
580 * stored.
581 */
582 private File createTemporaryFile( String fileName )
583 {
584 File tempFile = new File( fileName + System.currentTimeMillis() );
585 File parent = tempFile.getParentFile();
586 parent.mkdirs();
587
588 return tempFile;
589 }
590
591 /**
592 * Purge all files and directories under the specified directory.
593 * Prints out any errors encountered while attempting to delete the
594 * directory to <code>System.err</code>.
595 *
596 * @see org.rakeshv.io.FileUtilities#delete( File, boolean )
597 * @param directory The directory whose children are to be deleted.
598 */
599 private int purgeCache( File directory )
600 {
601 synchronized (this)
602 {
603 cachedPaths.clear();
604 }
605
606 int result = FileUtilities.delete( directory, false );
607 return result;
608 }
609
610 /**
611 * A data structure used to hold the cache validity period attributes.
612 * Fields in this data structure are used to control the <code>TTL
613 * </code> for cached files, or the interval at which the cache
614 * directory is to be purged.
615 */
616 public class Attributes
617 {
618 /**
619 * The value to indicate that the {@link #cleanBaseDirectory} is
620 * to be enabled.
621 */
622 public static final boolean CLEAN_BASE_DIRECTORY = true;
623
624 /**
625 * The value to indicate that the {@link #cleanBaseDirectory} is to
626 * be disabled.
627 */
628 public static final boolean NO_CLEAN_BASE_DIRECTORY = false;
629
630 /**
631 * The value to indicate that the {@link #timeToLive}
632 * variable should not be considered.
633 */
634 public static final int NO_TIME_TO_LIVE = -1;
635
636 /**
637 * The value to indicate that the {@link #purgeInterval}
638 * variable should not be considered.
639 */
640 public static final int NO_PURGE_INTERVAL = -1;
641
642 /**
643 * A flag that indicates whether the {@link #baseDirectory} should
644 * be cleaned on application start or not.
645 */
646 private boolean cleanBaseDirectory = NO_CLEAN_BASE_DIRECTORY;
647
648 /**
649 * The time in seconds till which an individual cache file must
650 * persist.
651 */
652 private int timeToLive = NO_TIME_TO_LIVE;
653
654 /**
655 * The time interval at which the cached files must be automatically
656 * purged.
657 */
658 private int purgeInterval = NO_PURGE_INTERVAL;
659
660 /**
661 * The time of day at which the cached files must be automatically
662 * purged.
663 */
664 private PurgeTime purgeTime;
665
666 /**
667 * Default constructor. Initialises the {@link #purgeTime}.
668 */
669 protected Attributes()
670 {
671 super();
672 purgeTime = new PurgeTime();
673 }
674
675 /**
676 * Initialise the {@link #cleanBaseDirectory} field. Set to {@link
677 * #NO_CLEAN_BASE_DIRECTORY} if no configuration was found, or if
678 * the value is the same as {@link #NO_CLEAN_BASE_DIRECTORY}
679 */
680 private void initCleanBaseDirectory()
681 {
682 cleanBaseDirectory = NO_CLEAN_BASE_DIRECTORY;
683
684 if ( filterConfig.getInitParameter( "cleanBaseDirectory" ) != null )
685 {
686 if ( filterConfig.getInitParameter( "cleanBaseDirectory" ).equals( "true" ) )
687 {
688 cleanBaseDirectory = CLEAN_BASE_DIRECTORY;
689 }
690 }
691 }
692
693 /**
694 * Initialise the {@link #timeToLive} field. Set to {@link
695 * #NO_TIME_TO_LIVE} if no configuration was found, or if the value
696 * is the same as {@link #NO_TIME_TO_LIVE}.
697 */
698 private void initTimeToLive()
699 {
700 if ( filterConfig.getInitParameter( "timeToLive" ) != null )
701 {
702 timeToLive = Integer.parseInt(
703 filterConfig.getInitParameter( "timeToLive" ) );
704 }
705 else
706 {
707 timeToLive = NO_TIME_TO_LIVE;
708 }
709 }
710
711 /**
712 * Initialise the {@link #purgeInterval} field. Set to {@link
713 * #NO_PURGE_INTERVAL} if no configuration was found, or if the
714 * value is the same as {@link #NO_PURGE_INTERVAL}.
715 */
716 private void initPurgeInterval()
717 {
718 if ( filterConfig.getInitParameter( "purgeInterval" ) != null )
719 {
720 purgeInterval = Integer.parseInt(
721 filterConfig.getInitParameter( "purgeInterval" ) );
722 }
723 else
724 {
725 purgeInterval = NO_PURGE_INTERVAL;
726 }
727 }
728
729 /**
730 * Initialise the {@link #purgeTime} field. Set to {@link
731 * CachingFilter#NO_PURGE_TIME} if no configuration was
732 * found, or if the * is the same as {@link
733 * CachingFilter#NO_PURGE_TIME}.
734 */
735 private void initPurgeTime()
736 {
737 purgeTime.init();
738 }
739 }
740
741 /**
742 * A data structure that is used to represent the time of day at
743 * which the cache directory should be purged.
744 */
745 public class PurgeTime
746 {
747 /**
748 * The hour of day at which the entire cache is to be purged.
749 */
750 private int hour;
751
752 /**
753 * The minute of hour at which the entire cache is to be purged.
754 *
755 * @see #hour
756 */
757 private int minute = 0;
758
759 /**
760 * The second of minute at which the entire cache is to be purged.
761 *
762 * @see #minute
763 */
764 private int second = 0;
765
766 /**
767 * Initialise the data structure with configuration from the filter
768 * configuration.
769 */
770 private void init()
771 {
772 if ( filterConfig.getInitParameter( "purgeTime" ) != null &&
773 ! filterConfig.getInitParameter( "purgeTime" ).equals(
774 CachingFilter.NO_PURGE_TIME ) )
775 {
776 String[] components =
777 filterConfig.getInitParameter( "purgeTime" ).split( ":" );
778 hour = Integer.parseInt( components[0] );
779 minute = Integer.parseInt( components[1] );
780 second = Integer.parseInt( components[2] );
781 }
782 else
783 {
784 hour = Integer.parseInt( CachingFilter.NO_PURGE_TIME );
785 minute = hour;
786 second = hour;
787 }
788 }
789 }
790
791 /**
792 * A <code>TimerTask</code> that is used to schedule cache purges.
793 */
794 protected class PurgeTask extends TimerTask
795 {
796 /**
797 * Default constructor. No special actions required.
798 */
799 protected PurgeTask()
800 {
801 super();
802 }
803
804 /**
805 * The action to be performed by this task when run by a <code>
806 * Timer</code>. Invokes {@link CachingFilter#purgeCache( File )}
807 * with {@link CachingFilter#baseDirectory}.
808 */
809 public void run()
810 {
811 System.out.println( new Date() + " INFO " +
812 getClass().getName() + ".run. Purging all files under " +
813 baseDirectory.toString() );
814 int total = purgeCache( baseDirectory );
815 System.out.println( new Date() + " INFO " +
816 getClass().getName() +
817 ".run. Finished purging all files under " +
818 baseDirectory.toString() + ". Removed a total of " +
819 total + " files." );
820 }
821 }
822
823 /**
824 * A data structure for storing the <code>HTTP headers</code>
825 * specified for the response that is being cached. These will be
826 * used when returning the cached version of a response.
827 */
828 public class FileProperties extends Object
829 {
830 /**
831 * The fully qualified name of the cache file.
832 */
833 private String fileName;
834
835 /**
836 * The <code>character-encoding</code> header value.
837 */
838 private String characterEncoding;
839
840 /**
841 * The <code>content-type</code> header value.
842 */
843 private String contentType;
844
845 /**
846 * The <code>locale</code> header value.
847 */
848 private Locale locale;
849
850 /**
851 * A flag that indicates whether the cache file generated for the
852 * request should be treated as valid. This is set to <code>false
853 * </code> when any response other than a <code>SC_OK</code> is
854 * returned for the request.
855 */
856 private boolean validResponse;
857
858 /**
859 * A map to hold all the additional header's specified for the
860 * response.
861 */
862 private Map headers;
863
864 /**
865 * Default constructor. Not meant for public use.
866 */
867 protected FileProperties()
868 {
869 super();
870 headers = new HashMap();
871 validResponse = true;
872 }
873
874 /**
875 * Create a new instance with the specified {@link #fileName}.
876 *
877 * @see #setFileName
878 * @param fileName The {@link #fileName} value to set.
879 */
880 protected FileProperties( String fileName )
881 {
882 this();
883 setFileName( fileName );
884 }
885
886 /**
887 * Create a new instance with the specified values.
888 *
889 * @see #setFileName
890 * @see #setCharacterEncoding
891 * @see #setContentType
892 * @see #setLocale
893 * @param fileName The {@link #fileName} value to set.
894 * @param characterEncoding The {@link #characterEncoding} value to
895 * set.
896 * @param contentType The {@link #contentType} value to set.
897 * @param locale The {@link #locale} value to set.
898 */
899 protected FileProperties( String fileName,
900 String characterEncoding, String contentType, Locale locale )
901 {
902 this( fileName );
903 setCharacterEncoding( characterEncoding );
904 setContentType( contentType );
905 setLocale( locale );
906 }
907
908 /**
909 * Set the <code>HTTP header</code> values denoting the type of
910 * content being returned from the values stored in the instance
911 * members.
912 *
913 * @param response The HTTP response whose content attributes are
914 * to be set.
915 */
916 protected void setHeadersFromFields( ServletResponse response )
917 {
918 if ( characterEncoding != null )
919 {
920 response.setCharacterEncoding( characterEncoding );
921 }
922
923 if ( contentType != null )
924 {
925 response.setContentType( contentType );
926 }
927
928 if ( locale != null )
929 {
930 response.setLocale( locale );
931 }
932
933 for ( Iterator iterator = headers.entrySet().iterator();
934 iterator.hasNext(); )
935 {
936 Map.Entry entry = (Map.Entry) iterator.next();
937 ( (HttpServletResponse) response ).addHeader(
938 (String) entry.getKey(), (String) entry.getValue() );
939 }
940 }
941
942 /**
943 * Add or modify the specified header with the value specified.
944 *
945 * @param name The name of the header
946 * @param value The value of the header
947 */
948 public void setHeader( String name, String value )
949 {
950 headers.put( name, value );
951 }
952
953 /**
954 * Returns {@link #fileName}.
955 *
956 * @return String The value/reference of/to fileName.
957 */
958 public final String getFileName()
959 {
960 return fileName;
961 }
962
963 /**
964 * Set {@link #fileName}.
965 *
966 * @param fileName The value to set.
967 */
968 protected final void setFileName( String fileName )
969 {
970 this.fileName = fileName;
971 }
972
973 /**
974 * Returns {@link #characterEncoding}.
975 *
976 * @return String The value/reference of/to characterEncoding.
977 */
978 public final String getCharacterEncoding()
979 {
980 return characterEncoding;
981 }
982
983 /**
984 * Set {@link #characterEncoding}.
985 *
986 * @param characterEncoding The value to set.
987 */
988 protected final void setCharacterEncoding( String characterEncoding )
989 {
990 this.characterEncoding = characterEncoding;
991 }
992
993 /**
994 * Returns {@link #contentType}.
995 *
996 * @return String The value/reference of/to contentType.
997 */
998 public final String getContentType()
999 {
1000 return contentType;
1001 }
1002
1003 /**
1004 * Set {@link #contentType}.
1005 *
1006 * @param contentType The value to set.
1007 */
1008 protected final void setContentType( String contentType )
1009 {
1010 this.contentType = contentType;
1011 }
1012
1013 /**
1014 * Returns {@link #locale}.
1015 *
1016 * @return Locale The value/reference of/to locale.
1017 */
1018 public final Locale getLocale()
1019 {
1020 return locale;
1021 }
1022
1023 /**
1024 * Set {@link #locale}.
1025 *
1026 * @param locale The value to set.
1027 */
1028 protected final void setLocale( Locale locale )
1029 {
1030 this.locale = locale;
1031 }
1032
1033 /**
1034 * Returns {@link #validResponse}.
1035 *
1036 * @return boolean The value/reference of/to validResponse.
1037 */
1038 public final boolean getValidResponse()
1039 {
1040 return validResponse;
1041 }
1042
1043 /**
1044 * Set {@link #validResponse}.
1045 *
1046 * @param validResponse The value to set.
1047 */
1048 protected final void setValidResponse( boolean validResponse )
1049 {
1050 this.validResponse = validResponse;
1051 }
1052
1053 /**
1054 * Returns {@link #headers}.
1055 *
1056 * @return Map The value/reference of/to headers.
1057 */
1058 public final Map getHeaders()
1059 {
1060 return headers;
1061 }
1062
1063 /**
1064 * Set {@link #headers}.
1065 *
1066 * @param headers The value to set.
1067 */
1068 protected final void setHeaders( Map headers )
1069 {
1070 this.headers = headers;
1071 }
1072 }
1073 }