CachingHashtable.java Source code

Java tutorial

Introduction

Here is the source code for CachingHashtable.java

Source

/*
 * 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;
        }
    }
}