//============================================================================== // DiskCacheManager.java //------------------------------------------------------------------------------ package tribble.io; // System imports import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.lang.String; import java.lang.Exception; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Iterator; // Local imports // (None) /******************************************************************************* * Disk file cache manager. * *
* Manages a set of files in a local disk directory as a cache of document files. * * *
* Usage * *
* To create a cache that stores files in a local disk directory, create a disk * cache manager object, and establish the local directory and index file it * should use: *
* try
* {
* DiskCacheManager cache;
* File dir;
*
* dir = ...;
* cache = new DiskCacheManager();
* cache.open(dir, "mycache.ind");
* }
* catch (IOException ex)
* {
* ... error ...
* }
*
*
* * New document files can be added to the directory cache: *
* try
* {
* File fname;
* String docId;
*
* docId = ...;
* fname = cache.createFile(id);
* }
* catch (IOException ex)
* {
* ... error ...
* }
*
*
* * The {@link #createFile} method creates a new file within the cache directory * and returns its name. The document-ID specified must be unique within the * cache, i.e., no other document file within the cache can have the same * document-ID. (A {@link java.io.IOException} is thrown if the document-ID is * not unique.) The returned filename is guaranteed to be unique within the * cache directory. From that point on, there is a one-to-one correspondence * between the document-ID and the cached filename. * *
* By default, the created filenames have a name that is prefixed with * "temp" and suffixed with "tmp". However, these can be * changed by client code: *
* cache.setFilePrefix("mine"); // Or whatever
* cache.setFileSuffix("doc"); // Or whatever
*
*
*
* Data can be written to and read from the cached filename by client code.
* The file need not remain open (or be opened at all, for that matter) while the
* cache manager is active.
*
*
* Once the cached filename is no longer needed, it can be removed from the
* cached directory:
*
* try
* {
* cache.removeFile(id);
* }
* catch (IOException ex)
* {
* ... error ...
* }
*
*
*
* Once this is done, there is no longer any association between the document-ID
* and any filename in the cache directory.
*
*
*
* Notes
*
*
* A control file is maintained in the local cache directory, containing the
* names of all of the files that currently reside in the cache. This file is
* updated any time a document file is added to or removed from the cache.
*
*
* The control file is read whenever the cache directory is opened by a document
* cache manager object, in order to re-establish the contents of the cache
* directory.
*
*
* The default name of the control file is "cache.ind", but this can be
* specified when calling the {@link #open} method.
*
*
* To allow multiple cache directory managers to use the same disk directory, a
* lock file is used to insure mutually exclusive access to the directory.
*
*
* @version $Revision: 1.2 $ $Date: 2002/06/07 04:10:25 $
* @since 2002-06-04
* @author
* David R. Tribble,
* david@tribble.com
*
* Copyright ©2002 by David R. Tribble, all rights reserved.
*/
public class DiskCacheManager
implements FileCacheManagerI
{
// Identification
/** Revision information. */
static final String REV =
"@(#)tribble/io/DiskCacheManager.java $Revision: 1.2 $ $Date: 2002/06/07 04:10:25 $\n";
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Public constants
/** Default cache directory control filename. */
public static final String DFL_CONTROL_FILENAME = "cache.ind";
/** Cache directory control file format version number. */
public static final String CONTROL_FILE_VERS = "100";
/** Default cache filename prefix. */
public static final String DFL_PREFIX = "xxx";
/** Default cache filename suffix. */
public static final String DFL_SUFFIX = ".tmp";
/** Default number of days until cached files expire. */
public static final int DFL_EXPIRY_DAYS = 10;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Package private constants
/** Cache control file entry field separator. */
static final char ENTRY_SEP = '|';
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Protected variables
/** Local cache directory name. */
protected File m_dir;
/** Local cache directory index (control) file. */
protected File m_indexFname;
/** Local cache directory lock (mutex) file. */
protected File m_lockFname;
/** Cache filename prefix. */
protected String m_pref = DFL_PREFIX;
/** Cache filename suffix. */
protected String m_suff = DFL_SUFFIX;
/** Days past the last access date that document files expire. */
protected int m_expiryDays = DFL_EXPIRY_DAYS;
/**
* Files currently residing in the local directory cache.
*
*
* This is a hash table keyed by document-IDs, which are unique to
* each document file contained in the local cache directory. The value of
* each hash entry is a {@link DiskCacheFile} object that contains the name
* of the file in the local cache, as well as other information, for the
* document file.
*/
protected HashMap m_files;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Public constructors
/***************************************************************************
* Default constructor.
*
* @since 1.1, 2002-06-04
*/
public DiskCacheManager()
{
// Do nothing
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Public methods
/***************************************************************************
* Open the disk cache directory and initialize this cache manager.
*
* @param dir
* A directory name to be used as the local file cache.
*
* @param index
* A filename residing within the local cache directory to be used as a index
* (control) file. This can be null or empty (""), in which case
* a default index filename is used.
*
* @throws IOException
* Thrown if access to the directory is denied or some other error occurs.
*
* @since 1.1, 2002-06-05
*
* @see #close
*/
public synchronized void open(File dir, String index)
throws IOException
// implements FileCacheManagerI
{
// Close the currently open cache directory, if any
close();
// Establish the new local cache directory
m_dir = dir;
if (!m_dir.exists())
{
// Create the cache directory
if (!m_dir.mkdir())
throw new IOException("Cannot create cache directory: "
+ m_dir);
}
else if (!m_dir.isDirectory())
{
// Cache filename is not a directory
throw new IOException("Cache is not a directory: " + m_dir);
}
// Establish the local cache index (control) file
if (index == null || index.equals(""))
index = DFL_CONTROL_FILENAME;
m_indexFname = new File(m_dir, index);
// Load the latest local cache directory index (control) file
loadEntries();
}
/***************************************************************************
* Close this cache directory manager.
*
* @throws IOException
* Thrown if access to the directory is denied or some other error occurs.
*
* @since 1.1, 2002-06-04
*
* @see #open
*/
public synchronized void close()
throws IOException
// implements FileCacheManagerI
{
// Sanity check
if (m_dir == null)
return;
// Clean up expired files
removeAllExpiredFiles();
// Store the active cached filenames into the local cache directory
storeEntries();
// Close the current cache directory
m_files = null;
m_dir = null;
}
/***************************************************************************
* Set the filename prefix for document files in this cache directory.
*
* @param pre
* A filename prefix. This should generally be a fairly short string, such
* as three characters.
*
* @throws IOException
* Thrown if pre is malformed.
*
* @since 1.1, 2002-06-04
*
* @see #createFile
*/
public synchronized void setFilePrefix(String pre)
throws IOException
{
// Check arg
if (pre == null)
pre = DFL_PREFIX;
if (pre.equals(""))
pre = DFL_PREFIX;
if (pre.indexOf(ENTRY_SEP) >= 0)
throw new IOException("Cache filename prefix cannot contain '"
+ ENTRY_SEP + "': \"" + pre + "\"");
// Establish a new cache filename prefix
m_pref = pre;
}
/***************************************************************************
* Set the filename suffix for document files in this cache directory.
*
* @param suf
* A filename suffix. This should generally be a fairly short string, such
* as three characters, plus a leading dot ('.').
*
* @throws IOException
* Thrown if suf is malformed.
*
* @since 1.1, 2002-06-04
*
* @see #createFile
*/
public synchronized void setFileSuffix(String suf)
throws IOException
{
// Check arg
if (suf == null)
suf = DFL_SUFFIX;
if (suf.equals(""))
suf = DFL_SUFFIX;
if (suf.indexOf(ENTRY_SEP) >= 0)
throw new IOException("Cache filename suffix cannot contain '"
+ ENTRY_SEP + "': \"" + suf + "\"");
// Establish a new cache filename suffix
m_suff = suf;
}
/***************************************************************************
* Establish the minimum lifetime of document files that reside in this cache
* directory.
*
* @param nDays
* The minimum number of days beyond their last access date that document
* files are allowed to reside in the cache directory before being
* automatically deleted.
*
*
* If nDays is zero or negative, cached files will be deleted when
* method {@link #close} is called for this cache directory.
*
*
* By default, cached files will exist for a minimum of
* {@link #DFL_EXPIRY_DAYS} days.
*
* @since 1.1, 2002-06-04
*
* @see #removeAllExpiredFiles
*/
public synchronized void setExpiryDays(int nDays)
{
// Establish the minimum number of days that cached files can exist
if (nDays < 0)
nDays = 0;
m_expiryDays = nDays;
}
/***************************************************************************
* Create a new cached filename in the cache directory for a given
* document-ID.
*
* @param docId
* A document-ID to create. This name must be unique within this cache.
* This name may contain any characters except newlines ('\n'),
* although it is recommended that it be composed of only printable
* characters.
*
* @return
* The filename of a newly created file in the cache directory corresponding
* to, and uniquely identified by, document-ID docId.
*
* @throws IOException
* Thrown if document-ID docId already exists in the cache, or if
* the cache directory could not be accessed, or if some other error
* occurs.
*
* @since 1.1, 2002-06-04
*
* @see #getFile
* @see #removeFile
*/
public synchronized File createFile(String docId)
throws IOException
// implements FileCacheManagerI
{
DiskCacheFile f;
File fname;
Date now;
// Check that the document-ID does not already exist
if (m_files.containsKey(docId))
throw new IOException("Document-ID \"" + docId
+ "\" aleady exists in cache \"" + m_dir + "\"");
// Create a new filename within the cache directory
fname = File.createTempFile(m_pref, m_suff, m_dir);
// Create a new cache file entry
f = new DiskCacheFile(docId, fname, this);
now = new Date();
f.m_createTime = now;
f.m_accessTime = now;
f.m_expiryDays = m_expiryDays;
// Add the new cache file entry to the cache index table
m_files.put(docId, f);
// Update the cache directory control file
storeEntries();
// Return the newly created cached filename
return (fname); // Immutable
}
/***************************************************************************
* Retrieve the cached filename for a given document-ID within the cache
* directory.
*
*
* This method updates the last access time for the cached file.
*
* @param docId
* A document-ID to locate.
*
* @return
* The filename of the document file residing in the local cache directory
* that is uniquely identified by docId.
*
* @throws IOException
* Thrown if the document file does not exist, or if the cache directory
* could not be accessed, or if some other error occurs.
*
* @since 1.1, 2002-06-04
*
* @see #createFile
* @see #removeFile
*/
public synchronized File getFile(String docId)
throws IOException
// implements FileCacheManagerI
{
DiskCacheFile f;
// Locate the document file within the cache
f = findFile(docId);
synchronized (f)
{
// Update the access time of the document file
f.m_accessTime = new Date();
// Return the local filename of the document file
return (f.m_fname); // Immutable
}
}
/***************************************************************************
* Delete a given document file from the cache directory.
*
* @param docId
* The document-ID of a document file to delete from the cache directory.
*
* @throws IOException
* Thrown if the document file does not exist, or if the cache directory
* could not be accessed, or if some other error occurs.
*
* @since 1.1, 2002-06-04
*
* @see #removeAllFiles
* @see #removeAllExpiredFiles
*/
public synchronized void removeFile(String docId)
throws IOException
// implements FileCacheManagerI
{
DiskCacheFile f;
// Locate the document file within the cache
f = findFile(docId);
synchronized (f)
{
// Delete the document file from the cache directory
if (!f.m_fname.delete())
throw new IOException("Cannot remove cache file: \""
+ f.m_fname + "\"");
// Remove the cached file entry from this cache manager
f.m_mgr = null;
m_files.remove(docId);
// Update the cache directory control file
storeEntries();
}
}
/***************************************************************************
* Delete all document files from this cache directory.
*
* @throws IOException
* Thrown if the cache directory could not be accessed, or if some other
* error occurs.
*
* @since 1.1, 2002-06-04
*
* @see #removeFile
* @see #removeAllExpiredFiles
*/
public synchronized void removeAllFiles()
throws IOException
// implements FileCacheManagerI
{
///+INCOMPLETE
///...
m_files = null;
storeEntries();
}
/***************************************************************************
* Delete all expired document files from this cache directory.
*
* @throws IOException
* Thrown if the cache directory could not be accessed, or if some other
* error occurs.
*
* @since 1.1, 2002-06-04
*
* @see #setExpiryDays
* @see #removeAllFiles
*/
public synchronized void removeAllExpiredFiles()
throws IOException
{
///+INCOMPLETE
///...
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Protected static methods
/***************************************************************************
* Read a text line from the cache directory index (control) file.
*
*
* Comment lines (which have a '#' in the leftmost column) are
* ignored.
*
* @return
* A single text line read from the index (control) file, or null if the end
* of the file has been reached.
*
* @throws IOException
* Thrown if a read error occurs, or if some other error occurs.
*
* @since 1.1, 2002-06-05
*
* @see #storeEntries
*/
protected static String readEntryLine(BufferedReader in)
throws IOException
{
String line;
// Read the next non-comment text line from the file
for (;;)
{
// Read a single text line from the file
line = in.readLine();
// Check for end of file
if (line == null)
break;
// Ignore comment lines
if (line.charAt(0) != '#')
break;
}
// Done
return (line);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Protected methods
/***************************************************************************
* Finalization.
*
* @since 1.1, 2002-06-04
*
* @see #close
*/
protected synchronized void finalize()
{
try
{
// Close this local cache directory
close();
}
catch (Exception ex)
{
// Ignore
}
}
/***************************************************************************
* Locate the cached filename for a given document-ID within the cache index.
*
* Note:
* Methods that call this method should be synchronized.
*
* @param docId
* A document-ID to locate.
*
* @return
* The document file entry residing in the local cache directory.
*
* @throws IOException
* Thrown if document-ID docId does not exist within this cache.
*
* @since 1.1, 2002-06-04
*
* @see #getFile
*/
protected DiskCacheFile findFile(String docId)
throws IOException
{
DiskCacheFile f;
// Search for a given document-ID within this directory cache
f = (DiskCacheFile) m_files.get(docId);
if (f == null)
throw new IOException("No such document-ID \"" + docId
+ "\" in cache \"" + m_dir + "\"");
return (f);
}
/***************************************************************************
* Read the index table for this cache directory manager from the cache
* directory.
*
* Note:
* Methods that call this method should be synchronized.
*
* @throws IOException
* Thrown if a read error occurs, or if some other error occurs.
*
* @since 1.1, 2002-06-04
*
* @see #storeEntries
*/
protected void loadEntries()
throws IOException
{
try
{
BufferedReader indexF;
String line;
// Acquire an exclusive lock on the index file
lock();
// Insure that there is a cache index file
if (!m_indexFname.exists())
{
// Create a new cache index file
m_indexFname.createNewFile();
}
// Open the cache index file
indexF = new BufferedReader(new FileReader(m_indexFname));
// Read the index file header
line = readEntryLine(indexF);
///+INCOMPLETE
///...check file format version number, cf. CONTROL_FILE_VERS
// Read cache index file entries
m_files = new HashMap();
for (;;)
{
DiskCacheFile f;
// Read a single cache index file entry
line = readEntryLine(indexF);
if (line == null)
break;
// Reconstruct a cached document file from the index entry
f = DiskCacheFile.fromEntryLine(line, m_dir);
f.m_mgr = this;
// Add the existing document file to this cache index
if (f.m_fname.exists())
m_files.put(f.m_docId, f);
}
// Done
indexF.close();
}
finally
{
// Release the exclusive lock on the index file
unlock();
}
}
/***************************************************************************
* Write the index table for this cache directory manager to the cache
* directory.
*
* Note:
* Methods that call this method should be synchronized.
*
* @throws IOException
* Thrown if a write error occurs, or if some other error occurs.
*
* @since 1.1, 2002-06-04
*
* @see #loadEntries
*/
protected void storeEntries()
throws IOException
{
PrintWriter indexF;
// Acquire an exclusive lock on the index file
lock();
// Open the cache index file
indexF = new PrintWriter(new FileWriter(m_indexFname));
// Write the index file header
indexF.println("# " + m_indexFname.getName()
+ " - Cache index, updated "
+ DiskCacheFile.toIso8601(new Date()));
indexF.println("+" + CONTROL_FILE_VERS);
// Write cache index file entries
if (m_files != null)
{
Iterator iter;
iter = m_files.values().iterator();
while (iter.hasNext())
{
DiskCacheFile f;
String line;
// Write a single cache index file entry
f = (DiskCacheFile) iter.next();
line = f.toEntryLine();
indexF.println(line);
}
}
// Write the index file trailer
indexF.println("# End");
// Done
indexF.flush();
indexF.close();
// Release the exclusive lock on the index file
unlock();
}
/***************************************************************************
* Remove all the expired document files from the cache directory.
*
* Note:
* Methods that call this method should be synchronized.
*
* @throws IOException
+INCOMPLETE
*
* @since 1.1, 2002-06-04
*
* @see #removeAllExpiredFiles
* @see DiskCacheFile#hasExpired
*/
protected void removeExpiredEntries()
throws IOException
{
///+INCOMPLETE
///...
}
/***************************************************************************
* Acquire an exclusive lock on the index file of this cache directory.
*
* Note:
* Methods that call this method should be synchronized.
*
* @throws IOException
* Thrown if an I/O error occurs.
*
* @since 1.1, 2002-06-05
*/
protected void lock()
throws IOException
{
///+INCOMPLETE
///...
}
/***************************************************************************
* Release an exclusive lock on the index file of this cache directory.
*
* Note:
* Methods that call this method should be synchronized.
*
* @throws IOException
* Thrown if an I/O error occurs.
*
* @since 1.1, 2002-06-05
*/
protected void unlock()
throws IOException
{
///+INCOMPLETE
///...
}
}
// End DiskCacheManager.java