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