/* * The contents of this file are subject to the Mozilla Public License Version 1.1 * (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at . * * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT * WARRANTY OF ANY KIND, either express or implied. See the License for the specific * language governing rights and limitations under the License. * * The Original Code is the Venice Web Communities System. * * The Initial Developer of the Original Code is Eric J. Bowersox , * for Silverwrist Design Studios. Portions created by Eric J. Bowersox are * Copyright (C) 2001 Eric J. Bowersox/Silverwrist Design Studios. All Rights Reserved. * * Contributor(s): */ package com.silverwrist.util.cache; import java.lang.ref.*; import java.util.*; /** * A special kind of Map that acts as a cache of data items. The CacheMap * uses SoftReferences, so its data values will be shed if the virtual machine needs * more memory; also, if too many entries are added to the map, certain entries will be removed in * accordance with the supplied or default CacheMapStrategy object. * * @author Eric J. Bowersox <erbo@silcom.com> * @version X * @see CacheMapStrategy * @see java.util.Map */ public class CacheMap implements Map { /*-------------------------------------------------------------------------------- * Internal class used to do comparisons for cache shrinkage *-------------------------------------------------------------------------------- */ static final class CacheOrdering implements Comparator { private CacheMapStrategy strategy; // CacheMap's strategy object private long tick; // when the sort operation started CacheOrdering(CacheMapStrategy strategy) { this.strategy = strategy; this.tick = System.currentTimeMillis(); } // end constructor public int compare(Object o1, Object o2) { long figm1 = strategy.getEntryValue((CacheMapEntry)o1,tick); long figm2 = strategy.getEntryValue((CacheMapEntry)o2,tick); return (int)(figm1 - figm2); // we want the largest figures of merit to go first } // end compare public boolean equals(Object o) { return (o instanceof CacheOrdering); } // end equals } // end class CacheOrdering /*-------------------------------------------------------------------------------- * Internal class implementing a default cache ordering strategy *-------------------------------------------------------------------------------- */ static final class DefaultStrategy implements CacheMapStrategy { private static final long SCALING_FACTOR = 5000; DefaultStrategy() { // do nothing } // end constructor public long getEntryValue(CacheMapEntry entry, long tick) { return (entry.getHits() * SCALING_FACTOR) - entry.getAge(tick); } // end getEntryValue } // end class DefaultStrategy /*-------------------------------------------------------------------------------- * Static data members *-------------------------------------------------------------------------------- */ private static final DefaultStrategy default_strategy_singleton = new DefaultStrategy(); /*-------------------------------------------------------------------------------- * Attributes *-------------------------------------------------------------------------------- */ private int capacity; // capacity of the CacheMap private int shrink_percentage; // what percentage we shrink by when full private CacheMapStrategy strategy; // strategy routine to use to purge entries private HashMap base_map; // maps keys to CacheMapEntry values private ArrayList element_list; // the actual elements private ReferenceQueue rq; // holds references that the garbage collector has cleared /*-------------------------------------------------------------------------------- * Constructors *-------------------------------------------------------------------------------- */ /** * Constructs a new CacheMap. * * @param capacity The maximum number of entries this map can contain. * @param shrink_percentage The percentage of entries which will be removed from the cache * whenever it needs to shrink itself. * @param strategy The strategy object which is used to determine which elements to remove. * @exception java.lang.IllegalArgumentException If the specified capacity is negative or zero, or * the shrink percentage is not in the range [1..100]. * @exception java.lang.NullPointerException If the strategy object reference is null. */ public CacheMap(int capacity, int shrink_percentage, CacheMapStrategy strategy) { if (capacity<=0) throw new IllegalArgumentException("capacity must be greater than 0"); if ((shrink_percentage<=0) || (shrink_percentage>100)) throw new IllegalArgumentException("shrink_percentage must be in [1, 100]"); if (strategy==null) throw new NullPointerException("no strategy passed to CacheMap"); this.capacity = capacity; this.shrink_percentage = shrink_percentage; this.strategy = strategy; this.base_map = new HashMap(10); this.element_list = new ArrayList(10); this.rq = new ReferenceQueue(); } // end constructor /** * Constructs a new CacheMap with a default strategy. * * @param capacity The maximum number of entries this map can contain. * @param shrink_percentage The percentage of entries which will be removed from the cache * whenever it needs to shrink itself. * @exception java.lang.IllegalArgumentException If the specified capacity is negative or zero, or * the shrink percentage is not in the range [1..100]. */ public CacheMap(int capacity, int shrink_percentage) { this(capacity,shrink_percentage,default_strategy_singleton); } // end constructor /** * Constructs a new CacheMap with a default strategy and shrink percentage. * * @param capacity The maximum number of entries this map can contain. * @exception java.lang.IllegalArgumentException If the specified capacity is negative or zero. */ public CacheMap(int capacity) { this(capacity,10,default_strategy_singleton); } // end constructor /*-------------------------------------------------------------------------------- * Internal operations *-------------------------------------------------------------------------------- */ private void doSweep() { Reference r = rq.poll(); // the reference that's been cleared ArrayList ditch = new ArrayList(); // a list of entries to ditch Iterator it; // current iterator while (r!=null) { // look for the dead reference in the element list it = element_list.iterator(); while (it.hasNext()) { // check each cache map entry in return CacheMapEntry ntry = (CacheMapEntry)(it.next()); if (ntry.matchReference(r)) { // remove the offending entry and save it in the temporary list it.remove(); ditch.add(ntry); break; } // end if } // end while r = rq.poll(); // get next dead reference } // end while if (ditch.isEmpty()) return; // nothing to prune it = ditch.iterator(); while (it.hasNext()) { // clear all entries from the base hashmap as well CacheMapEntry ntry = (CacheMapEntry)(it.next()); base_map.remove(ntry.getKey()); ntry.discard(); } // end while } // end doSweep public synchronized void doShrink(int num_remove) { // Sort the element list to figure out which elements to remove. Collections.sort(element_list,new CacheOrdering(strategy)); // The elements we want to remove are at the end of the array, so start from there. for (int i=0; iInteger.MAX_VALUE elements, returns Integer.MAX_VALUE. * * @return The number of key-value mappings in this map. */ public int size() { doSweep(); return base_map.size(); } // end size /** * Returns true if this map contains no key-value mappings. * * @return true if this map contains no key-value mappings. */ public boolean isEmpty() { doSweep(); return base_map.isEmpty(); } // end isEmpty /** * Returns true if this map contains a mapping for the specified key. * * @param key Key whose presence in this map is to be tested. * @return true if this map contains a mapping for the specified key. * @exception java.lang.ClassCastException If the key is of an inappropriate type for this map. * @exception java.lang.NullPointerException If the key is null. */ public boolean containsKey(Object key) { doSweep(); return base_map.containsKey(key); } // end containsKey /** * Returns true if this map maps one or more keys to the specified value. * * @param value Value whose presence in this map is to be tested. * @return true if this map maps one or more keys to the specified value. */ public boolean containsValue(Object value) { doSweep(); Iterator it = element_list.iterator(); while (it.hasNext()) { // look at all the CacheMapEntry values we have CacheMapEntry cme = (CacheMapEntry)(it.next()); Object my_val = cme.getValue(); if (my_val==null) { // test for also null if (value==null) return true; } // end if else { // make sure the other value is non-null before we test equality if ((value!=null) && my_val.equals(value)) return true; } // end else } // end while return false; // nope, sorry } // end containsValue /** * Returns the value to which this map maps the specified key. Returns null if the map * contains no mapping for this key. * * @param key Key whose associated value is to be returned. * @return The value to which this map maps the specified key, or null if the map contains * no mapping for this key. * @exception java.lang.ClassCastException If the key is of an inappropriate type for this map. * @exception java.lang.NullPointerException If the key is null. * @see #containsKey(java.lang.Object) */ public Object get(Object key) { doSweep(); CacheMapEntry cme = (CacheMapEntry)(base_map.get(key)); if (cme==null) return null; cme.touch(); return cme.getValue(); } // end get /** * Associates the specified value with the specified key in this map. If the map previously contained * a mapping for this key, the old value is replaced. * * @param key Key with which the specified value is to be associated. * @param value Value to be associated with the specified key. * @return The previous value associated with the specified key, or null if there was no * mapping for the key. * @exception java.lang.ClassCastException If the class of the specified key or value prevents it from * being stored in this map. * @exception java.lang.IllegalArgumentException If some aspect of this key or value prevents it from * being stored in this map. * @exception java.lang.NullPointerException If the specified key or value is null. */ public Object put(Object key, Object value) { doSweep(); Object rc = null; CacheMapEntry cme = (CacheMapEntry)(base_map.get(key)); if (cme==null) { // create a new CacheMapEntry for this key cme = new CacheMapEntry(key,value,rq); synchronized (this) { // insert it into the basic object if (base_map.size()==capacity) doShrink((element_list.size() * shrink_percentage) / 100); element_list.add(cme); base_map.put(cme.getKey(),cme); } // end synchronized block } // end if else { // we have an old value - replace it and touch the entry cme.touch(); rc = cme.setValue(value,rq); } // end else return rc; } // end put /** * Removes the mapping for this key from this map if present. * * @param key Key whose mapping is to be removed from the map. * @return The previous value associated with the specified key, or null if there was no * mapping for the key. */ public Object remove(Object key) { doSweep(); Object rc = null; CacheMapEntry cme = (CacheMapEntry)(base_map.get(key)); if (cme!=null) { // save the mapped value before we remove it rc = cme.getValue(); synchronized (this) { // remove the values base_map.remove(key); element_list.remove(cme); } // end synchronized block cme.discard(); // zap the reference } // end if return rc; } // end remove /** * Copies all of the mappings from the specified map to this map. These mappings will replace any mappings * that this map had for any of the keys currently in the specified map. * * @param map Mappings to be stored in this map. * @exception java.lang.ClassCastException If the class of a key or value in the specified map prevents * it from being stored in this map. * @exception java.lang.IllegalArgumentException If some aspect of a key or value in the specified map * prevents it from being stored in this map. * @exception java.lang.NullPointerException If the specified key or value is null. */ public void putAll(Map map) { doSweep(); synchronized (this) { // make sure we have enough space in the CacheMap for all the new elements! int nover = (map.size() + base_map.size()) - capacity; if (nover>0) doShrink(nover); } // end synchronized block Iterator it = map.entrySet().iterator(); while (it.hasNext()) { // add each element in turn Map.Entry me = (Map.Entry)(it.next()); put(me.getKey(),me.getValue()); } // end while } // end putAll /** * Removes all mappings from this map. */ public synchronized void clear() { base_map.clear(); Iterator it = element_list.iterator(); while (it.hasNext()) { // discard all entries we have CacheMapEntry cme = (CacheMapEntry)(it.next()); cme.discard(); } // end while element_list.clear(); } // end clear /** * Returns a set view of the keys contained in this map. * * @return A set view of the keys contained in this map. */ public Set keySet() { return base_map.keySet(); } // end keySet /** * Returns a collection view of the values contained in this map. * * @return A collection view of the values contained in this map. * @exception java.lang.UnsupportedOperationException This map does not support this operation. */ public Collection values() { throw new UnsupportedOperationException("CacheMap.values() is not implemented"); } // end values /** * Returns a set view of the mappings contained in this map. * * @return A set view of the mappings contained in this map. * @exception java.lang.UnsupportedOperationException This map does not support this operation. */ public Set entrySet() { throw new UnsupportedOperationException("CacheMap.entrySet() is not implemented"); } // end entrySet /** * Compares the specified object with this map for equality. Returns true if the given object * is also a map and the two Maps represent the same mappings. * * @param o Object to be compared for equality with this map. * @return true if the specified object is equal to this map. */ public boolean equals(Object o) { if ((o==null) || !(o instanceof Map)) return false; // not a map doSweep(); Map other = (Map)o; if (other.size()!=base_map.size()) return false; // size does matter! Iterator it = base_map.values().iterator(); while (it.hasNext()) { // get each of the entries out and use that to do a key-value comparison CacheMapEntry cme = (CacheMapEntry)(it.next()); Object o1 = cme.getValue(); Object o2 = other.get(cme.getKey()); if (o1==null) { // must have a matching null if (o2!=null) return false; } // end if else { // make sure we have a matching object (not null) if ((o2==null) || !(o2.equals(o1))) return false; } // end else } // end while return true; // all OK! } // end equals /** * Returns the hash code value for this map. The hash code of a map is defined to be the sum of the * hash codes of each entry in the map's entrySet view. * * @return The hash code value for this map. * @see #equals(java.lang.Object) * @see CacheMapEntry#hashCode() */ public int hashCode() { doSweep(); int rc = 0; Iterator it = base_map.values().iterator(); while (it.hasNext()) { // add up the hash codes and return them CacheMapEntry cme = (CacheMapEntry)(it.next()); rc += cme.hashCode(); } // end while return rc; } // end hashCode /*-------------------------------------------------------------------------------- * External getters/setters *-------------------------------------------------------------------------------- */ /** * Returns the capacity of this cache map. * * @return The capacity of this cache map. */ public int getCapacity() { return capacity; } // end getCapacity /** * Sets the capacity of this cache map. * * @param c The new capacity for this cache map. * @exception java.lang.IllegalArgumentException If the specified capacity is negative or zero. */ public void setCapacity(int c) { if (c<=0) throw new IllegalArgumentException("capacity must be greater than 0"); capacity = c; } // end setCapacity /** * Returns the shrink percentage of this cache map. * * @return The shrink percentage of this cache map. */ public int getShrinkPercentage() { return shrink_percentage; } // end getShrinkPercentage /** * Sets the shrink percentage of this cache map. * * @param p The new shrink percentage for this cache map. * @exception java.lang.IllegalArgumentException If the specified shrink percentage is not in the * range [1..100]. */ public void setShrinkPercentage(int p) { if ((p<=0) || (p>100)) throw new IllegalArgumentException("shrink_percentage must be in [1, 100]"); shrink_percentage = p; } // end setShrinkPercentage /** * Returns the strategy object associated with the cache map. * * @return The strategy object associated with the cache map. */ public CacheMapStrategy getStrategy() { return strategy; } // end getStrategy /** * Sets the strategy object associated with this cache map. * * @param s The new strategy object to be associated with the cache map. * @exception java.lang.NullPointerException If the strategy object reference is null. */ public void setStrategy(CacheMapStrategy s) { if (s==null) throw new NullPointerException("no strategy passed to CacheMap"); strategy = s; } // end setStrategy /*-------------------------------------------------------------------------------- * External operations *-------------------------------------------------------------------------------- */ /** * Causes the current size of the cache map to shrink by at least the shrink percentage specified * in the constructor or in setShrinkPercentage. Cached items already reclaimed by the * garbage collector are stripped out first. */ public synchronized void shrink() { // Figure out how many elements to remove. int num_remove = (element_list.size() * shrink_percentage) / 100; // Try a sweep first. int n1 = base_map.size(); doSweep(); n1 -= base_map.size(); if (n1