663 lines
21 KiB
Java
663 lines
21 KiB
Java
/*
|
|
* 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 <http://www.mozilla.org/MPL/>.
|
|
*
|
|
* 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 <erbo@silcom.com>,
|
|
* 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 <CODE>Map</CODE> that acts as a cache of data items. The <CODE>CacheMap</CODE>
|
|
* uses <CODE>SoftReferences</CODE>, 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 <CODE>CacheMapStrategy</CODE> 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 <CODE>CacheMap</CODE>.
|
|
*
|
|
* @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 <CODE>null</CODE>.
|
|
*/
|
|
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 <CODE>CacheMap</CODE> 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 <CODE>CacheMap</CODE> 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; i<num_remove; i++)
|
|
{ // remove the "removed" entries from the hash map
|
|
CacheMapEntry cme = (CacheMapEntry)(element_list.remove(element_list.size() - 1));
|
|
base_map.remove(cme.getKey());
|
|
|
|
} // end for
|
|
|
|
} // end doShrink
|
|
|
|
/*--------------------------------------------------------------------------------
|
|
* Implementations from interface Map
|
|
*--------------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Returns the number of key-value mappings in this map. If the map contains more than
|
|
* <CODE>Integer.MAX_VALUE</CODE> elements, returns <CODE>Integer.MAX_VALUE</CODE>.
|
|
*
|
|
* @return The number of key-value mappings in this map.
|
|
*/
|
|
public int size()
|
|
{
|
|
doSweep();
|
|
return base_map.size();
|
|
|
|
} // end size
|
|
|
|
/**
|
|
* Returns <CODE>true</CODE> if this map contains no key-value mappings.
|
|
*
|
|
* @return <CODE>true</CODE> if this map contains no key-value mappings.
|
|
*/
|
|
public boolean isEmpty()
|
|
{
|
|
doSweep();
|
|
return base_map.isEmpty();
|
|
|
|
} // end isEmpty
|
|
|
|
/**
|
|
* Returns <CODE>true</CODE> if this map contains a mapping for the specified key.
|
|
*
|
|
* @param key Key whose presence in this map is to be tested.
|
|
* @return <CODE>true</CODE> 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 <CODE>null</CODE>.
|
|
*/
|
|
public boolean containsKey(Object key)
|
|
{
|
|
doSweep();
|
|
return base_map.containsKey(key);
|
|
|
|
} // end containsKey
|
|
|
|
/**
|
|
* Returns <CODE>true</CODE> 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 <CODE>true</CODE> 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 <CODE>null</CODE> 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 <CODE>null</CODE> 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 <CODE>null</CODE>.
|
|
* @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 <CODE>null</CODE> 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 <CODE>null</CODE>.
|
|
*/
|
|
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 <CODE>null</CODE> 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 <CODE>null</CODE>.
|
|
*/
|
|
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 <CODE>true</CODE> 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 <CODE>true</CODE> 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 <CODE>null</CODE>.
|
|
*/
|
|
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 <CODE>setShrinkPercentage</CODE>. 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<num_remove)
|
|
doShrink(num_remove - n1);
|
|
|
|
} // end shrink
|
|
|
|
} // end class CacheMap
|
|
|
|
|
|
|