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