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