Clover coverage report -
Coverage timestamp: do jan 22 2004 21:12:32 CET
file stats: LOC: 861   Methods: 37
NCLOC: 355   Classes: 1
 
 Source file Conditionals Statements Methods TOTAL
Cache.java 74,5% 80,4% 73% 77,7%
coverage coverage
 1   
 /*
 2   
  * Copyright (c) 2002-2003 by OpenSymphony
 3   
  * All rights reserved.
 4   
  */
 5   
 package com.opensymphony.oscache.base;
 6   
 
 7   
 import com.opensymphony.oscache.base.algorithm.AbstractConcurrentReadCache;
 8   
 import com.opensymphony.oscache.base.algorithm.LRUCache;
 9   
 import com.opensymphony.oscache.base.algorithm.UnlimitedCache;
 10   
 import com.opensymphony.oscache.base.events.*;
 11   
 import com.opensymphony.oscache.base.persistence.PersistenceListener;
 12   
 import com.opensymphony.oscache.util.FastCronParser;
 13   
 
 14   
 import org.apache.commons.logging.Log;
 15   
 import org.apache.commons.logging.LogFactory;
 16   
 
 17   
 import java.io.Serializable;
 18   
 
 19   
 import java.text.ParseException;
 20   
 
 21   
 import java.util.*;
 22   
 
 23   
 import javax.swing.event.EventListenerList;
 24   
 
 25   
 /**
 26   
  * Provides an interface to the cache itself. Creating an instance of this class
 27   
  * will create a cache that behaves according to its construction parameters.
 28   
  * The public API provides methods to manage objects in the cache and configure
 29   
  * any cache event listeners.
 30   
  *
 31   
  * @version        $Revision: 1.6 $
 32   
  * @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
 33   
  * @author <a href="mailto:tgochenour@peregrine.com">Todd Gochenour</a>
 34   
  * @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
 35   
  * @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
 36   
  */
 37   
 public class Cache implements Serializable {
 38   
     /**
 39   
      * An event that origininated from within another event.
 40   
      */
 41   
     public static final String NESTED_EVENT = "NESTED";
 42   
     private static transient final Log log = LogFactory.getLog(Cache.class);
 43   
 
 44   
     /**
 45   
      * A list of all registered event listeners for this cache.
 46   
      */
 47   
     protected EventListenerList listenerList = new EventListenerList();
 48   
 
 49   
     /**
 50   
      * The actual cache map. This is where the cached objects are held.
 51   
      */
 52   
     private AbstractConcurrentReadCache cacheMap = null;
 53   
 
 54   
     /**
 55   
      * Date of last complete cache flush.
 56   
      */
 57   
     private Date flushDateTime = null;
 58   
 
 59   
     /**
 60   
      * A set that holds keys of cache entries that are currently being built.
 61   
      * The cache checks against this map when a stale entry is requested.
 62   
      * If the requested key is in here, we know the entry is currently being
 63   
      * built by another thread and hence we can either block and wait or serve
 64   
      * the stale entry (depending on whether cache blocking is enabled or not).
 65   
      * <p>
 66   
      * We need to isolate these here since the actual CacheEntry
 67   
      * objects may not normally be held in memory at all (eg, if no
 68   
      * memory cache is configured).
 69   
      */
 70   
     private Map updateStates = new HashMap();
 71   
 
 72   
     /**
 73   
      * Indicates whether the cache blocks requests until new content has
 74   
      * been generated or just serves stale content instead.
 75   
      */
 76   
     private boolean blocking = false;
 77   
 
 78   
     /**
 79   
      * Create a new Cache
 80   
      *
 81   
      * @param useMemoryCaching Specify if the memory caching is going to be used
 82   
      * @param unlimitedDiskCache Specify if the disk caching is unlimited
 83   
      */
 84  9
     public Cache(boolean useMemoryCaching, boolean unlimitedDiskCache) {
 85  9
         this(useMemoryCaching, unlimitedDiskCache, false, null, 0);
 86   
     }
 87   
 
 88   
     /**
 89   
      * Create a new Cache.
 90   
      *
 91   
      * If a valid algorithm class is specified, it will be used for this cache.
 92   
      * Otherwise if a capacity is specified, it will use LRUCache.
 93   
      * If no algorithm or capacity is specified UnlimitedCache is used.
 94   
      *
 95   
      * @see com.opensymphony.oscache.base.algorithm.LRUCache
 96   
      * @see com.opensymphony.oscache.base.algorithm.UnlimitedCache
 97   
      * @param useMemoryCaching Specify if the memory caching is going to be used
 98   
      * @param unlimitedDiskCache Specify if the disk caching is unlimited
 99   
      * @param blocking This parameter takes effect when a cache entry has
 100   
      * just expired and several simultaneous requests try to retrieve it. While
 101   
      * one request is rebuilding the content, the other requests will either
 102   
      * block and wait for the new content (<code>blocking == true</code>) or
 103   
      * instead receive a copy of the stale content so they don't have to wait
 104   
      * (<code>blocking == false</code>). the default is <code>false</code>,
 105   
      * which provides better performance but at the expense of slightly stale
 106   
      * data being served.
 107   
      * @param algorithmClass The class implementing the desired algorithm
 108   
      * @param capacity The capacity
 109   
      */
 110  63
     public Cache(boolean useMemoryCaching, boolean unlimitedDiskCache, boolean blocking, String algorithmClass, int capacity) {
 111   
         // Instantiate the algo class if valid
 112  63
         if (((algorithmClass != null) && (algorithmClass.length() > 0)) && (capacity > 0)) {
 113  0
             try {
 114  0
                 cacheMap = (AbstractConcurrentReadCache) Class.forName(algorithmClass).newInstance();
 115  0
                 cacheMap.setMaxEntries(capacity);
 116   
             } catch (Exception e) {
 117  0
                 log.error("Invalid class name for cache algorithm class. " + e.toString());
 118   
             }
 119   
         }
 120   
 
 121  63
         if (cacheMap == null) {
 122   
             // If we have a capacity, use LRU cache otherwise use unlimited Cache
 123  63
             if (capacity > 0) {
 124  34
                 cacheMap = new LRUCache(capacity);
 125   
             } else {
 126  29
                 cacheMap = new UnlimitedCache();
 127   
             }
 128   
         }
 129   
 
 130  63
         cacheMap.setUnlimitedDiskCache(unlimitedDiskCache);
 131  63
         cacheMap.setMemoryCaching(useMemoryCaching);
 132   
 
 133  63
         this.blocking = blocking;
 134   
     }
 135   
 
 136   
     /**
 137   
      * Allows the capacity of the cache to be altered dynamically. Note that
 138   
      * some cache implementations may choose to ignore this setting (eg the
 139   
      * {@link UnlimitedCache} ignores this call).
 140   
      *
 141   
      * @param capacity the maximum number of items to hold in the cache.
 142   
      */
 143  0
     public void setCapacity(int capacity) {
 144  0
         cacheMap.setMaxEntries(capacity);
 145   
     }
 146   
 
 147   
     /**
 148   
      * Checks if the cache was flushed more recently than the CacheEntry provided.
 149   
      * Used to determine whether to refresh the particular CacheEntry.
 150   
      *
 151   
      * @param cacheEntry The cache entry which we're seeing whether to refresh
 152   
      * @return Whether or not the cache has been flushed more recently than this cache entry was updated.
 153   
      */
 154  171
     public boolean isFlushed(CacheEntry cacheEntry) {
 155  171
         if (flushDateTime != null) {
 156  0
             long lastUpdate = cacheEntry.getLastUpdate();
 157   
 
 158  0
             return (flushDateTime.getTime() >= lastUpdate);
 159   
         } else {
 160  171
             return false;
 161   
         }
 162   
     }
 163   
 
 164   
     /**
 165   
      * Retrieve an object from the cache specifying its key.
 166   
      *
 167   
      * @param key             Key of the object in the cache.
 168   
      *
 169   
      * @return The object from cache
 170   
      *
 171   
      * @throws NeedsRefreshException Thrown when the object either
 172   
      * doesn't exist, or exists but is stale. When this exception occurs,
 173   
      * the CacheEntry corresponding to the supplied key will be locked
 174   
      * and other threads requesting this entry will potentially be blocked
 175   
      * until the caller repopulates the cache. If the caller choses not
 176   
      * to repopulate the cache, they <em>must</em> instead call
 177   
      * {@link #cancelUpdate(String)}.
 178   
      */
 179  0
     public Object getFromCache(String key) throws NeedsRefreshException {
 180  0
         return getFromCache(key, CacheEntry.INDEFINITE_EXPIRY, null);
 181   
     }
 182   
 
 183   
     /**
 184   
      * Retrieve an object from the cache specifying its key.
 185   
      *
 186   
      * @param key             Key of the object in the cache.
 187   
      * @param refreshPeriod   How long before the object needs refresh. To
 188   
      * allow the object to stay in the cache indefinitely, supply a value
 189   
      * of {@link CacheEntry#INDEFINITE_EXPIRY}.
 190   
      *
 191   
      * @return The object from cache
 192   
      *
 193   
      * @throws NeedsRefreshException Thrown when the object either
 194   
      * doesn't exist, or exists but is stale. When this exception occurs,
 195   
      * the CacheEntry corresponding to the supplied key will be locked
 196   
      * and other threads requesting this entry will potentially be blocked
 197   
      * until the caller repopulates the cache. If the caller choses not
 198   
      * to repopulate the cache, they <em>must</em> instead call
 199   
      * {@link #cancelUpdate(String)}.
 200   
      */
 201  291
     public Object getFromCache(String key, int refreshPeriod) throws NeedsRefreshException {
 202  291
         return getFromCache(key, refreshPeriod, null);
 203   
     }
 204   
 
 205   
     /**
 206   
      * Retrieve an object from the cache specifying its key.
 207   
      *
 208   
      * @param key             Key of the object in the cache.
 209   
      * @param refreshPeriod   How long before the object needs refresh. To
 210   
      * allow the object to stay in the cache indefinitely, supply a value
 211   
      * of {@link CacheEntry#INDEFINITE_EXPIRY}.
 212   
      * @param cronExpiry      A cron expression that specifies fixed date(s)
 213   
      *                        and/or time(s) that this cache entry should
 214   
      *                        expire on.
 215   
      *
 216   
      * @return The object from cache
 217   
      *
 218   
      * @throws NeedsRefreshException Thrown when the object either
 219   
      * doesn't exist, or exists but is stale. When this exception occurs,
 220   
      * the CacheEntry corresponding to the supplied key will be locked
 221   
      * and other threads requesting this entry will potentially be blocked
 222   
      * until the caller repopulates the cache. If the caller choses not
 223   
      * to repopulate the cache, they <em>must</em> instead call
 224   
      * {@link #cancelUpdate(String)}.
 225   
      */
 226  291
     public Object getFromCache(String key, int refreshPeriod, String cronExpiry) throws NeedsRefreshException {
 227  291
         CacheEntry cacheEntry = this.getCacheEntry(key, null, null);
 228   
 
 229  282
         Object content = cacheEntry.getContent();
 230  282
         CacheMapAccessEventType accessEventType = CacheMapAccessEventType.HIT;
 231   
 
 232  282
         boolean reload = false;
 233   
 
 234   
         // Check if this entry has expired or has not yet been added to the cache. If
 235   
         // so, we need to decide whether to block, serve stale content or throw a
 236   
         // NeedsRefreshException
 237  282
         if (this.isStale(cacheEntry, refreshPeriod, cronExpiry)) {
 238  111
             EntryUpdateState updateState = getUpdateState(key);
 239   
 
 240  111
             synchronized (updateState) {
 241  111
                 if (updateState.isAwaitingUpdate() || updateState.isCancelled()) {
 242   
                     // No one else is currently updating this entry - grab ownership
 243  96
                     updateState.startUpdate();
 244   
 
 245  96
                     if (cacheEntry.isNew()) {
 246  18
                         accessEventType = CacheMapAccessEventType.MISS;
 247   
                     } else {
 248  78
                         accessEventType = CacheMapAccessEventType.STALE_HIT;
 249   
                     }
 250  15
                 } else if (updateState.isUpdating()) {
 251   
                     // Another thread is already updating the cache. We block if this
 252   
                     // is a new entry, or blocking mode is enabled. Either putInCache()
 253   
                     // or cancelUpdate() can cause this thread to resume.
 254  15
                     if (cacheEntry.isNew() || blocking) {
 255  12
                         do {
 256  12
                             try {
 257  12
                                 updateState.wait();
 258   
                             } catch (InterruptedException e) {
 259   
                             }
 260  12
                         } while (updateState.isUpdating());
 261   
 
 262  12
                         if (updateState.isCancelled()) {
 263   
                             // The updating thread cancelled the update, let this one have a go.
 264  3
                             updateState.startUpdate();
 265   
 
 266   
                             // We put the updateState object back into the updateStates map so
 267   
                             // any remaining threads waiting on this cache entry will be notified
 268   
                             // once this thread has done its thing (either updated the cache or
 269   
                             // cancelled the update). Without this code they'll get left hanging...
 270  3
                             synchronized (updateStates) {
 271  3
                                 updateStates.put(key, updateState);
 272   
                             }
 273   
 
 274  3
                             if (cacheEntry.isNew()) {
 275  3
                                 accessEventType = CacheMapAccessEventType.MISS;
 276   
                             } else {
 277  0
                                 accessEventType = CacheMapAccessEventType.STALE_HIT;
 278   
                             }
 279  9
                         } else if (updateState.isComplete()) {
 280  9
                             reload = true;
 281   
                         } else {
 282  0
                             log.error("Invalid update state for cache entry " + key);
 283   
                         }
 284   
                     }
 285   
                 } else {
 286  0
                     reload = true;
 287   
                 }
 288   
             }
 289   
         }
 290   
 
 291   
         // If reload is true then another thread must have successfully rebuilt the cache entry
 292  282
         if (reload) {
 293  9
             cacheEntry = (CacheEntry) cacheMap.get(key);
 294   
 
 295  9
             if (cacheEntry != null) {
 296  9
                 content = cacheEntry.getContent();
 297   
             } else {
 298  0
                 log.error("Could not reload cache entry after waiting for it to be rebuilt");
 299   
             }
 300   
         }
 301   
 
 302  282
         dispatchCacheMapAccessEvent(accessEventType, cacheEntry, null);
 303   
 
 304   
         // If we didn't end up getting a hit then we need to throw a NRE
 305  282
         if (accessEventType != CacheMapAccessEventType.HIT) {
 306  99
             throw new NeedsRefreshException(content);
 307   
         }
 308   
 
 309  183
         return content;
 310   
     }
 311   
 
 312   
     /**
 313   
      * Set the listener to use for data persistence. Only one
 314   
      * <code>PersistenceListener</code> can be configured per cache.
 315   
      *
 316   
      * @param listener The implementation of a persistance listener
 317   
      */
 318  34
     public void setPersistenceListener(PersistenceListener listener) {
 319  34
         cacheMap.setPersistenceListener(listener);
 320   
     }
 321   
 
 322   
     /**
 323   
      * Retrieves the currently configured <code>PersistenceListener</code>.
 324   
      *
 325   
      * @return the cache's <code>PersistenceListener</code>, or <code>null</code>
 326   
      * if no listener is configured.
 327   
      */
 328  0
     public PersistenceListener getPersistenceListener() {
 329  0
         return cacheMap.getPersistenceListener();
 330   
     }
 331   
 
 332   
     /**
 333   
      * Register a listener for Cache events. The listener must implement
 334   
      * one of the child interfaces of the {@link CacheEventListener} interface.
 335   
      *
 336   
      * @param listener  The object that listens to events.
 337   
      */
 338  72
     public void addCacheEventListener(CacheEventListener listener, Class clazz) {
 339  72
         if (CacheEventListener.class.isAssignableFrom(clazz)) {
 340  72
             listenerList.add(clazz, listener);
 341   
         } else {
 342  0
             log.error("The class '" + clazz.getName() + "' is not a CacheEventListener. Ignoring this listener.");
 343   
         }
 344   
     }
 345   
 
 346   
     /**
 347   
      * Cancels any pending update for this cache entry. This should <em>only</em>
 348   
      * be called by the thread that is responsible for performing the update ie
 349   
      * the thread that received the original {@link NeedsRefreshException}.<p/>
 350   
      * If a cache entry is not updated (via {@link #putInCache} and this method is
 351   
      * not called to let OSCache know the update will not be forthcoming, subsequent
 352   
      * requests for this cache entry will either block indefinitely (if this is a new
 353   
      * cache entry or cache.blocking=true), or forever get served stale content. Note
 354   
      * however that there is no harm in cancelling an update on a key that either
 355   
      * does not exist or is not currently being updated.
 356   
      *
 357   
      * @param key The key for the cache entry in question.
 358   
      */
 359  84
     public void cancelUpdate(String key) {
 360  84
         EntryUpdateState state;
 361   
 
 362  84
         if (key != null) {
 363  84
             synchronized (updateStates) {
 364  84
                 state = (EntryUpdateState) updateStates.remove(key);
 365   
 
 366  84
                 if (state != null) {
 367  84
                     synchronized (state) {
 368  84
                         state.cancelUpdate();
 369  84
                         state.notify();
 370   
                     }
 371   
                 }
 372   
             }
 373   
         }
 374   
     }
 375   
 
 376   
     /**
 377   
      * Flush all entries in the cache on the given date/time.
 378   
      *
 379   
      * @param date The date at which all cache entries will be flushed.
 380   
      */
 381  0
     public void flushAll(Date date) {
 382  0
         flushAll(date, null);
 383   
     }
 384   
 
 385   
     /**
 386   
      * Flush all entries in the cache on the given date/time.
 387   
      *
 388   
      * @param date The date at which all cache entries will be flushed.
 389   
      * @param origin The origin of this flush request (optional)
 390   
      */
 391  0
     public void flushAll(Date date, String origin) {
 392  0
         flushDateTime = date;
 393  0
         dispatchCachewideEvent(CachewideEventType.CACHE_FLUSHED, date, origin);
 394   
     }
 395   
 
 396   
     /**
 397   
      * Flush the cache entry (if any) that corresponds to the cache key supplied.
 398   
      * This call will flush the entry from the cache and remove the references to
 399   
      * it from any cache groups that it is a member of. On completion of the flush,
 400   
      * a <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired.
 401   
      *
 402   
      * @param key The key of the entry to flush
 403   
      */
 404  0
     public void flushEntry(String key) {
 405  0
         flushEntry(key, null);
 406   
     }
 407   
 
 408   
     /**
 409   
      * Flush the cache entry (if any) that corresponds to the cache key supplied.
 410   
      * This call will mark the cache entry as flushed so that the next access
 411   
      * to it will cause a {@link NeedsRefreshException}. On completion of the
 412   
      * flush, a <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired.
 413   
      *
 414   
      * @param key The key of the entry to flush
 415   
      * @param origin The origin of this flush request (optional)
 416   
      */
 417  0
     public void flushEntry(String key, String origin) {
 418  0
         flushEntry(getCacheEntry(key, null, origin), origin);
 419   
     }
 420   
 
 421   
     /**
 422   
      * Flushes all objects that belong to the supplied group. On completion
 423   
      * this method fires a <tt>CacheEntryEventType.GROUP_FLUSHED</tt> event.
 424   
      *
 425   
      * @param group The group to flush
 426   
      */
 427  24
     public void flushGroup(String group) {
 428  24
         flushGroup(group, null);
 429   
     }
 430   
 
 431   
     /**
 432   
      * Flushes all unexpired objects that belong to the supplied group. On
 433   
      * completion this method fires a <tt>CacheEntryEventType.GROUP_FLUSHED</tt>
 434   
      * event.
 435   
      *
 436   
      * @param group The group to flush
 437   
      * @param origin The origin of this flush event (optional)
 438   
      */
 439  24
     public void flushGroup(String group, String origin) {
 440   
         // Flush all objects in the group
 441  24
         Set groupEntries = cacheMap.getGroup(group);
 442   
 
 443  24
         if (groupEntries != null) {
 444  23
             Iterator itr = groupEntries.iterator();
 445  23
             String key;
 446  23
             CacheEntry entry;
 447   
 
 448  23
             while (itr.hasNext()) {
 449  92
                 key = (String) itr.next();
 450  92
                 entry = (CacheEntry) cacheMap.get(key);
 451   
 
 452  92
                 if ((entry != null) && !entry.needsRefresh(CacheEntry.INDEFINITE_EXPIRY)) {
 453  39
                     flushEntry(entry, NESTED_EVENT);
 454   
                 }
 455   
             }
 456   
         }
 457   
 
 458  24
         dispatchCacheGroupEvent(CacheEntryEventType.GROUP_FLUSHED, group, origin);
 459   
     }
 460   
 
 461   
     /**
 462   
      * Flush all entries with keys that match a given pattern
 463   
      *
 464   
      * @param  pattern The key must contain this given value
 465   
      * @deprecated For performance and flexibility reasons it is preferable to
 466   
      * store cache entries in groups and use the {@link #flushGroup(String)} method
 467   
      * instead of relying on pattern flushing.
 468   
      */
 469  30
     public void flushPattern(String pattern) {
 470  30
         flushPattern(pattern, null);
 471   
     }
 472   
 
 473   
     /**
 474   
      * Flush all entries with keys that match a given pattern
 475   
      *
 476   
      * @param  pattern The key must contain this given value
 477   
      * @param origin The origin of this flush request
 478   
      * @deprecated For performance and flexibility reasons it is preferable to
 479   
      * store cache entries in groups and use the {@link #flushGroup(String, String)}
 480   
      * method instead of relying on pattern flushing.
 481   
      */
 482  30
     public void flushPattern(String pattern, String origin) {
 483   
         // Check the pattern
 484  30
         if ((pattern != null) && (pattern.length() > 0)) {
 485  18
             String key = null;
 486  18
             CacheEntry entry = null;
 487  18
             Iterator itr = cacheMap.keySet().iterator();
 488   
 
 489  18
             while (itr.hasNext()) {
 490  54
                 key = (String) itr.next();
 491   
 
 492  54
                 if (key.indexOf(pattern) >= 0) {
 493  6
                     entry = (CacheEntry) cacheMap.get(key);
 494   
 
 495  6
                     if (entry != null) {
 496  6
                         flushEntry(entry, origin);
 497   
                     }
 498   
                 }
 499   
             }
 500   
 
 501  18
             dispatchCachePatternEvent(CacheEntryEventType.PATTERN_FLUSHED, pattern, origin);
 502   
         } else {
 503   
             // Empty pattern, nothing to do
 504   
         }
 505   
     }
 506   
 
 507   
     /**
 508   
      * Put an object in the cache specifying the key to use.
 509   
      *
 510   
      * @param key       Key of the object in the cache.
 511   
      * @param content   The object to cache.
 512   
      */
 513  6
     public void putInCache(String key, Object content) {
 514  6
         putInCache(key, content, null, null, null);
 515   
     }
 516   
 
 517   
     /**
 518   
      * Put an object in the cache specifying the key and refresh policy to use.
 519   
      *
 520   
      * @param key       Key of the object in the cache.
 521   
      * @param content   The object to cache.
 522   
      * @param policy   Object that implements refresh policy logic
 523   
      */
 524  144
     public void putInCache(String key, Object content, EntryRefreshPolicy policy) {
 525  144
         putInCache(key, content, null, policy, null);
 526   
     }
 527   
 
 528   
     /**
 529   
      * Put in object into the cache, specifying both the key to use and the
 530   
      * cache groups the object belongs to.
 531   
      *
 532   
      * @param key       Key of the object in the cache
 533   
      * @param content   The object to cache
 534   
      * @param groups    The cache groups to add the object to
 535   
      */
 536  54
     public void putInCache(String key, Object content, String[] groups) {
 537  54
         putInCache(key, content, groups, null, null);
 538   
     }
 539   
 
 540   
     /**
 541   
      * Put an object into the cache specifying both the key to use and the
 542   
      * cache groups the object belongs to.
 543   
      *
 544   
      * @param key       Key of the object in the cache
 545   
      * @param groups    The cache groups to add the object to
 546   
      * @param content   The object to cache
 547   
      * @param policy    Object that implements the refresh policy logic
 548   
      */
 549  204
     public void putInCache(String key, Object content, String[] groups, EntryRefreshPolicy policy, String origin) {
 550  204
         CacheEntry cacheEntry = this.getCacheEntry(key, policy, origin);
 551  201
         boolean isNewEntry = cacheEntry.isNew();
 552  201
         cacheEntry.setContent(content);
 553  201
         cacheEntry.setGroups(groups);
 554  201
         cacheMap.put(key, cacheEntry);
 555   
 
 556   
         // Signal to any threads waiting on this update that it's now ready for them
 557   
         // in the cache!
 558  201
         completeUpdate(key);
 559   
 
 560  201
         CacheEntryEvent event = new CacheEntryEvent(this, cacheEntry, origin);
 561   
 
 562  201
         if (isNewEntry) {
 563  164
             dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_ADDED, event);
 564   
         } else {
 565  37
             dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_UPDATED, event);
 566   
         }
 567   
     }
 568   
 
 569   
     /**
 570   
      * Unregister a listener for Cache events.
 571   
      *
 572   
      * @param listener  The object that currently listens to events.
 573   
      */
 574  72
     public void removeCacheEventListener(CacheEventListener listener, Class clazz) {
 575  72
         listenerList.remove(clazz, listener);
 576   
     }
 577   
 
 578   
     /**
 579   
      * Get an entry from this cache or create one if it doesn't exist.
 580   
      *
 581   
      * @param key    The key of the cache entry
 582   
      * @param policy Object that implements refresh policy logic
 583   
      * @param origin The origin of request (optional)
 584   
      * @return CacheEntry for the specified key.
 585   
      */
 586  495
     protected CacheEntry getCacheEntry(String key, EntryRefreshPolicy policy, String origin) {
 587  495
         CacheEntry cacheEntry = null;
 588   
 
 589   
         // Verify that the key is valid
 590  495
         if ((key == null) || (key.length() == 0)) {
 591  12
             throw new IllegalArgumentException("getCacheEntry called with an empty or null key");
 592   
         }
 593   
 
 594  483
         cacheEntry = (CacheEntry) cacheMap.get(key);
 595   
 
 596   
         // if the cache entry does not exist, create a new one
 597  483
         if (cacheEntry == null) {
 598  191
             if (log.isDebugEnabled()) {
 599  0
                 log.debug("No cache entry exists for key='" + key + "', creating");
 600   
             }
 601   
 
 602  191
             cacheEntry = new CacheEntry(key, policy);
 603   
         }
 604   
 
 605  483
         return cacheEntry;
 606   
     }
 607   
 
 608   
     /**
 609   
      * Indicates whether or not the cache entry is stale.
 610   
      *
 611   
      * @param cacheEntry     The cache entry to test the freshness of.
 612   
      * @param refreshPeriod  The maximum allowable age of the entry, in seconds.
 613   
      * @param cronExpiry     A cron expression specifying absolute date(s) and/or time(s)
 614   
      * that the cache entry should expire at. If the cache entry was refreshed prior to
 615   
      * the most recent match for the cron expression, the entry will be considered stale.
 616   
      *
 617   
      * @return <code>true</code> if the entry is stale, <code>false</code> otherwise.
 618   
      */
 619  282
     protected boolean isStale(CacheEntry cacheEntry, int refreshPeriod, String cronExpiry) {
 620  282
         boolean result = cacheEntry.needsRefresh(refreshPeriod) || isFlushed(cacheEntry);
 621   
 
 622  282
         if ((cronExpiry != null) && (cronExpiry.length() > 0)) {
 623  0
             try {
 624  0
                 FastCronParser parser = new FastCronParser(cronExpiry);
 625  0
                 result = result || parser.hasMoreRecentMatch(cacheEntry.getLastUpdate());
 626   
             } catch (ParseException e) {
 627  0
                 log.warn(e);
 628   
             }
 629   
         }
 630   
 
 631  282
         return result;
 632   
     }
 633   
 
 634   
     /**
 635   
      * Get the updating cache entry from the update map. If one is not found,
 636   
      * create a new one (with state {@link EntryUpdateState#NOT_YET_UPDATING})
 637   
      * and add it to the map.
 638   
      *
 639   
      * @param key The cache key for this entry
 640   
      *
 641   
      * @return the CacheEntry that was found (or added to) the updatingEntries
 642   
      * map.
 643   
      */
 644  111
     protected EntryUpdateState getUpdateState(String key) {
 645  111
         EntryUpdateState updateState;
 646   
 
 647  111
         synchronized (updateStates) {
 648   
             // Try to find the matching state object in the updating entry map.
 649  111
             updateState = (EntryUpdateState) updateStates.get(key);
 650   
 
 651  111
             if (updateState == null) {
 652   
                 // It's not there so add it.
 653  96
                 updateState = new EntryUpdateState();
 654  96
                 updateStates.put(key, updateState);
 655   
             }
 656   
         }
 657   
 
 658  111
         return updateState;
 659   
     }
 660   
 
 661   
     /**
 662   
      * Completely clears the cache.
 663   
      */
 664  9
     protected void clear() {
 665  9
         cacheMap.clear();
 666   
     }
 667   
 
 668   
     /**
 669   
      * Removes the update state for the specified key and notifies any other
 670   
      * threads that are waiting on this object. This is called automatically
 671   
      * by the {@link #putInCache} method.
 672   
      *
 673   
      * @param key The cache key that is no longer being updated.
 674   
      */
 675  201
     protected void completeUpdate(String key) {
 676  201
         EntryUpdateState state;
 677   
 
 678  201
         synchronized (updateStates) {
 679  201
             state = (EntryUpdateState) updateStates.remove(key);
 680   
 
 681  201
             if (state != null) {
 682  15
                 synchronized (state) {
 683  15
                     state.completeUpdate();
 684  15
                     state.notifyAll();
 685   
                 }
 686   
             }
 687   
         }
 688   
     }
 689   
 
 690   
     /**
 691   
      * Completely removes a cache entry from the cache and its associated cache
 692   
      * groups.
 693   
      *
 694   
      * @param key The key of the entry to remove.
 695   
      */
 696  0
     protected void removeEntry(String key) {
 697  0
         removeEntry(key, null);
 698   
     }
 699   
 
 700   
     /**
 701   
      * Completely removes a cache entry from the cache and its associated cache
 702   
      * groups.
 703   
      *
 704   
      * @param key    The key of the entry to remove.
 705   
      * @param origin The origin of this remove request.
 706   
      */
 707  0
     protected void removeEntry(String key, String origin) {
 708  0
         CacheEntry cacheEntry = (CacheEntry) cacheMap.get(key);
 709  0
         cacheMap.remove(key);
 710   
 
 711  0
         CacheEntryEvent event = new CacheEntryEvent(this, cacheEntry, origin);
 712  0
         dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_REMOVED, event);
 713   
     }
 714   
 
 715   
     /**
 716   
      * Dispatch a cache entry event to all registered listeners.
 717   
      *
 718   
      * @param eventType   The type of event (used to branch on the proper method)
 719   
      * @param event       The event that was fired
 720   
      */
 721  246
     private void dispatchCacheEntryEvent(CacheEntryEventType eventType, CacheEntryEvent event) {
 722   
         // Guaranteed to return a non-null array
 723  246
         Object[] listeners = listenerList.getListenerList();
 724   
 
 725   
         // Process the listeners last to first, notifying
 726   
         // those that are interested in this event
 727  246
         for (int i = listeners.length - 2; i >= 0; i -= 2) {
 728  246
             if (listeners[i] == CacheEntryEventListener.class) {
 729  123
                 if (eventType.equals(CacheEntryEventType.ENTRY_ADDED)) {
 730  50
                     ((CacheEntryEventListener) listeners[i + 1]).cacheEntryAdded(event);
 731  73
                 } else if (eventType.equals(CacheEntryEventType.ENTRY_UPDATED)) {
 732  31
                     ((CacheEntryEventListener) listeners[i + 1]).cacheEntryUpdated(event);
 733  42
                 } else if (eventType.equals(CacheEntryEventType.ENTRY_FLUSHED)) {
 734  42
                     ((CacheEntryEventListener) listeners[i + 1]).cacheEntryFlushed(event);
 735  0
                 } else if (eventType.equals(CacheEntryEventType.ENTRY_REMOVED)) {
 736  0
                     ((CacheEntryEventListener) listeners[i + 1]).cacheEntryRemoved(event);
 737   
                 }
 738   
             }
 739   
         }
 740   
     }
 741   
 
 742   
     /**
 743   
      * Dispatch a cache group event to all registered listeners.
 744   
      *
 745   
      * @param eventType The type of event (this is used to branch to the correct method handler)
 746   
      * @param group     The cache group that the event applies to
 747   
      * @param origin      The origin of this event (optional)
 748   
      */
 749  24
     private void dispatchCacheGroupEvent(CacheEntryEventType eventType, String group, String origin) {
 750  24
         CacheGroupEvent event = new CacheGroupEvent(this, group, origin);
 751   
 
 752   
         // Guaranteed to return a non-null array
 753  24
         Object[] listeners = listenerList.getListenerList();
 754   
 
 755   
         // Process the listeners last to first, notifying
 756   
         // those that are interested in this event
 757  24
         for (int i = listeners.length - 2; i >= 0; i -= 2) {
 758  48
             if (listeners[i] == CacheEntryEventListener.class) {
 759  24
                 if (eventType.equals(CacheEntryEventType.GROUP_FLUSHED)) {
 760  24
                     ((CacheEntryEventListener) listeners[i + 1]).cacheGroupFlushed(event);
 761   
                 }
 762   
             }
 763   
         }
 764   
     }
 765   
 
 766   
     /**
 767   
      * Dispatch a cache map access event to all registered listeners.
 768   
      *
 769   
      * @param eventType     The type of event
 770   
      * @param entry         The entry that was affected.
 771   
      * @param origin        The origin of this event (optional)
 772   
      */
 773  282
     private void dispatchCacheMapAccessEvent(CacheMapAccessEventType eventType, CacheEntry entry, String origin) {
 774  282
         CacheMapAccessEvent event = new CacheMapAccessEvent(eventType, entry, origin);
 775   
 
 776   
         // Guaranteed to return a non-null array
 777  282
         Object[] listeners = listenerList.getListenerList();
 778   
 
 779   
         // Process the listeners last to first, notifying
 780   
         // those that are interested in this event
 781  282
         for (int i = listeners.length - 2; i >= 0; i -= 2) {
 782  270
             if (listeners[i] == CacheMapAccessEventListener.class) {
 783  135
                 ((CacheMapAccessEventListener) listeners[i + 1]).accessed(event);
 784   
             }
 785   
         }
 786   
     }
 787   
 
 788   
     /**
 789   
      * Dispatch a cache pattern event to all registered listeners.
 790   
      *
 791   
      * @param eventType The type of event (this is used to branch to the correct method handler)
 792   
      * @param pattern     The cache pattern that the event applies to
 793   
      * @param origin      The origin of this event (optional)
 794   
      */
 795  18
     private void dispatchCachePatternEvent(CacheEntryEventType eventType, String pattern, String origin) {
 796  18
         CachePatternEvent event = new CachePatternEvent(this, pattern, origin);
 797   
 
 798   
         // Guaranteed to return a non-null array
 799  18
         Object[] listeners = listenerList.getListenerList();
 800   
 
 801   
         // Process the listeners last to first, notifying
 802   
         // those that are interested in this event
 803  18
         for (int i = listeners.length - 2; i >= 0; i -= 2) {
 804  24
             if (listeners[i] == CacheEntryEventListener.class) {
 805  12
                 if (eventType.equals(CacheEntryEventType.PATTERN_FLUSHED)) {
 806  12
                     ((CacheEntryEventListener) listeners[i + 1]).cachePatternFlushed(event);
 807   
                 }
 808   
             }
 809   
         }
 810   
     }
 811   
 
 812   
     /**
 813   
      * Dispatches a cache-wide event to all registered listeners.
 814   
      *
 815   
      * @param eventType The type of event (this is used to branch to the correct method handler)
 816   
      * @param origin The origin of this event (optional)
 817   
      */
 818  0
     private void dispatchCachewideEvent(CachewideEventType eventType, Date date, String origin) {
 819  0
         CachewideEvent event = new CachewideEvent(this, date, origin);
 820   
 
 821   
         // Guaranteed to return a non-null array
 822  0
         Object[] listeners = listenerList.getListenerList();
 823   
 
 824   
         // Process the listeners last to first, notifying
 825   
         // those that are interested in this event
 826  0
         for (int i = listeners.length - 2; i >= 0; i -= 2) {
 827  0
             if (listeners[i] == CacheEntryEventListener.class) {
 828  0
                 if (eventType.equals(CachewideEventType.CACHE_FLUSHED)) {
 829  0
                     ((CacheEntryEventListener) listeners[i + 1]).cacheFlushed(event);
 830   
                 }
 831   
             }
 832   
         }
 833   
     }
 834   
 
 835   
     /**
 836   
      * Flush a cache entry. On completion of the flush, a
 837   
      * <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired.
 838   
      *
 839   
      * @param entry The entry to flush
 840   
      * @param origin The origin of this flush event (optional)
 841   
      */
 842  45
     private void flushEntry(CacheEntry entry, String origin) {
 843  45
         String key = entry.getKey();
 844   
 
 845   
         // Flush the object itself
 846  45
         entry.flush();
 847   
 
 848  45
         if (!entry.isNew()) {
 849   
             // Update the entry's state in the map
 850  45
             cacheMap.put(key, entry);
 851   
 
 852   
             // Trigger an ENTRY_FLUSHED event
 853  45
             CacheEntryEvent event = new CacheEntryEvent(this, entry, origin);
 854  45
             dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_FLUSHED, event);
 855   
         } else {
 856   
             // The entry did not exist in the cache anyway, nothing to flush
 857   
             // TODO: Do we need an event for this?
 858   
         }
 859   
     }
 860   
 }
 861