Clover coverage report - Coverage timestamp: do jan 22 2004 21:12:32 CET FRAMES NO FRAMES |
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% |

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 |  |