/*
* This software and supporting documentation were developed by
*
* Siemens Corporate Technology
* Competence Center Knowledge Management and Business Transformation
* D-81730 Munich, Germany
*
* Authors (representing a really great team ;-) )
* Stefan B. Augustin, Thorbj�rn Hansen, Manfred Langen
*
* This software is Open Source under GNU General Public License (GPL).
* Read the text of this license in LICENSE.TXT
* or look at www.opensource.org/licenses/
*
* Once more we emphasize, that:
* THIS SOFTWARE IS MADE AVAILABLE, AS IS, WITHOUT ANY WARRANTY
* REGARDING THE SOFTWARE, ITS PERFORMANCE OR
* FITNESS FOR ANY PARTICULAR USE, FREEDOM FROM ANY COMPUTER DISEASES OR
* ITS CONFORMITY TO ANY SPECIFICATION. THE ENTIRE RISK AS TO QUALITY AND
* PERFORMANCE OF THE SOFTWARE IS WITH THE USER.
*
*/
package KFM.Cache;
import java.io.*;
import java.util.*;
import com.coolservlets.util.*;
import com.coolservlets.util.LinkedList;
import KFM.Converter; // java.net.URLDecoder from JDK 1.2
import KFM.File.FileUtils;
import KFM.Exceptions.ProgrammerException;
import KFM.log.*;
import KFM.Cache.CachedHTMLItem;
/**
* NOTE: Do not use this class directly, use FileCache2 instead.
*
* File cache implementation. See base class for detailed
* documentation.<br>
* Instead of storing data in memory (as the base class does), it stores
* data on the file system.
*
* <p><b><i>Methods assume that the {@link appl.Portal.GUI.CachedHTMLItem
* CachedHTMLItem} class is used.</i></b>
*
* @see Cache
*
*/
public class FileCache extends com.coolservlets.util.Cache {
/**
* Cache directory.
*/
protected String mCacheDir;
/**
* Create a new cache with default values. Default cache size is 128K with
* no maximum lifetime.
*
* @param aCacheDir absolute path name of cache directory
*/
public FileCache(String aCacheDir) {
//Our primary data structure is a hash map. The default capacity of 11
//is too small in almost all cases, so we set it bigger.
cachedObjectsHash = new HashMap(103);
lastAccessedList = new LinkedList();
ageList = new LinkedList();
createOrReuseCacheDirectory(aCacheDir);
}
/**
* Create a new cache and specify the maximum size for the cache in bytes.
* Items added to the cache will have no maximum lifetime.
*
* @param maxSize the maximum size of the cache in bytes.
*/
public FileCache(int maxSize, String aCacheDir) {
this(aCacheDir);
this.maxSize = maxSize;
}
/**
* Create a new cache and specify the maximum lifetime of objects. The
* time should be specified in milleseconds. The minimum lifetime of any
* cache object is 1000 milleseconds (1 second). Additionally, cache
* expirations have a 1000 millesecond resolution, which means that all
* objects are guaranteed to be expired within 1000 milliseconds of their
* maximum lifetime.
*
* @param maxLifetime the maximum amount of time objects can exist in
* cache before being deleted.
* @param aCacheDir absolute path name of cache directory
*/
public FileCache(long maxLifetime, String aCacheDir) {
this(aCacheDir);
this.maxLifetime = maxLifetime;
}
/**
* Create a new cache and specify the maximum size of for the cache in
* bytes, and the maximum lifetime of objects.
*
* @param maxSize the maximum size of the cache in bytes.
* @param maxLifetime the maximum amount of time objects can exist in
* cache before being deleted.
* @param aCacheDir absolute path name of cache directory
*/
public FileCache(int maxSize, long maxLifetime, String aCacheDir) {
this(aCacheDir);
this.maxSize = maxSize;
this.maxLifetime = maxLifetime;
KFMSystem.log.info("FileCache::FileCache(): maxSize=" + maxSize + ", maxLifetime=" + maxLifetime + ", aCacheDir=" + aCacheDir);
}
private void createOrReuseCacheDirectory(String aCacheDir) {
this.mCacheDir = aCacheDir;
File tDir = new File(mCacheDir);
if (tDir.exists()) {
// assume old files in cache come from previous run, reuse them
// The current maxSize value, which was set in base class,
// is probably to small (as it will be set later),
// maxSize is temporarily set to MAX_VALUE.
// Otherwise files will be deleted by add().
int tMaxSizeTmp = this.maxSize; // default from base class (128 K)
this.maxSize = Integer.MAX_VALUE;
String[] tFiles = tDir.list();
List tFileList = new ArrayList();
for (int i=0; i < tFiles.length; i++) {
File tInputFile = new File(tDir, tFiles[i]);
tFileList.add(tInputFile);
}
Collections.sort(tFileList, new FileComparator());
KFMSystem.log.info("FileCache::createOrReuseCacheDirectory(): reuse "
+ tFileList.size() + " files");
for (int i = 0; i < tFileList.size(); i++) {
File tInputFile = (File) tFileList.get(i);
String tFilename = tInputFile.getAbsolutePath();
String tKey = null;
try {
tKey = Converter.decode(tInputFile.getName());
} catch (Exception e) {
throw new ProgrammerException("FileCache::createOrReuseCacheDirectory(): "
+ "Exception in Converter.decode()");
}
KFMSystem.log.info("FileCache::createOrReuseCacheDirectory(): reuse file from cache: " + tInputFile);
add(tKey, new CachedHTMLItem(null, // do not read contents of existing file
tKey),
tInputFile.lastModified(),
true // do reuse contents from file
);
}
cacheMisses = 0;
cacheHits = 0;
this.maxSize = tMaxSizeTmp;
} else {
if (! tDir.mkdirs()) {
KFMSystem.log.error("FileCache::createOrReuseCacheDirectory(): could not create "
+ "cache directory " + mCacheDir);
}
}
}
/**
* Adds a new Cacheable object to the cache. The key must be unique.
*
* @param key a unique key for the object being put into cache.
* @param object the Cacheable object to put into cache.
*/
public synchronized void add(Object key, Cacheable object) {
add(key, object, System.currentTimeMillis(), false);
}
/**
* Adds a new Cacheable object to the cache. The key must be unique.
*
* @param key a unique key for the object being put into cache.
* @param object the Cacheable object to put into cache.
* @param aTimestamp arbitrary timestamp
* @param aReuseExistingFile reuses an existing file in cache when <code>true</code>,
* writes object to cache in any case, else
*/
private synchronized void add(Object key, Cacheable object,
long aTimestamp, boolean aReuseExistingFile) {
//Don't add an object with the same key multiple times.
if (cachedObjectsHash.containsKey(key)) {
return;
}
int objectSize;
// Create filename for cached object
String tFilename = mCacheDir + File.separatorChar + Converter.quoteURL( (String) key);
File tFile = new File(tFilename);
if (! aReuseExistingFile) {
objectSize = object.getSize();
} else {
// Note: We have to take care of two special cases here:
// The file does not exist (it may have been deleted in between)
// The file has a length of 0 bytes (we had that problem once)
if (tFile.exists() && tFile.length() > 0) {
objectSize = (int) tFile.length(); // cast is ok: we don't intend to cache files over 2GB
}
else {
// Nothing to do.
return;
}
}
//If the object is bigger than half of the entire cache, simply don't add it.
if (objectSize > maxSize * .50) {
return;
}
if (! aReuseExistingFile) {
// store cache object to file named 'key' (URL encoded)
String tContent = ((CachedHTMLItem) object).getHTML();
try {
writeWholeTextFile(tFilename, tContent);
} catch (IOException e) {
KFMSystem.log.info("FileCache::add(): failed to add object: " + e.getMessage());
File tDir = new File(mCacheDir);
if (!tDir.exists()) {
if (! tDir.mkdirs()) {
KFMSystem.log.error("FileCache::add(): could not re-create "
+ "cache directory " + mCacheDir);
}
}
return;
}
}
// Create object to be cached in memory.
// done with intention: store object on filesystem, not in memory: (sic)
CacheObject cacheObject = new CacheObject(new CachedHTMLItem(null, // don't store html in memory,
(String) key), objectSize);
size += objectSize;
cachedObjectsHash.put(key, cacheObject);
//Make an entry into the cache order list.
LinkedListNode lastAccessedNode = lastAccessedList.addFirst(key);
//Store the cache order list entry so that we can get back to it
//during later lookups.
cacheObject.lastAccessedListNode = lastAccessedNode;
//Add the object to the age list
LinkedListNode ageNode = ageList.addFirst(key);
//We make an explicit call to currentTimeMillis() so that total accuracy
//of lifetime calculations is better than one second.
ageNode.timestamp = aTimestamp;
cacheObject.ageListNode = ageNode;
//If cache is too full, remove least used cache entries until it is
//not too full.
cullCache();
}
/**
* Gets an object from cache. This method will return null under two
* conditions:<ul>
* <li>The object referenced by the key was never added to cache.
* <li>The object referenced by the key has expired from cache.</ul>
*
* @param key the unique key of the object to get.
* @return the Cacheable object corresponding to unique key.
*/
public synchronized Cacheable get(Object key) {
//First, clear all entries that have been in cache longer than the
//maximum defined age.
deleteExpiredEntries();
CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key);
if (cacheObject == null) {
KFMSystem.log.info("FileCache::get(): key not found in cache: " + key); // DEBUG
//The object didn't exist in cache, so increment cache misses.
cacheMisses++;
return null;
}
//The object exists in cache, so increment cache hits.
cacheHits++;
//Remove the object from it's current place in the cache order list,
//and re-insert it at the front of the list.
cacheObject.lastAccessedListNode.remove();
lastAccessedList.addFirst(cacheObject.lastAccessedListNode);
// load file named key (URL encoded)
String tFilename = mCacheDir + File.separatorChar
+ Converter.quoteURL( (String) key);
String tContent;
try {
KFMSystem.log.info("FileCache::get(): loading key from cache: " + key);
tContent = FileUtils.readWholeTextFile(tFilename);
} catch (IOException e) {
// failed to load object, somebody deleted the cache manually
KFMSystem.log.info("FileCache::get(): file was deleted manually: " + e.getMessage());
// update internal structures
remove(key);
cacheMisses++;
cacheHits--;
return null;
}
return new CachedHTMLItem(tContent, (String) key);
}
/**
* Removes an object from cache.
*
* @param key the unique key of the object to remove.
*/
public synchronized void remove(Object key) {
KFMSystem.log.info("FileCache::remove(): removing key: " + key );
CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key);
//If the object is not in cache, stop trying to remove it.
if (cacheObject == null) {
return;
}
//remove from the hash map
cachedObjectsHash.remove(key);
//remove file named key
File file = new File(mCacheDir + File.separatorChar
+ Converter.quoteURL( (String) key));
if (! file.delete()) {
// failed to delete object, somebody deleted the cached or the cache directory manually
KFMSystem.log.info("FileCache::remove(): tried removing manually deleted file");
File tDir = new File(mCacheDir);
if (!tDir.exists()) {
if (! tDir.mkdirs()) {
KFMSystem.log.error("FileCache::remove(): could not re-create "
+ "cache directory " + mCacheDir);
}
}
}
//remove from the cache order list
cacheObject.lastAccessedListNode.remove();
cacheObject.ageListNode.remove();
//remove references to linked list nodes
cacheObject.ageListNode = null;
cacheObject.lastAccessedListNode = null;
//removed the object, so subtract its size from the total.
size -= cacheObject.size;
}
/**
* Sortes File's according to their last modified date. (Oldest file first.)
*/
private static class FileComparator implements Comparator {
public int compare(Object o1, Object o2) {
File file1 = (File) o1;
File file2 = (File) o2;
if (file1.lastModified() < file2.lastModified()) {
return -1;
} else if (file1.lastModified() > file2.lastModified()) {
return 1;
} else {
return 0;
}
}
}
// Note: candidate for class FileUtils
//
// throws IOException in case we couldn't create, write or close the file
protected static void writeWholeTextFile(String aFilename, String aContent) throws IOException
{
File tOutputFile = new File(aFilename);
try {
FileWriter tOut = new FileWriter(tOutputFile);
tOut.write(aContent);
tOut.close();
} catch (IOException ex) {
// We could either not create, write or close the file,
// so we rethrow the exception. To be clean, however, we try to remove it first.
tOutputFile.delete(); // Ignore the result of delete() indicating success/failure
throw ex;
}
}
}