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     *  &lt;filter&gt;
077     *    &lt;filter-name&gt;cachingFilter&lt;/filter-name&gt;
078     *    &lt;display-name&gt;Caching Filter&lt;/display-name&gt;
079     *    &lt;description&gt;A servlet filter for caching to file responses to requests.&lt;/description&gt;
080     *    &lt;filter-class&gt;org.rakeshv.filters.CachingFilter&lt;/filter-class&gt;
081     *    &lt;init-param&gt;
082     *      &lt;param-name&gt;baseDirectory&lt;/param-name&gt;
083     *      &lt;param-value&gt;/tmp/myapp/cache&lt;/param-value&gt;
084     *    &lt;/init-param&gt;
085     *    &lt;init-param&gt;
086     *      &lt;param-name&gt;cleanBaseDirectory&lt;/param-name&gt;
087     *      &lt;param-value&gt;false&lt;/param-value&gt;
088     *    &lt;/init-param&gt;
089     *    &lt;init-param&gt;
090     *      &lt;param-name&gt;timeToLive&lt;/param-name&gt;
091     *      &lt;param-value&gt;10800&lt;/param-value&gt;
092     *    &lt;/init-param&gt;
093     *    &lt;init-param&gt;
094     *      &lt;param-name&gt;purgeTime&lt;/param-name&gt;
095     *      &lt;param-value&gt;01:00:00&lt;/param-value&gt;
096     *    &lt;/init-param&gt;
097     *  &lt;/filter&gt;
098     *
099     *  &lt;filter-mapping&gt;
100     *    &lt;filter-name&gt;cachingFilter&lt;/filter-name&gt;
101     *    &lt;servlet-name&gt;myServlet&lt;/servlet-name&gt;
102     *  &lt;/filter-mapping&gt;
103     *
104     *  &lt;filter-mapping&gt;
105     *    &lt;filter-name&gt;cachingFilter&lt;/filter-name&gt;
106     *    &lt;url-pattern&gt;/docs/*&lt;/url-pattern&gt;
107     *  &lt;/filter-mapping&gt;
108     * </pre>
109     *
110     * <p>&copy; 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 &lt;init-param&gt; 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    }