Clover coverage report -
Coverage timestamp: do jan 22 2004 21:12:32 CET
file stats: LOC: 501   Methods: 20
NCLOC: 255   Classes: 1
 
 Source file Conditionals Statements Methods TOTAL
DiskPersistenceListener.java 57,5% 69,8% 70% 67,2%
coverage coverage
 1   
 /*
 2   
  * Copyright (c) 2002-2003 by OpenSymphony
 3   
  * All rights reserved.
 4   
  */
 5   
 package com.opensymphony.oscache.plugins.diskpersistence;
 6   
 
 7   
 import com.opensymphony.oscache.base.Config;
 8   
 import com.opensymphony.oscache.base.persistence.CachePersistenceException;
 9   
 import com.opensymphony.oscache.base.persistence.PersistenceListener;
 10   
 import com.opensymphony.oscache.web.ServletCacheAdministrator;
 11   
 
 12   
 import org.apache.commons.logging.Log;
 13   
 import org.apache.commons.logging.LogFactory;
 14   
 
 15   
 import java.io.*;
 16   
 
 17   
 import java.util.Set;
 18   
 
 19   
 import javax.servlet.jsp.PageContext;
 20   
 
 21   
 /**
 22   
  * Persist the cache data to disk.
 23   
  *
 24   
  * The code in this class is totally not thread safe it is the resonsibility
 25   
  * of the cache using this persistence listener to handle the concurrency.
 26   
  *
 27   
  * @version        $Revision: 1.2 $
 28   
  * @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
 29   
  * @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
 30   
  * @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
 31   
  */
 32   
 public final class DiskPersistenceListener implements PersistenceListener, Serializable {
 33   
     protected final static String CACHE_PATH_KEY = "cache.path";
 34   
 
 35   
     /**
 36   
      * File extension for disk cache file
 37   
      */
 38   
     private final static String CACHE_EXTENSION = "cache";
 39   
 
 40   
     /**
 41   
      * The directory that cache groups are stored under
 42   
      */
 43   
     private final static String GROUP_DIRECTORY = "__groups__";
 44   
 
 45   
     /**
 46   
      * Sub path name for application cache
 47   
      */
 48   
     private final static String APPLICATION_CACHE_SUBPATH = "application";
 49   
 
 50   
     /**
 51   
      * Sub path name for session cache
 52   
      */
 53   
     private final static String SESSION_CACHE_SUBPATH = "session";
 54   
 
 55   
     /**
 56   
      * Property to get the temporary working directory of the servlet container.
 57   
      */
 58   
     private static final String CONTEXT_TMPDIR = "javax.servlet.context.tempdir";
 59   
     private static transient final Log log = LogFactory.getLog(DiskPersistenceListener.class);
 60   
 
 61   
     /**
 62   
      * Base path where the disk cache reside.
 63   
      */
 64   
     private File cachePath = null;
 65   
     private File contextTmpDir;
 66   
 
 67   
     /**
 68   
      * Root path for disk cache
 69   
      */
 70   
     private String root = null;
 71   
 
 72   
     /**
 73   
        *        Get the physical cache path on disk.
 74   
        *
 75   
        *        @return        A file representing the physical cache location.
 76   
        */
 77  34
     public File getCachePath() {
 78  34
         return cachePath;
 79   
     }
 80   
 
 81   
     /**
 82   
      * Verify if a group exists in the cache
 83   
      *
 84   
      * @param group The group name to check
 85   
      * @return True if it exists
 86   
      * @throws CachePersistenceException
 87   
      */
 88  0
     public boolean isGroupStored(String group) throws CachePersistenceException {
 89  0
         try {
 90  0
             File file = getCacheGroupFile(group);
 91   
 
 92  0
             return file.exists();
 93   
         } catch (Exception e) {
 94  0
             throw new CachePersistenceException("Unable verify group '" + group + "' exists in the cache: " + e);
 95   
         }
 96   
     }
 97   
 
 98   
     /**
 99   
      * Verify if an object is currently stored in the cache
 100   
      *
 101   
      * @param key The object key
 102   
      * @return True if it exists
 103   
      * @throws CachePersistenceException
 104   
      */
 105  0
     public boolean isStored(String key) throws CachePersistenceException {
 106  0
         try {
 107  0
             File file = getCacheFile(key);
 108   
 
 109  0
             return file.exists();
 110   
         } catch (Exception e) {
 111  0
             throw new CachePersistenceException("Unable verify id '" + key + "' is stored in the cache: " + e);
 112   
         }
 113   
     }
 114   
 
 115   
     /**
 116   
      * Clears the whole cache directory, starting from the root
 117   
      *
 118   
      * @throws CachePersistenceException
 119   
      */
 120  6
     public void clear() throws CachePersistenceException {
 121  6
         clear(root);
 122   
     }
 123   
 
 124   
     /**
 125   
      * Initialises this <tt>DiskPersistenceListener</tt> using the supplied
 126   
      * configuration.
 127   
      *
 128   
      * @param config The OSCache configuration
 129   
      */
 130  34
     public PersistenceListener configure(Config config) {
 131  34
         String sessionId = null;
 132  34
         int scope = 0;
 133  34
         initFileCaching(config.getProperty(CACHE_PATH_KEY));
 134   
 
 135  34
         if (config.getProperty(ServletCacheAdministrator.HASH_KEY_SESSION_ID) != null) {
 136  0
             sessionId = config.getProperty(ServletCacheAdministrator.HASH_KEY_SESSION_ID);
 137   
         }
 138   
 
 139  34
         if (config.getProperty(ServletCacheAdministrator.HASH_KEY_SCOPE) != null) {
 140  0
             scope = Integer.parseInt(config.getProperty(ServletCacheAdministrator.HASH_KEY_SCOPE));
 141   
         }
 142   
 
 143  34
         StringBuffer root = new StringBuffer(getCachePath().getPath());
 144  34
         root.append("/");
 145  34
         root.append(getPathPart(scope));
 146   
 
 147  34
         if ((sessionId != null) && (sessionId.length() > 0)) {
 148  0
             root.append("/");
 149  0
             root.append(sessionId);
 150   
         }
 151   
 
 152  34
         this.root = root.toString();
 153  34
         this.contextTmpDir = (File) config.get(ServletCacheAdministrator.HASH_KEY_CONTEXT_TMPDIR);
 154   
 
 155  34
         return this;
 156   
     }
 157   
 
 158   
     /**
 159   
      * Delete a single cache entry.
 160   
      *
 161   
      * @param key The object key to delete
 162   
      * @throws CachePersistenceException
 163   
      */
 164  0
     public void remove(String key) throws CachePersistenceException {
 165  0
         File file = getCacheFile(key);
 166  0
         remove(file);
 167   
     }
 168   
 
 169   
     /**
 170   
      * Deletes an entire group from the cache.
 171   
      *
 172   
      * @param groupName The name of the group to delete
 173   
      * @throws CachePersistenceException
 174   
      */
 175  0
     public void removeGroup(String groupName) throws CachePersistenceException {
 176  0
         File file = getCacheGroupFile(groupName);
 177  0
         remove(file);
 178   
     }
 179   
 
 180   
     /**
 181   
      * Retrieve an object from the disk
 182   
      *
 183   
      * @param key The object key
 184   
      * @return The retrieved object
 185   
      * @throws CachePersistenceException
 186   
      */
 187  269
     public Object retrieve(String key) throws CachePersistenceException {
 188  269
         return retrieve(getCacheFile(key));
 189   
     }
 190   
 
 191   
     /**
 192   
      * Retrieves a group from the cache, or <code>null</code> if the group
 193   
      * file could not be found.
 194   
      *
 195   
      * @param groupName The name of the group to retrieve.
 196   
      * @return A <code>Set</code> containing keys of all of the cache
 197   
      * entries that belong to this group.
 198   
      * @throws CachePersistenceException
 199   
      */
 200  77
     public Set retrieveGroup(String groupName) throws CachePersistenceException {
 201  77
         File groupFile = getCacheGroupFile(groupName);
 202   
 
 203  77
         try {
 204  77
             return (Set) retrieve(groupFile);
 205   
         } catch (ClassCastException e) {
 206  0
             throw new CachePersistenceException("Group file " + groupFile + " was not persisted as a Set: " + e);
 207   
         }
 208   
     }
 209   
 
 210   
     /**
 211   
      * Stores an object in cache
 212   
      *
 213   
      * @param key The object's key
 214   
      * @param obj The object to store
 215   
      * @throws CachePersistenceException
 216   
      */
 217  161
     public void store(String key, Object obj) throws CachePersistenceException {
 218  161
         File file = getCacheFile(key);
 219  161
         store(file, obj);
 220   
     }
 221   
 
 222   
     /**
 223   
      * Stores a group in the persistent cache. This will overwrite any existing
 224   
      * group with the same name
 225   
      */
 226  68
     public void storeGroup(String groupName, Set group) throws CachePersistenceException {
 227  68
         File groupFile = getCacheGroupFile(groupName);
 228  68
         store(groupFile, group);
 229   
     }
 230   
 
 231   
     /**
 232   
      * Allows to translate to the temp dir of the servlet container if cachePathStr
 233   
      * is javax.servlet.context.tempdir.
 234   
      *
 235   
      * @param cachePathStr  Cache path read from the properties file.
 236   
      * @return Adjusted cache path
 237   
      */
 238  0
     protected String adjustFileCachePath(String cachePathStr) {
 239  0
         if (cachePathStr.compareToIgnoreCase(CONTEXT_TMPDIR) == 0) {
 240  0
             cachePathStr = contextTmpDir.getAbsolutePath();
 241   
         }
 242   
 
 243  0
         return cachePathStr;
 244   
     }
 245   
 
 246   
     /**
 247   
      *        Set caching to file on or off.
 248   
      *  If the <code>cache.path</code> property exists, we assume file caching is turned on.
 249   
      *        By the same token, to turn off file caching just remove this property.
 250   
      */
 251  34
     protected void initFileCaching(String cachePathStr) {
 252  34
         if (cachePathStr != null) {
 253  34
             cachePath = new File(cachePathStr);
 254   
 
 255  34
             try {
 256  34
                 if (!cachePath.exists()) {
 257  2
                     if (log.isInfoEnabled()) {
 258  2
                         log.info("cache.path '" + cachePathStr + "' does not exist, creating");
 259   
                     }
 260   
 
 261  2
                     cachePath.mkdirs();
 262   
                 }
 263   
 
 264  34
                 if (!cachePath.isDirectory()) {
 265  0
                     log.error("cache.path '" + cachePathStr + "' is not a directory");
 266  0
                     cachePath = null;
 267  34
                 } else if (!cachePath.canWrite()) {
 268  0
                     log.error("cache.path '" + cachePathStr + "' is not a writable location");
 269  0
                     cachePath = null;
 270   
                 }
 271   
             } catch (Exception e) {
 272  0
                 log.error("cache.path '" + cachePathStr + "' could not be used", e);
 273  0
                 cachePath = null;
 274   
             }
 275   
         } else {
 276   
             // Use default value
 277   
         }
 278   
     }
 279   
 
 280  0
     protected void remove(File file) throws CachePersistenceException {
 281  0
         try {
 282   
             // Loop until we are able to delete (No current read).
 283   
             // The cache must ensure that there are never two concurrent threads
 284   
             // doing write (store and delete) operations on the same item.
 285   
             // Delete only should be enough but file.exists prevents infinite loop
 286  0
             while (!file.delete() && file.exists()) {
 287   
                 ;
 288   
             }
 289   
         } catch (Exception e) {
 290  0
             throw new CachePersistenceException("Unable to remove '" + file + "' from the cache: " + e);
 291   
         }
 292   
     }
 293   
 
 294   
     /**
 295   
      * Stores an object using the supplied file object
 296   
      *
 297   
      * @param file The file to use for storing the object
 298   
      * @param obj the object to store
 299   
      * @throws CachePersistenceException
 300   
      */
 301  229
     protected void store(File file, Object obj) throws CachePersistenceException {
 302   
         // check if the directory structure required exists and create it if it doesn't
 303  229
         File filepath = new File(file.getParent());
 304   
 
 305  229
         try {
 306  229
             if (!filepath.exists()) {
 307  10
                 filepath.mkdirs();
 308   
             }
 309   
         } catch (Exception e) {
 310  0
             throw new CachePersistenceException("Unable to create the directory " + filepath);
 311   
         }
 312   
 
 313   
         // Loop until we are able to delete (No current read).
 314   
         // The cache must ensure that there are never two concurrent threads
 315   
         // doing write (store and delete) operations on the same item.
 316   
         // Delete only should be enough but file.exists prevents infinite loop
 317  229
         while (file.exists() && !file.delete()) {
 318   
             ;
 319   
         }
 320   
 
 321   
         // Write the object to disk
 322  229
         FileOutputStream fout = null;
 323  229
         ObjectOutputStream oout = null;
 324   
 
 325  229
         try {
 326  229
             fout = new FileOutputStream(file);
 327  229
             oout = new ObjectOutputStream(fout);
 328  229
             oout.writeObject(obj);
 329  229
             oout.flush();
 330   
         } catch (Exception e) {
 331  0
             throw new CachePersistenceException("Unable to write '" + file + "' in the cache. Exception: " + e.getClass().getName() + ", Message: " + e.getMessage());
 332   
         } finally {
 333  229
             try {
 334  229
                 fout.close();
 335   
             } catch (Exception e) {
 336   
             }
 337   
 
 338  229
             try {
 339  229
                 oout.close();
 340   
             } catch (Exception e) {
 341   
             }
 342   
         }
 343   
     }
 344   
 
 345   
     /**
 346   
      * Build fully qualified cache file name specifying a cache entry key.
 347   
      *
 348   
      * @param key   Cache Entry Key.
 349   
      * @return File reference.
 350   
      */
 351  430
     private File getCacheFile(String key) {
 352  430
         if ((key == null) || (key.length() == 0)) {
 353  0
             throw new IllegalArgumentException("Invalid key '" + key + "' specified to getCacheFile.");
 354   
         }
 355   
 
 356  430
         char[] chars = key.toCharArray();
 357  430
         char[] fileChars = new char[chars.length];
 358   
 
 359  430
         for (int i = 0; i < chars.length; i++) {
 360  6324
             char c = chars[i];
 361   
 
 362  6324
             switch (c) {
 363   
                 case '.':
 364   
                 case '/':
 365   
                 case '\\':
 366   
                 case ' ':
 367   
                 case ':':
 368   
                 case ';':
 369   
                 case '"':
 370   
                 case '\'':
 371  728
                     fileChars[i] = '_';
 372  728
                     break;
 373   
                 default:
 374  5596
                     fileChars[i] = c;
 375   
             }
 376   
         }
 377   
 
 378  430
         File file = new File(root, new String(fileChars) + "." + CACHE_EXTENSION);
 379   
 
 380  430
         return file;
 381   
     }
 382   
 
 383   
     /**
 384   
      * Builds a fully qualified file name that specifies a cache group entry.
 385   
      *
 386   
      * @param group The name of the group
 387   
      * @return A File reference
 388   
      */
 389  145
     private File getCacheGroupFile(String group) {
 390  145
         int AVERAGE_PATH_LENGTH = 30;
 391   
 
 392  145
         if ((group == null) || (group.length() == 0)) {
 393  0
             throw new IllegalArgumentException("Invalid group '" + group + "' specified to getCacheGroupFile.");
 394   
         }
 395   
 
 396  145
         StringBuffer path = new StringBuffer(AVERAGE_PATH_LENGTH);
 397   
 
 398   
         // Build a fully qualified file name for this group
 399  145
         path.append(GROUP_DIRECTORY).append('/');
 400  145
         path.append(group).append('.').append(CACHE_EXTENSION);
 401   
 
 402  145
         return new File(root, path.toString());
 403   
     }
 404   
 
 405   
     /**
 406   
      * This allows to persist different scopes in different path in the case of
 407   
      * file caching.
 408   
      *
 409   
      * @param scope   Cache scope.
 410   
      * @return The scope subpath
 411   
      */
 412  34
     private String getPathPart(int scope) {
 413  34
         if (scope == PageContext.SESSION_SCOPE) {
 414  0
             return SESSION_CACHE_SUBPATH;
 415   
         } else {
 416  34
             return APPLICATION_CACHE_SUBPATH;
 417   
         }
 418   
     }
 419   
 
 420   
     /**
 421   
      * Clears a whole directory, starting from the specified
 422   
      * directory
 423   
      *
 424   
      * @param baseDirName The root directory to delete
 425   
      * @throws CachePersistenceException
 426   
      */
 427  6
     private void clear(String baseDirName) throws CachePersistenceException {
 428  6
         File baseDir = new File(baseDirName);
 429  6
         File[] fileList = baseDir.listFiles();
 430   
 
 431  6
         try {
 432  6
             if (fileList != null) {
 433   
                 // Loop through all the files and directory to delete them
 434  6
                 for (int count = 0; count < fileList.length; count++) {
 435  6
                     if (fileList[count].isFile()) {
 436  6
                         fileList[count].delete();
 437   
                     } else {
 438   
                         // Make a recursive call to delete the directory
 439  0
                         clear(fileList[count].toString());
 440  0
                         fileList[count].delete();
 441   
                     }
 442   
                 }
 443   
             }
 444   
 
 445   
             // Delete the root directory
 446  6
             baseDir.delete();
 447   
         } catch (Exception e) {
 448  0
             throw new CachePersistenceException("Unable to clear the cache directory");
 449   
         }
 450   
     }
 451   
 
 452   
     /**
 453   
      * Retrives a serialized object from the supplied file, or returns
 454   
      * <code>null</code> if the file does not exist.
 455   
      *
 456   
      * @param file The file to deserialize
 457   
      * @return The deserialized object
 458   
      * @throws CachePersistenceException
 459   
      */
 460  346
     private Object retrieve(File file) throws CachePersistenceException {
 461  346
         Object readContent = null;
 462  346
         boolean fileExist;
 463   
 
 464  346
         try {
 465  346
             fileExist = file.exists();
 466   
         } catch (Exception e) {
 467  0
             throw new CachePersistenceException("Unable to verify if " + file + " exists: " + e);
 468   
         }
 469   
 
 470   
         // Read the file if it exists
 471  346
         if (fileExist) {
 472  220
             BufferedInputStream in = null;
 473  220
             ObjectInputStream oin = null;
 474   
 
 475  220
             try {
 476  220
                 in = new BufferedInputStream(new FileInputStream(file));
 477  220
                 oin = new ObjectInputStream(in);
 478  220
                 readContent = oin.readObject();
 479   
             } catch (Exception e) {
 480   
                 // We expect this exception to occur.
 481   
                 // This is when the item will be invalidated (written or deleted)
 482   
                 // during read.
 483   
                 // The cache has the logic to retry reading.
 484  0
                 throw new CachePersistenceException("Unable to read '" + file.getAbsolutePath() + "' from the cache: " + e);
 485   
             } finally {
 486  220
                 try {
 487  220
                     oin.close();
 488   
                 } catch (Exception ex) {
 489   
                 }
 490   
 
 491  220
                 try {
 492  220
                     in.close();
 493   
                 } catch (Exception ex) {
 494   
                 }
 495   
             }
 496   
         }
 497   
 
 498  346
         return readContent;
 499   
     }
 500   
 }
 501