View Javadoc

1   package org.apache.jcs.auxiliary.disk.file;
2   
3   import java.io.BufferedOutputStream;
4   import java.io.File;
5   import java.io.FileInputStream;
6   import java.io.FileOutputStream;
7   import java.io.IOException;
8   import java.io.InputStream;
9   import java.io.OutputStream;
10  import java.io.Serializable;
11  import java.util.Map;
12  import java.util.Set;
13  
14  import org.apache.commons.logging.Log;
15  import org.apache.commons.logging.LogFactory;
16  import org.apache.jcs.auxiliary.AuxiliaryCacheAttributes;
17  import org.apache.jcs.auxiliary.disk.AbstractDiskCache;
18  import org.apache.jcs.engine.behavior.ICacheElement;
19  import org.apache.jcs.engine.behavior.IElementSerializer;
20  import org.apache.jcs.engine.logging.behavior.ICacheEvent;
21  import org.apache.jcs.engine.logging.behavior.ICacheEventLogger;
22  import org.apache.jcs.utils.timing.SleepUtil;
23  
24  /**
25   * This disk cache writes each item to a separate file. This is for regions with very few items,
26   * perhaps big ones.
27   * <p>
28   * This is a fairly simple implementation. All the disk writing is handled right here. It's not
29   * clear that anything more complicated is needed.
30   */
31  public class FileDiskCache<K extends Serializable, V extends Serializable>
32      extends AbstractDiskCache<K, V>
33  {
34      /** Don't change */
35      private static final long serialVersionUID = 1L;
36  
37      /** The logger. */
38      private static final Log log = LogFactory.getLog( FileDiskCache.class );
39  
40      /** The name to prefix all log messages with. */
41      private final String logCacheName;
42  
43      /** The config values. */
44      private final FileDiskCacheAttributes diskFileCacheAttributes;
45  
46      /** The directory where the files are stored */
47      private File directory;
48  
49      /**
50       * Constructor for the DiskCache object.
51       * <p>
52       * @param cacheAttributes
53       */
54      public FileDiskCache( FileDiskCacheAttributes cacheAttributes )
55      {
56          this( cacheAttributes, null );
57      }
58  
59      /**
60       * Constructor for the DiskCache object. Will be marked alive if the directory cannot be
61       * created.
62       * <p>
63       * @param cattr
64       * @param elementSerializer used if supplied, the super's super will not set a null
65       */
66      public FileDiskCache( FileDiskCacheAttributes cattr, IElementSerializer elementSerializer )
67      {
68          super( cattr );
69          setElementSerializer( elementSerializer );
70          this.diskFileCacheAttributes = cattr;
71          this.logCacheName = "Region [" + getCacheName() + "] ";
72          alive = initializeFileSystem( cattr );
73      }
74  
75      /**
76       * Tries to create the root directory if it does not already exist.
77       * <p>
78       * @param cattr
79       * @return does the directory exist.
80       */
81      private boolean initializeFileSystem( FileDiskCacheAttributes cattr )
82      {
83          // TODO, we might need to make this configurable
84          String rootDirName = cattr.getDiskPath() + "/" + cattr.getCacheName();
85          this.setDirectory( new File( rootDirName ) );
86          boolean createdDirectories = getDirectory().mkdirs();
87          if ( log.isInfoEnabled() )
88          {
89              log.info( logCacheName + "Cache file root directory: " + rootDirName );
90              log.info( logCacheName + "Created root directory: " + createdDirectories );
91          }
92  
93          // TODO consider throwing.
94          boolean exists = getDirectory().exists();
95          if ( !exists )
96          {
97              log.error( "Could not initialize File Disk Cache.  The root directory does not exist." );
98          }
99          return exists;
100     }
101 
102     /**
103      * Creates the file for a key. Filenames and keys can be passed into this method. It must be
104      * idempotent.
105      * <p>
106      * Protected for testing.
107      * <p>
108      * @param key
109      * @return the file for the key
110      */
111     protected <KK extends Serializable> File file( KK key )
112     {
113         StringBuffer fileNameBuffer = new StringBuffer();
114 
115         // add key as filename in a file system safe way
116         String keys = key.toString();
117         int l = keys.length();
118         for ( int i = 0; i < l; i++ )
119         {
120             char c = keys.charAt( i );
121             if ( !Character.isLetterOrDigit( c ) )
122             {
123                 c = '_';
124             }
125             fileNameBuffer.append( c );
126         }
127         String fileName = fileNameBuffer.toString();
128 
129         if ( log.isDebugEnabled() )
130         {
131             log.debug( logCacheName + "Creating file for name: [" + fileName + "] based on key: [" + key + "]" );
132         }
133 
134         return new File( getDirectory().getAbsolutePath(), fileName );
135     }
136 
137     /**
138      * Gets the set of keys of objects currently in the group.
139      * <p>
140      * @param group
141      * @return a Set of group keys.
142      */
143     @Override
144     public Set<K> getGroupKeys(String groupName)
145     {
146         throw new UnsupportedOperationException();
147     }
148 
149     /**
150      * Gets the set of group names in the cache
151      * <p>
152      * @return a Set of group names.
153      */
154     @Override
155     public Set<String> getGroupNames()
156     {
157         throw new UnsupportedOperationException();
158     }
159 
160     /**
161      * @return dir.list().length
162      */
163     @Override
164     public int getSize()
165     {
166         if ( getDirectory().exists() )
167         {
168             return getDirectory().list().length;
169         }
170         return 0;
171     }
172 
173     /**
174      * @return AuxiliaryCacheAttributes
175      */
176     public AuxiliaryCacheAttributes getAuxiliaryCacheAttributes()
177     {
178         return diskFileCacheAttributes;
179     }
180 
181     /**
182      * @return String the path to the directory
183      */
184     @Override
185     protected String getDiskLocation()
186     {
187         return getDirectory().getAbsolutePath();
188     }
189 
190     /**
191      * Sets alive to false.
192      * <p>
193      * @throws IOException
194      */
195     @Override
196     protected synchronized void processDispose()
197         throws IOException
198     {
199         ICacheEvent<String> cacheEvent = createICacheEvent( cacheName, "none", ICacheEventLogger.DISPOSE_EVENT );
200         try
201         {
202             if ( !alive )
203             {
204                 log.error( logCacheName + "Not alive and dispose was called, directgory: " + getDirectory() );
205                 return;
206             }
207 
208             // Prevents any interaction with the cache while we're shutting down.
209             alive = false;
210 
211             // TODO consider giving up the handle on the directory.
212             if ( log.isInfoEnabled() )
213             {
214                 log.info( logCacheName + "Shutdown complete." );
215             }
216         }
217         finally
218         {
219             logICacheEvent( cacheEvent );
220         }
221     }
222 
223     /**
224      * Looks for a file matching the key. If it exists, reads the file.
225      * <p>
226      * @param key
227      * @return ICacheElement
228      * @throws IOException
229      */
230     @Override
231     protected ICacheElement<K, V> processGet( K key )
232         throws IOException
233     {
234         File file = file( key );
235 
236         if ( !file.exists() )
237         {
238             if ( log.isDebugEnabled() )
239             {
240                 log.debug( "File does not exist.  Returning null from Get." + file );
241             }
242             return null;
243         }
244 
245         ICacheElement<K, V> element = null;
246 
247         FileInputStream fis = null;
248         try
249         {
250             fis = new FileInputStream( file );
251 
252             long length = file.length();
253             // Create the byte array to hold the data
254             byte[] bytes = new byte[(int) length];
255 
256             int offset = 0;
257             int numRead = 0;
258             while ( offset < bytes.length && ( numRead = fis.read( bytes, offset, bytes.length - offset ) ) >= 0 )
259             {
260                 offset += numRead;
261             }
262 
263             // Ensure all the bytes have been read in
264             if ( offset < bytes.length )
265             {
266                 throw new IOException( "Could not completely read file " + file.getName() );
267             }
268 
269             element = getElementSerializer().deSerialize( bytes );
270 
271             // test that the retrieved object has equal key
272             if ( element != null && !key.equals( element.getKey() ) )
273             {
274                 if ( log.isInfoEnabled() )
275                 {
276                     log.info( logCacheName + "key: [" + key + "] point to cached object with key: [" + element.getKey()
277                         + "]" );
278                 }
279                 element = null;
280             }
281         }
282         catch ( IOException e )
283         {
284             log.error( logCacheName + "Failure getting element, key: [" + key + "]", e );
285         }
286         catch ( ClassNotFoundException e )
287         {
288             log.error( logCacheName + "Failure getting element, key: [" + key + "]", e );
289         }
290         finally
291         {
292             silentClose( fis );
293         }
294 
295         // If this is true and we have a max file size, the Least Recently Used file will be removed.
296         if ( element != null && diskFileCacheAttributes.isTouchOnGet() )
297         {
298             touchWithRetry( file );
299         }
300         return element;
301     }
302 
303     /**
304      * @param pattern
305      * @return Map
306      * @throws IOException
307      */
308     @Override
309     protected Map<K, ICacheElement<K, V>> processGetMatching( String pattern )
310         throws IOException
311     {
312         // TODO get a list of file and return those with matching keys.
313         // the problem will be to handle the underscores.
314         return null;
315     }
316 
317     /**
318      * Removes the file.
319      * <p>
320      * @param key
321      * @return true if the item was removed
322      * @throws IOException
323      */
324     @Override
325     protected boolean processRemove( K key )
326         throws IOException
327     {
328         return _processRemove(key);
329     }
330 
331     /**
332      * Removes the file.
333      * <p>
334      * @param key
335      * @return true if the item was removed
336      * @throws IOException
337      */
338     private <T extends Serializable> boolean _processRemove( T key )
339         throws IOException
340     {
341         File file = file( key );
342         if ( log.isDebugEnabled() )
343         {
344             log.debug( logCacheName + "Removing file " + file );
345         }
346         return deleteWithRetry( file );
347     }
348 
349     /**
350      * Remove all the files in the directory.
351      * <p>
352      * Assumes that this is the only region in the directory. We could add a region prefix to the
353      * files and only delete those, but the region should create a directory.
354      * <p>
355      * @throws IOException
356      */
357     @Override
358     protected void processRemoveAll()
359         throws IOException
360     {
361         String[] fileNames = getDirectory().list();
362         for ( int i = 0; i < fileNames.length; i++ )
363         {
364             _processRemove( fileNames[i] );
365         }
366     }
367 
368     /**
369      * We create a temp file with the new contents, remove the old if it exists, and then rename the
370      * temp.
371      * <p>
372      * @param element
373      * @throws IOException
374      */
375     @Override
376     protected void processUpdate( ICacheElement<K, V> element )
377         throws IOException
378     {
379         removeIfLimitIsSetAndReached();
380 
381         File file = file( element.getKey() );
382 
383         File tmp = null;
384         OutputStream os = null;
385         try
386         {
387             byte[] bytes = getElementSerializer().serialize( element );
388 
389             tmp = File.createTempFile( "JCS_DiskFileCache", null, getDirectory() );
390 
391             FileOutputStream fos = new FileOutputStream( tmp );
392             os = new BufferedOutputStream( fos );
393 
394             if ( bytes != null )
395             {
396                 if ( log.isDebugEnabled() )
397                 {
398                     log.debug( logCacheName + "Wrote " + bytes.length + " bytes to file " + tmp );
399                 }
400                 os.write( bytes );
401                 os.close();
402             }
403             deleteWithRetry( file );
404             tmp.renameTo( file );
405             if ( log.isDebugEnabled() )
406             {
407                 log.debug( logCacheName + "Renamed to: " + file );
408             }
409         }
410         catch ( IOException e )
411         {
412             log.error( logCacheName + "Failure updating element, key: [" + element.getKey() + "]", e );
413         }
414         finally
415         {
416             silentClose( os );
417             if ( ( tmp != null ) && tmp.exists() )
418             {
419                 deleteWithRetry( tmp );
420             }
421         }
422     }
423 
424     /**
425      * If a limit has been set and we have reached it, remove the least recently modified file.
426      * <p>
427      * We will probably need to touch the files. If we touch, the LRM file will be based on age
428      * (i.e. FIFO). If we touch, it will be based on access time (i.e. LRU).
429      */
430     private void removeIfLimitIsSetAndReached()
431     {
432         if ( diskFileCacheAttributes.getMaxNumberOfFiles() > 0 )
433         {
434             // TODO we might want to synchronize this block.
435             if ( getSize() >= diskFileCacheAttributes.getMaxNumberOfFiles() )
436             {
437                 if ( log.isDebugEnabled() )
438                 {
439                     log.debug( logCacheName + "Max reached, removing least recently modifed" );
440                 }
441 
442                 long oldestLastModified = System.currentTimeMillis();
443                 File theLeastRecentlyModified = null;
444                 String[] fileNames = getDirectory().list();
445                 for ( int i = 0; i < fileNames.length; i++ )
446                 {
447                     File file = file( fileNames[i] );
448                     long lastModified = file.lastModified();
449                     if ( lastModified < oldestLastModified )
450                     {
451                         oldestLastModified = lastModified;
452                         theLeastRecentlyModified = file;
453                     }
454                 }
455                 if ( theLeastRecentlyModified != null )
456                 {
457                     if ( log.isDebugEnabled() )
458                     {
459                         log.debug( logCacheName + "Least recently modifed: " + theLeastRecentlyModified );
460                     }
461                     deleteWithRetry( theLeastRecentlyModified );
462                 }
463             }
464         }
465     }
466 
467     /**
468      * Tries to delete a file. If it fails, it tries several more times, pausing a few ms. each
469      * time.
470      * <p>
471      * @param file
472      * @return true if the file does not exist or if it was removed
473      */
474     private boolean deleteWithRetry( File file )
475     {
476         boolean success = file.delete();
477 
478         // TODO: The following should be identical to success == false, but it isn't
479         if ( file.exists() )
480         {
481             int maxRetries = diskFileCacheAttributes.getMaxRetriesOnDelete();
482             for ( int i = 0; i < maxRetries && !success; i++ )
483             {
484                 SleepUtil.sleepAtLeast( 5 );
485                 success = file.delete();
486             }
487         }
488         else
489         {
490             success = true;
491         }
492         if ( log.isDebugEnabled() )
493         {
494             log.debug( logCacheName + "deleteWithRetry.  success= " + success + " file: " + file );
495         }
496         return success;
497     }
498 
499     /**
500      * Tries to set the last access time to now.
501      * <p>
502      * @param file to touch
503      * @return was it successful
504      */
505     private boolean touchWithRetry( File file )
506     {
507         boolean success = file.setLastModified( System.currentTimeMillis() );
508         if ( !success )
509         {
510             int maxRetries = diskFileCacheAttributes.getMaxRetriesOnTouch();
511             if ( file.exists() )
512             {
513                 for ( int i = 0; i < maxRetries && !success; i++ )
514                 {
515                     SleepUtil.sleepAtLeast( 5 );
516                     success = file.delete();
517                 }
518             }
519         }
520         if ( log.isDebugEnabled() )
521         {
522             log.debug( logCacheName + "Last modified, success: " + success );
523         }
524         return success;
525     }
526 
527     /**
528      * Closes a stream and swallows errors.
529      * <p>
530      * @param s the stream
531      */
532     private void silentClose( InputStream s )
533     {
534         if ( s != null )
535         {
536             try
537             {
538                 s.close();
539             }
540             catch ( IOException e )
541             {
542                 log.error( logCacheName + "Failure closing stream", e );
543             }
544         }
545     }
546 
547     /**
548      * Closes a stream and swallows errors.
549      * <p>
550      * @param s the stream
551      */
552     private void silentClose( OutputStream s )
553     {
554         if ( s != null )
555         {
556             try
557             {
558                 s.close();
559             }
560             catch ( IOException e )
561             {
562                 log.error( logCacheName + "Failure closing stream", e );
563             }
564         }
565     }
566 
567     /**
568      * @param directory the directory to set
569      */
570     protected void setDirectory( File directory )
571     {
572         this.directory = directory;
573     }
574 
575     /**
576      * @return the directory
577      */
578     protected File getDirectory()
579     {
580         return directory;
581     }
582 }