/****************************************************************************** * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for * the specific language governing rights and limitations under the License. * * The Original Code is: Jsoda * The Initial Developer of the Original Code is: William Wong (williamw520@gmail.com) * Portions created by William Wong are Copyright (C) 2012 William Wong, All Rights Reserved. * ******************************************************************************/ package wwutil.model; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import java.io.Serializable; /** * Simple cache service for single process JVM. Thread-safe. */ public class MemCacheableSimple implements MemCacheable { private Map<String, CacheEntry> lruCache; private AtomicInteger hits = new AtomicInteger(); private AtomicInteger misses = new AtomicInteger(); private int defaultExpirationSec = 0; private Loadable objectLoader; /** * Create a LRU cache. * @param maxEntries the maximum entries in the cache. Oldest entries will be removed when capacity exceeded. */ public MemCacheableSimple(int maxEntries) { this.lruCache = Collections.synchronizedMap(new LruCache<String, CacheEntry>(maxEntries)); } /** * Create a LRU cache. * @param maxEntries the maximum entries in the cache. Oldest entries will be removed when capacity exceeded. * @param objectLoader the callback interface to load object if it can't be found in the cache. * @param defaultExpirationSec the expiration for cached objects when loading via objectLoader. */ public MemCacheableSimple(int maxEntries, Loadable objectLoader, int defaultExpirationSec) { this.lruCache = Collections.synchronizedMap(new LruCache<String, CacheEntry>(maxEntries)); this.objectLoader = objectLoader; this.defaultExpirationSec = defaultExpirationSec; } /** * Get an object from the cache. If it doesn't exist, load it via the objectLoader. If objectLoader is not set, return null. * @param key Unique key of the object. */ public Serializable get(String key) { Serializable obj = getFromCache(key); if (obj == null && objectLoader != null) { obj = objectLoader.load(key); put(key, defaultExpirationSec, obj); } return obj; } @SuppressWarnings("unchecked") private Serializable getFromCache(String key) { CacheEntry<Serializable> entry = (CacheEntry<Serializable>)lruCache.get(key); if (entry == null) { misses.incrementAndGet(); return null; } if (entry.hasExpired()) { delete(key); misses.incrementAndGet(); return null; } hits.incrementAndGet(); return entry.obj; } /** * Put an object into the cache. * @param key Unique key of the object. * @param expireInSeconds time to let object stay in cache before eviction. * @param obj Object to cache. */ public void put(String key, int expireInSeconds, Serializable obj) { lruCache.put(key, new CacheEntry<Serializable>(expireInSeconds, obj)); } /** * Remove an object from the cache. * @param key Unique key of the object. */ public void delete(String key) { lruCache.remove(key); } /** * Clear all objects in cache. */ public void clearAll() { resetStats(); lruCache.clear(); } public int size() { return lruCache.size(); } public void shutdown() { clearAll(); } /** * Reset caching statistics. */ public void resetStats() { hits.set(0); misses.set(0); } /** * Get the number of cache hits. */ public int getHits() { return hits.intValue(); } /** * Get the number cache misses. */ public int getMisses() { return misses.intValue(); } /** * Dump caching statistics. */ public String dumpStats() { int hits = getHits(); int misses = getMisses(); int total = hits + misses; int total2 = total == 0 ? 1 : total; return "total: " + total + " hits: " + hits + " " + (hits*100/total2) + "% misses: " + misses + " " + (misses*100/total2) + "%"; } /** * Interface for objectLoader */ public static interface Loadable { public Serializable load(String key); } private static class CacheEntry<B> { long expirationMS; B obj; CacheEntry(int expireInSeconds, B obj) { this.expirationMS = expireInSeconds == 0 ? 0 : System.currentTimeMillis() + expireInSeconds*1000L; this.obj = obj; } boolean hasExpired() { return expirationMS != 0 && (expirationMS - System.currentTimeMillis()) < 0; } } private static class LruCache<A, B> extends LinkedHashMap<A, B> { private final int maxEntries; public LruCache(final int maxEntries) { super(maxEntries + 1, 1.0f, true); this.maxEntries = maxEntries; } @Override protected boolean removeEldestEntry(final Map.Entry<A, B> eldest) { return super.size() > maxEntries; } } }