/**
* Copyright (C) 2009 Orbeon, Inc.
*
* This program 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 program 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.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.cache;
import org.apache.log4j.Logger;
import org.orbeon.oxf.util.LoggerFactory;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.*;
/**
* The cache contains two entry caches, one with strong references, the other
* one with soft references. The latter should be kept by the VM as long as
* possible, according to memory usage. The strong cache has a limited size,
* but the soft cache can grow indefinitely if strong references are kept on the
* objects from outside the cache.
*
* The cache can contain more elements than the maximum size passed during
* construction, since elements can be moved to the soft cache and stay there
* if memory conditions allow it.
*/
public class SoftCacheImpl {
static private Logger logger = LoggerFactory.createLogger(SoftCacheImpl.class);
private int maximumSize;
private String[] keyNames;
private HashMap<String, Integer> keyNameToIndex;
/*
* Strong and soft caches.
*/
private EntryCache strongCache;
private EntryCache softCache;
/**
* Simple Entry cache.
*/
protected static class EntryCache {
private int keyNum;
private HashMap<Object, Key>[] keyMaps;
private TreeMap<Key, Object>[] maps;
public EntryCache(int keyNum) {
this.keyNum = keyNum;
keyMaps = new HashMap[keyNum];
maps = new TreeMap[keyNum];
for (int i = 0; i < keyNum; i++) {
keyMaps[i] = new HashMap<Object, Key>();
maps[i] = new TreeMap<Key, Object>(Key.getComparator());
}
}
public void put(Entry entry) {
Object[] keys = entry.getKeys();
for (int i = 0; i < keyNum; i++) {
Key key = new Key(keys[i]);
keyMaps[i].put(keys[i], key);
maps[i].put(key, entry);
}
}
public Entry get(int keyIndex, Object key) {
return (Entry) maps[keyIndex].get(keyMaps[keyIndex].get(key));
}
public Entry remove(int keyIndex, Object key) {
return remove(get(keyIndex, key));
}
public Entry remove(Entry entry) {
if (entry == null) return null;
return remove(entry.getKeys());
}
public Entry remove(Object[] keys) {
Entry entry = null;
for (int i = 0; i < keyNum; i++) {
Entry e = (Entry) maps[i].remove(keyMaps[i].get(keys[i]));
if (entry != null && !entry.equals(e))
throw new IllegalStateException();
entry = e;
keyMaps[i].remove(keys[i]);
}
return entry;
}
public Entry removeOne() {
Entry entry = (Entry) maps[0].get(maps[0].firstKey());
return remove(entry);
}
public int flush() {
int count = maps[0].size();
for (int i = 0; i < keyNum; i++) {
keyMaps[i].clear();
maps[i].clear();
}
return count;
}
public boolean contains(Object[] keys) {
for (int i = 0; i < keys.length; i++) {
Entry entry = get(i, keys[i]);
if (entry != null && entry.getObject() != null)
return true;
}
return false;
}
public int size() {
return maps[0].size();
}
/**
* Return an iteration of all non-null objects (not entries) contained in the cache.
*/
public Iterator elements() {
return new ObjectIterator(maps[0].values().iterator());
}
public void applyOnKeys(Action action) {
for (Object o: keyMaps[0].keySet()) {
action.perform(o);
}
}
protected static class ObjectIterator implements Iterator {
public ObjectIterator(Iterator iterator) {
this.iterator = iterator;
}
public boolean hasNext() {
while (current == null || current.getObject() == null) {
if (!iterator.hasNext())
return false;
current = (Entry) iterator.next();
}
return true;
}
public Object next() {
if (!hasNext())
throw new NoSuchElementException();
Entry result = current;
current = null;
return result.getObject();
}
public void remove() {
throw new UnsupportedOperationException();
}
private Entry current;
private Iterator iterator;
}
public String dump() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < keyNum; i++) {
int j = 0;
for (Object key: maps[i].keySet()) {
sb.append("Key[" + j++ + "]="
+ ((Key) key).getKey()
+ ", element="
+ ((Entry) maps[i].get(key)).getObject()
+ "\n");
}
}
return sb.toString();
}
}
/**
* Key encapsulate a key object and defines a Comparator. This is
* used to implement LRU behavior.
*/
protected static class Key {
private static int globalOrder;
private static Comparator comparator = new Comparator();
public static Comparator getComparator() {
return comparator;
}
private Object key;
private int order;
private int hash;
/**
* Create a new Key.
*/
public Key(Object key) {
if (key == null)
throw new IllegalArgumentException("Key must not be null.");
this.key = key;
this.order = globalOrder++;
this.hash = key.hashCode();
}
public Object getKey() {
return key;
}
public int getOrder() {
return order;
}
/*
public boolean equals(Object obj) {
System.err.println("Key 1");
if (obj == this)
return true;
System.err.println("Key 2");
if (!(obj instanceof Key))
return false;
System.err.println("Key 3");
return key.equals(((Key)obj).key);
}
*/
public int hashCode() {
return hash;
}
protected static class Comparator implements java.util.Comparator<Key> {
public int compare(Key o1, Key o2) {
if (o1 == null || o2 == null) return -1;// what else?
int order1 = o1.getOrder();
int order2 = o2.getOrder();
if (order1 == order2)
return 0;
else if (order1 < order2)
return -1;
else
return 1;
}
}
}
public static interface Action {
public void perform(Object o);
}
/**
* Entry encapsulate an array of keys and an object.
*/
public static class Entry {
private Object[] keys;
private Object object;
public Entry(Entry entry) {
this(entry.getKeys(), entry.getObject());
}
public Entry(Object[] keys, Object object) {
this.keys = keys;
this.object = object;
}
public Object[] getKeys() {
return keys;
}
public Object getObject() {
return object;
}
}
private ReferenceQueue queue = new ReferenceQueue();
public abstract class Stats {
public abstract String getFormatString();
}
private class LocalStats extends Stats {
public int strongSize;
public int softSize;
public int movedToSoft;
public int movedToStrong;
public int collectedSoft;
public String getFormatString() {
return "(strongSize, softSize, movedToSoft, movedToStrong, collectedSoft)";
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("(").append(strongSize);
sb.append(", ").append(softSize);
sb.append(", ").append(movedToSoft);
sb.append(", ").append(movedToStrong);
sb.append(", ").append(collectedSoft).append(")");
return sb.toString();
}
}
private LocalStats stats = new LocalStats();
/**
* Return all stats.
*/
public Stats getStats() {
stats.strongSize = strongCache.size();
stats.softSize = softCache.size();
return stats;
}
protected void checkQueue() {
//System.out.println("In checkQueue");
SmartReference ref;
int i = 0;
while ((ref = (SmartReference) queue.poll()) != null) {
// System.out.println("checkQueue: " + ref.getKeys());
// System.out.println("Removing soft reference: " + ref.getKeys()[0].toString());
softCache.remove(ref.getKeys());
i++;
}
if (i > 0) {
stats.collectedSoft += i;
if(logger.isInfoEnabled())
logger.info("Removed soft entries: " + i
+ " (" + (100*i/(softCache.size() + i)) + "%)");
}
}
/**
* SmartReference extends SoftReference and encapsulates an array
* of keys, needed to remove entries from the soft cache when soft
* entries are freed by the VM.
*/
protected static class SmartReference extends SoftReference {
private Object[] keys;
public SmartReference(Object referent, ReferenceQueue queue, Object[] keys) {
super(referent, queue);
this.keys = keys;
}
public Object[] getKeys() {
return keys;
}
}
/**
* SoftEntry is an entry that encapsulates its object in a SoftReference.
*/
protected static class SoftEntry extends Entry {
public SoftEntry(ReferenceQueue queue, Entry entry) {
this(queue, entry.getKeys(), entry.getObject());
}
public SoftEntry(ReferenceQueue queue, Object[] keys, Object object) {
super(keys, new SmartReference(object, queue, keys));
}
public Object[] getKeys() {
return super.getKeys();
}
public Object getObject() {
return ((SoftReference) super.getObject()).get();
}
}
/**
* Create a cache of the specified maximum strong cache size.
*/
public SoftCacheImpl(int maximumSize) {
this(maximumSize, new String[]{""});
}
/**
* Create a cache of the specified maximum strong cache size.
* Elements can be indexed using keyNum keys.
*/
public SoftCacheImpl(int maximumSize, String[] keyNames) {
this.maximumSize = maximumSize;
this.keyNames = keyNames;
// Map key names to key indices
keyNameToIndex = new HashMap<String, Integer>();
for (int i = 0; i < keyNames.length; i++)
keyNameToIndex.put(keyNames[i], i);
// Create entry caches
strongCache = new EntryCache(keyNames.length);
softCache = new EntryCache(keyNames.length);
}
/**
* Set the strong cache's maximum size.
*/
public void setMaximumSize(int maximumSize) {
this.maximumSize = maximumSize;
}
/**
* Put an object in the cache.
* The object can be accessed using the given key.
*/
public synchronized void put(Object key, Object value) {
put(new Object[]{key}, value);
}
/**
* Put an object in the cache.
* The object can be accessed using any of the given keys and a key index.
*/
public synchronized void put(Object[] keys, Object value) {
checkQueue();
if(logger.isDebugEnabled())
logger.debug("put(Object[] keys, Object value)" + arrayToString(keys) + ", " + value);
// Check params
if (keys.length != keyNames.length)
throw new IllegalArgumentException("Bad number of keys, should be " + keyNames.length);
//if (maximumSize == 0) return;
if (strongCache.contains(keys) || softCache.contains(keys))
throw new IllegalArgumentException("Object already in cache for keys: " + arrayToString(keys));
// Check for cache limit and move one element to soft cache if needed
checkLimit();
// Add element
if (maximumSize == 0)
softCache.put(new SoftEntry(queue, keys, value));
else
strongCache.put(new Entry(keys, value));
//Logger.log("CACHE", logger.debug_L2, "dump after put: " + dump());
}
/**
* Check the size limit of the strong cache and move as much as
* needed to the soft cache. Typically called before adding
* entries to the strong cache.
*/
protected void checkLimit() {
if (maximumSize == 0)
return;
while (strongCache.size() >= maximumSize) {
stats.movedToSoft++;
softCache.put(new SoftEntry(queue, strongCache.removeOne()));
}
}
/**
* Remove the specified object.
*/
public synchronized Object remove(Object key) {
return remove("", key);
}
/**
* Remove the specified object.
*/
public synchronized Object remove(String keyName, Object key) {
//if (maximumSize == 0) return null;
checkQueue();
try {
if (logger.isDebugEnabled())
logger.debug("remove(String keyName, Object key), keyName = " + keyName + ", key = " + key);
int keyIndex = getKeyIndex(keyName);
Entry entry = strongCache.remove(keyIndex, key);
if (entry != null)
return entry.getObject();
entry = softCache.remove(keyIndex, key);
if (entry != null)
return entry.getObject();
throw new IllegalArgumentException("Object not found for: keyName = " + keyName + ", key = " + key);
} finally {
// avernet 03/01/21
checkQueue();
}
}
/**
* Refresh the given entry.
* The entry will be put at the beginning of the cache.
*/
public synchronized Object refresh(Object key) {
return refresh("", key);
}
public synchronized Object refresh(String keyName, Object key) {
//if (maximumSize == 0) return null;
checkQueue();
int keyIndex = getKeyIndex(keyName);
Entry entry = strongCache.remove(keyIndex, key);
if (entry == null) {
stats.movedToStrong++;
entry = softCache.remove(keyIndex, key);
}
if (entry == null || entry.getObject() == null)
throw new IllegalArgumentException("Object not found for: keyName = " + keyName + ", key = " + key);
checkLimit();
if (maximumSize == 0)
softCache.put(new SoftEntry(queue, entry));
else
strongCache.put(new Entry(entry));
return entry.getObject();
}
/**
* Get the specified object.
*/
public synchronized Object get(Object key) {
return get("", key);
}
/**
* Get the specified object.
*/
public synchronized Object get(String keyName, Object key) {
//if (maximumSize == 0) return null;
checkQueue();
if(logger.isDebugEnabled())
logger.debug("get(String keyName, Object key) " + keyName + ", " + key);
Entry entry = getEntry(keyName, key);
if (entry != null && entry.getObject() != null) {
refresh(keyName, key);
return entry.getObject();
} else
return null;
}
/**
* Replace the object associated with the given key by the given value.
*/
public synchronized Object replace(Object key, Object value) {
return replace(new Object[]{key}, value);
}
/**
* Replace the object associated with the given keys by the given value.
*/
public synchronized Object replace(Object[] keys, Object value) {
//if (maximumSize == 0) return null;
Object oldValue = get(keyNames[0], keys[0]);
if (oldValue != null)
remove(keyNames[0], keys[0]);
put(keys, value);
return oldValue;
}
/**
* Flush the whole cache.
*
* WARNING: The objects are completely removed from the cache, even
* if they still have outside references on them.
*/
public synchronized int flush() {
int count = strongCache.flush();
return count + softCache.flush();
}
public synchronized void applyOnSoftCacheKeys(Action action) {
softCache.applyOnKeys(action);
}
/**
* Return an iteration of all elements in the cache in LRU order.
*
* NOTE: the cache should not be modified while the Iteration is read.
*/
public Iterator elements() {
return new SequenceIterator(strongCache.elements(), softCache.elements());
}
protected Entry getEntry(String keyName, Object key) {
//System.err.println("getEntry: " + keyName + ", " + key);
//System.err.println("getEntry: " + getKeyIndex(keyName));
//System.err.println("getEntry: " + strongMaps[getKeyIndex(keyName)]);
// Try the strong cache first, then the soft cache
int keyIndex = getKeyIndex(keyName);
Entry entry = strongCache.get(keyIndex, key);
if (entry != null)
return entry;
return softCache.get(keyIndex, key);
}
protected int getKeyIndex(String keyName) {
return keyNameToIndex.get(keyName);
}
protected String[] getKeyNames() {
return keyNames;
}
public synchronized String dump() {
StringBuilder sb = new StringBuilder();
if (true) {
for (Iterator it = elements(); it.hasNext();) {
Object o = it.next();
System.err.println(o);
sb.append("[");
sb.append(o);
sb.append("]");
}
} else {
sb.append("-- Strong Cache --------------------------------------\n");
sb.append(strongCache.dump());
sb.append("-- Soft Cache ----------------------------------------\n");
sb.append(softCache.dump());
sb.append("\n");
}
return sb.toString();
}
private static String arrayToString(Object[] array) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < array.length; i++)
sb.append("[element[").append(i).append("]=").append(array[i]).append("]");
return sb.toString();
}
private class SequenceIterator implements Iterator {
public SequenceIterator(Iterator iterators) {
this.iterators = iterators;
}
public SequenceIterator(Iterator[] array) {
this(Arrays.asList(array).iterator());
}
public SequenceIterator(Iterator e1, Iterator e2) {
this(new Iterator[]{e1, e2});
}
public boolean hasNext() {
while (current == null || !current.hasNext()) {
if (!iterators.hasNext())
return false;
current = (Iterator) iterators.next();
}
return true;
}
public Object next() {
if (!hasNext())
throw new NoSuchElementException();
return current.next();
}
public void remove() {
if (current == null)
throw new NoSuchElementException();
current.remove();
}
private Iterator iterators;
private Iterator current;
}
}