/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package net.sourceforge.phpdt.internal.core.util;
import java.util.Enumeration;
import java.util.Hashtable;
/**
* The LRUCache
is a hashtable that stores a finite number of
* elements. When an attempt is made to add values to a full cache, the least
* recently used values in the cache are discarded to make room for the new
* values as necessary.
*
*
* The data structure is based on the LRU virtual memory paging scheme. * *
* Objects can take up a variable amount of cache space by implementing the
* ILRUCacheable
interface.
*
*
* This implementation is NOT thread-safe. Synchronization wrappers would have
* to be added to ensure atomic insertions and deletions from the cache.
*
* @see net.sourceforge.phpdt.internal.core.util.ILRUCacheable
*/
public class LRUCache implements Cloneable {
/**
* This type is used internally by the LRUCache to represent entries stored
* in the cache. It is static because it does not require a pointer to the
* cache which contains it.
*
* @see LRUCache
*/
protected static class LRUCacheEntry {
/**
* Hash table key
*/
public Object _fKey;
/**
* Hash table value (an LRUCacheEntry object)
*/
public Object _fValue;
/**
* Time value for queue sorting
*/
public int _fTimestamp;
/**
* Cache footprint of this entry
*/
public int _fSpace;
/**
* Previous entry in queue
*/
public LRUCacheEntry _fPrevious;
/**
* Next entry in queue
*/
public LRUCacheEntry _fNext;
/**
* Creates a new instance of the receiver with the provided values for
* key, value, and space.
*/
public LRUCacheEntry(Object key, Object value, int space) {
_fKey = key;
_fValue = value;
_fSpace = space;
}
/**
* Returns a String that represents the value of this object.
*/
public String toString() {
return "LRUCacheEntry [" + _fKey + "-->" + _fValue + "]"; //$NON-NLS-3$ //$NON-NLS-1$ //$NON-NLS-2$
}
}
/**
* Amount of cache space used so far
*/
protected int fCurrentSpace;
/**
* Maximum space allowed in cache
*/
protected int fSpaceLimit;
/**
* Counter for handing out sequential timestamps
*/
protected int fTimestampCounter;
/**
* Hash table for fast random access to cache entries
*/
protected Hashtable fEntryTable;
/**
* Start of queue (most recently used entry)
*/
protected LRUCacheEntry fEntryQueue;
/**
* End of queue (least recently used entry)
*/
protected LRUCacheEntry fEntryQueueTail;
/**
* Default amount of space in the cache
*/
protected static final int DEFAULT_SPACELIMIT = 100;
/**
* Creates a new cache. Size of cache is defined by
* DEFAULT_SPACELIMIT
.
*/
// public LRUCache() {
//
// this(DEFAULT_SPACELIMIT);
// }
/**
* Creates a new cache.
*
* @param size
* Size of Cache
*/
public LRUCache(int size) {
fTimestampCounter = fCurrentSpace = 0;
fEntryQueue = fEntryQueueTail = null;
fEntryTable = new Hashtable(size);
fSpaceLimit = size;
}
/**
* Returns a new cache containing the same contents.
*
* @return New copy of object.
*/
public Object clone() {
LRUCache newCache = newInstance(fSpaceLimit);
LRUCacheEntry qEntry;
/* Preserve order of entries by copying from oldest to newest */
qEntry = this.fEntryQueueTail;
while (qEntry != null) {
newCache.privateAdd(qEntry._fKey, qEntry._fValue, qEntry._fSpace);
qEntry = qEntry._fPrevious;
}
return newCache;
}
/**
* Flushes all entries from the cache.
*/
public void flush() {
fCurrentSpace = 0;
LRUCacheEntry entry = fEntryQueueTail; // Remember last entry
fEntryTable = new Hashtable(); // Clear it out
fEntryQueue = fEntryQueueTail = null;
while (entry != null) { // send deletion notifications in LRU order
privateNotifyDeletionFromCache(entry);
entry = entry._fPrevious;
}
}
/**
* Flushes the given entry from the cache. Does nothing if entry does not
* exist in cache.
*
* @param key
* Key of object to flush
*/
// public void flush(Object key) {
//
// LRUCacheEntry entry;
//
// entry = (LRUCacheEntry) fEntryTable.get(key);
//
// /* If entry does not exist, return */
// if (entry == null)
// return;
//
// this.privateRemoveEntry(entry, false);
// }
/**
* Answers the value in the cache at the given key. If the value is not in
* the cache, returns null
*
* @param key
* Hash table key of object to retrieve
* @return Retreived object, or null if object does not exist
*/
public Object get(Object key) {
LRUCacheEntry entry = (LRUCacheEntry) fEntryTable.get(key);
if (entry == null) {
return null;
}
this.updateTimestamp(entry);
return entry._fValue;
}
/**
* Returns the amount of space that is current used in the cache.
*/
public int getCurrentSpace() {
return fCurrentSpace;
}
/**
* Returns the maximum amount of space available in the cache.
*/
public int getSpaceLimit() {
return fSpaceLimit;
}
/**
* Returns an Enumeration of the keys currently in the cache.
*/
public Enumeration keys() {
return fEntryTable.keys();
}
/**
* Returns an enumeration that iterates over all the keys and values
* currently in the cache.
*/
// public ICacheEnumeration keysAndValues() {
// return new ICacheEnumeration() {
//
// Enumeration fValues = fEntryTable.elements();
//
// LRUCacheEntry fEntry;
//
// public boolean hasMoreElements() {
// return fValues.hasMoreElements();
// }
//
// public Object nextElement() {
// fEntry = (LRUCacheEntry) fValues.nextElement();
// return fEntry._fKey;
// }
//
// public Object getValue() {
// if (fEntry == null) {
// throw new java.util.NoSuchElementException();
// }
// return fEntry._fValue;
// }
// };
// }
/**
* Ensures there is the specified amount of free space in the receiver, by
* removing old entries if necessary. Returns true if the requested space
* was made available, false otherwise.
*
* @param space
* Amount of space to free up
*/
protected boolean makeSpace(int space) {
int limit;
limit = this.getSpaceLimit();
/* if space is already available */
if (fCurrentSpace + space <= limit) {
return true;
}
/* if entry is too big for cache */
if (space > limit) {
return false;
}
/* Free up space by removing oldest entries */
while (fCurrentSpace + space > limit && fEntryQueueTail != null) {
this.privateRemoveEntry(fEntryQueueTail, false);
}
return true;
}
/**
* Returns a new LRUCache instance
*/
protected LRUCache newInstance(int size) {
return new LRUCache(size);
}
/**
* Adds an entry for the given key/value/space.
*/
protected void privateAdd(Object key, Object value, int space) {
LRUCacheEntry entry;
entry = new LRUCacheEntry(key, value, space);
this.privateAddEntry(entry, false);
}
/**
* Adds the given entry from the receiver.
*
* @param shuffle
* Indicates whether we are just shuffling the queue (in which
* case, the entry table is not modified).
*/
protected void privateAddEntry(LRUCacheEntry entry, boolean shuffle) {
if (!shuffle) {
fEntryTable.put(entry._fKey, entry);
fCurrentSpace += entry._fSpace;
}
entry._fTimestamp = fTimestampCounter++;
entry._fNext = this.fEntryQueue;
entry._fPrevious = null;
if (fEntryQueue == null) {
/* this is the first and last entry */
fEntryQueueTail = entry;
} else {
fEntryQueue._fPrevious = entry;
}
fEntryQueue = entry;
}
/**
* An entry has been removed from the cache, for example because it has
* fallen off the bottom of the LRU queue. Subclasses could over-ride this
* to implement a persistent cache below the LRU cache.
*/
protected void privateNotifyDeletionFromCache(LRUCacheEntry entry) {
// Default is NOP.
}
/**
* Removes the entry from the entry queue.
*
* @param shuffle
* indicates whether we are just shuffling the queue (in which
* case, the entry table is not modified).
*/
protected void privateRemoveEntry(LRUCacheEntry entry, boolean shuffle) {
LRUCacheEntry previous, next;
previous = entry._fPrevious;
next = entry._fNext;
if (!shuffle) {
fEntryTable.remove(entry._fKey);
fCurrentSpace -= entry._fSpace;
privateNotifyDeletionFromCache(entry);
}
/* if this was the first entry */
if (previous == null) {
fEntryQueue = next;
} else {
previous._fNext = next;
}
/* if this was the last entry */
if (next == null) {
fEntryQueueTail = previous;
} else {
next._fPrevious = previous;
}
}
/**
* Sets the value in the cache at the given key. Returns the value.
*
* @param key
* Key of object to add.
* @param value
* Value of object to add.
* @return added value.
*/
public Object put(Object key, Object value) {
int newSpace, oldSpace, newTotal;
LRUCacheEntry entry;
/* Check whether there's an entry in the cache */
newSpace = spaceFor(key, value);
entry = (LRUCacheEntry) fEntryTable.get(key);
if (entry != null) {
/**
* Replace the entry in the cache if it would not overflow the
* cache. Otherwise flush the entry and re-add it so as to keep
* cache within budget
*/
oldSpace = entry._fSpace;
newTotal = getCurrentSpace() - oldSpace + newSpace;
if (newTotal <= getSpaceLimit()) {
updateTimestamp(entry);
entry._fValue = value;
entry._fSpace = newSpace;
this.fCurrentSpace = newTotal;
return value;
} else {
privateRemoveEntry(entry, false);
}
}
if (makeSpace(newSpace)) {
privateAdd(key, value, newSpace);
}
return value;
}
/**
* Removes and returns the value in the cache for the given key. If the key
* is not in the cache, returns null.
*
* @param key
* Key of object to remove from cache.
* @return Value removed from cache.
*/
public Object removeKey(Object key) {
LRUCacheEntry entry = (LRUCacheEntry) fEntryTable.get(key);
if (entry == null) {
return null;
}
Object value = entry._fValue;
this.privateRemoveEntry(entry, false);
return value;
}
/**
* Sets the maximum amount of space that the cache can store
*
* @param limit
* Number of units of cache space
*/
// public void setSpaceLimit(int limit) {
// if (limit < fSpaceLimit) {
// makeSpace(fSpaceLimit - limit);
// }
// fSpaceLimit = limit;
// }
/**
* Returns the space taken by the given key and value.
*/
protected int spaceFor(Object key, Object value) {
if (value instanceof ILRUCacheable) {
return ((ILRUCacheable) value).getCacheFootprint();
} else {
return 1;
}
}
/**
* Returns a String that represents the value of this object. This method is
* for debugging purposes only.
*/
public String toString() {
return "LRUCache " + (fCurrentSpace * 100.0 / fSpaceLimit) + "% full\n" + //$NON-NLS-1$ //$NON-NLS-2$
this.toStringContents();
}
/**
* Returns a String that represents the contents of this object. This method
* is for debugging purposes only.
*/
protected String toStringContents() {
StringBuffer result = new StringBuffer();
int length = fEntryTable.size();
Object[] unsortedKeys = new Object[length];
String[] unsortedToStrings = new String[length];
Enumeration e = this.keys();
for (int i = 0; i < length; i++) {
Object key = e.nextElement();
unsortedKeys[i] = key;
unsortedToStrings[i] = (key instanceof net.sourceforge.phpdt.internal.core.JavaElement) ? ((net.sourceforge.phpdt.internal.core.JavaElement) key)
.getElementName()
: key.toString();
}
ToStringSorter sorter = new ToStringSorter();
sorter.sort(unsortedKeys, unsortedToStrings);
for (int i = 0; i < length; i++) {
String toString = sorter.sortedStrings[i];
Object value = this.get(sorter.sortedObjects[i]);
result.append(toString);
result.append(" -> "); //$NON-NLS-1$
result.append(value);
result.append("\n"); //$NON-NLS-1$
}
return result.toString();
}
/**
* 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._fTimestamp = fTimestampCounter++;
if (fEntryQueue != entry) {
this.privateRemoveEntry(entry, true);
this.privateAddEntry(entry, true);
}
return;
}
}