Clover coverage report -
Coverage timestamp: Sat Apr 30 2005 21:58:28 PDT
file stats: LOC: 884   Methods: 37
NCLOC: 373   Classes: 1
 
 Source file Conditionals Statements Methods TOTAL
Cache.java 75.4% 81% 73% 78.3%
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.13.2.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  92
     public Cache(boolean useMemoryCaching, boolean unlimitedDiskCache, boolean overflowPersistence, boolean blocking, String algorithmClass, int capacity) {
 113   
         // Instantiate the algo class if valid
 114  92
         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  92
         if (cacheMap == null) {
 124   
             // If we have a capacity, use LRU cache otherwise use unlimited Cache
 125  92
             if (capacity > 0) {
 126  36
                 cacheMap = new LRUCache(capacity);
 127   
             } else {
 128  56
                 cacheMap = new UnlimitedCache();
 129   
             }
 130   
         }
 131   
 
 132  92
         cacheMap.setUnlimitedDiskCache(unlimitedDiskCache);
 133  92
         cacheMap.setOverflowPersistence(overflowPersistence);
 134  92
         cacheMap.setMemoryCaching(useMemoryCaching);
 135   
 
 136  92
         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  215
     public boolean isFlushed(CacheEntry cacheEntry) {
 158  215
         if (flushDateTime != null) {
 159  0
             long lastUpdate = cacheEntry.getLastUpdate();
 160   
 
 161  0
             return (flushDateTime.getTime() >= lastUpdate);
 162   
         } else {
 163  215
             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  2000384
     public Object getFromCache(String key, int refreshPeriod) throws NeedsRefreshException {
 205  2000394
         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  2000395
     public Object getFromCache(String key, int refreshPeriod, String cronExpiry) throws NeedsRefreshException {
 230  2000396
         CacheEntry cacheEntry = this.getCacheEntry(key, null, null);
 231   
 
 232  2000381
         Object content = cacheEntry.getContent();
 233  2000357
         CacheMapAccessEventType accessEventType = CacheMapAccessEventType.HIT;
 234   
 
 235  2000384
         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  2000382
         if (this.isStale(cacheEntry, refreshPeriod, cronExpiry)) {
 241  2000168
             EntryUpdateState updateState = getUpdateState(key);
 242   
 
 243  2000169
             synchronized (updateState) {
 244  2000169
                 if (updateState.isAwaitingUpdate() || updateState.isCancelled()) {
 245   
                     // No one else is currently updating this entry - grab ownership
 246  1974209
                     updateState.startUpdate();
 247   
 
 248  1974209
                     if (cacheEntry.isNew()) {
 249  25
                         accessEventType = CacheMapAccessEventType.MISS;
 250   
                     } else {
 251  1974184
                         accessEventType = CacheMapAccessEventType.STALE_HIT;
 252   
                     }
 253  25960
                 } 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  25960
                     if (cacheEntry.isNew() || blocking) {
 258  25956
                         do {
 259  1976484
                             try {
 260  1976484
                                 updateState.wait();
 261   
                             } catch (InterruptedException e) {
 262   
                             }
 263  1976484
                         } while (updateState.isUpdating());
 264   
 
 265  25956
                         if (updateState.isCancelled()) {
 266   
                             // The updating thread cancelled the update, let this one have a go.
 267  25932
                             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  25932
                             synchronized (updateStates) {
 274  25932
                                 updateStates.put(key, updateState);
 275   
                             }
 276   
 
 277  25932
                             if (cacheEntry.isNew()) {
 278  4
                                 accessEventType = CacheMapAccessEventType.MISS;
 279   
                             } else {
 280  25928
                                 accessEventType = CacheMapAccessEventType.STALE_HIT;
 281   
                             }
 282  24
                         } else if (updateState.isComplete()) {
 283  24
                             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  2000384
         if (reload) {
 296  24
             cacheEntry = (CacheEntry) cacheMap.get(key);
 297   
 
 298  24
             if (cacheEntry != null) {
 299  24
                 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  2000384
         dispatchCacheMapAccessEvent(accessEventType, cacheEntry, null);
 306   
 
 307   
         // If we didn't end up getting a hit then we need to throw a NRE
 308  2000384
         if (accessEventType != CacheMapAccessEventType.HIT) {
 309  2000141
             throw new NeedsRefreshException(content);
 310   
         }
 311   
 
 312  243
         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  54
     public void setPersistenceListener(PersistenceListener listener) {
 322  54
         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  2000120
     public void cancelUpdate(String key) {
 363  2000120
         EntryUpdateState state;
 364   
 
 365  2000120
         if (key != null) {
 366  2000120
             synchronized (updateStates) {
 367  2000120
                 state = (EntryUpdateState) updateStates.get(key);
 368   
 
 369  2000120
                 if (state != null) {
 370  2000120
                     synchronized (state) {
 371  2000120
                         state.cancelUpdate();
 372  2000120
                         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  194
     public void putInCache(String key, Object content, EntryRefreshPolicy policy) {
 535  194
         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  288
     public void putInCache(String key, Object content, String[] groups, EntryRefreshPolicy policy, String origin) {
 560  288
         CacheEntry cacheEntry = this.getCacheEntry(key, policy, origin);
 561  285
         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  285
         if (!isNewEntry) {
 565  49
             cacheEntry = new CacheEntry(key, policy);
 566   
         }
 567   
 
 568  285
         cacheEntry.setContent(content);
 569  285
         cacheEntry.setGroups(groups);
 570  285
         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  285
         completeUpdate(key);
 575   
 
 576  285
         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  2000680
     protected CacheEntry getCacheEntry(String key, EntryRefreshPolicy policy, String origin) {
 605  2000681
         CacheEntry cacheEntry = null;
 606   
 
 607   
         // Verify that the key is valid
 608  2000677
         if ((key == null) || (key.length() == 0)) {
 609  16
             throw new IllegalArgumentException("getCacheEntry called with an empty or null key");
 610   
         }
 611   
 
 612  2000664
         cacheEntry = (CacheEntry) cacheMap.get(key);
 613   
 
 614   
         // if the cache entry does not exist, create a new one
 615  2000668
         if (cacheEntry == null) {
 616  285
             if (log.isDebugEnabled()) {
 617  0
                 log.debug("No cache entry exists for key='" + key + "', creating");
 618   
             }
 619   
 
 620  285
             cacheEntry = new CacheEntry(key, policy);
 621   
         }
 622   
 
 623  2000665
         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  2000383
     protected boolean isStale(CacheEntry cacheEntry, int refreshPeriod, String cronExpiry) {
 638  2000381
         boolean result = cacheEntry.needsRefresh(refreshPeriod) || isFlushed(cacheEntry);
 639   
 
 640  2000383
         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  2000383
         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  2000167
     protected EntryUpdateState getUpdateState(String key) {
 663  2000167
         EntryUpdateState updateState;
 664   
 
 665  2000168
         synchronized (updateStates) {
 666   
             // Try to find the matching state object in the updating entry map.
 667  2000169
             updateState = (EntryUpdateState) updateStates.get(key);
 668   
 
 669  2000169
             if (updateState == null) {
 670   
                 // It's not there so add it.
 671  137
                 updateState = new EntryUpdateState();
 672  137
                 updateStates.put(key, updateState);
 673   
             }
 674   
         }
 675   
 
 676  2000169
         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  285
     protected void completeUpdate(String key) {
 694  285
         EntryUpdateState state;
 695   
 
 696  285
         synchronized (updateStates) {
 697  285
             state = (EntryUpdateState) updateStates.remove(key);
 698   
 
 699  285
             if (state != null) {
 700  45
                 synchronized (state) {
 701  45
                     if (!state.isUpdating()) {
 702  24
                         state.startUpdate();
 703   
                     }
 704   
 
 705  45
                     state.completeUpdate();
 706  45
                     state.notifyAll();
 707   
                 }
 708   
             }
 709   
         }
 710   
     }
 711   
 
 712   
     /**
 713   
      * Completely removes a cache entry from the cache and its associated cache
 714   
      * groups.
 715   
      *
 716   
      * @param key The key of the entry to remove.
 717   
      */
 718  0
     protected void removeEntry(String key) {
 719  0
         removeEntry(key, null);
 720   
     }
 721   
 
 722   
     /**
 723   
      * Completely removes a cache entry from the cache and its associated cache
 724   
      * groups.
 725   
      *
 726   
      * @param key    The key of the entry to remove.
 727   
      * @param origin The origin of this remove request.
 728   
      */
 729  0
     protected void removeEntry(String key, String origin) {
 730  0
         CacheEntry cacheEntry = (CacheEntry) cacheMap.get(key);
 731  0
         cacheMap.remove(key);
 732   
 
 733  0
         if (listenerList.getListenerCount() > 0) {
 734  0
             CacheEntryEvent event = new CacheEntryEvent(this, cacheEntry, origin);
 735  0
             dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_REMOVED, event);
 736   
         }
 737   
     }
 738   
 
 739   
     /**
 740   
      * Dispatch a cache entry event to all registered listeners.
 741   
      *
 742   
      * @param eventType   The type of event (used to branch on the proper method)
 743   
      * @param event       The event that was fired
 744   
      */
 745  174
     private void dispatchCacheEntryEvent(CacheEntryEventType eventType, CacheEntryEvent event) {
 746   
         // Guaranteed to return a non-null array
 747  174
         Object[] listeners = listenerList.getListenerList();
 748   
 
 749   
         // Process the listeners last to first, notifying
 750   
         // those that are interested in this event
 751  174
         for (int i = listeners.length - 2; i >= 0; i -= 2) {
 752  348
             if (listeners[i] == CacheEntryEventListener.class) {
 753  174
                 if (eventType.equals(CacheEntryEventType.ENTRY_ADDED)) {
 754  80
                     ((CacheEntryEventListener) listeners[i + 1]).cacheEntryAdded(event);
 755  94
                 } else if (eventType.equals(CacheEntryEventType.ENTRY_UPDATED)) {
 756  40
                     ((CacheEntryEventListener) listeners[i + 1]).cacheEntryUpdated(event);
 757  54
                 } else if (eventType.equals(CacheEntryEventType.ENTRY_FLUSHED)) {
 758  54
                     ((CacheEntryEventListener) listeners[i + 1]).cacheEntryFlushed(event);
 759  0
                 } else if (eventType.equals(CacheEntryEventType.ENTRY_REMOVED)) {
 760  0
                     ((CacheEntryEventListener) listeners[i + 1]).cacheEntryRemoved(event);
 761   
                 }
 762   
             }
 763   
         }
 764   
     }
 765   
 
 766   
     /**
 767   
      * Dispatch a cache group event to all registered listeners.
 768   
      *
 769   
      * @param eventType The type of event (this is used to branch to the correct method handler)
 770   
      * @param group     The cache group that the event applies to
 771   
      * @param origin      The origin of this event (optional)
 772   
      */
 773  36
     private void dispatchCacheGroupEvent(CacheEntryEventType eventType, String group, String origin) {
 774  36
         CacheGroupEvent event = new CacheGroupEvent(this, group, origin);
 775   
 
 776   
         // Guaranteed to return a non-null array
 777  36
         Object[] listeners = listenerList.getListenerList();
 778   
 
 779   
         // Process the listeners last to first, notifying
 780   
         // those that are interested in this event
 781  36
         for (int i = listeners.length - 2; i >= 0; i -= 2) {
 782  72
             if (listeners[i] == CacheEntryEventListener.class) {
 783  36
                 if (eventType.equals(CacheEntryEventType.GROUP_FLUSHED)) {
 784  36
                     ((CacheEntryEventListener) listeners[i + 1]).cacheGroupFlushed(event);
 785   
                 }
 786   
             }
 787   
         }
 788   
     }
 789   
 
 790   
     /**
 791   
      * Dispatch a cache map access event to all registered listeners.
 792   
      *
 793   
      * @param eventType     The type of event
 794   
      * @param entry         The entry that was affected.
 795   
      * @param origin        The origin of this event (optional)
 796   
      */
 797  2000384
     private void dispatchCacheMapAccessEvent(CacheMapAccessEventType eventType, CacheEntry entry, String origin) {
 798  2000384
         CacheMapAccessEvent event = new CacheMapAccessEvent(eventType, entry, origin);
 799   
 
 800   
         // Guaranteed to return a non-null array
 801  2000384
         Object[] listeners = listenerList.getListenerList();
 802   
 
 803   
         // Process the listeners last to first, notifying
 804   
         // those that are interested in this event
 805  2000384
         for (int i = listeners.length - 2; i >= 0; i -= 2) {
 806  368
             if (listeners[i] == CacheMapAccessEventListener.class) {
 807  184
                 ((CacheMapAccessEventListener) listeners[i + 1]).accessed(event);
 808   
             }
 809   
         }
 810   
     }
 811   
 
 812   
     /**
 813   
      * Dispatch a cache pattern 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 pattern     The cache pattern that the event applies to
 817   
      * @param origin      The origin of this event (optional)
 818   
      */
 819  16
     private void dispatchCachePatternEvent(CacheEntryEventType eventType, String pattern, String origin) {
 820  16
         CachePatternEvent event = new CachePatternEvent(this, pattern, origin);
 821   
 
 822   
         // Guaranteed to return a non-null array
 823  16
         Object[] listeners = listenerList.getListenerList();
 824   
 
 825   
         // Process the listeners last to first, notifying
 826   
         // those that are interested in this event
 827  16
         for (int i = listeners.length - 2; i >= 0; i -= 2) {
 828  32
             if (listeners[i] == CacheEntryEventListener.class) {
 829  16
                 if (eventType.equals(CacheEntryEventType.PATTERN_FLUSHED)) {
 830  16
                     ((CacheEntryEventListener) listeners[i + 1]).cachePatternFlushed(event);
 831   
                 }
 832   
             }
 833   
         }
 834   
     }
 835   
 
 836   
     /**
 837   
      * Dispatches a cache-wide event to all registered listeners.
 838   
      *
 839   
      * @param eventType The type of event (this is used to branch to the correct method handler)
 840   
      * @param origin The origin of this event (optional)
 841   
      */
 842  0
     private void dispatchCachewideEvent(CachewideEventType eventType, Date date, String origin) {
 843  0
         CachewideEvent event = new CachewideEvent(this, date, origin);
 844   
 
 845   
         // Guaranteed to return a non-null array
 846  0
         Object[] listeners = listenerList.getListenerList();
 847   
 
 848   
         // Process the listeners last to first, notifying
 849   
         // those that are interested in this event
 850  0
         for (int i = listeners.length - 2; i >= 0; i -= 2) {
 851  0
             if (listeners[i] == CacheEntryEventListener.class) {
 852  0
                 if (eventType.equals(CachewideEventType.CACHE_FLUSHED)) {
 853  0
                     ((CacheEntryEventListener) listeners[i + 1]).cacheFlushed(event);
 854   
                 }
 855   
             }
 856   
         }
 857   
     }
 858   
 
 859   
     /**
 860   
      * Flush a cache entry. On completion of the flush, a
 861   
      * <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired.
 862   
      *
 863   
      * @param entry The entry to flush
 864   
      * @param origin The origin of this flush event (optional)
 865   
      */
 866  58
     private void flushEntry(CacheEntry entry, String origin) {
 867  58
         String key = entry.getKey();
 868   
 
 869   
         // Flush the object itself
 870  58
         entry.flush();
 871   
 
 872  58
         if (!entry.isNew()) {
 873   
             // Update the entry's state in the map
 874  58
             cacheMap.put(key, entry);
 875   
         }
 876   
 
 877   
         // Trigger an ENTRY_FLUSHED event. [CACHE-107] Do this for all flushes.
 878  58
         if (listenerList.getListenerCount() > 0) {
 879  54
             CacheEntryEvent event = new CacheEntryEvent(this, entry, origin);
 880  54
             dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_FLUSHED, event);
 881   
         }
 882   
     }
 883   
 }
 884