package er.extensions.foundation;
import java.lang.ref.WeakReference;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
/**
* Cache that expires its entries based on time or version changes. Version can
* be any object that represents the current state of a cached value. When
* retrieving the value, you can retrieve it with a version key. If the version
* key used in the retrieval does not match the original version key of the
* object in the cache, then the cache will invalidate the value for that key
* and return null. An example version key might be the count of an array, if
* the count changes, you want to invalidate the cached object.
*
* Note that on a time-expiring cache, if you do not use the reaper with
* startBackgroundExpiration(), or manually call removeStaleEntries(), unexpired
* entries will remain in the cache for the lifetime of the cache.
*
* @author ak
* @author mschrag
*/
public class ERXExpiringCache<K, V> {
public static class Entry<V> {
private long _expiration;
private Object _versionKey;
private V _object;
private boolean _stale;
public Entry(V o, long expiration, Object version) {
_expiration = expiration;
_versionKey = version;
_object = o;
}
protected boolean isStale(long currentTime, Object currentVersionKey) {
if (!_stale) {
if (_expiration != ERXExpiringCache.NO_TIMEOUT && currentTime != ERXExpiringCache.NO_TIMEOUT && _expiration < currentTime) {
_stale = true;
}
else if (_versionKey == ERXExpiringCache.NO_VERSION || currentVersionKey == ERXExpiringCache.NO_VERSION) {
_stale = false;
}
else if (_object == null) {
_stale = currentVersionKey != null;
}
else {
_stale = !_versionKey.equals(currentVersionKey);
}
}
return _stale;
}
public V object() {
return _object;
}
@Override
public String toString() {
return super.toString() + " { " + "expiration = " + (_expiration == ERXExpiringCache.NO_TIMEOUT ? "NO_TIMEOUT" : new java.util.Date(_expiration)) + ", version = " + (_versionKey == ERXExpiringCache.NO_VERSION ? "NO_VERSION" : _versionKey) + ", object = " + _object + " }";
}
}
/**
* Designates that no timeout was specified.
*/
public static final long NO_TIMEOUT = 0L;
/**
* Designates that no explicit version was specified.
*/
public static final Object NO_VERSION = new Object();
/**
* The reaper for ERXExpiringCaches.
*/
private static ERXExpiringCache.GrimReaper _reaper;
private NSMutableDictionary<K, ERXExpiringCache.Entry<V>> _backingDictionary;
private long _expiryTime;
private long _cleanupPause;
private long _lastCleanupTime;
/**
* Constructs an ERXExpiringCache with a 60 second expiration.
*/
public ERXExpiringCache() {
this(60);
}
/**
* Constructs an ERXExpiringCache with a cleanup time that matches
* expiryTimeInSeconds.
*
* @param expiryTimeInSeconds
* the lifetime in seconds of an object in the cache or
* NO_TIMEOUT
*/
public ERXExpiringCache(long expiryTimeInSeconds) {
this(expiryTimeInSeconds, expiryTimeInSeconds);
}
/**
* @param expiryTimeInSeconds
* the lifetime in seconds of an object in the cache or
* NO_TIMEOUT
* @param cleanupPauseInSeconds
* the number of seconds to pause between cleanups
*/
public ERXExpiringCache(long expiryTimeInSeconds, long cleanupPauseInSeconds) {
_expiryTime = expiryTimeInSeconds * 1000L;
_cleanupPause = cleanupPauseInSeconds * 1000L;
if (_cleanupPause == 0) {
_cleanupPause = 60 * 1000L;
}
_lastCleanupTime = 0L;
_backingDictionary = new NSMutableDictionary<K, Entry<V>>();
}
/**
* Removes all the objects in this cache.
*/
public synchronized void removeAllObjects() {
for (Iterator<K> iterator = _backingDictionary.allKeys().iterator(); iterator.hasNext();) {
K key = iterator.next();
removeEntryForKey(entryForKey(key), key);
}
}
private long expiryTime() {
return _expiryTime;
}
/**
* Sets the object for the specified key in this cache with no version
* specified.
*
* @param object
* the value to set
* @param key
* the lookup key
*/
public synchronized void setObjectForKey(V object, K key) {
setObjectForKeyWithVersion(object, key, ERXExpiringCache.NO_VERSION);
}
/**
* Sets the object for the specified key and current version key.
*
* @param object
* the object to set
* @param key
* the lookup key
* @param currentVersionKey
* the version of the object right now
*/
public synchronized void setObjectForKeyWithVersion(V object, K key, Object currentVersionKey, long expirationTime) {
removeStaleEntries();
if (expirationTime != ERXExpiringCache.NO_TIMEOUT) {
expirationTime = System.currentTimeMillis() + expirationTime;
}
Entry<V> entry = new Entry<>(object, expirationTime, currentVersionKey);
setEntryForKey(entry, key);
}
/**
* Sets the object for the specified key and current version key.
*
* @param object
* the object to set
* @param key
* the lookup key
* @param currentVersionKey
* the version of the object right now
*/
public synchronized void setObjectForKeyWithVersion(V object, K key, Object currentVersionKey) {
setObjectForKeyWithVersion(object, key, currentVersionKey, _expiryTime);
}
/**
* Returns the value of the given key with an unspecified version.
*
* @param key
* the key to lookup with
* @return the value in the cache or null
*/
public synchronized V objectForKey(K key) {
return objectForKeyWithVersion(key, ERXExpiringCache.NO_VERSION);
}
/**
* Returns the value of the given key passing in the current version of the
* cache value. If the version key passed in does not match the version key
* in the cache, the cache will invalidate that key.
*
* @param key
* the key to lookup with
* @param currentVersionKey
* the current version of this key
* @return the value in the cache or null
*/
public synchronized V objectForKeyWithVersion(K key, Object currentVersionKey) {
Entry<V> entry = entryForKey(key);
V value = null;
if (entry != null) {
if (entry.isStale(System.currentTimeMillis(), currentVersionKey)) {
removeEntryForKey(entry, key);
}
else {
value = entry.object();
}
}
return value;
}
/**
* Returns whether or not the object for the given key is a stale cache
* entry.
*
* @param key
* the key to lookup
* @return true if the value is stale
*/
public synchronized boolean isStale(K key) {
return isStaleWithVersion(key, ERXExpiringCache.NO_VERSION);
}
/**
* Returns whether or not the object for the given key is a stale cache
* entry given the context of the current version of the key.
*
* @param key
* the key to lookup
* @param currentVersionKey
* the current version of this key
* @return true if the value is stale
*/
public synchronized boolean isStaleWithVersion(K key, Object currentVersionKey) {
Entry<V> entry = entryForKey(key);
boolean isStale = true;
if (entry != null) {
isStale = entry.isStale(System.currentTimeMillis(), currentVersionKey);
}
return isStale;
}
/**
* Removes the object for the given key.
*
* @param key
* the key to remove
* @return the removed object
*/
public synchronized V removeObjectForKey(K key) {
removeStaleEntries();
Entry<V> entry = entryForKey(key);
V value = null;
if (entry != null) {
removeEntryForKey(entry, key);
value = entry.object();
}
return value;
}
/**
* Removes all stale entries.
*/
public synchronized void removeStaleEntries() {
if (_backingDictionary.count() > 0) {
long now = System.currentTimeMillis();
if ((_lastCleanupTime + _cleanupPause) < now) {
_lastCleanupTime = System.currentTimeMillis();
for (Enumeration<K> keyEnum = _backingDictionary.keyEnumerator(); keyEnum.hasMoreElements();) {
K key = keyEnum.nextElement();
Entry<V> entry = entryForKey(key);
// (AR): It's wrong to add 10 seconds, subtracting 10 makes objects
// live longer but this really isn't necessary. It appears
// no "fudge factor" is needed.
if (entry.isStale(now, ERXExpiringCache.NO_VERSION)) {
removeEntryForKey(entry, key);
}
}
}
}
}
protected synchronized void removeEntryForKey(Entry<V> entry, K key) {
_backingDictionary.removeObjectForKey(key);
}
protected synchronized void setEntryForKey(Entry<V> entry, K key) {
_backingDictionary.setObjectForKey(entry, key);
}
protected synchronized Entry<V> entryForKey(K key) {
return _backingDictionary.objectForKey( key);
}
@Override
public String toString() {
return super.toString() + " " + _backingDictionary;
}
/**
* Adds this cache to the background thread that reaps time-expired entries
* from expiring caches. If this cache is not a time-expiration cache, this
* will throw an IllegalArgumentException.
*/
public void startBackgroundExpiration() {
if (_expiryTime == ERXExpiringCache.NO_TIMEOUT) {
throw new IllegalArgumentException("This ERXExpiringCache does not have an expiration time.");
}
ERXExpiringCache.reaper().addCache(this);
}
/**
* Stops the background reaper for this cache.
*/
public synchronized void stopBackgroundExpiration() {
ERXExpiringCache.reaper().stop(this);
}
/**
* Returns the repear for all ERXExpringCaches.
*
* @return the repear for all ERXExpringCaches
*/
protected static synchronized ERXExpiringCache.GrimReaper reaper() {
if (_reaper == null) {
_reaper = new GrimReaper(ERXProperties.intForKeyWithDefault("er.extensions.ERXExpiringCache.reaperFrequency", 5000));
}
return ERXExpiringCache._reaper;
}
/**
* The reaper runnable for ERXExpiringCache.
*
* @author mschrag
*/
protected static class GrimReaper implements Runnable {
private List<WeakReference<ERXExpiringCache>> _caches;
private long _reapFrequencyInMillis;
private boolean _stopped;
public GrimReaper(long reapFrequencyInMillis) {
_caches = new LinkedList<WeakReference<ERXExpiringCache>>();
_reapFrequencyInMillis = reapFrequencyInMillis;
_stopped = true;
}
public void addCache(ERXExpiringCache cache) {
synchronized (_caches) {
_caches.add(new WeakReference<>(cache));
if (_stopped) {
start();
}
}
}
public void start() {
synchronized (_caches) {
if (_stopped) {
_stopped = false;
Thread reaperThread = new Thread(this);
reaperThread.start();
}
}
}
public void stop() {
synchronized (_caches) {
_stopped = true;
}
}
public void stop(ERXExpiringCache cache) {
synchronized (_caches) {
Iterator<WeakReference<ERXExpiringCache>> cacheIter = _caches.iterator();
while (cacheIter.hasNext()) {
WeakReference<ERXExpiringCache> cacheRef = cacheIter.next();
ERXExpiringCache reapingCache = cacheRef.get();
if (reapingCache == cache) {
cacheIter.remove();
break;
}
}
}
}
public void run() {
boolean stopped = false;
do {
try {
Thread.sleep(_reapFrequencyInMillis);
}
catch (InterruptedException e) {
// IGNORE
}
synchronized (_caches) {
Iterator<WeakReference<ERXExpiringCache>> cacheIter = _caches.iterator();
while (cacheIter.hasNext()) {
WeakReference<ERXExpiringCache> cacheRef = cacheIter.next();
ERXExpiringCache cache = cacheRef.get();
if (cache == null) {
cacheIter.remove();
}
else {
cache.removeStaleEntries();
}
}
if (_caches.size() == 0) {
_stopped = true;
stopped = true;
}
}
}
while (!stopped);
}
}
/**
* Returns all keys.
*/
public synchronized NSArray<K> allKeys() {
NSMutableArray<K> result = new NSMutableArray<>(_backingDictionary.count());
for (K key : _backingDictionary.allKeys()) {
result.addObject(key);
}
return result;
}
}