001: /*
002: * Copyright (c) 2002-2003 by OpenSymphony
003: * All rights reserved.
004: */
005: package com.opensymphony.oscache.plugins.diskpersistence;
006:
007: import com.opensymphony.oscache.base.Config;
008: import com.opensymphony.oscache.base.persistence.CachePersistenceException;
009: import com.opensymphony.oscache.base.persistence.PersistenceListener;
010: import com.opensymphony.oscache.web.ServletCacheAdministrator;
011:
012: import org.apache.commons.logging.Log;
013: import org.apache.commons.logging.LogFactory;
014:
015: import java.io.*;
016:
017: import java.util.Set;
018:
019: import javax.servlet.jsp.PageContext;
020:
021: /**
022: * Persist the cache data to disk.
023: *
024: * The code in this class is totally not thread safe it is the resonsibility
025: * of the cache using this persistence listener to handle the concurrency.
026: *
027: * @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
028: * @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
029: * @author <a href="mailto:chris@swebtec.com">Chris Miller</a>
030: * @author <a href="mailto:amarch@soe.sony.com">Andres March</a>
031: */
032: public abstract class AbstractDiskPersistenceListener implements
033: PersistenceListener, Serializable {
034: public final static String CACHE_PATH_KEY = "cache.path";
035:
036: /**
037: * File extension for disk cache file
038: */
039: protected final static String CACHE_EXTENSION = "cache";
040:
041: /**
042: * The directory that cache groups are stored under
043: */
044: protected final static String GROUP_DIRECTORY = "__groups__";
045:
046: /**
047: * Sub path name for application cache
048: */
049: protected final static String APPLICATION_CACHE_SUBPATH = "application";
050:
051: /**
052: * Sub path name for session cache
053: */
054: protected final static String SESSION_CACHE_SUBPATH = "session";
055:
056: /**
057: * Property to get the temporary working directory of the servlet container.
058: */
059: protected static final String CONTEXT_TMPDIR = "javax.servlet.context.tempdir";
060: private static transient final Log log = LogFactory
061: .getLog(AbstractDiskPersistenceListener.class);
062:
063: /**
064: * Base path where the disk cache reside.
065: */
066: private File cachePath = null;
067: private File contextTmpDir;
068:
069: /**
070: * Root path for disk cache
071: */
072: private String root = null;
073:
074: /**
075: * Get the physical cache path on disk.
076: *
077: * @return A file representing the physical cache location.
078: */
079: public File getCachePath() {
080: return cachePath;
081: }
082:
083: /**
084: * Get the root directory for persisting the cache on disk.
085: * This path includes scope and sessionId, if any.
086: *
087: * @return A String representing the root directory.
088: */
089: public String getRoot() {
090: return root;
091: }
092:
093: /**
094: * Get the servlet context tmp directory.
095: *
096: * @return A file representing the servlet context tmp directory.
097: */
098: public File getContextTmpDir() {
099: return contextTmpDir;
100: }
101:
102: /**
103: * Verify if a group exists in the cache
104: *
105: * @param group The group name to check
106: * @return True if it exists
107: * @throws CachePersistenceException
108: */
109: public boolean isGroupStored(String group)
110: throws CachePersistenceException {
111: try {
112: File file = getCacheGroupFile(group);
113:
114: return file.exists();
115: } catch (Exception e) {
116: throw new CachePersistenceException("Unable verify group '"
117: + group + "' exists in the cache: " + e);
118: }
119: }
120:
121: /**
122: * Verify if an object is currently stored in the cache
123: *
124: * @param key The object key
125: * @return True if it exists
126: * @throws CachePersistenceException
127: */
128: public boolean isStored(String key)
129: throws CachePersistenceException {
130: try {
131: File file = getCacheFile(key);
132:
133: return file.exists();
134: } catch (Exception e) {
135: throw new CachePersistenceException("Unable verify id '"
136: + key + "' is stored in the cache: " + e);
137: }
138: }
139:
140: /**
141: * Clears the whole cache directory, starting from the root
142: *
143: * @throws CachePersistenceException
144: */
145: public void clear() throws CachePersistenceException {
146: clear(root);
147: }
148:
149: /**
150: * Initialises this <tt>DiskPersistenceListener</tt> using the supplied
151: * configuration.
152: *
153: * @param config The OSCache configuration
154: */
155: public PersistenceListener configure(Config config) {
156: String sessionId = null;
157: int scope = 0;
158: initFileCaching(config.getProperty(CACHE_PATH_KEY));
159:
160: if (config
161: .getProperty(ServletCacheAdministrator.HASH_KEY_SESSION_ID) != null) {
162: sessionId = config
163: .getProperty(ServletCacheAdministrator.HASH_KEY_SESSION_ID);
164: }
165:
166: if (config
167: .getProperty(ServletCacheAdministrator.HASH_KEY_SCOPE) != null) {
168: scope = Integer
169: .parseInt(config
170: .getProperty(ServletCacheAdministrator.HASH_KEY_SCOPE));
171: }
172:
173: StringBuffer root = new StringBuffer(getCachePath().getPath());
174: root.append("/");
175: root.append(getPathPart(scope));
176:
177: if ((sessionId != null) && (sessionId.length() > 0)) {
178: root.append("/");
179: root.append(sessionId);
180: }
181:
182: this .root = root.toString();
183: this .contextTmpDir = (File) config
184: .get(ServletCacheAdministrator.HASH_KEY_CONTEXT_TMPDIR);
185:
186: return this ;
187: }
188:
189: /**
190: * Delete a single cache entry.
191: *
192: * @param key The object key to delete
193: * @throws CachePersistenceException
194: */
195: public void remove(String key) throws CachePersistenceException {
196: File file = getCacheFile(key);
197: remove(file);
198: }
199:
200: /**
201: * Deletes an entire group from the cache.
202: *
203: * @param groupName The name of the group to delete
204: * @throws CachePersistenceException
205: */
206: public void removeGroup(String groupName)
207: throws CachePersistenceException {
208: File file = getCacheGroupFile(groupName);
209: remove(file);
210: }
211:
212: /**
213: * Retrieve an object from the disk
214: *
215: * @param key The object key
216: * @return The retrieved object
217: * @throws CachePersistenceException
218: */
219: public Object retrieve(String key) throws CachePersistenceException {
220: return retrieve(getCacheFile(key));
221: }
222:
223: /**
224: * Retrieves a group from the cache, or <code>null</code> if the group
225: * file could not be found.
226: *
227: * @param groupName The name of the group to retrieve.
228: * @return A <code>Set</code> containing keys of all of the cache
229: * entries that belong to this group.
230: * @throws CachePersistenceException
231: */
232: public Set retrieveGroup(String groupName)
233: throws CachePersistenceException {
234: File groupFile = getCacheGroupFile(groupName);
235:
236: try {
237: return (Set) retrieve(groupFile);
238: } catch (ClassCastException e) {
239: throw new CachePersistenceException("Group file "
240: + groupFile + " was not persisted as a Set: " + e);
241: }
242: }
243:
244: /**
245: * Stores an object in cache
246: *
247: * @param key The object's key
248: * @param obj The object to store
249: * @throws CachePersistenceException
250: */
251: public void store(String key, Object obj)
252: throws CachePersistenceException {
253: File file = getCacheFile(key);
254: store(file, obj);
255: }
256:
257: /**
258: * Stores a group in the persistent cache. This will overwrite any existing
259: * group with the same name
260: */
261: public void storeGroup(String groupName, Set group)
262: throws CachePersistenceException {
263: File groupFile = getCacheGroupFile(groupName);
264: store(groupFile, group);
265: }
266:
267: /**
268: * Allows to translate to the temp dir of the servlet container if cachePathStr
269: * is javax.servlet.context.tempdir.
270: *
271: * @param cachePathStr Cache path read from the properties file.
272: * @return Adjusted cache path
273: */
274: protected String adjustFileCachePath(String cachePathStr) {
275: if (cachePathStr.compareToIgnoreCase(CONTEXT_TMPDIR) == 0) {
276: cachePathStr = contextTmpDir.getAbsolutePath();
277: }
278:
279: return cachePathStr;
280: }
281:
282: /**
283: * Set caching to file on or off.
284: * If the <code>cache.path</code> property exists, we assume file caching is turned on.
285: * By the same token, to turn off file caching just remove this property.
286: */
287: protected void initFileCaching(String cachePathStr) {
288: if (cachePathStr != null) {
289: cachePath = new File(cachePathStr);
290:
291: try {
292: if (!cachePath.exists()) {
293: if (log.isInfoEnabled()) {
294: log.info("cache.path '" + cachePathStr
295: + "' does not exist, creating");
296: }
297:
298: cachePath.mkdirs();
299: }
300:
301: if (!cachePath.isDirectory()) {
302: log.error("cache.path '" + cachePathStr
303: + "' is not a directory");
304: cachePath = null;
305: } else if (!cachePath.canWrite()) {
306: log.error("cache.path '" + cachePathStr
307: + "' is not a writable location");
308: cachePath = null;
309: }
310: } catch (Exception e) {
311: log.error("cache.path '" + cachePathStr
312: + "' could not be used", e);
313: cachePath = null;
314: }
315: } else {
316: // Use default value
317: }
318: }
319:
320: // try 30s to delete the file
321: private static final long DELETE_THREAD_SLEEP = 500;
322: private static final int DELETE_COUNT = 60;
323:
324: protected void remove(File file) throws CachePersistenceException {
325: int count = DELETE_COUNT;
326: try {
327: // Loop until we are able to delete (No current read).
328: // The cache must ensure that there are never two concurrent threads
329: // doing write (store and delete) operations on the same item.
330: // Delete only should be enough but file.exists prevents infinite loop
331: while (file.exists() && !file.delete() && count != 0) {
332: count--;
333: try {
334: Thread.sleep(DELETE_THREAD_SLEEP);
335: } catch (InterruptedException ignore) {
336: }
337: }
338: } catch (Exception e) {
339: throw new CachePersistenceException("Unable to remove '"
340: + file + "' from the cache: " + e);
341: }
342: if (file.exists() && count == 0) {
343: throw new CachePersistenceException("Unable to delete '"
344: + file + "' from the cache. " + DELETE_COUNT
345: + " attempts at " + DELETE_THREAD_SLEEP
346: + " milliseconds intervals.");
347: }
348: }
349:
350: /**
351: * Stores an object using the supplied file object
352: *
353: * @param file The file to use for storing the object
354: * @param obj the object to store
355: * @throws CachePersistenceException
356: */
357: protected void store(File file, Object obj)
358: throws CachePersistenceException {
359: // check if file exists before testing if parent exists
360: if (!file.exists()) {
361: // check if the directory structure required exists and create it if it doesn't
362: File filepath = new File(file.getParent());
363:
364: try {
365: if (!filepath.exists()) {
366: filepath.mkdirs();
367: }
368: } catch (Exception e) {
369: throw new CachePersistenceException(
370: "Unable to create the directory " + filepath);
371: }
372: }
373:
374: // Write the object to disk
375: try {
376: FileOutputStream fout = new FileOutputStream(file);
377: try {
378: ObjectOutputStream oout = new ObjectOutputStream(
379: new BufferedOutputStream(fout));
380: try {
381: oout.writeObject(obj);
382: oout.flush();
383: } finally {
384: try {
385: oout.close();
386: } catch (Exception e) {
387: }
388: }
389: } finally {
390: try {
391: fout.close();
392: } catch (Exception e) {
393: }
394: }
395: } catch (Exception e) {
396: int count = DELETE_COUNT;
397: while (file.exists() && !file.delete() && count != 0) {
398: count--;
399: try {
400: Thread.sleep(DELETE_THREAD_SLEEP);
401: } catch (InterruptedException ignore) {
402: }
403: }
404: throw new CachePersistenceException("Unable to write '"
405: + file + "' in the cache. Exception: "
406: + e.getClass().getName() + ", Message: "
407: + e.getMessage());
408: }
409: }
410:
411: /**
412: * Build fully qualified cache file for the specified cache entry key.
413: *
414: * @param key Cache Entry Key.
415: * @return File reference.
416: */
417: protected File getCacheFile(String key) {
418: char[] fileChars = getCacheFileName(key);
419:
420: File file = new File(root, new String(fileChars) + "."
421: + CACHE_EXTENSION);
422:
423: return file;
424: }
425:
426: /**
427: * Build cache file name for the specified cache entry key.
428: *
429: * @param key Cache Entry Key.
430: * @return char[] file name.
431: */
432: protected abstract char[] getCacheFileName(String key);
433:
434: /**
435: * Builds a fully qualified file name that specifies a cache group entry.
436: *
437: * @param group The name of the group
438: * @return A File reference
439: */
440: private File getCacheGroupFile(String group) {
441: int AVERAGE_PATH_LENGTH = 30;
442:
443: if ((group == null) || (group.length() == 0)) {
444: throw new IllegalArgumentException("Invalid group '"
445: + group + "' specified to getCacheGroupFile.");
446: }
447:
448: StringBuffer path = new StringBuffer(AVERAGE_PATH_LENGTH);
449:
450: // Build a fully qualified file name for this group
451: path.append(GROUP_DIRECTORY).append('/');
452: path.append(getCacheFileName(group)).append('.').append(
453: CACHE_EXTENSION);
454:
455: return new File(root, path.toString());
456: }
457:
458: /**
459: * This allows to persist different scopes in different path in the case of
460: * file caching.
461: *
462: * @param scope Cache scope.
463: * @return The scope subpath
464: */
465: private String getPathPart(int scope) {
466: if (scope == PageContext.SESSION_SCOPE) {
467: return SESSION_CACHE_SUBPATH;
468: } else {
469: return APPLICATION_CACHE_SUBPATH;
470: }
471: }
472:
473: /**
474: * Clears a whole directory, starting from the specified
475: * directory
476: *
477: * @param baseDirName The root directory to delete
478: * @throws CachePersistenceException
479: */
480: private void clear(String baseDirName)
481: throws CachePersistenceException {
482: File baseDir = new File(baseDirName);
483: File[] fileList = baseDir.listFiles();
484:
485: try {
486: if (fileList != null) {
487: // Loop through all the files and directory to delete them
488: for (int count = 0; count < fileList.length; count++) {
489: if (fileList[count].isFile()) {
490: fileList[count].delete();
491: } else {
492: // Make a recursive call to delete the directory
493: clear(fileList[count].toString());
494: fileList[count].delete();
495: }
496: }
497: }
498:
499: // Delete the root directory
500: baseDir.delete();
501: } catch (Exception e) {
502: throw new CachePersistenceException(
503: "Unable to clear the cache directory");
504: }
505: }
506:
507: /**
508: * Retrives a serialized object from the supplied file, or returns
509: * <code>null</code> if the file does not exist.
510: *
511: * @param file The file to deserialize
512: * @return The deserialized object
513: * @throws CachePersistenceException
514: */
515: private Object retrieve(File file) throws CachePersistenceException {
516: Object readContent = null;
517: boolean fileExist;
518:
519: try {
520: fileExist = file.exists();
521: } catch (Exception e) {
522: throw new CachePersistenceException("Unable to verify if "
523: + file + " exists: " + e);
524: }
525:
526: // Read the file if it exists
527: if (fileExist) {
528: ObjectInputStream oin = null;
529:
530: try {
531: BufferedInputStream in = new BufferedInputStream(
532: new FileInputStream(file));
533: oin = new ObjectInputStream(in);
534: readContent = oin.readObject();
535: } catch (Exception e) {
536: // We expect this exception to occur.
537: // This is when the item will be invalidated (written or deleted)
538: // during read.
539: // The cache has the logic to retry reading.
540: throw new CachePersistenceException("Unable to read '"
541: + file.getAbsolutePath() + "' from the cache: "
542: + e);
543: } finally {
544: // HHDE: no need to close in. Will be closed by oin
545: try {
546: oin.close();
547: } catch (Exception ex) {
548: }
549: }
550: }
551:
552: return readContent;
553: }
554: }
|