blob: ad02b6f05803e8d0502f0f73ca851cb7e3851f4b [file] [log] [blame]
package org.eclipse.jdt.internal.core.util;
/*
* (c) Copyright IBM Corp. 2000, 2001.
* All Rights Reserved.
*/
import org.eclipse.jdt.internal.core.Util;
import java.io.*;
import java.util.*;
/**
* A <code>DiskCache</code> is an on-disk cache of files with a configurable maximum size.
* It implements a Least-Recently-Used (LRU) policy.
* <p>
* Entries are keyed by string. The storage for an entry is written to a stream when an
* entry is added, but the size of the entry must be known a-priori.
* Extra information can optionally be associated with an entry.
* <p>
* Volatile state in the cache is saved periodically. The interval between saves can be
* set using <code>setPeriodicSaveInterval(int)</code>.
* <p>
* The disk cache must be explicitly closed using <code>close()</code> when it is no longer needed.
*/
public class DiskCache {
/**
* The directory containing the cache's files.
*/
File fDirectory;
/**
* Max size of the cache in K.
*/
int fSpaceLimit;
/**
* Used for creating cache entry files.
*/
AnonymousFileSource fFileSource;
/**
* List of files to be deleted later, due to failures to delete
* or because file is still open.
*/
Vector fFilesToBeDeleted = new Vector();
/**
* Extends the LRU cache entry to add extra fields needed by disk cache.
* We could avoided overriding the cache entry class by creating another object
* to hold these fields in the value field of a normal cache entry, but this way
* uses fewer objects.
* It is static because it doesn't require an outer instance.
*/
protected static class Entry extends LRUCache.LRUCacheEntry {
long fLastAccess;
byte[] fExtraInfo;
int fFlags;
static final int F_COMPLETE = 1;
Entry(String key, int size, String fileName, long lastAccess, byte[] extraInfo, int flags) {
// use super's key, value and space fields for our key, fileName and size.
super(key, fileName, size);
fLastAccess = lastAccess;
fExtraInfo = extraInfo;
fFlags = flags;
}
DiskCacheEntry asDiskCacheEntry() {
return new DiskCacheEntry((String)_fKey, _fSpace, (String)_fValue, fLastAccess, fExtraInfo);
}
}
/**
* The maximum size of the cache, in number of K.
*/
protected class EntryCache extends LRUCache {
/**
* Add a new entry.
*/
void add(Entry entry) {
removeKey(entry._fKey);
makeSpace(entry._fSpace);
privateAddEntry(entry, false);
}
/**
* An item is being removed from the cache. Delete its file.
*/
protected void privateNotifyDeletionFromCache(LRUCacheEntry entry) {
String fileName = (String)entry._fValue;
if ((((Entry)entry).fFlags & Entry.F_COMPLETE) != 0) {
/* Entry has been complete written, so we can delete it. */
boolean success = deleteFile(fileName);
if (success) return;
/* If success == false, then it is inconsistent with the flag's
* F_COMPLETE mask. Something is wrong. */
}
/* Otherwise, or if file deletion failed (probably due to
* a stream on it still being open), mark the file for deletion later
* (at save or reload time) checking again to see if it's closed. */
rememberToDelete(fileName);
}
/**
* Lookup an entry. Expose the Entry object.
*/
Entry get(String key) {
Entry entry = (Entry) fEntryTable.get(key);
if (entry == null) {
return null;
}
updateTimestamp (entry);
return entry;
}
/**
* Remove an entry. Return it if present.
*/
Entry removeKey(String key) {
Entry entry = (Entry) fEntryTable.get(key);
if (entry == null) {
return null;
}
this.privateRemoveEntry (entry, false);
return entry;
}
/**
* Updates the timestamp for the given entry, ensuring that the queue is
* kept in correct order. The entry must exist
*/
protected void updateTimestamp (LRUCacheEntry entry) {
((Entry)entry).fLastAccess = System.currentTimeMillis();
super.updateTimestamp(entry);
}
/**
* Return the number of entries.
*/
int getNumEntries() {
return fEntryTable.size();
}
/**
* Return an enumeration of the Entry objects in the cache
* in most-recently- to least-recently-used order.
*/
public Enumeration getEntries() {
return new Enumeration() {
Entry current = (Entry)fEntryQueue;
public boolean hasMoreElements() {
return current != null;
}
public Object nextElement() {
if (current == null) {
throw new NoSuchElementException();
}
Entry result = current;
current = (Entry)current._fNext;
return result;
}
};
}
}
EntryCache fEntryCache;
static final String CONTENTS_FILE_NAME = "contents"; //$NON-NLS-1$
static final String TEMP_CONTENTS_FILE_NAME = "tempcont"; //$NON-NLS-1$
static final String CONTENTS_VERSION = "DiskCache.V1.0"; //$NON-NLS-1$
/**
* Set to true when cache is modified, cleared when saved.
*/
boolean fIsDirty = false;
/**
* Thread for doing periodic saves. Created only if needed.
*/
Thread fPeriodicSaveThread;
/**
* Interval for periodic save checks, in ms. 5 sec by default.
*/
int fPeriodicSaveInterval = 60000;
/**
* Create a disk cache which uses the given filesystem directory
* and has the given space limit (in kilobytes).
*/
public DiskCache(File directory, int spaceLimit) {
fDirectory = directory;
fFileSource = new AnonymousFileSource(directory);
fSpaceLimit = spaceLimit;
createEntryCache();
try {
read();
deleteFilesToBeDeleted();
}
catch (IOException e) {
}
}
/**
* Add an entry to the cache. The size of the entry in bytes must be given.
* Extra information in the form of a byte array may optionally be associated
* with the entry (pass null if no extra info is needed).
* Returns an initially empty OutputStream to which the contents of the entry
* should be written immediately following this operation.
*
* @throws IOException if a file for the entry's contents could not be created
*/
public OutputStream add(final String key, int size, byte[] extraInfo) throws IOException {
if (size > fSpaceLimit*1024) {
throw new IOException("Entry size greater than cache size"); //$NON-NLS-1$
}
final File file;
OutputStream output;
// Allocate the file first. If errors occur, no entry is added.
synchronized(fFileSource) {
file = fFileSource.getAnonymousFile();
output = new FileOutputStream(file);
}
long lastAccess = System.currentTimeMillis();
if (extraInfo != null && extraInfo.length == 0) {
extraInfo = null;
}
final Entry entry = new Entry(key, size, file.getName(), lastAccess, extraInfo, 0);
synchronized(fEntryCache) {
// Only use the simple file name.
fEntryCache.add(entry);
}
// Mark as modified here, as well as when the entry is complete.
// Hopefully the content is received before the periodic save interval,
// so state is saved at most once for this entry.
modified();
// Return a filter stream which sets the complete flag when the stream is closed.
return new FilterOutputStream(output) {
public void write(byte b[], int off, int len) throws IOException {
out.write(b, off, len);
}
private void closeHelper() throws IOException {
super.close();
// Ensure entry is still valid.
// It may have been removed or replaced in the interim.
synchronized(fEntryCache) {
if (fEntryCache.get(key) != entry) {
String fileName = (String)entry._fValue;
boolean success = deleteFile(fileName);
if (!success) {
rememberToDelete(fileName);
}
}
}
modified();
}
public void close() throws IOException {
entry.fFlags |= Entry.F_COMPLETE;
closeHelper();
}
public void finalize() throws Throwable {
closeHelper();
super.finalize();
}
/* For debugging/testing ONLY. Do not use outside of test suites.*/
public String toString() {
return file.toString();
}
};
}
/**
* Clear the cache. All entries are removed and their associated files are deleted.
*/
public void clearCache() {
synchronized(fEntryCache) {
fEntryCache.flush();
}
// Be sure to save the cleared contents.
modified();
}
/**
* Close the cache.
* Flush incomplete entries, save volatile state if needed, and halt periodic saves.
*/
public synchronized void close() {
if (fIsDirty || flushIncompleteEntries()) {
try {
save();
}
catch (IOException e) {
// Ignore
}
}
Thread thread = fPeriodicSaveThread;
if (thread != null) {
fPeriodicSaveThread = null;
// Interrupt its sleep.
thread.interrupt();
}
}
/**
* Internal - (Re)create the internal entry cache.
*/
protected void createEntryCache() {
fEntryCache = new EntryCache();
fEntryCache.setSpaceLimit(fSpaceLimit*1024);
}
/**
* Internal - Delete the file for an entry. Returns the
* "success flag" result from java.io.File's delete().
*/
protected boolean deleteFile(String fileName) {
File fileToDelete = fFileSource.fileForName(fileName);
fileToDelete.delete();
// Could have already been deleted.
boolean success = !fileToDelete.exists();
if(success && fFilesToBeDeleted.contains(fileName))
fFilesToBeDeleted.removeElement(fileName);
return success;
}
/**
* Internal - Deletes the files that have been flagged for deletion. Returns
* the number of flagged files that remain undeleted.
*/
public int deleteFilesToBeDeleted() {
synchronized (fFilesToBeDeleted) {
Vector clone = (Vector) fFilesToBeDeleted.clone();
Enumeration e = clone.elements();
while(e.hasMoreElements()) {
String fileName = (String) e.nextElement();
boolean success = deleteFile(fileName);
if (success)
fFilesToBeDeleted.removeElement(fileName);
}
return fFilesToBeDeleted.size();
}
}
/**
* Flush all incomplete entries from the cache.
* Returns true if some were flushed.
*/
protected boolean flushIncompleteEntries() {
Vector v = new Vector();
for (Enumeration e = fEntryCache.getEntries(); e.hasMoreElements();) {
Entry entry = (Entry)e.nextElement();
if ((entry.fFlags & Entry.F_COMPLETE) == 0) {
v.addElement(entry._fKey);
}
}
for (int i = 0, size = v.size(); i < size; ++i) {
fEntryCache.removeKey((String)v.elementAt(i));
}
return v.size() > 0;
}
/**
* Returns the directory which holds the cache.
*/
public File getDirectory() {
return fDirectory;
}
/**
* For debugging/testing purposes.
*/
public int getNumberOfFilesToBeDeleted() {
return fFilesToBeDeleted.size();
}
/**
* Return the number of entries in the cache.
*/
public int getNumEntries() {
return fEntryCache.getNumEntries();
}
/**
* Returns the delay in milliseconds between periodic saves.
*/
public int getPeriodicSaveInterval() {
return fPeriodicSaveInterval;
}
/**
* Returns the space limit of the cache, in Kilobytes.
*/
public int getSpaceLimit() {
return fSpaceLimit;
}
/**
* Returns the space used by the cache, in Kilobytes.
*/
public int getSpaceUsed() {
synchronized(fEntryCache) {
return (fEntryCache.getCurrentSpace() + 1023) / 1024; // Take ceiling.
}
}
/**
* Returns an enumeration on the keys (Strings)
* of the entries in the cache.
*/
public Enumeration keys() {
return fEntryCache.keys();
}
/**
* Look up an entry in the cache. Answer the entry if found, or null if not found.
* If the contents are to be retrieved, use the <code>open</code> method rather than
* directly referring to the file using the returned file name.
*
* @see DiskCache#open
*/
public DiskCacheEntry lookup(String key) {
Entry entry;
synchronized(fEntryCache) {
entry = fEntryCache.get(key);
}
if (entry == null || (entry.fFlags & Entry.F_COMPLETE) == 0) {
return null;
}
else {
// If the entry was found, then the contents should be
// resaved because the order of items in it has changed.
modified();
return entry.asDiskCacheEntry();
}
}
/**
* Internal - The cache has been modified. Remember to save the state.
* Method is synchronized to protect fPeriodicSaveThread from concurrent access.
*/
protected synchronized void modified() {
fIsDirty = true;
if (fPeriodicSaveThread == null || !fPeriodicSaveThread.isAlive()) {
startPeriodicSaveThread();
}
}
/**
* Open an input stream on the contents of the given entry.
* The passed entry must be the result of a previous lookup operation.
* If the cache has been modified since the lookup operation, and the entry
* entry has since been flushed from the cache, this operation will fail.
*
* @throws FileNotFoundException if the entry is no longer in the cache
*/
public InputStream open(DiskCacheEntry entry) throws FileNotFoundException {
final String key = entry.getKey();
final String fileName = entry.getFileName();
// Has the entry been removed or replaced since the lookup was done?
final Entry finalEntry = fEntryCache.get(key);
if (finalEntry == null || !fileName.equals((String) finalEntry._fValue)) {
throw new FileNotFoundException();
}
final File file = fFileSource.fileForName(fileName);
class DiskCachePrivateFileInputStream extends FileInputStream {
DiskCachePrivateFileInputStream(File file) throws FileNotFoundException {
super(file);
}
public void close() throws IOException {
super.close();
// Ensure entry is still valid.
// It may have been removed or replaced in the interim.
synchronized(fEntryCache) {
if (fEntryCache.get(key) != finalEntry) {
boolean success = deleteFile(fileName);
if (!success) {
rememberToDelete(fileName);
}
}
}
modified();
}
protected void finalize() throws IOException {
close();
super.finalize();
}
/* For debugging/testing ONLY. Do not use outside of test suites.*/
public String toString() {
return file.toString();
}
};
try {
return new DiskCachePrivateFileInputStream(file);
}
catch (FileNotFoundException e) {
// If there's an error while opening the file,
// delete the entry and the file from the cache.
remove(key);
// Propagate the exception
throw e;
}
}
/**
* Read the cache state from persistent storage.
* Any undeleted entry files left at the last save the cach now tries to delete.
*/
public void read() throws IOException {
File file = new File(fDirectory, CONTENTS_FILE_NAME);
if (!file.exists()) {
return;
}
DataInputStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream(file)));
try {
String sig = in.readUTF();
int spaceLimit = in.readInt(); /* Ignored -- use current limit */
int spaceUsed = in.readInt(); /* Ignored -- updated as entries are read */
int numEntries = in.readInt();
if (!sig.equals(CONTENTS_VERSION)) {
throw new IOException(Util.bind("file.badFormat")); //$NON-NLS-1$
}
/* Read to a temp. array of entries. The entries are in most- to
* least-recently used order. */
Entry[] entries = new Entry[numEntries];
for (int i = 0; i < numEntries; ++i) {
entries[i] = readEntry(in);
}
createEntryCache();
/* Add entries in least- to most-recently used order, so that the order in
* the new cache is preserved. */
for (int i = numEntries - 1; i >= 0; --i) {
fEntryCache.add(entries[i]);
}
/* Read in names of files to be deleted. */
/* But first, check that we are not at the end of the file (which occurs
* in pre-existing file formats). Try reading an int to find this out. */
boolean moreToRead = false;
int numFilesToBeDeleted = -1;
try {
numFilesToBeDeleted = in.readInt();
moreToRead = true;
} catch (IOException ignoreMe) {
/* We're at the end of file, so it's a pre-existing format. */
}
if(moreToRead) {
fFilesToBeDeleted = new Vector();
for (int i = 0; i < numFilesToBeDeleted; i++) {
String fileName = in.readUTF();
fFilesToBeDeleted.addElement(fileName);
}
}
}
finally {
in.close();
}
}
/**
* Internal - Read an entry from a stream. Returns an internal cache Entry.
*/
protected Entry readEntry(DataInputStream in) throws IOException {
String key = in.readUTF();
int size = in.readInt();
String fileName = in.readUTF();
long lastAccess = in.readLong();
int flags = in.readInt();
byte[] extraInfo = null;
int extraLen = in.readInt();
if (extraLen > 0) {
extraInfo = new byte[extraLen];
in.readFully(extraInfo);
}
return new Entry(key, size, fileName, lastAccess, extraInfo, flags);
}
/**
* Internal - Remember to delete a file later.
*/
protected void rememberToDelete(String fileName) {
if (!fFilesToBeDeleted.contains(fileName))
fFilesToBeDeleted.addElement(fileName);
}
/**
* Remove an entry from the cache and delete its contents.
* Answer the entry if found, or null if not found.
*/
public DiskCacheEntry remove(String key) {
Entry entry;
synchronized(fEntryCache) {
entry = fEntryCache.removeKey(key);
}
if (entry == null) {
return null;
}
else {
modified();
return entry.asDiskCacheEntry();
}
}
public synchronized void removeAll(Vector keys) {
Enumeration enum = keys.elements();
while (enum.hasMoreElements()) {
String key = (String) enum.nextElement();
remove(key);
}
}
/**
* Renames an entry in the cache.
* Returns true if the rename succeeded, false otherwise.
*/
public boolean rename(String oldKey, String newKey) {
long lastAccess = System.currentTimeMillis();
synchronized(fEntryCache) {
Entry oldEntry = (Entry) fEntryCache.get(oldKey);
if (oldEntry == null) {
return false;
}
else {
Entry newEntry = new Entry(newKey, oldEntry._fSpace, (String) oldEntry._fValue, lastAccess, oldEntry.fExtraInfo, oldEntry.fFlags);
fEntryCache.add(newEntry);
}
}
modified();
return true;
}
/**
* Save all in-memory cache state to persistent storage.
* Tries to deleted undeleted entry files.
*/
public synchronized void save() throws IOException {
/* Try deleting "files to be deleted". */
deleteFilesToBeDeleted();
/*
* Write the table of contents to a temporary file,
* rather than the actual file, to be more resilient in
* the face of errors such as disk full.
*/
File tempFile = new File(fDirectory, TEMP_CONTENTS_FILE_NAME);
DataOutputStream out =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(tempFile)));
boolean ok = false;
try {
synchronized(fEntryCache) {
out.writeUTF(CONTENTS_VERSION);
out.writeInt(getSpaceLimit());
out.writeInt(getSpaceUsed());
int numEntries = fEntryCache.getNumEntries();
out.writeInt(numEntries);
for (Enumeration e = fEntryCache.getEntries(); e.hasMoreElements();) {
Entry entry = (Entry)e.nextElement();
writeEntry(entry, out);
}
int numFilesToBeDeleted = fFilesToBeDeleted.size();
out.writeInt(numFilesToBeDeleted);
Enumeration e = fFilesToBeDeleted.elements();
while(e.hasMoreElements()) {
String fileName = (String) e.nextElement();
out.writeUTF(fileName);
}
}
out.close();
/* Replace the old TOC (if any) with the temp file */
File contentsFile = new File(fDirectory, CONTENTS_FILE_NAME);
contentsFile.delete();
if (tempFile.renameTo(contentsFile)) {
ok = true;
fIsDirty = false;
}
}
finally {
if (!ok) {
out.close();
tempFile.delete();
}
}
}
/**
* Sets the delay in milliseconds between periodic saves.
*/
public void setPeriodicSaveInterval(int interval) {
fPeriodicSaveInterval = interval;
}
/**
* Sets the space limit of the cache, in Kilobytes.
*/
public void setSpaceLimit(int limit) {
fSpaceLimit = limit;
synchronized(fEntryCache) {
fEntryCache.setSpaceLimit(limit * 1024);
}
}
/**
* Internal -
* Create and start a thread which checks every so often whether
* the cache needs to be saved.
* The thread should only run while there are modifications.
*/
protected void startPeriodicSaveThread() {
fPeriodicSaveThread = new Thread() {
public void run() {
try {
Thread.sleep(fPeriodicSaveInterval);
}
catch (InterruptedException e) {
}
for (;;) {
synchronized(DiskCache.this) {
if (fIsDirty) {
try {
save(); // clears dirty flag
}
catch (IOException e) {
// Ignore
}
}
else {
// drop ref to thread and terminate
fPeriodicSaveThread = null;
return;
}
}
try {
Thread.sleep(fPeriodicSaveInterval);
}
catch (InterruptedException e) {
}
}
}
};
fPeriodicSaveThread.setName("DiskCache periodic save"); //$NON-NLS-1$
fPeriodicSaveThread.start();
}
/**
* Internal - Write an internal cache Entry to a stream.
*/
protected void writeEntry(Entry entry, DataOutputStream out) throws IOException {
out.writeUTF((String)entry._fKey);
out.writeInt(entry._fSpace);
out.writeUTF((String)entry._fValue); // fileName
out.writeLong(entry.fLastAccess);
out.writeInt(entry.fFlags);
byte[] extraInfo = entry.fExtraInfo;
if (extraInfo == null) {
out.writeInt(0);
}
else {
out.writeInt(extraInfo.length);
out.write(extraInfo);
}
}
}