package com.plexobject.rbac.cache; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; import org.apache.log4j.Logger; import com.plexobject.rbac.domain.Pair; import com.plexobject.rbac.utils.TimeUtils; /** * CacheMap - provides lightweight caching based on LRU size and timeout and * asynchronous reloads. * */ public class CachedMap<K, V> implements Map<K, V>, CacheFlushable { private static final Logger LOGGER = Logger.getLogger(CachedMap.class); private final static int MAX_THREADS = 2; // for all cache items across VM final static int MAX_ITEMS = 1000; // max size private final static int EXPIRES_IN_SECS = 0; // indefinite private final static ExecutorService executorService = Executors .newFixedThreadPool(MAX_THREADS); class FixedSizeLruLinkedTreeMap<KK, VV> extends LinkedHashMap<KK, VV> { private static final long serialVersionUID = 1L; private final int maxSize; public FixedSizeLruLinkedTreeMap(int initialCapacity, float loadFactor, int maxSize) { super(initialCapacity, loadFactor, true); this.maxSize = maxSize; } @Override protected boolean removeEldestEntry(java.util.Map.Entry<KK, VV> eldest) { return super.size() > maxSize; } } final long expiresInSecs; private final CacheLoader<K, V> cacheLoader; private final Map<K, Pair<Long, V>> map; private final Map<Object, ReentrantLock> locks; public CachedMap() { this(EXPIRES_IN_SECS, MAX_ITEMS, null); } public CachedMap(final long expiresInSecs, final int maxSize) { this(expiresInSecs, maxSize, null); } public CachedMap(final long expiresInSecs, final int maxSize, final CacheLoader<K, V> cacheLoader) { this.expiresInSecs = expiresInSecs; this.map = Collections .synchronizedMap(new FixedSizeLruLinkedTreeMap<K, Pair<Long, V>>( maxSize / 10, 0.75f, maxSize)); this.cacheLoader = cacheLoader; this.locks = new TreeMap<Object, ReentrantLock>(); CacheFlusher.getInstance().addCacheFlushable(this); } @Override public int size() { return map.size(); } @Override public boolean isEmpty() { return map.isEmpty(); } @Override public boolean containsKey(Object key) { return map.containsKey(key); } @Override public boolean containsValue(Object value) { for (Map.Entry<K, V> e : entrySet()) { if (value == e.getValue() || (value != null && value.equals(e.getValue()))) { return true; } } return false; } @Override public V put(K key, V value) { Pair<Long, V> previous = map.put(key, new Pair<Long, V>(TimeUtils .getCurrentTimeMillis(), value)); return previous != null ? previous.getSecond() : null; } @Override public V remove(Object key) { Pair<Long, V> previous = map.remove(key); return previous != null ? previous.getSecond() : null; } @Override public void putAll(Map<? extends K, ? extends V> m) { for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { put(e.getKey(), e.getValue()); } } @Override public void clear() { map.clear(); } @Override public Set<K> keySet() { return map.keySet(); } @Override public Collection<V> values() { List<V> list = new ArrayList<V>(); for (Pair<Long, V> e : map.values()) { list.add(e.getSecond()); } return list; } @Override public Set<Map.Entry<K, V>> entrySet() { Set<Map.Entry<K, V>> set = new HashSet<Map.Entry<K, V>>(); for (final Map.Entry<K, Pair<Long, V>> e : map.entrySet()) { set.add(new Map.Entry<K, V>() { public K getKey() { return e.getKey(); } public V getValue() { return e.getValue().getSecond(); } public V setValue(V value) { Pair<Long, V> pair = e.getValue(); pair.setSecond(value); return pair.getSecond(); } }); } return set; } /** * This method is simple get without any loading behavior */ @SuppressWarnings("unchecked") @Override public V get(Object key) { Pair<Long, V> pair = this.map.get(key); if (pair == null) { if (cacheLoader != null) { load((K) key, true, false); pair = this.map.get(key); } else { return null; } } if (expiresInSecs > 0 && TimeUtils.getCurrentTimeMillis() - pair.getFirst() > expiresInSecs * 1000) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("expiring " + key); } map.remove(key); if (cacheLoader != null) { load((K) key, false, false); } } return pair.getSecond(); } private void load(final K key, final boolean synchronizeAccess, final boolean lockAccess) { ReentrantLock lock = null; try { synchronized (this) { if (lockAccess) { lock = lock(key); } } if (synchronizeAccess) { put(key, cacheLoader.get(key)); if (LOGGER.isInfoEnabled()) { LOGGER.info("Loaded " + key); } } else { executorService.submit(new Callable<Object>() { public Object call() throws Exception { if (LOGGER.isInfoEnabled()) { LOGGER.info("Loading " + key + " asynchronously"); } load(key, true, false); return null; } }); } } finally { if (lock != null) { lock.unlock(); } } } private ReentrantLock lock(Object key) { ReentrantLock lock = null; synchronized (locks) { lock = (ReentrantLock) locks.get(key); if (lock == null) lock = new ReentrantLock(); } lock.lock(); return lock; } @Override public void flushCache() { map.clear(); } @Override public int cacheSize() { return map.size(); } /** * @see java.lang.Object#equals(Object) */ @SuppressWarnings("unchecked") @Override public boolean equals(Object object) { if (!(object instanceof CachedMap)) { return false; } CachedMap<K, V> rhs = (CachedMap<K, V>) object; return new EqualsBuilder().append(this.map, rhs.map).isEquals(); } /** * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return new HashCodeBuilder(786529047, 1924536713).append(this.map) .toHashCode(); } /** * @see java.lang.Object#toString() */ @Override public String toString() { final StringBuilder sb = new StringBuilder(); for (Map.Entry<K, V> e : entrySet()) { sb.append(e.getKey() + "=" + e.getValue() + ";"); } return sb.toString(); } }