Clover coverage report - Coverage timestamp: Sa Jul 7 2007 09:11:40 CEST FRAMES NO FRAMES |
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% |

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