Collections Data Structure Java

/*
 * 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 map) {
        init(threaded,ttl,0,map);
    }
    /**
     *
     */
    public CachingHashtable(long ttl,Map map) {
        init(threaded,ttl,0,map);
    }
    /**
     *
     */
    public CachingHashtable(boolean threaded,long ttl,Map 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 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 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;
        }
    }
}