//============================================================================== // 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