001package com.randomnoun.common;
002
003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
005 */
006
007import java.util.*;
008
009import org.apache.log4j.Logger;
010
011/**
012 * Most-recently used cache class. 
013 * 
014 * <p>You probably want to use something in the guava Collections library these days instead.
015 * 
016 * <p>This class can be used to implement serverside data caches. The constructor
017 * takes three parameters: maximum size, expiry time (in milliseconds) and a 
018 * 'RetrievalCallback' object (defined as an interface in this class). 
019 * 
020 * <p>As items are put into this object, they are timestamped; if a key is applied
021 * to retrieve an object that has 'expired', then this object will return null
022 * (if no callback has been supplied), or will execute the callback to refresh
023 * the value. A callback can (optionally) be supplied with every get() invocation
024 * so that this object knows precisely how to generate the requested object. (This
025 * works since it is usually computationally cheaper to generate the Callback object
026 * than to generate the result of the callback, which generally needs to fetch
027 * information from a database).
028 * 
029 * <p>There are two possible expiry mechanisms: either an item is expired from the
030 * time it is entered into the cache, or expired from the time it is last retrieved
031 * from the cache. The former makes sense for 'dynamic' data that may change over time
032 * (e.g. rowcounts); the latter makes sense for 'static' data that is just used to
033 * cache large objects that don't need to be in memory at all times 
034 * (e.g. message definitions). By default this object will operate as per 'static'
035 * rules; the other method can be selected by calling the setDynamicCaching() after
036 * construction.
037 *
038 * <p><b>Implementation notes</b>
039 *    
040 * <p>It may be possible to extend this class to use WeakReferences, so that it can
041 * shrink in size as memory constraints within the VM become increased. This is 
042 * unlikely to be useful in practice, however, since this is probably an indication
043 * of a memory leak or over-utilised server. It may make more sense to just reduce the
044 * maximum size in these cases. Performance tuning would be required to know 
045 * for sure.
046 * 
047 * <p>Also note that if an object exceeds it's expiryTime, it is *not* automatically
048 * removed from the cache. This would involve having an additional Timer thread
049 * running through these objects and scanning for expired objects, which is unlikely
050 * to be useful in practice. An additional method could be implemented to trigger these
051 * cleanups if this becomes necessary.
052 * 
053 * @author knoxg
054 * 
055 *
056 */
057
058// this probably isn't as efficient as it could be (it contains a mruList AND a 
059// HashMap of expiryTimes, which I'm sure is overkill. To do this properly
060// would involve extending Map.Entry to include a timestamp value, however,
061// which would require a fair amount of re-engineering. (would need to subclass 
062// Map.Entry, and use "this.putAll()" to insert subclasses into this instance. 
063// you'd still need a way of keeping a sorted list of timestamped entries anyway,
064// now that I think about it (to choose the next expiry candidate), so this may not 
065// be so bad after all. 
066public class MRUCache<K, V>
067    extends HashMap<K, V>
068{
069    
070    /** Generated serialVersionUID */
071    private static final long serialVersionUID = -6826049000932880050L;
072
073        /** Logger instance for this class */
074    public static Logger logger = Logger.getLogger(MRUCache.class);
075    
076    /** The callback interface. Caches should provide one of these classes to 
077     * perform any time-consuming tasks required to generate values in this cache.
078     * If the time taken to generate a value isn't time-consuming, then perhaps
079     * you shouldn't be using a cache to store them.
080     */
081    public static interface RetrievalCallback<K, V> {
082        
083        /** Dynamically generates the value of an entity in a cache
084         * 
085         * @param key The key used to reference the cached object
086         * 
087         * @return The newly generated (or updated) cached object
088         */ 
089        public V get(K key);
090    }
091    
092    /** If set to true, expires data from the time it was <i>entered</i> into the cache,
093     * rather than the time if was <i>fetched</i> from the cache. Suitable when we 
094     * wish to apply a strict expiry time for dynamically-updating data, e.g.
095     * rowcounts.
096     */
097    private boolean dynamicCaching = false;
098
099    /** An ordered list of the objects in this cache. Objects at position 0 on the list
100     *  have been the most recently accessed (i.e. objects will be expired from the
101     * end of this list). */
102    private LinkedList<K> mruList;
103
104    /** The maximum number of elements that this cache can hold. If &lt;= 0, then the
105     * size of the map is unlimited. */
106    private int cacheSize;
107
108    /** A callback which can be used to populate entries in the cache that are unknown,
109     *  or have expired. */
110    private RetrievalCallback<K, V> callbackInstance;
111
112    /** A Map whose keys are elements in this cache, and who's entries are the last time
113     *  that entry was 'touched' (i.e. added to or retrieved from the cache) */
114    private Map<K, Long> lastUpdatedTimes;
115
116    /** The amount of time an entry can remain valid, measured from when the element
117     *  is first added to the cache. If expiryTime is set to &lt;= 0, then items never expire. */
118    private int expiryTime;
119
120    /**
121     * Creates a new MRUCache object.
122     *
123     * @param cacheSize  The maximum number of elements that this cache can hold. A value &lt;=0 means 
124     *    that the size of this map is not limited.
125     * @param expiryTime The amount of time (in ms) an entry can remain valid, measured from when the element
126     *    is first added to the cache (or last retrieved if {@link #setDynamicCaching()} is 
127     *    in effect. A value &lt;=0 means that items do not have an
128     *    expiry period.
129     * @param callback   A callback which can be used to populate entries in the cache that are unknown,
130     *    or have expired. This parameter can be set to null <b>if and only if</b> the
131     *    two-parameter {@link #get(Object, RetrievalCallback)} method is used to retrieve
132     *    elements from the map.
133     */
134    public MRUCache(int cacheSize, int expiryTime, RetrievalCallback<K, V> callback)
135    {
136        mruList = new LinkedList<>();
137        lastUpdatedTimes = new HashMap<>();
138        this.cacheSize = cacheSize;
139        this.callbackInstance = callback;
140        this.expiryTime = expiryTime;
141    }
142
143    /**
144     * Retrieve an element from the cache. If a callback was defined in the cache
145     * constructor, then this will be used to refresh
146     * values in the cache if they are missing, or have expired.
147     *
148     * @param key key whose associated value is to be returned
149     *
150     * @return the value to which this map maps the specified key, or
151     *   null if the map contains no mapping for this key.
152     */
153    @SuppressWarnings("unchecked")
154        @Override
155    public synchronized V get(Object key)
156    {
157        return get((K) key, callbackInstance);
158    }
159
160    /**
161     * Retrieves an element from the cache. The supplied callback will be used to
162     * refresh the value in the cache if it is missing, or has expired. If the
163     * requested element is missing and the callback is set to null, then
164     * null is returned.
165     *
166     * @param key key whose associated value is to be returned
167     * @param callback a callback used to populate the cache if necessary
168     *
169     * @return the value to which this map maps the specified key, or
170     *   null if the map contains no mapping for this key.
171     */
172    public synchronized V get(K key, RetrievalCallback<K, V> callback)
173    {
174        long now = System.currentTimeMillis();
175
176        if (!this.containsKey(key))
177        {
178            if (callback == null) {
179                return null;
180            } else {
181                // cache is missing key; retrieve from callback and update access times
182                synchronized (this) {
183                    if (cacheSize > 0 && mruList.size() >= cacheSize) {
184                        this.remove(mruList.getLast());
185                        mruList.removeLast();
186                    }
187
188                    super.put(key, callback.get(key));
189                    lastUpdatedTimes.put(key, Long.valueOf(now));
190                    mruList.addFirst(key);
191                }
192            }
193            
194        } else {
195            
196            // cache contains key
197            if (expiryTime > 0) {
198                
199                // expire this element if it's old
200                Long lastUpdatedTime = (Long) lastUpdatedTimes.get(key);
201                if (lastUpdatedTime==null) {
202                        logger.warn("MRUCache contains key '" + key + "' with no lastUpdatedTime set");
203                }
204                if (lastUpdatedTime == null || (lastUpdatedTime.longValue() + expiryTime < now)) {
205                    synchronized (this) {
206                        if (callback == null) {
207                            // no callback, we just remove it
208                            super.remove(key);
209                            lastUpdatedTimes.remove(key);
210                            mruList.remove(key);
211
212                            return null;
213                        } else {
214                            super.put((K) key, callback.get((K) key));
215                            lastUpdatedTimes.put(key, Long.valueOf(now));
216                        }
217                    }
218                }
219            }
220
221            // we have an element and it has been retrieved, move it to the
222            // top of the mruList. (we may not necessarily want to do this;
223            // this should be a constructor preference). There are pros & cons 
224            // to this approach:
225            // pros: if we just got an element, we probably don't want it expiring out
226            //   of the cache
227            // cons: this moves 'stale' objects to the top of the mruList
228            //   stack; this means that when we remove elements from mruList
229            //   to fit in more elements, we may be expiring older elements.
230            // this should probably be a decision made by the developer, rather than
231            // this code, but I'd like to keep the interface simple.
232            if (!dynamicCaching) {
233                synchronized (this) {
234                    mruList.remove(key);
235                    mruList.addFirst(key);
236                }
237            }
238        }
239
240        return super.get(key);
241    }
242
243    /**
244     * Returns the object if it is in the cache and has not expired.
245     * This method will return null if the the key is not in cache, or
246     * if it is in the cache but has expired.
247     *
248     * @param key key whose associated value is to be returned
249     *
250     * @return the value to which this map maps the specified key, or
251     *   null if the map contains no mapping for this key.
252     */
253    public synchronized Object getNoCallback(Object key) {
254        if (!this.containsKey(key)) {
255            return null;
256        }
257
258        if (expiryTime > 0) {
259            if (((Long)lastUpdatedTimes.get(key)).longValue() + expiryTime < 
260              System.currentTimeMillis()) 
261            {
262                return null;
263            }
264        }
265
266        return super.get(key);
267    }
268
269    /**
270     * Add an element into the cache.
271     *
272     * @param key  key with which the specified value is to be associated.
273     * @param value value to be associated with the specified key.
274     * 
275     * @return the previous value of this map entry, if one exists, otherwise null
276     */
277    @Override
278    public synchronized V put(K key, V value) {
279        
280        synchronized (this) {
281                        V lastValue = super.put(key, value);
282            lastUpdatedTimes.put(key, Long.valueOf(System.currentTimeMillis()));
283            return lastValue;
284        }
285    }
286    
287    /** Enforces dynamic caching rules. When called, data will be expired from the time it 
288     * is <i>entered</i> into the cache, rather than the time if is <i>fetched</i> from 
289     * the cache. Suitable when we wish to apply a strict expiry time for 
290     * dynamically-updating data, e.g. rowcounts.
291     */
292    public void setDynamicCaching() {
293        dynamicCaching = true;
294    }
295}