Java tutorial
/* * 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. * * <p>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.</p> * * <p>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.</p> * * <p>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.</p> * * <p>When no value is specified, threaded is true and TTL is 1 minute.</p> * @version $Rev: 86 $ * @author <a href="mailto:cooper@screaming-penguin.com">Robert Cooper</a> */ public class CachingHashtable<K, V> extends Hashtable<K, V> { /** * DOCUMENT ME! */ private Cleaner cleaner; /** * DOCUMENT ME! */ private Hashtable<K, CacheElement<V>> 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<K, Long> cacheTimes() { HashMap<K, Long> set = new HashMap<K, Long>(); 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<K, V> o = new CachingHashtable<K, V>(threaded, ttl); o.cache = (Hashtable<K, CacheElement<V>>) 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<V> 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<V> elements() { return new CacheEnumeration(super.elements()); } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public Set<Map.Entry<K, V>> 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<V> 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<K> keySet() { if (threaded) { return cache.keySet(); } else { cleaner.clean(); return cache.keySet(); } } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public Enumeration<K> 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<V> element = new CacheElement<V>(value); CacheElement<V> 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<V>(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<V> values() { if (!threaded) { cleaner.clean(); } ArrayList<V> values = new ArrayList<V>(cache.size()); for (CacheElement<V> 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<K, CacheElement<V>>(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<V> { /** * 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<V> implements Enumeration<V> { /** * DOCUMENT ME! */ Enumeration<CacheElement<V>> enu; /** * Creates a new CacheEnumeration object. * * @param enu DOCUMENT ME! */ CacheEnumeration(Enumeration<CacheElement<V>> 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<K, ? extends CacheElement> 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<K, ? extends CacheElement> 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<K> toRemove = new ArrayList<K>(); 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<K, V> implements Map.Entry { /** * DOCUMENT ME! */ CacheElement<V> element; /** * DOCUMENT ME! */ K key; /** * Creates a new MapEntry object. * * @param key DOCUMENT ME! * @param element DOCUMENT ME! */ MapEntry(K key, CacheElement<V> 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; } } }