package org.cdlib.xtf.cache; /** * Copyright (c) 2004, Regents of the University of California * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the University of California nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import org.cdlib.xtf.util.EmbeddedList; import org.cdlib.xtf.util.LinkableImpl; /** * Cache is an abstract class used for code shared by SimpleCache and * GeneratingCache. Contains the workhorse functions for maintaining * a cache, expiring entries based on age or count, checking dependencies, * and checking for a key. */ public abstract class Cache<K,V> { /** * Constructor - sets up the parameters of the cache. * * @param maxEntries Number of entries allowed. If additional entries * are created, older ones will be removed. Zero * means no limit. * @param maxTime Time (in seconds) that a cache entry will remain. * If the entry hasn't been used in that time, it * is removed from the cache. Zero means no time * limit. */ public Cache(int maxEntries, int maxTime) { this.maxEntries = maxEntries; this.maxTime = maxTime; clear(); } /** * Checks if the cache currently contains an entry for the given key. * If an entry exists but has stale dependencies, it is removed and * false is returned. Otherwise, if one exists it is freshened (i.e. its * expiration countdown is reset). * * @param key The key to look for. * @return true iff the key has a valid entry in the cache. */ public synchronized boolean has(K key) { cleanup(); if (keyMap.containsKey(key)) { ListEntry entry = (ListEntry)keyMap.get(key); // If dependency checks pass, freshen the entry and return. if (dependenciesValid(key)) { entry.lastUsedTime = System.currentTimeMillis(); ageList.moveToTail(entry); cleanup(); return true; } else { ageList.remove(entry); keyMap.remove(key); logAction("Removed (stale dependencies)", key, entry.value); cleanup(); } } return false; } // has() /** * Gets the time the entry for the given key was created, or zero if the * key isn't present. The time is number of milliseconds since the epoch, * just like System.currentTimeMillis(). * * @param key The key to look for * @return The time (in milliseconds from the epoch) that the entry * was created, or zero if not present. */ public synchronized long lastSet(K key) { ListEntry ent = (ListEntry)keyMap.get(key); return (ent == null) ? 0 : ent.setTime; } // lastSet() /** * Check the dependencies of a cache entry, if present. * * @param key The key to check * @return true iff the cache entry for the key is still valid. */ public synchronized boolean dependenciesValid(K key) { cleanup(); ListEntry ent = (ListEntry)keyMap.get(key); if (ent == null) return false; Iterator i = ent.dependencies.iterator(); while (i.hasNext()) { Dependency d = (Dependency)i.next(); if (!d.validate()) return false; } return true; } // dependenciesValid() /** * Get the list of dependencies for a cache entry, if present. * * @param key The key to check * @return An iterator that will produce each dependency, or * null if no dependencies. */ public synchronized Iterator getDependencies(K key) { cleanup(); ListEntry ent = (ListEntry)keyMap.get(key); return (ent == null) ? new NullIterator() : ent.dependencies.iterator(); } // getDependencies() /** * Remove an entry from the cache. * * @param key The key to look up * @return The value that was held for the key, or null if not found. */ public synchronized V remove(K key) { cleanup(); // If we have the key, remove it and return the object. if (keyMap.containsKey(key)) { ListEntry entry = (ListEntry)keyMap.get(key); ageList.remove(entry); keyMap.remove(key); logAction("Removed", key, entry.value); cleanup(); return entry.value; } // Not found? Let the caller know. return null; } // remove() /** * Remove all entries from the cache. */ public synchronized void clear() { // Clear the list and map ageList = new EmbeddedList(); keyMap = new HashMap(maxEntries); } // clear() /** Tells how many entries are currently cached */ public synchronized int size() { return keyMap.size(); } /** * Maintains the maxEntries and maxTime constraints imposed on the cache. * Schedules additional cleanup when necessary. */ protected synchronized void cleanup() { // Do we have a size constraint? if (maxEntries >= 0) { // Remove entries until we meet the maxEntries restriction. while (ageList.getCount() > maxEntries) { ListEntry ent = (ListEntry)ageList.removeHead(); logAction( "Expired to maintain max # cache entries... was " + (ageList.getCount() + 1) + ", must be <= " + maxEntries, ent.key, ent.value); keyMap.remove(ent.key); } } // Do we have a time constraint? if (maxTime > 0) { // Remove entries older than the max time. long maxTimeMillis = maxTime * 1000; long expireTime = System.currentTimeMillis() - maxTimeMillis; while (ageList.getCount() > 0 && ((ListEntry)ageList.getHead()).lastUsedTime < expireTime) { ListEntry ent = (ListEntry)ageList.removeHead(); logAction( "Expired due to over-age... age is " + ((System.currentTimeMillis() - ent.lastUsedTime) / 1000) + " sec, must be < " + maxTime + " sec.", ent.key, ent.value); keyMap.remove(ent.key); } } } // cleanup() /** * Derived classes can override this method to print out log messages * when significant things happen (entries are added, removed, expired, * etc.) * * @param action What happened ("Added", "Removed", etc.) * @param key The key involved in the action * @param value The value involved in the action */ protected void logAction(String action, K key, V value) { } /** Used to return an iterator that does nothing */ protected class NullIterator implements Iterator { public boolean hasNext() { return false; } public Object next() { return null; } public void remove() { } } // class NullIterator() /** An entry in the age list maintained by the cache */ protected class ListEntry extends LinkableImpl { /** The key being tracked */ K key; /** The generated or set value for that key */ V value; /** The time (millis since epoch) since the entry was used */ long lastUsedTime; /** The time (millis since epoch) the entry was created */ long setTime; /** Things this entry depends on */ LinkedList dependencies = new LinkedList(); } // class ListEntry /** Maximum number of entries the cache may contain */ private int maxEntries; /** * Maximum amount of time (in seconds) an entry can stay in the cache * without being used. */ private int maxTime; /** Maintains a mapping of key to ListEntry, for fast key lookups */ protected HashMap<K,ListEntry> keyMap; /** * A list, kept sorted by descending age, of all the entries. This is * used to find the least-recently-used entry to remove when the cache * constraints (time or # of entries) are exceeded. */ protected EmbeddedList ageList; } // class Cache