/** * Copyright (C) 2010 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.commons.collections.Transformer; import org.apache.commons.collections.iterators.TransformIterator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; /** * Memory cache implementation. * * @noinspection SimplifiableIfStatement */ public class MemoryCacheImpl implements Cache { private int maxSize; private Map<CacheKey, CacheEntry> keyToEntryMap = new HashMap<CacheKey, CacheEntry>(); private CacheLinkedList linkedList = new CacheLinkedList(); private int currentSize; public MemoryCacheImpl(int maxSize) { this.maxSize = maxSize; } public synchronized void add(CacheKey key, Object validity, Object cacheable) { if (key == null || validity == null || maxSize == 0) return; CacheEntry entry = keyToEntryMap.get(key); if (entry == null) { // No existing entry found if (currentSize == maxSize) { // Cache is full, try to evict one entry, starting from the end tryEvictLast(); // If somehow we couldn't manage to evict an entry (e.g. all were locked), the cache will grow over // maxsize. } currentSize++; entry = new CacheEntry(); entry.key = key; entry.validity = validity; entry.cacheable = cacheable; keyToEntryMap.put(key, entry); entry.listEntry = linkedList.addFirst(entry); // Notify object notifyAdded(entry.cacheable); } else { // Update validity and move to the front entry.validity = validity; entry.cacheable = cacheable; linkedList.remove(entry.listEntry); entry.listEntry = linkedList.addFirst(entry); } } private boolean tryEvictLast() { for (final Iterator<CacheEntry> i = linkedList.reverseIterator(); i.hasNext();) { final CacheEntry entryToTry = i.next(); if (tryEvict(entryToTry)) { return true; } } return false; } private boolean tryEvict(CacheEntry entry) { assert keyToEntryMap.containsKey(entry.key); // Obtain lock if possible final Lock lock; final boolean canEvict; if (entry.cacheable instanceof Cacheable) { lock = ((Cacheable) entry.cacheable).getEvictionLock(); canEvict = lock == null || lock.tryLock(); } else { lock = null; canEvict = true; } // Only remove object if we are allowed to if (canEvict) { try { remove(entry.key, true, false); } finally { // Release lock if we got one if (lock != null) lock.unlock(); } } return canEvict; } public synchronized void remove(CacheKey key) { remove(key, false, true); // don't consider this an eviction } private synchronized void remove(CacheKey key, boolean isEvict, boolean isRemove) { final CacheEntry entry = keyToEntryMap.get(key); if (entry != null) { keyToEntryMap.remove(key); linkedList.remove(entry.listEntry); currentSize--; // Notify object if (isEvict) { notifyEvicted(entry.cacheable); } else if (isRemove) { notifyRemoved(entry.cacheable); } } } private void notifyAdded(Object object) { if (object instanceof Cacheable) { ((Cacheable) object).added(); } } private void notifyRemoved(Object object) { if (object instanceof Cacheable) { ((Cacheable) object).removed(); } } private void notifyEvicted(Object object) { if (object instanceof Cacheable) { ((Cacheable) object).evicted(); } } public synchronized int removeAll() { final int previousSize = currentSize; // Notify objects for (final Iterator i = iterateCacheObjects(); i.hasNext();) { notifyRemoved(i.next()); } keyToEntryMap = new HashMap<CacheKey, CacheEntry>(); linkedList = new CacheLinkedList(); currentSize = 0; return previousSize; } // Find valid entry and move it to the first position public Object findValid(CacheKey key, Object validity) { return getValid(key, validity, false); } // Like findValid but remove from the cache (with removed() notification) public Object takeValid(CacheKey key, Object validity) { return getValid(key, validity, true); } private synchronized Object getValid(CacheKey key, Object validity, boolean remove) { final CacheEntry entry = keyToEntryMap.get(key); if (entry != null && lowerOrEqual(validity, entry.validity)) { if (remove) { // Remove and notify remove(key, false, true); } else if (linkedList.getFirst() != entry) { // Place in first position and return linkedList.remove(entry.listEntry); entry.listEntry = linkedList.addFirst(entry); } return entry.cacheable; } else { // Not latest validity return null; } } public CacheEntry findAny(CacheKey key) { // Don't update statistics here return keyToEntryMap.get(key); } public int getCurrentSize() { return currentSize; } public int getMaxSize() { return maxSize; } public synchronized void setMaxSize(int maxSize) { if (maxSize != this.maxSize) { // Decrease size if necessary // Try to evict entries, but don't try more times than the number of elements initially in the cache int tryCount = 0; final int maxTries = currentSize; while(currentSize > maxSize && tryCount < maxTries) { tryEvictLast(); tryCount++; } this.maxSize = maxSize; } } public Iterator<CacheKey> iterateCacheKeys() { return new TransformIterator(linkedList.iterator(), new Transformer() { public Object transform(Object o) { return ((CacheEntry) o).key; } }); } public Iterator<Object> iterateCacheObjects() { return new TransformIterator(linkedList.iterator(), new Transformer() { public Object transform(Object o) { return ((CacheEntry) o).cacheable; } }); } private boolean lowerOrEqual(Object left, Object right) { if (left instanceof List && right instanceof List) { List leftList = (List) left; List rightList = (List) right; if (leftList.size() != rightList.size()) return false; for (Iterator leftIterator = leftList.iterator(), rightIterator = rightList.iterator();leftIterator.hasNext();) { Object leftObject = leftIterator.next(); Object rightObject = rightIterator.next(); if (!lowerOrEqual(leftObject, rightObject)) return false; } return true; } else if (left instanceof Long && right instanceof Long) { return (Long) left <= (Long) right; } else { return false; } } }