/*
* CachingHashtable.java
*
* Created on January 13, 2005, 1:49 PM
*
* Copyright (C) 2005 Robert Cooper, Temple of the Screaming Penguin
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
/**
* This class provides a Hashtable that has a time-based limit on how long something
* remains in the table.
*
* There are two modes that this class can operate in: threaded and unthreaded. When
* operating in threaded mode, it will spawn a separate thread process to clean elements
* out of the table as they expire. When in unthreaded mode, it will check expiration
* state as requests to the object are made.
*
* Each of these has advantages and disadvantages. As a rule, if you expect the table
* to grow large and be around for a while, best to use the threaded mode as it will
* help keep the static memory state lower and performance of table-wide access calls like
* .keys() will be better. If you expect to have a small, or short lived table, unthreaded
* will eliminate the overhead of the cleaner thread. Another consideration follows.
*
* The Time to Live value operates slightly differently between these two modes.
* In threaded mode, TTL is both the checking bound on an item AND the sleep timer
* between cleans of the cache. It is, therefore, possible to have a cache element
* returned with 2 * TTL - 1 millisecond since incept. When in unthreaded mode,
* objects are guaranteed not to have a lifespan exceeding the TTL.
*
* When no value is specified, threaded is true and TTL is 1 minute.
* @version $Rev: 86 $
* @author Robert Cooper
*/
public class CachingHashtable extends Hashtable {
/**
* DOCUMENT ME!
*/
private Cleaner cleaner;
/**
* DOCUMENT ME!
*/
private Hashtable> cache;
/**
* DOCUMENT ME!
*/
private boolean threaded = true;
/**
* DOCUMENT ME!
*/
private long ttl = 60000;
/**
* Creates a new CachingHashtable object.
*/
public CachingHashtable() {
init(threaded,ttl,0,null);
}
/**
*
*/
public CachingHashtable(boolean threaded) {
init(threaded,ttl,0,null);
}
/**
*
*/
public CachingHashtable(long ttl) {
init(threaded,ttl,0,null);
}
/**
*
*/
public CachingHashtable(boolean threaded,long ttl) {
init(threaded,ttl,0,null);
}
/**
*
*/
public CachingHashtable(boolean threaded,long ttl,int initialCapacity) {
init(threaded,ttl,initialCapacity,null);
}
/**
* Creates a new CachingHashtable object.
*
* @param initialCapacity DOCUMENT ME!
*/
public CachingHashtable(int initialCapacity) {
init(threaded,ttl,initialCapacity,null);
}
/**
*
*/
public CachingHashtable(boolean threaded,int initialCapacity) {
init(threaded,ttl,initialCapacity,null);
}
/**
*
*/
public CachingHashtable(long ttl,int initialCapacity) {
init(threaded,ttl,initialCapacity,null);
}
/**
* Creates a new CachingHashtable object.
*
* @param map DOCUMENT ME!
*/
public CachingHashtable(Map extends K,? extends V> map) {
init(threaded,ttl,0,map);
}
/**
*
*/
public CachingHashtable(long ttl,Map extends K,? extends V> map) {
init(threaded,ttl,0,map);
}
/**
*
*/
public CachingHashtable(boolean threaded,long ttl,Map extends K,? extends V> map) {
init(threaded,ttl,0,map);
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public boolean isEmpty() {
if(threaded) {
return cache.isEmpty();
} else {
cleaner.clean();
return cache.isEmpty();
}
}
/**
*
* @param ttl new Time to Live value for this table
*/
public void setTimeToLive(long ttl) {
this.ttl = ttl;
this.cleaner.ttl = ttl;
}
/**
*
* @return the Time to Live for elements in this table
*/
public long getTimeToLive() {
return this.ttl;
}
/**
* DOCUMENT ME!
*
* @param key DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Long cacheTime(K key) {
CacheElement ce = cache.get(key);
if(ce == null) {
return null;
}
return new Long(ce.incept);
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Map cacheTimes() {
HashMap set = new HashMap();
if(!threaded) {
cleaner.clean();
}
for(K key : cache.keySet()) {
set.put(key,new Long(cache.get(key).incept));
}
return set;
}
/**
* DOCUMENT ME!
*/
//begin the long march of Hashtable overrides
public void clear() {
cache.clear();
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Object clone() {
CachingHashtable o = new CachingHashtable(threaded,ttl);
o.cache = (Hashtable>)this.cache.clone();
return o;
}
/**
* DOCUMENT ME!
*
* @param o DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public boolean contains(Object o) {
if(!threaded) {
cleaner.clean();
}
for(CacheElement element : cache.values()) {
if((element.payload == o)||o.equals(element.payload)) {
return true;
}
}
return false;
}
/**
* DOCUMENT ME!
*
* @param o DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public boolean containsKey(Object o) {
if(!threaded) {
cleaner.clean();
}
return cache.containsKey(o);
}
/**
* DOCUMENT ME!
*
* @param o DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public boolean containsValue(Object o) {
return contains(o);
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Enumeration elements() {
return new CacheEnumeration(super.elements());
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Set> entrySet() {
HashSet set = new HashSet();
if(!threaded) {
cleaner.clean();
}
for(K key : cache.keySet()) {
set.add(new MapEntry(key,cache.get(key)));
}
return set;
}
/**
* DOCUMENT ME!
*
* @param o DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public boolean equals(Object o) {
if(o instanceof CachingHashtable&&((CachingHashtable)o).cache.equals(this.cache)) {
return true;
} else {
return false;
}
}
/**
* DOCUMENT ME!
*/
public void finalize() {
cleaner.shutdown();
}
/**
* DOCUMENT ME!
*
* @param o DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public V get(Object o) {
K key = (K)o;
if(threaded) {
if(cache.get(key) != null) {
return cache.get(key).payload;
} else {
return null;
}
} else {
CacheElement ce = cache.get(key);
if((ce == null)||((System.currentTimeMillis() - ce.incept) >= ttl)) {
cache.remove(key);
return null;
} else {
return ce.payload;
}
}
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public int hashCode() {
return cache.hashCode();
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Set keySet() {
if(threaded) {
return cache.keySet();
} else {
cleaner.clean();
return cache.keySet();
}
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Enumeration keys() {
if(threaded) {
return cache.keys();
} else {
cleaner.clean();
return cache.keys();
}
}
/**
* DOCUMENT ME!
*
* @param key DOCUMENT ME!
* @param value DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public V put(K key,V value) {
CacheElement element = new CacheElement(value);
CacheElement old = cache.put(key,element);
if(old != null) {
return old.payload;
} else {
return null;
}
}
/**
* DOCUMENT ME!
*
* @param map DOCUMENT ME!
*/
public void putAll(Map extends K,? extends V> map) {
for(K key : map.keySet()) {
cache.put(key,new CacheElement(map.get(key)));
}
}
/**
* DOCUMENT ME!
*
* @param o DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public V remove(Object o) {
K key = (K)o;
if(threaded) {
return cache.remove(key).payload;
} else {
V value = this.get(key);
cache.remove(key);
return value;
}
}
/** Stops processing.
*/
public void shutdown() {
this.threaded = false;
cleaner.shutdown();
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public int size() {
if(threaded) {
return cache.size();
} else {
cleaner.clean();
return cache.size();
}
}
/** Starts the processing.
*/
public void startup() {
this.threaded = true;
cleaner.startup();
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Collection values() {
if(!threaded) {
cleaner.clean();
}
ArrayList values = new ArrayList(cache.size());
for(CacheElement element : cache.values())
values.add(element.payload);
return values;
}
/**
* DOCUMENT ME!
*
* @param threaded DOCUMENT ME!
* @param ttl DOCUMENT ME!
* @param initialCapacity DOCUMENT ME!
* @param map DOCUMENT ME!
*/
private void init(boolean threaded,long ttl,int initialCapacity,Map extends K,? extends V> map) {
if(map != null) {
initialCapacity = map.size();
}
cache = new Hashtable>(initialCapacity);
this.ttl = ttl;
this.threaded = threaded;
if(map != null) {
putAll(map);
}
this.cleaner = new Cleaner(ttl,cache);
if(threaded) {
cleaner.startup();
}
}
/**
* DOCUMENT ME!
*
* @author $author$
* @version $Revision: 1.4 $
*/
private static class CacheElement {
/**
* DOCUMENT ME!
*/
public V payload;
/**
* DOCUMENT ME!
*/
public long incept = System.currentTimeMillis();
/**
* Creates a new CacheElement object.
*
* @param payload DOCUMENT ME!
*/
public CacheElement(V payload) {
this.payload = payload;
}
}
/**
* DOCUMENT ME!
*
* @author $author$
* @version $Revision: 1.4 $
*/
private static class CacheEnumeration implements Enumeration {
/**
* DOCUMENT ME!
*/
Enumeration> enu;
/**
* Creates a new CacheEnumeration object.
*
* @param enu DOCUMENT ME!
*/
CacheEnumeration(Enumeration> enu) {
this.enu = enu;
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public boolean hasMoreElements() {
return enu.hasMoreElements();
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public V nextElement() {
return enu.nextElement().payload;
}
}
/**
* DOCUMENT ME!
*
* @author $author$
* @version $Revision: 1.4 $
*/
private class Cleaner extends Thread {
/**
* DOCUMENT ME!
*/
private Hashtable cache;
/**
* DOCUMENT ME!
*/
private boolean running = false;
/**
* DOCUMENT ME!
*/
private long ttl;
/**
* Creates a new Cleaner object.
*
* @param ttl DOCUMENT ME!
* @param cache DOCUMENT ME!
*/
Cleaner(long ttl,Hashtable cache) {
this.ttl = ttl;
this.cache = cache;
this.setDaemon(true);
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public boolean isRunning() {
return running;
}
/**
* DOCUMENT ME!
*/
public void clean() {
ArrayList toRemove = new ArrayList();
for(K key : cache.keySet()) {
CachingHashtable.CacheElement element = cache.get(key);
if((System.currentTimeMillis() - element.incept) >= ttl) {
toRemove.add(key);
}
}
for(K key : toRemove) {
cache.remove(key);
}
}
/**
* DOCUMENT ME!
*/
public void run() {
while(running) {
clean();
try {
Thread.sleep(ttl);
} catch(InterruptedException e) {
}
}
}
/**
* DOCUMENT ME!
*/
public void shutdown() {
this.running = false;
}
/**
* DOCUMENT ME!
*/
public void startup() {
this.running = true;
super.start();
}
}
/**
* DOCUMENT ME!
*
* @author $author$
* @version $Revision: 1.4 $
*/
private static class MapEntry implements Map.Entry {
/**
* DOCUMENT ME!
*/
CacheElement element;
/**
* DOCUMENT ME!
*/
K key;
/**
* Creates a new MapEntry object.
*
* @param key DOCUMENT ME!
* @param element DOCUMENT ME!
*/
MapEntry(K key,CacheElement element) {
this.key = key;
this.element = element;
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Object getKey() {
return key;
}
/**
* DOCUMENT ME!
*
* @param obj DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Object setValue(Object obj) {
return element.payload = (V)obj;
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Object getValue() {
return element.payload;
}
}
}