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 }