|
|||||||||||||||||||
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="mailto:chris@swebtec.com">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 |
|
|