/* CacheMap.java Purpose: Description: History: 2001/11/23 15:26:21, Create, Tom M. Yeh. Copyright (C) 2001 Potix Corporation. All Rights Reserved. {{IS_RIGHT This program is distributed under LGPL Version 2.1 in the hope that it will be useful, but WITHOUT ANY WARRANTY. }}IS_RIGHT */ package org.zkoss.util; import java.util.AbstractCollection; import java.util.Collection; import java.util.Map; import java.util.AbstractSet; import java.util.Set; import java.util.LinkedHashMap; import java.util.Iterator; import java.lang.ref.WeakReference; import java.lang.ref.ReferenceQueue; import org.zkoss.lang.Objects; /** * The cache map. The key-to-value mappings hold in this map is * temporary. They are removed when GC demanding memory and a * criteria is met. The criteria is whether the mapping is old enough * (called lifetime), or the upper bound is hit (called max-size). * * <p>The criteria can be changed by overriding {@link #canExpunge}. * When to check the criteria can be changed by overriding * {@link #shallExpunge}. * * <p>If the criteria is totally independent of GC, you could override * {@link #shallExpunge} to always return true * (rather than when GC is activated). * * <p>It is different from WeakHashMap: * * <ul> * <li>The mapping might be removed even if the key is hold somewhere * (i.e., strong reachable).</li> * <li>The mapping might not be removed when GC demanding memory * if the criteria doesn't meet.</li> * <li>It is not serializable.</li> * </ul> * * <p>Like other maps, it is not thread-safe. To get one, use * java.util.Collections.synchronizedMap. * * <p>Implementation Note: there is another version of CacheMap that * uses WeakReference for each value (refer to obsolete). * The drawback is that all mapping will be queued and need to be examined, * because GC tends to release all reference at once. * * <p>We don't use PhantomReference because it is still required to * re-create the reference after enqueued. * * @author tomyeh */ public class CacheMap<K,V> implements Map<K,V>, Cache<K,V>, java.io.Serializable, Cloneable { private static final long serialVersionUID = 20070907L; //private static final Logger log = LoggerFactory.getLogger(CacheMap.class); /** The map to store the mappings. */ private Map<K, Value<V>> _map; //it is OK to serialized /** The minimal lifetime. */ private int _lifetime = DEFAULT_LIFETIME; /** The maximal allowed size. */ private int _maxsize = DEFAULT_MAX_SIZE; /** The reference queue. */ private transient ReferenceQueue<X> _que; /** The reference. */ private transient WeakReference<X> _ref; /** A flag used for debug purpose. */ private transient boolean _inExpunge; private final boolean _accessOrder; /** The class to be hold in the reference (to know GC is demanding). */ private static class X { } /** The class to hold key/value. */ protected static final class Value<V> implements java.io.Serializable, Cloneable { private V value; private long access; //when the mapping is accessed /** Creates an instance to store in the map. */ private Value(V value) { this.value = value; updateAccessTime(); } private final void updateAccessTime() { this.access = System.currentTimeMillis(); } //-- utilities--// /** Returns the value. */ public final V getValue() { return this.value; } /** Returns the last access time. */ public final long getAccessTime() { return this.access; } //-- Cloneable --// public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(); } } //-- Object --// public final String toString() { return "(" + this.value + '@' + this.access + ')'; } } //-- deriving to override --// /** * Called when a pair of key and value having been expunged. * This method is called after it is removed, so you could * add it back. * * <p>Default: does nothing */ protected void onExpunge(Value<V> v) { } /** Returns by {@link #canExpunge} to denote it shall not be expunged. */ protected static final int EXPUNGE_NO = 0x0; /** Returns by {@link #canExpunge} to denote it shall be expunged. */ protected static final int EXPUNGE_YES = 0x1; //must not zero /** Returns by {@link #canExpunge} to denote the searching of the * next mapping shall continue. */ protected static final int EXPUNGE_CONTINUE = 0x0; /** Returns by {@link #canExpunge} to denote the searching of the * next mapping shall stop. */ protected static final int EXPUNGE_STOP = 0x2; //must not zero /** Returns whether it is time to expunge. * Once shallExpunge returns true, values are examined one-by-one thru * {@link #canExpunge}, and expunged if EXPUNGE_YES. * * <p>This implementation returns true only if GC was activated. * You might override it to return true, such that expunge is enforced * no matter GC was activated. * * @see #canExpunge */ protected boolean shallExpunge() { return _que == null || _que.poll() != null; } /** * Tests whether certain value is OK to expunge. * * <p>Note: values are tested thru {@link #canExpunge} only if * {@link #shallExpunge} returns true. * * <p>Deriving classes might override this method to return different * value for different criteria. * * <p>The return value could be a combination of EXPUNGE_xxx. * One of EXPUNGE_YES and EXPUNGE_NO is returned to denote * whether to expunge the mapping. One of EXPUNGE_CONTINUE and * EXPUNGE_STOP is returned to denote whether to continue the * searching of the next mapping for expunging. * * <p>Normally, you return either (EXPUNGE_YES|EXPUNGE_CONTINUE) * or (EXPUNG_NO|EXPUNGE_STOP). * Notice that the mapping is queried in the last-access order. * Thus, you rarely needs to return (EXPUNGE_NO|EXPUNGE_CONTINUE) * unless the appropriate one might be out of this order. * * <p>This implementation compares the access time and size. * It returns (EXPUNGE_YES|EXPUNGE_CONTINUE) if OK, and * (EXPUNGE_NO|EXPUNGE_STOP) if not. * * @param size the current size. It is used instead of size(), since * the entry might not be removed yet (such as {@link FastReadCache}). * @return a combination of EXPUNGE_xxx * @see #shallExpunge */ protected int canExpunge(int size, Value<V> v) { return size > getMaxSize() || (System.currentTimeMillis() - v.access) > getLifetime() ? (EXPUNGE_YES|EXPUNGE_CONTINUE): (EXPUNGE_NO|EXPUNGE_STOP); } /** Expunges if {@link #shallExpunge} is true. */ private void tryExpunge() { if (shallExpunge()) doExpunge(); } /*package*/ void doExpunge() { //FastReadCache overrides it if (_inExpunge) throw new IllegalStateException("expunge in expunge?"); try { expunge(); } finally { newRef(); } } /** Enforces to expunge items that exceeds the maximal allowed number * or lifetime. * <p>By default, this method is called only GC takes places. * @return number of items left ({@link #size}) after expunged * @since 3.6.1 */ public int expunge() { if (_inExpunge || _map.isEmpty()) return _map.size(); //nothing to do _inExpunge = true; try { //dennis, bug 1815633, remove some control code here int size = _map.size(); for (final Iterator<Map.Entry<K, Value<V>>> it = _map.entrySet().iterator(); it.hasNext();) { final Map.Entry<K, Value<V>> entry = it.next(); final Value<V> v = entry.getValue(); final int result = canExpunge(size, v); if ((result & EXPUNGE_YES) != 0) { --size; removeInExpunge(it, entry.getKey()); //remove it onExpunge(v); } if ((result & EXPUNGE_STOP) != 0) break; //stop } return size; } finally { _inExpunge = false; } } //for FastReadCache to override (not sure worth to be protected) /*package*/ void removeInExpunge(Iterator<Map.Entry<K, Value<V>>> it, K k) { it.remove(); } /** Re-create the reference so we can detect if GC was activated. */ private void newRef() { if (_que != null) _ref = new WeakReference<X>(new X(), _que); } //-- constructors --// /** Constructs a cache map with the specified max size and lifetime. * Unlike LinkedHashMap, the default order is the access order, * i.e., the order is changed once accessed, including get(). * @since 3.0.0 */ public CacheMap(int maxSize, int lifetime) { this(maxSize, lifetime, true); } /** Constructs a cache map. * Unlike LinkedHashMap, the default order is the access order, * i.e., the order is changed once accessed, including get(). */ public CacheMap() { this(16, 0.75f, true); } /** Constructs a cache map. * Unlike LinkedHashMap, the default order is the access order, * i.e., the order is changed once accessed, including get(). */ public CacheMap(int cap) { this(cap, 0.75f, true); } /** Constructs a cache map. * Unlike LinkedHashMap, the default order is the access order, * i.e., the order is changed once accessed, including get(). */ public CacheMap(int cap, float load) { this(cap, load, true); } /** Constructs a cache map. * @param accessOrder whether to use the access order. * Specify false for the insertion order. * @since 6.0.0 */ public CacheMap(boolean accessOrder) { this(16, 0.75f, accessOrder); } /** Constructs a cache map with the specified max size and lifetime. * @param accessOrder whether to use the access order. * Specify false for the insertion order. * @since 6.0.0 */ public CacheMap(int maxSize, int lifetime, boolean accessOrder) { this(accessOrder); setMaxSize(maxSize); setLifetime(lifetime); } /** Constructs a cache map. * @param accessOrder whether to use the access order. * Specify false for the insertion order. * @since 6.0.0 */ public CacheMap(int cap, float load, boolean accessOrder) { _accessOrder = accessOrder; _map = new LinkedHashMap<K, Value<V>>(cap, load, accessOrder); init(); } /** Initialization for constructor and de-serialized. */ private void init() { _que = new ReferenceQueue<X>(); newRef(); } //-- extra api --// /** * Gets the minimal lifetime, unit=milliseconds. * An mapping won't be removed by GC unless the minimal lifetime * or the maximal allowed size exceeds. * @see #getMaxSize */ public int getLifetime() { return _lifetime; } /** * Sets the minimal lifetime. Default: {@link #DEFAULT_LIFETIME}. * * @param lifetime the lifetime, unit=milliseconds; * if non-positive, they will be removed immediately. * @see #getLifetime */ public void setLifetime(int lifetime) { _lifetime = lifetime; } /** * Gets the maximal allowed size. Default: {@link #DEFAULT_MAX_SIZE}. * An mapping won't be removed by GC unless the minimal lifetime * or the maximal allowed size exceeds. * <p>Notice: getMaxSize() is only a soft limit. It takes effect only if * GC takes place. * @see #getLifetime */ public int getMaxSize() { return _maxsize; } /** * Sets the maximal allowed size. * @see #getMaxSize */ public void setMaxSize(int maxsize) { _maxsize = maxsize; } //-- Map --// public boolean isEmpty() { tryExpunge(); return _map.isEmpty(); } /** Returns whether it is empty without trying to expunge first. * @since 3.0.1 */ public boolean isEmptyWithoutExpunge() { return _map.isEmpty(); } public int size() { tryExpunge(); return _map.size(); } /** Returns the size without trying to expunge first. * @since 3.0.1 */ public int sizeWithoutExpunge() { return _map.size(); } public void clear() { _map.clear(); } public V remove(Object key) { final Value<V> v = _map.remove(key); tryExpunge(); return v != null ? v.value: null; } public V get(Object key) { final V v = getWithoutExpunge(key); tryExpunge(); //expunge later to increase the hit rate return v; } /** Returns the value without trying to expunge for more * memory. * It is useful if you want to preserve all entries. */ public V getWithoutExpunge(Object key) { final Value<V> v = _map.get(key); //re-order if (v != null) { if (_accessOrder) v.updateAccessTime(); return v.value; } return null; } public boolean containsKey(Object key) { tryExpunge(); return containsKeyWithoutExpunge(key); } /** Tests if the given key exists without trying to expunge for more * memory. */ public boolean containsKeyWithoutExpunge(Object key) { return _map.containsKey(key); } public boolean containsValue(Object value) { tryExpunge(); for (Value<V> v: _map.values()) { if (Objects.equals(value, v.value)) return true; } return false; } public V put(K key, V value) { final Value<V> v = _map.put(key, new Value<V>(value)); //Bug ZK-1841: current desktopCache size should also check if equal to max desktop per session size tryExpunge(); return v != null ? v.value: null; } public void putAll(java.util.Map<? extends K,? extends V> map) { for (Map.Entry<? extends K, ? extends V> me: map.entrySet()) put(me.getKey(), me.getValue()); } /** It wraps what is stored in _map, such that the caller * won't know the value is wrapped with Value. */ private static class Entry<K,V> implements Map.Entry<K, V> { final Map.Entry<K,Value<V>> _me; @SuppressWarnings("unchecked") private Entry(Map.Entry me) { _me = me; } public K getKey() { return _me.getKey(); } public V getValue() { return _me.getValue().value; } public V setValue(V o) { //we don't re-order it to avoid co-modification error final Value<V> v = _me.getValue(); final V old = v.value; v.value = o; return old; } //-- Object --// public int hashCode() { return _me.hashCode(); } public boolean equals(Object o) { if (this == o) return true; return (o instanceof Entry) && _me.equals(((Entry)o)._me); } } /** Abstract iterator. */ private static class KeyIter implements Iterator { private Iterator _it; private KeyIter(Iterator it) { _it = it; } public boolean hasNext() { return _it.hasNext(); } public void remove() { _it.remove(); //remove from map } public Object next() { return _it.next(); } } /** Entry iterator. Don't call expunge to avoid co-modified exception. */ private static class EntryIter extends KeyIter { private EntryIter(Iterator it) { super(it); } public Object next() { return new Entry((Map.Entry)super.next()); } } /** The entry set. */ private class EntrySet extends AbstractSet { private EntrySet() { } //-- Set --// public Iterator iterator() { tryExpunge(); return new EntryIter(_map.entrySet().iterator()); } public boolean contains(Object o) { return (o instanceof Map.Entry) && CacheMap.this.containsKey(((Map.Entry)o).getKey()); } public boolean remove(Object o) { return (o instanceof Map.Entry) && CacheMap.this.remove(((Map.Entry)o).getKey()) != null; } public int size() { return CacheMap.this.size(); } public void clear() { CacheMap.this.clear(); } } @SuppressWarnings("unchecked") public Set<Map.Entry<K,V>> entrySet() { tryExpunge(); return new EntrySet(); } /** The entry set. */ private class KeySet extends AbstractSet { private KeySet() { } //-- Set --// public Iterator iterator() { tryExpunge(); return new KeyIter(_map.keySet().iterator()); } public boolean contains(Object o) { return CacheMap.this.containsKey(o); } public boolean remove(Object o) { return CacheMap.this.remove(o) != null; } public int size() { return CacheMap.this.size(); } public void clear() { CacheMap.this.clear(); } } @SuppressWarnings("unchecked") public Set<K> keySet() { tryExpunge(); return new KeySet(); } /** Value iterator. Don't call expunge to avoid co-modified exception. */ private static class ValueIter extends KeyIter { private ValueIter(Iterator it) { super(it); } public Object next() { return ((Value)super.next()).value; } } /** The value collection. */ private class Values extends AbstractCollection { public Iterator iterator() { return new ValueIter(_map.values().iterator()); } public int size() { return CacheMap.this.size(); } public boolean contains(Object o) { return CacheMap.this.containsValue(o); } public void clear() { CacheMap.this.clear(); } } @SuppressWarnings("unchecked") public Collection<V> values() { tryExpunge(); return new Values(); } //-- Object --// public int hashCode() { tryExpunge(); return _map.hashCode(); } public boolean equals(Object o) { tryExpunge(); return o == this || ((o instanceof CacheMap) && _map.equals(((CacheMap)o)._map)) || ((o instanceof Map) && _map.equals(o)); } public String toString() { tryExpunge(); final StringBuffer sb = new StringBuffer(128).append('{'); if (!_map.isEmpty()) { for (final Iterator it = _map.entrySet().iterator();;) { final Map.Entry me = (Map.Entry)it.next(); sb.append(me.getKey()).append('=') .append(Objects.toString(((Value)me.getValue()).value)); if (it.hasNext()) sb.append(", "); else break; //done } } return sb.append('}').toString(); } //Cloneable// @SuppressWarnings("unchecked") public Object clone() { final CacheMap<K,V> clone; try { clone = (CacheMap<K,V>)super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(); } clone._inExpunge = false; clone._map = new LinkedHashMap<K, Value<V>>(16, 0.75f, _accessOrder); for (Map.Entry<K, Value<V>> me: _map.entrySet()) { clone._map.put(me.getKey(), (Value<V>)me.getValue().clone()); } clone.init(); return clone; } //Serializable// //NOTE: they must be declared as private private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); init(); } }