/* ESXX - The friendly ECMAscript/XML Application Server Copyright (C) 2007-2015 Martin Blom <martin@blom.org> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.esxx.cache; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.Map; public class LRUCache<K, V> { public interface ValueFactory<K, V> { public V create(K key, long age) throws Exception; } public interface EntryFilter<K, V> { public boolean isStale(K key, V value, long created); } public interface LRUListener<K, V> { public void entryAdded(K key, V value); public void entryRemoved(K key, V value); } public LRUCache(int max_entries, long max_age) { map = new LRUMap(); maxEntries = max_entries; maxAge = max_age; } /** Returns a value from the cache. * * @param key The key * * @result The value from the cache or null if not found. */ public V get(K key) { LRUEntry entry; synchronized (map) { // (map.get() will put the entry last in the linked list!) entry = map.get(key); } if (entry != null) { synchronized (entry) { if (!entry.isDeleted()) { entry.updateExpires(System.currentTimeMillis()); return entry.value; } } } return null; } /** Adds a value if and only if there was no previous value in the * cache. * * @param key The key * @param value The value * @param age The maximum number of milliseconds to keep the value * in the cache. If 0, the cache's global maximum age * is used. * * @result The value in the cache after this call (existing or new). */ public V add(K key, final V value, long age) { try { return add(key, new ValueFactory<K, V>() { public V create(K key, long age) { return value; } }, age); } catch (Exception ex) { throw new IllegalStateException("add() should not have thrown!", ex); } } /** Creates and adds a value if and only if there was no previous * value in the cache. * * @param key The key * @param factory A ValueFactory * @param age The maximum number of milliseconds to keep the value * in the cache. If 0, the cache's global maximum age * is used. * * @result The value in the cache after this call (existing or new). */ public V add(K key, ValueFactory<K, V> factory, long age) throws Exception { if (age == 0) { age = maxAge; } while (true) { // Repeat until successful LRUEntry entry = getEntry(key); synchronized (entry) { if (!entry.isDeleted()) { long now = System.currentTimeMillis(); if (entry.value == null) { entry.maxAge = age; entry.created = now; entry.updateExpires(now); entry.value = factory.create(key, entry.expires); fireAddedEvent(key, entry.value); } else { entry.updateExpires(now); } return entry.value; } } Thread.yield(); } } /** Unconditionally inserts a value into the cache. * * @param key The key * @param value The value * @param age The maximum number of milliseconds to keep the value * in the cache. If 0, the cache's global maximum age * is used. * * @result The value that was replaced, or null if there were no * previous value in the cache. */ public V set(K key, final V value, long age) { try { return set(key, new ValueFactory<K, V>() { public V create(K key, long age) { return value; } }, age); } catch (Exception ex) { throw new IllegalStateException("set() should not have thrown!", ex); } } /** Unconditionally inserts a value into the cache. * * @param key The key * @param factory A ValueFactory * @param age The maximum number of milliseconds to keep the value * in the cache. If 0, the cache's global maximum age * is used. * * @result The value that was replaced, or null if there were no * previous value in the cache. */ public V set(K key, ValueFactory<K, V> factory, long age) throws Exception { if (age == 0) { age = maxAge; } while (true) { // Repeat until successful LRUEntry entry = getEntry(key); synchronized (entry) { if (!entry.isDeleted()) { V old_value = entry.value; if (old_value != null) { fireRemovedEvent(key, old_value); } long now = System.currentTimeMillis(); entry.maxAge = age; entry.created = now; entry.updateExpires(now); entry.value = factory.create(key, entry.expires); fireAddedEvent(key, entry.value); return old_value; } } Thread.yield(); } } /** Replaces a value if and only if it already exists. * * @param key The key * @param value The value * @param age The maximum number of milliseconds to keep the value * in the cache. If 0, the cache's global maximum age * is used. * * @result The value that was replaced, or null if there were no * previous value in the cache. */ public V replace(K key, final V value, long age) { try { return replace(key, new ValueFactory<K, V>() { public V create(K key, long age) { return value; } }, age); } catch (Exception ex) { throw new IllegalStateException("add() should not have thrown!", ex); } } /** Creates and replaces a value if and only if it already exists. * * @param key The key * @param factory A ValueFactory * @param age The maximum number of milliseconds to keep the value * in the cache. If 0, the cache's global maximum age * is used. * * @result The value that was replaced, or null if there were no * previous value in the cache. */ public V replace(K key, ValueFactory<K, V> factory, long age) throws Exception { if (age == 0) { age = maxAge; } while (true) { // Repeat until successful LRUEntry entry = getEntry(key); synchronized (entry) { if (!entry.isDeleted()) { V old_value = entry.value; long now = System.currentTimeMillis(); if (old_value != null) { fireRemovedEvent(key, old_value); entry.maxAge = age; entry.created = now; entry.updateExpires(now); entry.value = factory.create(key, entry.expires); fireAddedEvent(key, entry.value); } else { entry.updateExpires(now); } return old_value; } } Thread.yield(); } } /** Removes and returns a value from the cache. * * @param key The key * * @result The old value, or null. */ public V remove(K key) { LRUEntry entry = getEntry(key); V old_value; synchronized (entry) { old_value = entry.value; if (old_value != null) { fireRemovedEvent(key, old_value); // Mark entry as deleted entry.markAsDeleted(); } } synchronized (map) { synchronized (entry) { // NOTE: Lock order: first map, then entry if (entry.isDeleted()) { // Still marked for deletion map.remove(key); } } } return old_value; } /** Removes all entries from the cache. * * This operation locks the whole map! */ public void clear() { synchronized (map) { for (Map.Entry<K, LRUEntry> e : map.entrySet()) { LRUEntry entry = e.getValue(); synchronized (entry) { // NOTE: Lock order: first map, then entry if (entry.value != null) { fireRemovedEvent(e.getKey(), entry.value); } entry.markAsDeleted(); } } map.clear(); } } /** Iterates all entries and removes all for which * EntryFilter.isStale() returns true, or has already expired. * * @param filter An EntryFilter. May be null. */ public void filterEntries(EntryFilter<K, V> filter) { LinkedList<Map.Entry<K, LRUEntry>> entries; synchronized (map) { entries = new LinkedList<Map.Entry<K, LRUEntry>>(map.entrySet()); } long now = System.currentTimeMillis(); for (Map.Entry<K, LRUEntry> e : entries) { LRUEntry entry = e.getValue(); boolean remove = false; synchronized (entry) { if (!entry.isDeleted() && entry.value != null && (entry.expires < now || filter != null && filter.isStale(e.getKey(), entry.value, entry.created))) { fireRemovedEvent(e.getKey(), entry.value); entry.markAsDeleted(); remove = true; } } if (remove) { synchronized (map) { synchronized (entry) { // NOTE: Lock order: first map, then entry if (entry.isDeleted()) { // Still marked for deletion map.remove(e.getKey()); } } } } } } public void addListener(LRUListener<K, V> l) { synchronized (entryListeners) { entryListeners.add(l); } } public void removeListener(LRUListener<K, V> l) { synchronized (entryListeners) { entryListeners.remove(l); } } public void fireAddedEvent(K key, V value) { synchronized (entryListeners) { for (LRUListener<K, V> l : entryListeners) { l.entryAdded(key, value); } } } public void fireRemovedEvent(K key, V value) { synchronized (entryListeners) { for (LRUListener<K, V> l : entryListeners) { l.entryRemoved(key, value); } } } /** Return an LRUEntry, creating a newly inserted one if not already present. * * @param key The key. * * @return An LRUEntry */ private LRUEntry getEntry(K key) { synchronized (map) { LRUEntry entry = map.get(key); if (entry == null) { entry = new LRUEntry(); map.put(key, entry); } return entry; } } private class LRUMap extends LinkedHashMap<K, LRUEntry> { private static final long serialVersionUID = 8027701661709058455L; public LRUMap() { super (128, 0.75f, true); } private boolean isFull(LRUEntry eldest, long now) { return (size() > maxEntries || (eldest.expires != 0 && eldest.expires < now)); } @Override public boolean removeEldestEntry(Map.Entry<K, LRUEntry> eldest) { long now = System.currentTimeMillis(); LRUEntry entry = eldest.getValue(); synchronized (entry) { // NOTE: Lock order: first map, then entry (map locked by caller) if (!isFull(entry, now)) { entry = null; } } if (entry != null) { Iterator<Map.Entry<K, LRUEntry>> i = entrySet().iterator(); while (i.hasNext()) { Map.Entry<K, LRUEntry> e = i.next(); entry = e.getValue(); synchronized (entry) { // NOTE: Lock order: first map, then entry (map locked by caller) if (!isFull(entry, now)) { break; } if (entry.value != null) { fireRemovedEvent(e.getKey(), entry.value); } entry.markAsDeleted(); i.remove(); } } } // Tell implementation not to auto-modify the hash table return false; } } class LRUEntry { public void markAsDeleted() { maxAge = Long.MIN_VALUE; expires = Long.MIN_VALUE; created = Long.MIN_VALUE; value = null; } public boolean isDeleted() { return expires == Long.MIN_VALUE; } public void updateExpires(long now) { expires = maxAge == 0 ? Long.MAX_VALUE : now + maxAge; } long expires; long created; long maxAge; V value; } private final LRUMap map; private int maxEntries; private long maxAge; private final LinkedList<LRUListener<K, V>> entryListeners = new LinkedList<LRUListener<K, V>>(); static final long serialVersionUID = 8565024717836226408L; }