001    package echopoint.util.collections;
002    
003    /* 
004     * This file is part of the Echo Point Project.  This project is a collection
005     * of Components that have extended the Echo Web Application Framework.
006     *
007     * Version: MPL 1.1/GPL 2.0/LGPL 2.1
008     *
009     * The contents of this file are subject to the Mozilla Public License Version
010     * 1.1 (the "License"); you may not use this file except in compliance with
011     * the License. You may obtain a copy of the License at
012     * http://www.mozilla.org/MPL/
013     *
014     * Software distributed under the License is distributed on an "AS IS" basis,
015     * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
016     * for the specific language governing rights and limitations under the
017     * License.
018     *
019     * Alternatively, the contents of this file may be used under the terms of
020     * either the GNU General Public License Version 2 or later (the "GPL"), or
021     * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
022     * in which case the provisions of the GPL or the LGPL are applicable instead
023     * of those above. If you wish to allow use of your version of this file only
024     * under the terms of either the GPL or the LGPL, and not to allow others to
025     * use your version of this file under the terms of the MPL, indicate your
026     * decision by deleting the provisions above and replace them with the notice
027     * and other provisions required by the GPL or the LGPL. If you do not delete
028     * the provisions above, a recipient may use your version of this file under
029     * the terms of any one of the MPL, the GPL or the LGPL.
030     */
031    
032    import java.lang.ref.SoftReference;
033    import java.text.SimpleDateFormat;
034    import java.util.Collection;
035    import java.util.Date;
036    import java.util.Iterator;
037    import java.util.Map;
038    import java.util.Set;
039    
040    /**
041     * <code>ExpiryCache</code> implements a <code>Map</code> cache contains
042     * objects that "expire".
043     * <p>
044     * By default, soft references are used to the cached data so that they can be
045     * reclaimed in low memory conditions regardless of whether they have expired or
046     * not.
047     * <p>
048     * The time-to-live and access-timeout is used to decide when an object has
049     * expired and needs to be removed from the cache.
050     * <p>
051     * Time-to-live is simple. Once the specified period elapses, the object is
052     * removed from the cache, regardless of how many times its been accessed.
053     * <p>
054     * Access-timeout is a little more complicated. Each time the object is taken
055     * from the cache, its lastAccessTime is tracked. If the access-timeout has
056     * expired (since its last access) then the object is taken from the cache.
057     * <p>
058     * If both the time-to-live and access-timeout is -1, then the object will never
059     * expire from the cache.
060     * <p>
061     * The objVersion value, which can be specified at put time, can be later
062     * retrieved along with the object. This allows for application level versioning
063     * of objects against when they put in the cache. For example if you cached the
064     * contents of a File read, you might want to store the File.lastModified() in
065     * the objVersion number. Later when you want the cached contents, you could
066     * check the put time objVersion number against the current File.lastModified().
067     * So even though the cached content has not expired, the underlying content has
068     * and hence should be re-read.
069     * <p>
070     * Under the cover this class uses a ConcurrentReaderHashMap and hence is thread
071     * safe for writes to the cache.
072     */
073    public class ExpiryCache implements Map {
074    
075            /** the default cache time-to-live is 60 minutes */
076            public static final long DEFAULT_TIME_TO_LIVE = 60 * 60 * 1000;
077    
078            /** the default cache access time out 5 minutes */
079            public static final long DEFAULT_ACCESS_TIMEOUT = 5 * 60 * 1000;
080    
081            private Map cacheMap = new ConcurrentReaderHashMap();
082    
083            private long ttl = DEFAULT_TIME_TO_LIVE;
084    
085            private long ato = DEFAULT_ACCESS_TIMEOUT;
086    
087            private boolean softReferences;
088    
089            /**
090             * <code>CacheEntry</code> is used to wrap cached objects and can track
091             * their time-to-live, last access time and access count.
092             */
093            private class CacheEntry {
094    
095                    private Object cachedData;
096    
097                    private long timeCached;
098    
099                    private long timeAccessedLast;
100    
101                    private int numberOfAccesses;
102    
103                    private long objTTL;
104    
105                    private long objATO;
106    
107                    private long objVersion;
108    
109                    private boolean customTimes;
110    
111                    private CacheEntry(Object cachedData, long ttl, long ato, long objVersion) {
112                            long now = System.currentTimeMillis();
113                            this.cachedData = cachedData;
114                            this.objVersion = objVersion;
115                            objTTL = ttl;
116                            objATO = ato;
117                            customTimes = true;
118                            timeCached = now;
119                            timeAccessedLast = now;
120                            ++numberOfAccesses;
121                    }
122    
123                    private Object getCachedData() {
124                            timeAccessedLast = System.currentTimeMillis();
125                            ++numberOfAccesses;
126                            return cachedData;
127                    }
128    
129                    private boolean hasExpired(long now) {
130                            long usedTTL = customTimes ? objTTL : ExpiryCache.this.ttl;
131                            long usedATO = customTimes ? objATO : ExpiryCache.this.ato;
132                            if (usedTTL != -1) {
133                                    usedTTL = timeCached + usedTTL;
134                                    if (now > usedTTL)
135                                            return true;
136                            }
137                            if (usedATO != -1) {
138                                    usedATO = timeAccessedLast + usedATO;
139                                    if (now > usedATO)
140                                            return true;
141                            }
142                            return false;
143                    }
144    
145                    public String toString() {
146                            long now = System.currentTimeMillis();
147                            long usedTTL = customTimes ? objTTL : ExpiryCache.this.ttl;
148                            long usedATO = customTimes ? objATO : ExpiryCache.this.ato;
149    
150                            StringBuffer buf = new StringBuffer();
151                            buf.append(String.valueOf(this.cachedData));
152                            buf.append(" ");
153                            buf.append("[put version ");
154                            buf.append(objVersion);
155                            buf.append("] ");
156    
157                            buf.append("[time to live ");
158                            buf.append((timeCached + usedTTL) - now);
159                            buf.append("ms] ");
160    
161                            buf.append("[access timeout in ");
162                            buf.append((timeAccessedLast + usedATO) - now);
163                            buf.append("ms]");
164    
165                            return buf.toString();
166                    }
167            }
168    
169            /**
170             * Constructs a default <code>ExpiryCache</code> that uses
171             * <code>SoftReference</code>s
172             */
173            public ExpiryCache() {
174                    this(DEFAULT_TIME_TO_LIVE, DEFAULT_ACCESS_TIMEOUT, true);
175            }
176    
177            /**
178             * Constructs a <code>ExpiryCache</code> that uses
179             * <code>SoftReference</code>s
180             * 
181             * @param timeToLive -
182             *            the default time-to-live for a cache entry
183             * @param accessTimeout -
184             *            the default access timeout for a cache entry
185             */
186            public ExpiryCache(long timeToLive, long accessTimeout) {
187                    this(timeToLive, accessTimeout, true);
188            }
189    
190            /**
191             * Constructs a <code>ExpiryCache</code> with all the parameters
192             * 
193             * @param timeToLive -
194             *            the default time-to-live for a cache entry
195             * @param accessTimeout -
196             *            the default access timeout for a cache entry
197             * @param softReferences -
198             *            whether <code>SoftReference</code>s are used to cached data
199             */
200            public ExpiryCache(long timeToLive, long accessTimeout, boolean softReferences) {
201                    ttl = timeToLive;
202                    ato = accessTimeout;
203                    this.softReferences = softReferences;
204            }
205    
206            /**
207             * Sets the default 'time-to-live' for a cache entry
208             * 
209             * @param milliSecs -
210             *            'time-to-live' for a cache entry
211             */
212            public void setTimeToLive(long milliSecs) {
213                    ttl = milliSecs;
214            }
215    
216            /**
217             * Sets the default access timeout for a cache entry
218             * 
219             * @param milliSecs -
220             *            access timeout for a cache entry
221             */
222            public void setAccessTimeout(long milliSecs) {
223                    ato = milliSecs;
224            }
225    
226            /**
227             * Returns the time when the object was cached under a given key
228             * 
229             * @param key -
230             *            the key to the cached object
231             * @return the time when the object was cached under a given key
232             */
233            public long whenCached(Object key) {
234                    CacheEntry ce = (CacheEntry) cacheMap.get(key);
235                    if (ce == null)
236                            return 0;
237                    return ce.timeCached;
238            }
239    
240            /**
241             * Returns the time when the object was last accessed under a given key
242             * 
243             * @param key -
244             *            the key to the cached object
245             * @return the time when the object was last accessed under a given key
246             */
247            public long whenLastAccessed(Object key) {
248                    CacheEntry ce = (CacheEntry) cacheMap.get(key);
249                    if (ce == null)
250                            return 0;
251                    return ce.timeAccessedLast;
252            }
253    
254            /**
255             * Returns the version number that was provided when the object was placed
256             * in the cache.
257             * 
258             * @param key -
259             *            the key to the cached object
260             * @return the version number that was provided when the object was placed
261             *         in the cache or -1 if it was not provided at cache put.
262             */
263            public long whenVersion(Object key) {
264                    CacheEntry ce = (CacheEntry) cacheMap.get(key);
265                    if (ce == null)
266                            return -1;
267                    return ce.objVersion;
268            }
269    
270            /**
271             * Returns the number of times the object was accessed under a given key
272             * 
273             * @param key -
274             *            the key to the cached object
275             * @return the number of times the object was accessed under a given key
276             */
277            public int howManyTimesAccessed(Object key) {
278                    CacheEntry ce = (CacheEntry) cacheMap.get(key);
279                    if (ce == null)
280                            return 0;
281                    return ce.numberOfAccesses;
282            }
283    
284            /**
285             * Returns true if <code>SoftReference</code>s are used to cached data
286             * 
287             * @return true if <code>SoftReference</code>s are used to cached data
288             */
289            public boolean isSoftReferences() {
290                    return softReferences;
291            }
292    
293            /**
294             * Sets whether <code>SoftReference</code>s are used to hold cache
295             * entries.
296             * 
297             * @param newValue -
298             *            the new value of the flag
299             */
300            public void setSoftReferences(boolean newValue) {
301                    this.softReferences = newValue;
302            }
303    
304            /**
305             * @see java.util.Map#clear()
306             */
307            public void clear() {
308                    cacheMap.clear();
309            }
310    
311            /**
312             * @see java.util.Map#containsKey(java.lang.Object)
313             */
314            public boolean containsKey(Object key) {
315                    return cacheMap.containsKey(key);
316            }
317    
318            /**
319             * @see java.util.Map#remove(java.lang.Object)
320             */
321            public Object remove(Object key) {
322                    CacheEntry ce = (CacheEntry) cacheMap.remove(key);
323                    if (ce != null)
324                            return dereferenceCacheEntry(ce);
325                    return null;
326            }
327    
328            /**
329             * Note this may return a size larger than the number of non expired
330             * objects. A traversal of cached objects is NOT done here to work out a
331             * correct size value.
332             * 
333             * @see java.util.Map#size()
334             */
335            public int size() {
336                    return cacheMap.size();
337            }
338    
339            /**
340             * Note this may return false when in fact all objects in the cache have
341             * expired. A traversal of cached objects is NOT done here to work out if it
342             * is empty.
343             * 
344             * @see java.util.Map#isEmpty()
345             */
346            public boolean isEmpty() {
347                    return cacheMap.isEmpty();
348            }
349    
350            /**
351             * @see java.util.Map#putAll(java.util.Map)
352             */
353            public void putAll(Map t) {
354                    for (Iterator iter = t.keySet().iterator(); iter.hasNext();) {
355                            Object key = iter.next();
356                            Object objToCache = t.get(key);
357                            this.put(key, objToCache);
358                    }
359            }
360    
361            /**
362             * Returns a Set of all the keys in the cache. It may contain keys to
363             * objects that have expired so be careful when using this.
364             * 
365             * @return a Set of all the keys in the cache
366             */
367            public Set keySet() {
368                    return cacheMap.keySet();
369            }
370    
371            /**
372             * This operation is not supported on ExpiryCache.
373             * 
374             * @throws UnsupportedOperationException
375             */
376            public Set entrySet() {
377                    throw new UnsupportedOperationException("public Set entrySet() is an unsupported operation.");
378            }
379    
380            /**
381             * This operation is not supported on ExpiryCache.
382             * 
383             * @throws UnsupportedOperationException
384             */
385            public Collection values() {
386                    throw new UnsupportedOperationException("public Collection values() is an unsupported operation.");
387            }
388    
389            /**
390             * This operation is not supported on ExpiryCache.
391             * 
392             * @throws UnsupportedOperationException
393             */
394            public boolean containsValue(Object value) {
395                    throw new UnsupportedOperationException("public boolean containsValue(Object value) is an unsupported operation.");
396            }
397    
398            /**
399             * Places an object into the cache. The object will have a caches default
400             * 'time-to-live' and a caches default 'access time out' value.
401             * 
402             * @param key -
403             *            the key of the cached object
404             * @param objToCache -
405             *            the object to cache
406             * @return - the old object at this cache key
407             */
408            public Object put(Object key, Object objToCache) {
409                    return put(key, objToCache, ttl, ato, -1);
410            }
411    
412            /**
413             * Places an object into the cache. The object will have a caches default
414             * 'time-to-live' and a caches default 'access time out' value.
415             * 
416             * @param key -
417             *            the key of the cached object
418             * @param objToCache -
419             *            the object to cache
420             * @param objVersion -
421             *            the version of the object at the time it is put
422             * @return - the old object at this cache key
423             */
424            public Object put(Object key, Object objToCache, long objVersion) {
425                    return put(key, objToCache, ttl, ato, objVersion);
426            }
427    
428            /**
429             * Places an object into the cache with the specified 'time-to-live' and a
430             * 'access time out' value.
431             * 
432             * @param key -
433             *            the key of the cached object
434             * @param objToCache -
435             *            the object to cache
436             * @param timeToLive -
437             *            the time-to-live on the object or -1 to live for ever
438             * @param accessTimeout -
439             *            the accessTimeout on the object or -1 to never time out
440             * 
441             * @return - the old object at this cache key
442             */
443            public Object put(Object key, Object objToCache, long timeToLive, long accessTimeout) {
444                    return put(key, objToCache, timeToLive, accessTimeout, -1);
445            }
446    
447            /**
448             * Places an object into the cache with the specified 'time-to-live' and a
449             * 'access time out' value as well as a version number.
450             * 
451             * @param key -
452             *            the key of the cached object
453             * @param objToCache -
454             *            the object to cache
455             * @param timeToLive -
456             *            the time-to-live on the object or -1 to live for ever
457             * @param accessTimeout -
458             *            the accessTimeout on the object or -1 to never time out
459             * @param objVersion -
460             *            a version number that can be used later
461             * 
462             * 
463             * @return - the old object at this cache key
464             */
465            public Object put(Object key, Object objToCache, long timeToLive, long accessTimeout, long objVersion) {
466    
467                    // System.err.println("Putting :" + key);
468                    //
469                    // Since we are now using ConcurrentReaderHashMap we can remove
470                    // the global put synchronisation
471                    //
472                    CacheEntry ce = (CacheEntry) cacheMap.get(key);
473                    if (ce == null) {
474                            putCacheEntry(key, objToCache, timeToLive, accessTimeout, objVersion);
475                            return null;
476                    } else {
477                            Object obj = dereferenceCacheEntry(ce);
478                            if (obj == null) {
479                                    if (objToCache == null) {
480                                            // Avoids creating unnecessary new CacheEntry
481                                            // Number of accesses is not reset because object is the
482                                            // same
483                                            ce.timeCached = ce.timeAccessedLast = System.currentTimeMillis();
484                                            return null;
485                                    } else {
486                                            putCacheEntry(key, objToCache, timeToLive, accessTimeout, objVersion);
487                                            return null;
488                                    }
489                            } else if (obj.equals(objToCache)) {
490                                    // Avoids creating unnecessary new CacheEntry
491                                    // Number of accesses is not reset because object is the same
492                                    ce.timeCached = ce.timeAccessedLast = System.currentTimeMillis();
493                                    return null;
494                            } else {
495                                    putCacheEntry(key, objToCache, timeToLive, accessTimeout, objVersion);
496                                    return obj;
497                            }
498                    }
499            }
500    
501            /**
502             * Retrieves an object from the cache. If the object has expired, then it
503             * will return null
504             * 
505             * @param key -
506             *            the key to the cached object
507             * @return the object for the key or null
508             */
509            public Object get(Object key) {
510                    return _getObject(key, -1, false);
511            }
512    
513            /**
514             * Retrieves an object from the cache. If the object has expired or its
515             * recorded put version is less then the objVersion, then it will return
516             * null.
517             * 
518             * @param key -
519             *            the key to the cached object
520             * @param objVersion -
521             *            the object version number to compare against
522             * @return the object for the key or null
523             */
524            public Object get(Object key, long objVersion) {
525                    return _getObject(key, objVersion, true);
526            }
527    
528            /**
529             * Do the actual cache get and lokk for expired'ness
530             */
531            Object _getObject(Object key, long objVersion, boolean useVersioning) {
532                    logMessage("get",key);
533                    CacheEntry ce = (CacheEntry) cacheMap.get(key);
534                    if (ce == null) {
535                            // TODO - remove this
536                            logMessage("miss",key);
537                            return null;
538                    } else {
539                            //
540                            // has it expired. Stale objects are no good
541                            // and in this version of the class we should
542                            // reap it now. This slows us down a bit but
543                            // there is no need for background cleanups
544                            //
545                            long now = System.currentTimeMillis();
546                            if (ce.hasExpired(now)) {
547                                    onExpiredObject(key);
548                                    return null;
549                            }
550                            //
551                            // has the put version we have less than the current version
552                            // we have
553                            if (useVersioning && ce.objVersion < objVersion) {
554                                    onExpiredObject(key);
555                                    return null;
556                            }
557                            Object value = dereferenceCacheEntry(ce);
558                            if (value == null && isSoftReferences()) {
559                                    logMessage("null value possible memory reclaim",key);
560                            }
561                            return value;
562                    }
563            }
564    
565            /**
566             * Called when an object has been detected as expired or versioned out of
567             * existence.
568             * 
569             * @param key -
570             *            the key to the object
571             */
572            protected void onExpiredObject(Object key) {
573                    cacheMap.remove(key);
574                    // TODO - remove this
575                    logMessage("expired",key);
576            }
577            
578            private void logMessage(String message, Object key) {
579                    if (true) return;
580                    StringBuffer sb = new StringBuffer();
581                    sb.append("Expiry Cache - ");
582                    sb.append(new SimpleDateFormat("hh:mm:ss").format(new Date()));
583                    sb.append(" - ");
584                    sb.append(message);
585                    sb.append(" - ");
586                    sb.append(String.valueOf(key));
587                    System.out.println(sb);
588            }
589    
590            /**
591             * Called to see if a cache entry has expired or not at a given point in
592             * time.
593             * 
594             * @param key -
595             *            the key to the cached object
596             * @param when -
597             *            the time to do the comparision against
598             * @return true if the object has expired in the cache or cant be found in
599             *         the cache.
600             */
601            public boolean hasExpired(Object key, long when) {
602                    CacheEntry ce = (CacheEntry) cacheMap.get(key);
603                    if (ce != null)
604                            return ce.hasExpired(when);
605                    return true;
606            }
607    
608            /**
609             * Called to dereference a CacheEntry's reference to an object, depending on
610             * whether soft references have been used.
611             */
612            private Object dereferenceCacheEntry(CacheEntry ce) {
613                    Object cachedData = ce.getCachedData();
614                    if (cachedData instanceof SoftReference) {
615                            return ((SoftReference) cachedData).get();
616                    }
617                    return cachedData;
618            }
619    
620            /**
621             * Called to encode a CacheEntry's reference to an object into the cache,
622             * depending on whether soft references are used
623             */
624            private void putCacheEntry(Object key, Object objToCache, long timeToLive, long accessTimeout, long objVersion) {
625                    logMessage("put",key);
626                    if (isSoftReferences())
627                            cacheMap.put(key, new CacheEntry(new SoftReference(objToCache), timeToLive, accessTimeout, objVersion));
628                    else
629                            cacheMap.put(key, new CacheEntry(objToCache, timeToLive, accessTimeout, objVersion));
630            }
631    
632    }