/*
 * Copyright (c) 2002-2003 by OpenSymphony
 * All rights reserved.
 */
package com.opensymphony.oscache.base;

import com.opensymphony.oscache.base.algorithm.AbstractConcurrentReadCache;
import com.opensymphony.oscache.base.algorithm.LRUCache;
import com.opensymphony.oscache.base.algorithm.UnlimitedCache;
import com.opensymphony.oscache.base.events.*;
import com.opensymphony.oscache.base.persistence.PersistenceListener;
import com.opensymphony.oscache.util.FastCronParser;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.Serializable;

import java.text.ParseException;

import java.util.*;

import javax.swing.event.EventListenerList;

/**
 * Provides an interface to the cache itself. Creating an instance of this class
 * will create a cache that behaves according to its construction parameters.
 * The public API provides methods to manage objects in the cache and configure
 * any cache event listeners.
 *
 * @version        $Revision: 1.6 $
 * @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
 * @author <a href="mailto:tgochenour@peregrine.com">Todd Gochenour</a>
 * @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
 * @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
 */
public class Cache implements Serializable {
    /**
     * An event that origininated from within another event.
     */
    public static final String NESTED_EVENT = "NESTED";
    private static transient final Log log = LogFactory.getLog(Cache.class);

    /**
     * A list of all registered event listeners for this cache.
     */
    protected EventListenerList listenerList = new EventListenerList();

    /**
     * The actual cache map. This is where the cached objects are held.
     */
    private AbstractConcurrentReadCache cacheMap = null;

    /**
     * Date of last complete cache flush.
     */
    private Date flushDateTime = null;

    /**
     * A set that holds keys of cache entries that are currently being built.
     * The cache checks against this map when a stale entry is requested.
     * If the requested key is in here, we know the entry is currently being
     * built by another thread and hence we can either block and wait or serve
     * the stale entry (depending on whether cache blocking is enabled or not).
     * <p>
     * We need to isolate these here since the actual CacheEntry
     * objects may not normally be held in memory at all (eg, if no
     * memory cache is configured).
     */
    private Map updateStates = new HashMap();

    /**
     * Indicates whether the cache blocks requests until new content has
     * been generated or just serves stale content instead.
     */
    private boolean blocking = false;

    /**
     * Create a new Cache
     *
     * @param useMemoryCaching Specify if the memory caching is going to be used
     * @param unlimitedDiskCache Specify if the disk caching is unlimited
     */
    public Cache(boolean useMemoryCaching, boolean unlimitedDiskCache) {
        this(useMemoryCaching, unlimitedDiskCache, false, null, 0);
    }

    /**
     * Create a new Cache.
     *
     * If a valid algorithm class is specified, it will be used for this cache.
     * Otherwise if a capacity is specified, it will use LRUCache.
     * If no algorithm or capacity is specified UnlimitedCache is used.
     *
     * @see com.opensymphony.oscache.base.algorithm.LRUCache
     * @see com.opensymphony.oscache.base.algorithm.UnlimitedCache
     * @param useMemoryCaching Specify if the memory caching is going to be used
     * @param unlimitedDiskCache Specify if the disk caching is unlimited
     * @param blocking This parameter takes effect when a cache entry has
     * just expired and several simultaneous requests try to retrieve it. While
     * one request is rebuilding the content, the other requests will either
     * block and wait for the new content (<code>blocking == true</code>) or
     * instead receive a copy of the stale content so they don't have to wait
     * (<code>blocking == false</code>). the default is <code>false</code>,
     * which provides better performance but at the expense of slightly stale
     * data being served.
     * @param algorithmClass The class implementing the desired algorithm
     * @param capacity The capacity
     */
    public Cache(boolean useMemoryCaching, boolean unlimitedDiskCache, boolean blocking, String algorithmClass, int capacity) {
        // Instantiate the algo class if valid
        if (((algorithmClass != null) && (algorithmClass.length() > 0)) && (capacity > 0)) {
            try {
                cacheMap = (AbstractConcurrentReadCache) Class.forName(algorithmClass).newInstance();
                cacheMap.setMaxEntries(capacity);
            } catch (Exception e) {
                log.error("Invalid class name for cache algorithm class. " + e.toString());
            }
        }

        if (cacheMap == null) {
            // If we have a capacity, use LRU cache otherwise use unlimited Cache
            if (capacity > 0) {
                cacheMap = new LRUCache(capacity);
            } else {
                cacheMap = new UnlimitedCache();
            }
        }

        cacheMap.setUnlimitedDiskCache(unlimitedDiskCache);
        cacheMap.setMemoryCaching(useMemoryCaching);

        this.blocking = blocking;
    }

    /**
     * Allows the capacity of the cache to be altered dynamically. Note that
     * some cache implementations may choose to ignore this setting (eg the
     * {@link UnlimitedCache} ignores this call).
     *
     * @param capacity the maximum number of items to hold in the cache.
     */
    public void setCapacity(int capacity) {
        cacheMap.setMaxEntries(capacity);
    }

    /**
     * Checks if the cache was flushed more recently than the CacheEntry provided.
     * Used to determine whether to refresh the particular CacheEntry.
     *
     * @param cacheEntry The cache entry which we're seeing whether to refresh
     * @return Whether or not the cache has been flushed more recently than this cache entry was updated.
     */
    public boolean isFlushed(CacheEntry cacheEntry) {
        if (flushDateTime != null) {
            long lastUpdate = cacheEntry.getLastUpdate();

            return (flushDateTime.getTime() >= lastUpdate);
        } else {
            return false;
        }
    }

    /**
     * Retrieve an object from the cache specifying its key.
     *
     * @param key             Key of the object in the cache.
     *
     * @return The object from cache
     *
     * @throws NeedsRefreshException Thrown when the object either
     * doesn't exist, or exists but is stale. When this exception occurs,
     * the CacheEntry corresponding to the supplied key will be locked
     * and other threads requesting this entry will potentially be blocked
     * until the caller repopulates the cache. If the caller choses not
     * to repopulate the cache, they <em>must</em> instead call
     * {@link #cancelUpdate(String)}.
     */
    public Object getFromCache(String key) throws NeedsRefreshException {
        return getFromCache(key, CacheEntry.INDEFINITE_EXPIRY, null);
    }

    /**
     * Retrieve an object from the cache specifying its key.
     *
     * @param key             Key of the object in the cache.
     * @param refreshPeriod   How long before the object needs refresh. To
     * allow the object to stay in the cache indefinitely, supply a value
     * of {@link CacheEntry#INDEFINITE_EXPIRY}.
     *
     * @return The object from cache
     *
     * @throws NeedsRefreshException Thrown when the object either
     * doesn't exist, or exists but is stale. When this exception occurs,
     * the CacheEntry corresponding to the supplied key will be locked
     * and other threads requesting this entry will potentially be blocked
     * until the caller repopulates the cache. If the caller choses not
     * to repopulate the cache, they <em>must</em> instead call
     * {@link #cancelUpdate(String)}.
     */
    public Object getFromCache(String key, int refreshPeriod) throws NeedsRefreshException {
        return getFromCache(key, refreshPeriod, null);
    }

    /**
     * Retrieve an object from the cache specifying its key.
     *
     * @param key             Key of the object in the cache.
     * @param refreshPeriod   How long before the object needs refresh. To
     * allow the object to stay in the cache indefinitely, supply a value
     * of {@link CacheEntry#INDEFINITE_EXPIRY}.
     * @param cronExpiry      A cron expression that specifies fixed date(s)
     *                        and/or time(s) that this cache entry should
     *                        expire on.
     *
     * @return The object from cache
     *
     * @throws NeedsRefreshException Thrown when the object either
     * doesn't exist, or exists but is stale. When this exception occurs,
     * the CacheEntry corresponding to the supplied key will be locked
     * and other threads requesting this entry will potentially be blocked
     * until the caller repopulates the cache. If the caller choses not
     * to repopulate the cache, they <em>must</em> instead call
     * {@link #cancelUpdate(String)}.
     */
    public Object getFromCache(String key, int refreshPeriod, String cronExpiry) throws NeedsRefreshException {
        CacheEntry cacheEntry = this.getCacheEntry(key, null, null);

        Object content = cacheEntry.getContent();
        CacheMapAccessEventType accessEventType = CacheMapAccessEventType.HIT;

        boolean reload = false;

        // Check if this entry has expired or has not yet been added to the cache. If
        // so, we need to decide whether to block, serve stale content or throw a
        // NeedsRefreshException
        if (this.isStale(cacheEntry, refreshPeriod, cronExpiry)) {
            EntryUpdateState updateState = getUpdateState(key);

            synchronized (updateState) {
                if (updateState.isAwaitingUpdate() || updateState.isCancelled()) {
                    // No one else is currently updating this entry - grab ownership
                    updateState.startUpdate();

                    if (cacheEntry.isNew()) {
                        accessEventType = CacheMapAccessEventType.MISS;
                    } else {
                        accessEventType = CacheMapAccessEventType.STALE_HIT;
                    }
                } else if (updateState.isUpdating()) {
                    // Another thread is already updating the cache. We block if this
                    // is a new entry, or blocking mode is enabled. Either putInCache()
                    // or cancelUpdate() can cause this thread to resume.
                    if (cacheEntry.isNew() || blocking) {
                        do {
                            try {
                                updateState.wait();
                            } catch (InterruptedException e) {
                            }
                        } while (updateState.isUpdating());

                        if (updateState.isCancelled()) {
                            // The updating thread cancelled the update, let this one have a go.
                            updateState.startUpdate();

                            // We put the updateState object back into the updateStates map so
                            // any remaining threads waiting on this cache entry will be notified
                            // once this thread has done its thing (either updated the cache or
                            // cancelled the update). Without this code they'll get left hanging...
                            synchronized (updateStates) {
                                updateStates.put(key, updateState);
                            }

                            if (cacheEntry.isNew()) {
                                accessEventType = CacheMapAccessEventType.MISS;
                            } else {
                                accessEventType = CacheMapAccessEventType.STALE_HIT;
                            }
                        } else if (updateState.isComplete()) {
                            reload = true;
                        } else {
                            log.error("Invalid update state for cache entry " + key);
                        }
                    }
                } else {
                    reload = true;
                }
            }
        }

        // If reload is true then another thread must have successfully rebuilt the cache entry
        if (reload) {
            cacheEntry = (CacheEntry) cacheMap.get(key);

            if (cacheEntry != null) {
                content = cacheEntry.getContent();
            } else {
                log.error("Could not reload cache entry after waiting for it to be rebuilt");
            }
        }

        dispatchCacheMapAccessEvent(accessEventType, cacheEntry, null);

        // If we didn't end up getting a hit then we need to throw a NRE
        if (accessEventType != CacheMapAccessEventType.HIT) {
            throw new NeedsRefreshException(content);
        }

        return content;
    }

    /**
     * Set the listener to use for data persistence. Only one
     * <code>PersistenceListener</code> can be configured per cache.
     *
     * @param listener The implementation of a persistance listener
     */
    public void setPersistenceListener(PersistenceListener listener) {
        cacheMap.setPersistenceListener(listener);
    }

    /**
     * Retrieves the currently configured <code>PersistenceListener</code>.
     *
     * @return the cache's <code>PersistenceListener</code>, or <code>null</code>
     * if no listener is configured.
     */
    public PersistenceListener getPersistenceListener() {
        return cacheMap.getPersistenceListener();
    }

    /**
     * Register a listener for Cache events. The listener must implement
     * one of the child interfaces of the {@link CacheEventListener} interface.
     *
     * @param listener  The object that listens to events.
     */
    public void addCacheEventListener(CacheEventListener listener, Class clazz) {
        if (CacheEventListener.class.isAssignableFrom(clazz)) {
            listenerList.add(clazz, listener);
        } else {
            log.error("The class '" + clazz.getName() + "' is not a CacheEventListener. Ignoring this listener.");
        }
    }

    /**
     * Cancels any pending update for this cache entry. This should <em>only</em>
     * be called by the thread that is responsible for performing the update ie
     * the thread that received the original {@link NeedsRefreshException}.<p/>
     * If a cache entry is not updated (via {@link #putInCache} and this method is
     * not called to let OSCache know the update will not be forthcoming, subsequent
     * requests for this cache entry will either block indefinitely (if this is a new
     * cache entry or cache.blocking=true), or forever get served stale content. Note
     * however that there is no harm in cancelling an update on a key that either
     * does not exist or is not currently being updated.
     *
     * @param key The key for the cache entry in question.
     */
    public void cancelUpdate(String key) {
        EntryUpdateState state;

        if (key != null) {
            synchronized (updateStates) {
                state = (EntryUpdateState) updateStates.remove(key);

                if (state != null) {
                    synchronized (state) {
                        state.cancelUpdate();
                        state.notify();
                    }
                }
            }
        }
    }

    /**
     * Flush all entries in the cache on the given date/time.
     *
     * @param date The date at which all cache entries will be flushed.
     */
    public void flushAll(Date date) {
        flushAll(date, null);
    }

    /**
     * Flush all entries in the cache on the given date/time.
     *
     * @param date The date at which all cache entries will be flushed.
     * @param origin The origin of this flush request (optional)
     */
    public void flushAll(Date date, String origin) {
        flushDateTime = date;
        dispatchCachewideEvent(CachewideEventType.CACHE_FLUSHED, date, origin);
    }

    /**
     * Flush the cache entry (if any) that corresponds to the cache key supplied.
     * This call will flush the entry from the cache and remove the references to
     * it from any cache groups that it is a member of. On completion of the flush,
     * a <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired.
     *
     * @param key The key of the entry to flush
     */
    public void flushEntry(String key) {
        flushEntry(key, null);
    }

    /**
     * Flush the cache entry (if any) that corresponds to the cache key supplied.
     * This call will mark the cache entry as flushed so that the next access
     * to it will cause a {@link NeedsRefreshException}. On completion of the
     * flush, a <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired.
     *
     * @param key The key of the entry to flush
     * @param origin The origin of this flush request (optional)
     */
    public void flushEntry(String key, String origin) {
        flushEntry(getCacheEntry(key, null, origin), origin);
    }

    /**
     * Flushes all objects that belong to the supplied group. On completion
     * this method fires a <tt>CacheEntryEventType.GROUP_FLUSHED</tt> event.
     *
     * @param group The group to flush
     */
    public void flushGroup(String group) {
        flushGroup(group, null);
    }

    /**
     * Flushes all unexpired objects that belong to the supplied group. On
     * completion this method fires a <tt>CacheEntryEventType.GROUP_FLUSHED</tt>
     * event.
     *
     * @param group The group to flush
     * @param origin The origin of this flush event (optional)
     */
    public void flushGroup(String group, String origin) {
        // Flush all objects in the group
        Set groupEntries = cacheMap.getGroup(group);

        if (groupEntries != null) {
            Iterator itr = groupEntries.iterator();
            String key;
            CacheEntry entry;

            while (itr.hasNext()) {
                key = (String) itr.next();
                entry = (CacheEntry) cacheMap.get(key);

                if ((entry != null) && !entry.needsRefresh(CacheEntry.INDEFINITE_EXPIRY)) {
                    flushEntry(entry, NESTED_EVENT);
                }
            }
        }

        dispatchCacheGroupEvent(CacheEntryEventType.GROUP_FLUSHED, group, origin);
    }

    /**
     * Flush all entries with keys that match a given pattern
     *
     * @param  pattern The key must contain this given value
     * @deprecated For performance and flexibility reasons it is preferable to
     * store cache entries in groups and use the {@link #flushGroup(String)} method
     * instead of relying on pattern flushing.
     */
    public void flushPattern(String pattern) {
        flushPattern(pattern, null);
    }

    /**
     * Flush all entries with keys that match a given pattern
     *
     * @param  pattern The key must contain this given value
     * @param origin The origin of this flush request
     * @deprecated For performance and flexibility reasons it is preferable to
     * store cache entries in groups and use the {@link #flushGroup(String, String)}
     * method instead of relying on pattern flushing.
     */
    public void flushPattern(String pattern, String origin) {
        // Check the pattern
        if ((pattern != null) && (pattern.length() > 0)) {
            String key = null;
            CacheEntry entry = null;
            Iterator itr = cacheMap.keySet().iterator();

            while (itr.hasNext()) {
                key = (String) itr.next();

                if (key.indexOf(pattern) >= 0) {
                    entry = (CacheEntry) cacheMap.get(key);

                    if (entry != null) {
                        flushEntry(entry, origin);
                    }
                }
            }

            dispatchCachePatternEvent(CacheEntryEventType.PATTERN_FLUSHED, pattern, origin);
        } else {
            // Empty pattern, nothing to do
        }
    }

    /**
     * Put an object in the cache specifying the key to use.
     *
     * @param key       Key of the object in the cache.
     * @param content   The object to cache.
     */
    public void putInCache(String key, Object content) {
        putInCache(key, content, null, null, null);
    }

    /**
     * Put an object in the cache specifying the key and refresh policy to use.
     *
     * @param key       Key of the object in the cache.
     * @param content   The object to cache.
     * @param policy   Object that implements refresh policy logic
     */
    public void putInCache(String key, Object content, EntryRefreshPolicy policy) {
        putInCache(key, content, null, policy, null);
    }

    /**
     * Put in object into the cache, specifying both the key to use and the
     * cache groups the object belongs to.
     *
     * @param key       Key of the object in the cache
     * @param content   The object to cache
     * @param groups    The cache groups to add the object to
     */
    public void putInCache(String key, Object content, String[] groups) {
        putInCache(key, content, groups, null, null);
    }

    /**
     * Put an object into the cache specifying both the key to use and the
     * cache groups the object belongs to.
     *
     * @param key       Key of the object in the cache
     * @param groups    The cache groups to add the object to
     * @param content   The object to cache
     * @param policy    Object that implements the refresh policy logic
     */
    public void putInCache(String key, Object content, String[] groups, EntryRefreshPolicy policy, String origin) {
        CacheEntry cacheEntry = this.getCacheEntry(key, policy, origin);
        boolean isNewEntry = cacheEntry.isNew();
        cacheEntry.setContent(content);
        cacheEntry.setGroups(groups);
        cacheMap.put(key, cacheEntry);

        // Signal to any threads waiting on this update that it's now ready for them
        // in the cache!
        completeUpdate(key);

        CacheEntryEvent event = new CacheEntryEvent(this, cacheEntry, origin);

        if (isNewEntry) {
            dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_ADDED, event);
        } else {
            dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_UPDATED, event);
        }
    }

    /**
     * Unregister a listener for Cache events.
     *
     * @param listener  The object that currently listens to events.
     */
    public void removeCacheEventListener(CacheEventListener listener, Class clazz) {
        listenerList.remove(clazz, listener);
    }

    /**
     * Get an entry from this cache or create one if it doesn't exist.
     *
     * @param key    The key of the cache entry
     * @param policy Object that implements refresh policy logic
     * @param origin The origin of request (optional)
     * @return CacheEntry for the specified key.
     */
    protected CacheEntry getCacheEntry(String key, EntryRefreshPolicy policy, String origin) {
        CacheEntry cacheEntry = null;

        // Verify that the key is valid
        if ((key == null) || (key.length() == 0)) {
            throw new IllegalArgumentException("getCacheEntry called with an empty or null key");
        }

        cacheEntry = (CacheEntry) cacheMap.get(key);

        // if the cache entry does not exist, create a new one
        if (cacheEntry == null) {
            if (log.isDebugEnabled()) {
                log.debug("No cache entry exists for key='" + key + "', creating");
            }

            cacheEntry = new CacheEntry(key, policy);
        }

        return cacheEntry;
    }

    /**
     * Indicates whether or not the cache entry is stale.
     *
     * @param cacheEntry     The cache entry to test the freshness of.
     * @param refreshPeriod  The maximum allowable age of the entry, in seconds.
     * @param cronExpiry     A cron expression specifying absolute date(s) and/or time(s)
     * that the cache entry should expire at. If the cache entry was refreshed prior to
     * the most recent match for the cron expression, the entry will be considered stale.
     *
     * @return <code>true</code> if the entry is stale, <code>false</code> otherwise.
     */
    protected boolean isStale(CacheEntry cacheEntry, int refreshPeriod, String cronExpiry) {
        boolean result = cacheEntry.needsRefresh(refreshPeriod) || isFlushed(cacheEntry);

        if ((cronExpiry != null) && (cronExpiry.length() > 0)) {
            try {
                FastCronParser parser = new FastCronParser(cronExpiry);
                result = result || parser.hasMoreRecentMatch(cacheEntry.getLastUpdate());
            } catch (ParseException e) {
                log.warn(e);
            }
        }

        return result;
    }

    /**
     * Get the updating cache entry from the update map. If one is not found,
     * create a new one (with state {@link EntryUpdateState#NOT_YET_UPDATING})
     * and add it to the map.
     *
     * @param key The cache key for this entry
     *
     * @return the CacheEntry that was found (or added to) the updatingEntries
     * map.
     */
    protected EntryUpdateState getUpdateState(String key) {
        EntryUpdateState updateState;

        synchronized (updateStates) {
            // Try to find the matching state object in the updating entry map.
            updateState = (EntryUpdateState) updateStates.get(key);

            if (updateState == null) {
                // It's not there so add it.
                updateState = new EntryUpdateState();
                updateStates.put(key, updateState);
            }
        }

        return updateState;
    }

    /**
     * Completely clears the cache.
     */
    protected void clear() {
        cacheMap.clear();
    }

    /**
     * Removes the update state for the specified key and notifies any other
     * threads that are waiting on this object. This is called automatically
     * by the {@link #putInCache} method.
     *
     * @param key The cache key that is no longer being updated.
     */
    protected void completeUpdate(String key) {
        EntryUpdateState state;

        synchronized (updateStates) {
            state = (EntryUpdateState) updateStates.remove(key);

            if (state != null) {
                synchronized (state) {
                    state.completeUpdate();
                    state.notifyAll();
                }
            }
        }
    }

    /**
     * Completely removes a cache entry from the cache and its associated cache
     * groups.
     *
     * @param key The key of the entry to remove.
     */
    protected void removeEntry(String key) {
        removeEntry(key, null);
    }

    /**
     * Completely removes a cache entry from the cache and its associated cache
     * groups.
     *
     * @param key    The key of the entry to remove.
     * @param origin The origin of this remove request.
     */
    protected void removeEntry(String key, String origin) {
        CacheEntry cacheEntry = (CacheEntry) cacheMap.get(key);
        cacheMap.remove(key);

        CacheEntryEvent event = new CacheEntryEvent(this, cacheEntry, origin);
        dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_REMOVED, event);
    }

    /**
     * Dispatch a cache entry event to all registered listeners.
     *
     * @param eventType   The type of event (used to branch on the proper method)
     * @param event       The event that was fired
     */
    private void dispatchCacheEntryEvent(CacheEntryEventType eventType, CacheEntryEvent event) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();

        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == CacheEntryEventListener.class) {
                if (eventType.equals(CacheEntryEventType.ENTRY_ADDED)) {
                    ((CacheEntryEventListener) listeners[i + 1]).cacheEntryAdded(event);
                } else if (eventType.equals(CacheEntryEventType.ENTRY_UPDATED)) {
                    ((CacheEntryEventListener) listeners[i + 1]).cacheEntryUpdated(event);
                } else if (eventType.equals(CacheEntryEventType.ENTRY_FLUSHED)) {
                    ((CacheEntryEventListener) listeners[i + 1]).cacheEntryFlushed(event);
                } else if (eventType.equals(CacheEntryEventType.ENTRY_REMOVED)) {
                    ((CacheEntryEventListener) listeners[i + 1]).cacheEntryRemoved(event);
                }
            }
        }
    }

    /**
     * Dispatch a cache group event to all registered listeners.
     *
     * @param eventType The type of event (this is used to branch to the correct method handler)
     * @param group     The cache group that the event applies to
     * @param origin      The origin of this event (optional)
     */
    private void dispatchCacheGroupEvent(CacheEntryEventType eventType, String group, String origin) {
        CacheGroupEvent event = new CacheGroupEvent(this, group, origin);

        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();

        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == CacheEntryEventListener.class) {
                if (eventType.equals(CacheEntryEventType.GROUP_FLUSHED)) {
                    ((CacheEntryEventListener) listeners[i + 1]).cacheGroupFlushed(event);
                }
            }
        }
    }

    /**
     * Dispatch a cache map access event to all registered listeners.
     *
     * @param eventType     The type of event
     * @param entry         The entry that was affected.
     * @param origin        The origin of this event (optional)
     */
    private void dispatchCacheMapAccessEvent(CacheMapAccessEventType eventType, CacheEntry entry, String origin) {
        CacheMapAccessEvent event = new CacheMapAccessEvent(eventType, entry, origin);

        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();

        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == CacheMapAccessEventListener.class) {
                ((CacheMapAccessEventListener) listeners[i + 1]).accessed(event);
            }
        }
    }

    /**
     * Dispatch a cache pattern event to all registered listeners.
     *
     * @param eventType The type of event (this is used to branch to the correct method handler)
     * @param pattern     The cache pattern that the event applies to
     * @param origin      The origin of this event (optional)
     */
    private void dispatchCachePatternEvent(CacheEntryEventType eventType, String pattern, String origin) {
        CachePatternEvent event = new CachePatternEvent(this, pattern, origin);

        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();

        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == CacheEntryEventListener.class) {
                if (eventType.equals(CacheEntryEventType.PATTERN_FLUSHED)) {
                    ((CacheEntryEventListener) listeners[i + 1]).cachePatternFlushed(event);
                }
            }
        }
    }

    /**
     * Dispatches a cache-wide event to all registered listeners.
     *
     * @param eventType The type of event (this is used to branch to the correct method handler)
     * @param origin The origin of this event (optional)
     */
    private void dispatchCachewideEvent(CachewideEventType eventType, Date date, String origin) {
        CachewideEvent event = new CachewideEvent(this, date, origin);

        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();

        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == CacheEntryEventListener.class) {
                if (eventType.equals(CachewideEventType.CACHE_FLUSHED)) {
                    ((CacheEntryEventListener) listeners[i + 1]).cacheFlushed(event);
                }
            }
        }
    }

    /**
     * Flush a cache entry. On completion of the flush, a
     * <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired.
     *
     * @param entry The entry to flush
     * @param origin The origin of this flush event (optional)
     */
    private void flushEntry(CacheEntry entry, String origin) {
        String key = entry.getKey();

        // Flush the object itself
        entry.flush();

        if (!entry.isNew()) {
            // Update the entry's state in the map
            cacheMap.put(key, entry);

            // Trigger an ENTRY_FLUSHED event
            CacheEntryEvent event = new CacheEntryEvent(this, entry, origin);
            dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_FLUSHED, event);
        } else {
            // The entry did not exist in the cache anyway, nothing to flush
            // TODO: Do we need an event for this?
        }
    }
}
