Anonymous avatar Anonymous committed 1f419a0

Initial revision

Comments (0)

Files changed (8)

src/plugins/clustersupport/java/com/opensymphony/oscache/plugins/clustersupport/BroadcastingCacheEventListener.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.oscache.plugins.clustersupport;
+
+import com.opensymphony.oscache.base.*;
+import com.opensymphony.oscache.base.events.*;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Implementation of a CacheEntryEventListener. It broadcasts the flush events
+ * to other listening caches. This allows caches to be clustered.
+ *
+ * @version        $Revision$
+ * @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
+ */
+public class BroadcastingCacheEventListener implements CacheEntryEventListener, LifecycleAware {
+    private final static Log log = LogFactory.getLog(BroadcastingCacheEventListener.class);
+    static ClusterManager cm;
+
+    /**
+     * Reference count to keep track of how many instances of this listener
+     * are in existence. Once this listener is no longer used we can shut
+     * down the cluster manager.
+     */
+    static int referenceCount = 0;
+
+    public BroadcastingCacheEventListener() {
+        log.info("BroadcastingCacheEventListener registered");
+    }
+
+    // --------------------------------------------------------
+    // The remaining events are of no interest to this listener
+    // --------------------------------------------------------
+    public void cacheEntryAdded(CacheEntryEvent event) {
+    }
+
+    /**
+     * Event fired when an entry is flushed from the cache. This broadcasts
+     * the flush message to any listening nodes on the network.
+     */
+    public void cacheEntryFlushed(CacheEntryEvent event) {
+        Cache cache = event.getMap();
+
+        if ((cache.getName() != null) && !Cache.NESTED_EVENT.equals(event.getOrigin()) && !ClusterManager.CLUSTER_ORIGIN.equals(event.getOrigin())) {
+            if (log.isDebugEnabled()) {
+                log.debug("cacheEntryFlushed called (" + event + ")");
+            }
+
+            cm.signalEntryFlush(event.getKey(), cache.getName());
+        }
+    }
+
+    public void cacheEntryRemoved(CacheEntryEvent event) {
+    }
+
+    public void cacheEntryUpdated(CacheEntryEvent event) {
+    }
+
+    public void cacheGroupAdded(CacheGroupEvent event) {
+    }
+
+    public void cacheGroupEntryAdded(CacheGroupEvent event) {
+    }
+
+    public void cacheGroupEntryRemoved(CacheGroupEvent event) {
+    }
+
+    /**
+     * Event fired when an entry is removed from the cache. This broadcasts
+     * the remove method to any listening nodes on the network, as long as
+     * this event wasn't from a broadcast in the first place. The
+     */
+    public void cacheGroupFlushed(CacheGroupEvent event) {
+        Cache cache = event.getMap();
+
+        if ((cache.getName() != null) && !Cache.NESTED_EVENT.equals(event.getOrigin()) && !ClusterManager.CLUSTER_ORIGIN.equals(event.getOrigin())) {
+            if (log.isDebugEnabled()) {
+                log.debug("cacheGroupFushed called (" + event + ")");
+            }
+
+            cm.signalGroupFlush(event.getGroup(), cache.getName());
+        }
+    }
+
+    public void cacheGroupRemoved(CacheGroupEvent event) {
+    }
+
+    public void cacheGroupUpdated(CacheGroupEvent event) {
+    }
+
+    public void cachePatternFlushed(CachePatternEvent event) {
+        Cache cache = event.getMap();
+
+        if ((cache.getName() != null) && !Cache.NESTED_EVENT.equals(event.getOrigin()) && !ClusterManager.CLUSTER_ORIGIN.equals(event.getOrigin())) {
+            if (log.isDebugEnabled()) {
+                log.debug("cachePatternFushed called (" + event + ")");
+            }
+
+            cm.signalPatternFlush(event.getPattern(), cache.getName());
+        }
+    }
+
+    public void cacheFlushed(CachewideEvent event) {
+        Cache cache = event.getMap();
+
+        if ((cache.getName() != null) && !Cache.NESTED_EVENT.equals(event.getOrigin()) && !ClusterManager.CLUSTER_ORIGIN.equals(event.getOrigin())) {
+            if (log.isDebugEnabled()) {
+                log.debug("cacheFushed called (" + event + ")");
+            }
+
+            cm.signalCacheFlush(cache.getName());
+        }
+    }
+
+    /**
+     * Shuts down the {@link ClusterManager} instance being managed by this listener
+     * once this listener is no longer in use.
+     *
+     * @throws FinalizationException
+     */
+    public synchronized void finialize() throws FinalizationException {
+        referenceCount--;
+
+        if (referenceCount == 0) {
+            cm.shutdown();
+        }
+    }
+
+    /**
+     * Initializes the broadcasting listener by creating a {@link ClusterManager}
+     * instance to handle incoming and outgoing messages. If this listener is
+     * used multiple times, it is reference-counted so we know when to shutdown
+     * the <code>ClusterManagaer</code> again.
+     *
+     * @param config An OSCache configuration object.
+     * @throws InitializationException If this listener has already been initialized.
+     */
+    public synchronized void initialize(AbstractCacheAdministrator admin, Config config) throws InitializationException {
+        if (referenceCount == 0) {
+            cm = new ClusterManager(config);
+            cm.setAdministrator(admin);
+        }
+
+        referenceCount++;
+    }
+}

src/plugins/clustersupport/java/com/opensymphony/oscache/plugins/clustersupport/ClusterManager.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.oscache.plugins.clustersupport;
+
+import com.opensymphony.oscache.base.*;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.javagroups.Address;
+import org.javagroups.Channel;
+
+import org.javagroups.blocks.NotificationBus;
+
+import java.io.Serializable;
+
+/**
+ * Handles the sending of notification messages across the cluster. This
+ * implementation is based on the JavaGroups library.
+ *
+ * @version        $Revision$
+ * @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
+ */
+public final class ClusterManager implements NotificationBus.Consumer {
+    private static final String BUS_NAME = "OSCacheBus";
+    private static final String CHANNEL_PROPERTIES = "cache.cluster.properties";
+    private static final String MULTICAST_IP_PROPERTY = "cache.cluster.multicast.ip";
+
+    /**
+     * The name to use for the origin of cluster events. Using this ensures
+     * events are not fired recursively back over the cluster.
+     */
+    static final String CLUSTER_ORIGIN = "CLUSTER";
+
+    /**
+     * The first half of the default channel properties. They default channel properties are:
+     * <pre>
+     * UDP(mcast_addr=*.*.*.*;mcast_port=45566;ip_ttl=32;mcast_send_buf_size=150000;mcast_recv_buf_size=80000):PING(timeout=2000;num_initial_members=3):MERGE2(min_interval=5000;max_interval=10000):FD_SOCK:VERIFY_SUSPECT(timeout=1500):pbcast.STABLE(desired_avg_gossip=20000):pbcast.NAKACK(gc_lag=50;retransmit_timeout=300,600,1200,2400,4800):UNICAST(timeout=5000):FRAG(frag_size=8096;down_thread=false;up_thread=false):pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;shun=false;print_local_addr=true)
+     * </pre>
+     * Where <code>*.*.*.*</code> is the specified multicast IP, which defaults to <code>231.12.21.132</code>.
+     */
+    private static final String DEFAULT_CHANNEL_PROPERTIES_PRE = "UDP(mcast_addr=";
+
+    /**
+     * The second half of the default channel properties. They default channel properties are:
+     * <pre>
+     * UDP(mcast_addr=*.*.*.*;mcast_port=45566;ip_ttl=32;mcast_send_buf_size=150000;mcast_recv_buf_size=80000):PING(timeout=2000;num_initial_members=3):MERGE2(min_interval=5000;max_interval=10000):FD_SOCK:VERIFY_SUSPECT(timeout=1500):pbcast.STABLE(desired_avg_gossip=20000):pbcast.NAKACK(gc_lag=50;retransmit_timeout=300,600,1200,2400,4800):UNICAST(timeout=5000):FRAG(frag_size=8096;down_thread=false;up_thread=false):pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;shun=false;print_local_addr=true)
+     * </pre>
+     * Where <code>*.*.*.*</code> is the specified multicast IP, which defaults to <code>231.12.21.132</code>.
+     */
+    private static final String DEFAULT_CHANNEL_PROPERTIES_POST = ";mcast_port=45566;ip_ttl=32;mcast_send_buf_size=150000;mcast_recv_buf_size=80000):PING(timeout=2000;num_initial_members=3):MERGE2(min_interval=5000;max_interval=10000):FD_SOCK:VERIFY_SUSPECT(timeout=1500):pbcast.STABLE(desired_avg_gossip=20000):pbcast.NAKACK(gc_lag=50;retransmit_timeout=300,600,1200,2400,4800):UNICAST(timeout=5000):FRAG(frag_size=8096;down_thread=false;up_thread=false):pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;shun=false;print_local_addr=true)";
+    private static final String DEFAULT_MULTICAST_IP = "231.12.21.132";
+    private final Log log = LogFactory.getLog(ClusterManager.class);
+    private AbstractCacheAdministrator admin;
+    private NotificationBus bus;
+    private boolean shuttingDown = false;
+
+    /**
+     * Creates a <code>ClusterManager</code> instance using the supplied
+     * configuration.
+     */
+    ClusterManager(Config config) throws InitializationException {
+        String properties = config.getProperty(CHANNEL_PROPERTIES);
+        String multicastIP = config.getProperty(MULTICAST_IP_PROPERTY);
+
+        if ((properties == null) && (multicastIP == null)) {
+            multicastIP = DEFAULT_MULTICAST_IP;
+        }
+
+        if (properties == null) {
+            properties = DEFAULT_CHANNEL_PROPERTIES_PRE + multicastIP.trim() + DEFAULT_CHANNEL_PROPERTIES_POST;
+        } else {
+            properties = properties.trim();
+        }
+
+        if (log.isInfoEnabled()) {
+            log.info("Starting a new ClusterManager with properties=" + properties);
+        }
+
+        try {
+            bus = new NotificationBus(BUS_NAME, properties);
+            bus.start();
+            bus.getChannel().setOpt(Channel.LOCAL, new Boolean(false));
+            bus.setConsumer(this);
+            log.info("ClusterManager started successfully");
+        } catch (Exception e) {
+            throw new InitializationException("Initialization failed: " + e);
+        }
+    }
+
+    /**
+     * We are not using the caching, so we just return something that identifies
+     * us. This method should never be called directly.
+     */
+    public Serializable getCache() {
+        return "ClusterManager: " + bus.getLocalAddress();
+    }
+
+    /**
+     * Handles incoming notification messages. This method should never be called
+     * directly.
+     *
+     * @param serializable The incoming message object.
+     */
+    public void handleNotification(Serializable serializable) {
+        if (!(serializable instanceof ClusterNotification)) {
+            log.error("An unknown cluster notification message received (class=" + serializable.getClass().getName() + "). Notification ignored.");
+
+            return;
+        }
+
+        ClusterNotification msg = (ClusterNotification) serializable;
+
+        if (admin == null) {
+            log.warn("Since no cache administrator has been specified for this ClusterManager, the cache named '" + msg.getCacheName() + "' cannot be retrieved. Cluster notification ignored.");
+            return;
+        }
+
+        // Retrieve the named cache that this message applies to
+        Cache cache = admin.getNamedCache(msg.getCacheName());
+
+        if (cache == null) {
+            log.warn("A cluster notification (" + msg + ") was received, but no matching cache is registered on this machine. Notification ignored.");
+
+            return;
+        }
+
+        if (log.isInfoEnabled()) {
+            log.info("Cluster notification (" + msg + ") was received.");
+        }
+
+        switch (msg.getType()) {
+            case ClusterNotification.FLUSH_KEY:
+                cache.flushEntry(msg.getData(), CLUSTER_ORIGIN);
+                break;
+            case ClusterNotification.FLUSH_GROUP:
+                cache.flushGroup(msg.getData(), CLUSTER_ORIGIN);
+                break;
+            case ClusterNotification.FLUSH_PATTERN:
+                cache.flushPattern(msg.getData(), CLUSTER_ORIGIN);
+                break;
+            default:
+                log.error("The cluster notification (" + msg + ") is of an unknown type. Notification ignored.");
+        }
+    }
+
+    /**
+     * A callback that is fired when a new member joins the cluster. This
+     * method should never be called directly.
+     *
+     * @param address The address of the member who just joined.
+     */
+    public void memberJoined(Address address) {
+        log.info("A new member at address '" + address + "' has joined the cluster");
+    }
+
+    /**
+     * A callback that is fired when an existing member leaves the cluster.
+     * This method should never be called directly.
+     *
+     * @param address The address of the member who left.
+     */
+    public void memberLeft(Address address) {
+        log.info("Member at address '" + address + "' left the cluster");
+    }
+
+    /**
+     * Shuts down this <code>ClusterManager</code> instance. Care should be
+     * taken to ensure that no more notification messages will be sent using
+     * this instance once this method is called. The manager however is still
+     * able to receive notification events from other nodes in the cluster
+     * without any risk of problems.
+     */
+    void shutdown() {
+        if (!shuttingDown) {
+            shuttingDown = true;
+            log.info("ClusterManager shutting down...");
+            bus.stop();
+            bus = null;
+            log.info("ClusterManager shutdown complete.");
+        }
+    }
+
+    /**
+     * Broadcasts a flush message for the given cache entry.
+     * place.
+     *
+     * @param key The object key to broadcast a flush notification message for
+     * @param cacheName The name of the cache to flush
+     */
+    void signalEntryFlush(String key, String cacheName) {
+        if (log.isDebugEnabled()) {
+            log.debug("flushEntry called for cache '" + cacheName + "', key '" + key + "'");
+        }
+
+        if (!shuttingDown) {
+            bus.sendNotification(new ClusterNotification(ClusterNotification.FLUSH_KEY, cacheName, key));
+        }
+    }
+
+    /**
+     * Broadcasts a flush message for the given cache group.
+     *
+     * @param group The group to broadcast a flush message for
+     * @param cacheName The name of the cache to flush
+     */
+    void signalGroupFlush(String group, String cacheName) {
+        if (log.isDebugEnabled()) {
+            log.debug("flushGroup called for cache '" + cacheName + "', group '" + group + "'");
+        }
+
+        if (!shuttingDown) {
+            bus.sendNotification(new ClusterNotification(ClusterNotification.FLUSH_GROUP, cacheName, group));
+        }
+    }
+
+    /**
+     * Broadcasts a flush message for the given cache pattern.
+     *
+     * @param pattern The pattern to broadcast a flush message for
+     * @param cacheName The name of the cache to flush
+     */
+    void signalPatternFlush(String pattern, String cacheName) {
+        if (log.isDebugEnabled()) {
+            log.debug("flushPattern called for cache '" + cacheName + "', pattern '" + pattern + "'");
+        }
+
+        if (!shuttingDown) {
+            bus.sendNotification(new ClusterNotification(ClusterNotification.FLUSH_PATTERN, cacheName, pattern));
+        }
+    }
+
+    /**
+     * Broadcasts a flush message for an entire cache
+     *
+     * @param cacheName The name of the cache to flush
+     */
+    void signalCacheFlush(String cacheName) {
+        if (log.isDebugEnabled()) {
+            log.debug("flushCache called for cache '" + cacheName + "'");
+        }
+
+        if (!shuttingDown) {
+            bus.sendNotification(new ClusterNotification(ClusterNotification.FLUSH_CACHE, cacheName, null));
+        }
+    }
+
+    /**
+     * Sets the adminstrator for this cluster manager. We need this so we can
+     * look up named caches.
+     */
+    public void setAdministrator(AbstractCacheAdministrator admin) {
+        this.admin = admin;
+    }
+}

src/plugins/clustersupport/java/com/opensymphony/oscache/plugins/clustersupport/ClusterNotification.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.oscache.plugins.clustersupport;
+
+import java.io.Serializable;
+
+/**
+ * A notification message that holds information about a cache event. This
+ * class is <code>Serializable</code> to allow it to be sent across the
+ * network to other machines running in a cluster.
+ *
+ * @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
+ * @author $Author$
+ * @version $Revision$
+ */
+public class ClusterNotification implements Serializable {
+    /**
+     * Specifies a notification message that indicates a particular cache key
+     * should be flushed.
+     */
+    static final int FLUSH_KEY = 1;
+
+    /**
+     * Specifies a notification message that indicates an entire cache group
+     * should be flushed.
+     */
+    static final int FLUSH_GROUP = 2;
+
+    /**
+     * Specifies a notification message that indicates all entries in the cache
+     * that match the specified pattern should be flushed.
+     */
+    static final int FLUSH_PATTERN = 3;
+
+    /**
+     * Specifies a notification message indicating that an entire cache should
+     * be flushed.
+     */
+    static final int FLUSH_CACHE = 4;
+
+    /**
+     * The name of the cache that this notification applies to
+     */
+    private String cacheName;
+
+    /**
+     * Any additional data that may be required
+     */
+    private String data;
+
+    /**
+     * The type of notification message.
+     */
+    private int type;
+
+    /**
+     * Creates a new notification message object to broadcast to other
+     * listening nodes in the cluster.
+     *
+     * @param type       The type of notification message. Valid types are
+     *                   {@link #FLUSH_KEY} and {@link #FLUSH_GROUP}.
+     * @param cacheName  The name of the cache that this message applies to.
+     *                   This is required so the remote listeners can locate
+     *                   the correct cache to flush.
+     * @param data       Specifies the object key or group name to flush.
+     */
+    public ClusterNotification(int type, String cacheName, String data) {
+        this.type = type;
+        this.cacheName = cacheName;
+        this.data = data;
+    }
+
+    /**
+     * The name of the cache that this message should be applied to. The cache
+     * will be looked up using the cache's administrator object.
+     */
+    public String getCacheName() {
+        return cacheName;
+    }
+
+    /**
+     * Specifies the object key or group name to flush.
+     */
+    public String getData() {
+        return data;
+    }
+
+    /**
+     * The type of notification message.
+     */
+    public int getType() {
+        return type;
+    }
+
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("type=").append(type).append(", cacheName=");
+        buf.append(cacheName).append(", data=").append(data);
+
+        return buf.toString();
+    }
+}

src/plugins/clustersupport/test/com/opensymphony/oscache/plugins/clustersupport/TestBroadcastingCacheEventListener.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.oscache.plugins.clustersupport;
+
+import com.opensymphony.oscache.base.Cache;
+import com.opensymphony.oscache.base.CacheEntry;
+import com.opensymphony.oscache.base.Config;
+import com.opensymphony.oscache.base.InitializationException;
+import com.opensymphony.oscache.base.events.CacheEntryEvent;
+import com.opensymphony.oscache.base.events.CacheGroupEvent;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+/**
+ * Test all the public methods of the broadcasting listener and assert the
+ * return values
+ *
+ * @version        $Revision$
+ * @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
+ */
+public final class TestBroadcastingCacheEventListener extends TestCase {
+    /**
+     * The persistance listener used for the tests
+     */
+    private static BroadcastingCacheEventListener listener = null;
+
+    /**
+     * A cache instance to use for the tests
+     */
+    private static Cache cache = null;
+
+    /**
+     * Cache group
+     */
+    private final String GROUP = "test group";
+
+    /**
+     * Object key
+     */
+    private final String KEY = "Test clustersupport persistence listener key";
+
+    public TestBroadcastingCacheEventListener(String str) {
+        super(str);
+    }
+
+    /**
+     * This methods returns the name of this test class to JUnit
+     * <p>
+     * @return The test for this class
+     */
+    public static Test suite() {
+        return new TestSuite(TestBroadcastingCacheEventListener.class);
+    }
+
+    /**
+     * This method is invoked before each testXXXX methods of the
+     * class. It set ups the variables required for each tests.
+     */
+    public void setUp() {
+        // At first invocation, create a listener
+        if (listener == null) {
+            listener = new BroadcastingCacheEventListener();
+
+            Config config = new Config();
+            config.set("cache.cluster.multicast.ip", "231.12.21.132");
+
+            try {
+                listener.initialize(null, config);
+            } catch (InitializationException e) {
+                fail(e.getMessage());
+            }
+
+            cache = new Cache(true, false);
+            assertNotNull(listener);
+            assertNotNull(cache);
+        }
+    }
+
+    public void testCacheEntryAdded() {
+        CacheEntry entry = new CacheEntry(KEY, null);
+        CacheEntryEvent event = new CacheEntryEvent(cache, entry);
+        listener.cacheEntryAdded(event);
+    }
+
+    public void testCacheEntryFlushed() {
+        CacheEntry entry = new CacheEntry(KEY, null);
+        CacheEntryEvent event = new CacheEntryEvent(cache, entry);
+        listener.cacheEntryFlushed(event);
+    }
+
+    public void testCacheEntryRemoved() {
+        CacheEntry entry = new CacheEntry(KEY, null);
+        CacheEntryEvent event = new CacheEntryEvent(cache, entry);
+        listener.cacheEntryRemoved(event);
+    }
+
+    public void testCacheEntryUpdated() {
+        CacheEntry entry = new CacheEntry(KEY, null);
+        CacheEntryEvent event = new CacheEntryEvent(cache, entry);
+        listener.cacheEntryUpdated(event);
+    }
+
+    public void testCacheGroupFlushed() {
+        CacheEntry entry = new CacheEntry(KEY, null);
+        CacheGroupEvent event = new CacheGroupEvent(cache, GROUP);
+        listener.cacheGroupFlushed(event);
+    }
+}

src/plugins/clustersupport/test/com/opensymphony/oscache/plugins/clustersupport/TestClusterManager.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.oscache.plugins.clustersupport;
+
+import com.opensymphony.oscache.base.Config;
+import com.opensymphony.oscache.base.InitializationException;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+/**
+ * Test the ClusterManager class
+ *
+ * @version        $Revision$
+ * @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
+ */
+public final class TestClusterManager extends TestCase {
+    /**
+     * Name of the cache to flush
+     */
+    private final String CACHE_NAME = "test";
+
+    /**
+     * Cache group
+     */
+    private final String GROUP = "test group";
+
+    /**
+     * Object key
+     */
+    private final String KEY = "Test clustersupport persistence listener key";
+
+    public TestClusterManager(String str) {
+        super(str);
+    }
+
+    /**
+     * This methods returns the name of this test class to JUnit
+     * <p>
+     * @return The test for this class
+     */
+    public static Test suite() {
+        return new TestSuite(TestClusterManager.class);
+    }
+
+    /**
+     * This method is invoked before each testXXXX methods of the
+     * class. It set ups the variables required for each tests.
+     */
+    public void setUp() {
+    }
+
+    public void testCacheManager() {
+        Config config = new Config();
+
+        try {
+            ClusterManager cm = new ClusterManager(config);
+
+            // Send some flush signals
+            cm.signalEntryFlush(KEY, CACHE_NAME);
+            cm.signalGroupFlush(GROUP, CACHE_NAME);
+
+            // Simulate receiving some signals
+            cm.handleNotification(new ClusterNotification(ClusterNotification.FLUSH_KEY, CACHE_NAME, GROUP));
+            cm.handleNotification(new ClusterNotification(ClusterNotification.FLUSH_GROUP, CACHE_NAME, GROUP));
+
+            // Shutdown the cache manager
+            cm.shutdown();
+        } catch (InitializationException e) {
+            fail("Could not initialize ClusterManager - " + e);
+        }
+    }
+}

src/plugins/diskpersistence/java/com/opensymphony/oscache/plugins/diskpersistence/DiskPersistenceListener.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.oscache.plugins.diskpersistence;
+
+import com.opensymphony.oscache.base.Config;
+import com.opensymphony.oscache.base.persistence.CachePersistenceException;
+import com.opensymphony.oscache.base.persistence.PersistenceListener;
+import com.opensymphony.oscache.web.ServletCacheAdministrator;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.io.*;
+
+import java.util.Set;
+
+import javax.servlet.jsp.PageContext;
+
+/**
+ * Persist the cache data to disk.
+ *
+ * The code in this class is totally not thread safe it is the resonsibility
+ * of the cache using this persistence listener to handle the concurrency.
+ *
+ * @version        $Revision$
+ * @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
+ * @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
+ * @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
+ */
+public final class DiskPersistenceListener implements PersistenceListener {
+    protected final static String CACHE_PATH_KEY = "cache.path";
+
+    /**
+     * File extension for disk cache file
+     */
+    private final static String CACHE_EXTENSION = "cache";
+
+    /**
+     * The directory that cache groups are stored under
+     */
+    private final static String GROUP_DIRECTORY = "__groups__";
+
+    /**
+     * Sub path name for application cache
+     */
+    private final static String APPLICATION_CACHE_SUBPATH = "application";
+
+    /**
+     * Sub path name for session cache
+     */
+    private final static String SESSION_CACHE_SUBPATH = "session";
+
+    /**
+     * Property to get the temporary working directory of the servlet container.
+     */
+    private static final String CONTEXT_TMPDIR = "javax.servlet.context.tempdir";
+    private static transient final Log log = LogFactory.getLog(DiskPersistenceListener.class);
+
+    /**
+     * Base path where the disk cache reside.
+     */
+    private File cachePath = null;
+    private File contextTmpDir;
+
+    /**
+     * Root path for disk cache
+     */
+    private String root = null;
+
+    /**
+       *        Get the physical cache path on disk.
+       *
+       *        @return        A file representing the physical cache location.
+       */
+    public File getCachePath() {
+        return cachePath;
+    }
+
+    /**
+     * Verify if a group exists in the cache
+     *
+     * @param group The group name to check
+     * @return True if it exists
+     * @throws CachePersistenceException
+     */
+    public boolean isGroupStored(String group) throws CachePersistenceException {
+        try {
+            File file = getCacheGroupFile(group);
+
+            return file.exists();
+        } catch (Exception e) {
+            throw new CachePersistenceException("Unable verify group '" + group + "' exists in the cache: " + e);
+        }
+    }
+
+    /**
+     * Verify if an object is currently stored in the cache
+     *
+     * @param key The object key
+     * @return True if it exists
+     * @throws CachePersistenceException
+     */
+    public boolean isStored(String key) throws CachePersistenceException {
+        try {
+            File file = getCacheFile(key);
+
+            return file.exists();
+        } catch (Exception e) {
+            throw new CachePersistenceException("Unable verify id '" + key + "' is stored in the cache: " + e);
+        }
+    }
+
+    /**
+     * Clears the whole cache directory, starting from the root
+     *
+     * @throws CachePersistenceException
+     */
+    public void clear() throws CachePersistenceException {
+        clear(root);
+    }
+
+    /**
+     * Initialises this <tt>DiskPersistenceListener</tt> using the supplied
+     * configuration.
+     *
+     * @param config The OSCache configuration
+     */
+    public PersistenceListener configure(Config config) {
+        String sessionId = null;
+        int scope = 0;
+        initFileCaching(config.getProperty(CACHE_PATH_KEY));
+
+        if (config.getProperty(ServletCacheAdministrator.HASH_KEY_SESSION_ID) != null) {
+            sessionId = config.getProperty(ServletCacheAdministrator.HASH_KEY_SESSION_ID);
+        }
+
+        if (config.getProperty(ServletCacheAdministrator.HASH_KEY_SCOPE) != null) {
+            scope = Integer.parseInt(config.getProperty(ServletCacheAdministrator.HASH_KEY_SCOPE));
+        }
+
+        StringBuffer root = new StringBuffer(getCachePath().getPath());
+        root.append("/");
+        root.append(getPathPart(scope));
+
+        if ((sessionId != null) && (sessionId.length() > 0)) {
+            root.append("/");
+            root.append(sessionId);
+        }
+
+        this.root = root.toString();
+        this.contextTmpDir = (File) config.get(ServletCacheAdministrator.HASH_KEY_CONTEXT_TMPDIR);
+
+        return this;
+    }
+
+    /**
+     * Delete a single cache entry.
+     *
+     * @param key The object key to delete
+     * @throws CachePersistenceException
+     */
+    public void remove(String key) throws CachePersistenceException {
+        File file = getCacheFile(key);
+        remove(file);
+    }
+
+    /**
+     * Deletes an entire group from the cache.
+     *
+     * @param groupName The name of the group to delete
+     * @throws CachePersistenceException
+     */
+    public void removeGroup(String groupName) throws CachePersistenceException {
+        File file = getCacheGroupFile(groupName);
+        remove(file);
+    }
+
+    /**
+     * Retrieve an object from the disk
+     *
+     * @param key The object key
+     * @return The retrieved object
+     * @throws CachePersistenceException
+     */
+    public Object retrieve(String key) throws CachePersistenceException {
+        return retrieve(getCacheFile(key));
+    }
+
+    /**
+     * Retrieves a group from the cache, or <code>null</code> if the group
+     * file could not be found.
+     *
+     * @param groupName The name of the group to retrieve.
+     * @return A <code>Set</code> containing keys of all of the cache
+     * entries that belong to this group.
+     * @throws CachePersistenceException
+     */
+    public Set retrieveGroup(String groupName) throws CachePersistenceException {
+        File groupFile = getCacheGroupFile(groupName);
+
+        try {
+            return (Set) retrieve(groupFile);
+        } catch (ClassCastException e) {
+            throw new CachePersistenceException("Group file " + groupFile + " was not persisted as a Set: " + e);
+        }
+    }
+
+    /**
+     * Stores an object in cache
+     *
+     * @param key The object's key
+     * @param obj The object to store
+     * @throws CachePersistenceException
+     */
+    public void store(String key, Object obj) throws CachePersistenceException {
+        File file = getCacheFile(key);
+        store(file, obj);
+    }
+
+    /**
+     * Stores a group in the persistent cache. This will overwrite any existing
+     * group with the same name
+     */
+    public void storeGroup(String groupName, Set group) throws CachePersistenceException {
+        File groupFile = getCacheGroupFile(groupName);
+        store(groupFile, group);
+    }
+
+    /**
+     * Allows to translate to the temp dir of the servlet container if cachePathStr
+     * is javax.servlet.context.tempdir.
+     *
+     * @param cachePathStr  Cache path read from the properties file.
+     * @return Adjusted cache path
+     */
+    protected String adjustFileCachePath(String cachePathStr) {
+        if (cachePathStr.compareToIgnoreCase(CONTEXT_TMPDIR) == 0) {
+            cachePathStr = contextTmpDir.getAbsolutePath();
+        }
+
+        return cachePathStr;
+    }
+
+    /**
+     *        Set caching to file on or off.
+     *  If the <code>cache.path</code> property exists, we assume file caching is turned on.
+     *        By the same token, to turn off file caching just remove this property.
+     */
+    protected void initFileCaching(String cachePathStr) {
+        if (cachePathStr != null) {
+            cachePath = new File(cachePathStr);
+
+            try {
+                if (!cachePath.exists()) {
+                    if (log.isInfoEnabled()) {
+                        log.info("cache.path '" + cachePathStr + "' does not exist, creating");
+                    }
+
+                    cachePath.mkdirs();
+                }
+
+                if (!cachePath.isDirectory()) {
+                    log.error("cache.path '" + cachePathStr + "' is not a directory");
+                    cachePath = null;
+                } else if (!cachePath.canWrite()) {
+                    log.error("cache.path '" + cachePathStr + "' is not a writable location");
+                    cachePath = null;
+                }
+            } catch (Exception e) {
+                log.error("cache.path '" + cachePathStr + "' could not be used", e);
+                cachePath = null;
+            }
+        } else {
+            // Use default value
+        }
+    }
+
+    protected void remove(File file) throws CachePersistenceException {
+        try {
+            // Loop until we are able to delete (No current read).
+            // The cache must ensure that there are never two concurrent threads
+            // doing write (store and delete) operations on the same item.
+            // Delete only should be enough but file.exists prevents infinite loop
+            while (!file.delete() && file.exists()) {
+                ;
+            }
+        } catch (Exception e) {
+            throw new CachePersistenceException("Unable to remove '" + file + "' from the cache: " + e);
+        }
+    }
+
+    /**
+     * Stores an object using the supplied file object
+     *
+     * @param file The file to use for storing the object
+     * @param obj the object to store
+     * @throws CachePersistenceException
+     */
+    protected void store(File file, Object obj) throws CachePersistenceException {
+        // check if the directory structure required exists and create it if it doesn't
+        File filepath = new File(file.getParent());
+
+        try {
+            if (!filepath.exists()) {
+                filepath.mkdirs();
+            }
+        } catch (Exception e) {
+            throw new CachePersistenceException("Unable to create the directory " + filepath);
+        }
+
+        // Loop until we are able to delete (No current read).
+        // The cache must ensure that there are never two concurrent threads
+        // doing write (store and delete) operations on the same item.
+        // Delete only should be enough but file.exists prevents infinite loop
+        while (file.exists() && !file.delete()) {
+            ;
+        }
+
+        // Write the object to disk
+        FileOutputStream fout = null;
+        ObjectOutputStream oout = null;
+
+        try {
+            fout = new FileOutputStream(file);
+            oout = new ObjectOutputStream(fout);
+            oout.writeObject(obj);
+            oout.flush();
+        } catch (Exception e) {
+            throw new CachePersistenceException("Unable to write '" + file + "' in the cache. Exception: " + e.getClass().getName() + ", Message: " + e.getMessage());
+        } finally {
+            try {
+                fout.close();
+            } catch (Exception e) {
+            }
+
+            try {
+                oout.close();
+            } catch (Exception e) {
+            }
+        }
+    }
+
+    /**
+     * Build fully qualified cache file name specifying a cache entry key.
+     *
+     * @param key   Cache Entry Key.
+     * @return File reference.
+     */
+    private File getCacheFile(String key) {
+        if ((key == null) || (key.length() == 0)) {
+            throw new IllegalArgumentException("Invalid key '" + key + "' specified to getCacheFile.");
+        }
+
+        char[] chars = key.toCharArray();
+        char[] fileChars = new char[chars.length];
+
+        for (int i = 0; i < chars.length; i++) {
+            char c = chars[i];
+
+            switch (c) {
+                case '.':
+                case '/':
+                case '\\':
+                case ' ':
+                case ':':
+                case ';':
+                case '"':
+                case '\'':
+                    fileChars[i] = '_';
+                    break;
+                default:
+                    fileChars[i] = c;
+            }
+        }
+
+        File file = new File(root, new String(fileChars) + "." + CACHE_EXTENSION);
+
+        return file;
+    }
+
+    /**
+     * Builds a fully qualified file name that specifies a cache group entry.
+     *
+     * @param group The name of the group
+     * @return A File reference
+     */
+    private File getCacheGroupFile(String group) {
+        int AVERAGE_PATH_LENGTH = 30;
+
+        if ((group == null) || (group.length() == 0)) {
+            throw new IllegalArgumentException("Invalid group '" + group + "' specified to getCacheGroupFile.");
+        }
+
+        StringBuffer path = new StringBuffer(AVERAGE_PATH_LENGTH);
+
+        // Build a fully qualified file name for this group
+        path.append(GROUP_DIRECTORY).append('/');
+        path.append(group).append('.').append(CACHE_EXTENSION);
+
+        return new File(root, path.toString());
+    }
+
+    /**
+     * This allows to persist different scopes in different path in the case of
+     * file caching.
+     *
+     * @param scope   Cache scope.
+     * @return The scope subpath
+     */
+    private String getPathPart(int scope) {
+        if (scope == PageContext.SESSION_SCOPE) {
+            return SESSION_CACHE_SUBPATH;
+        } else {
+            return APPLICATION_CACHE_SUBPATH;
+        }
+    }
+
+    /**
+     * Clears a whole directory, starting from the specified
+     * directory
+     *
+     * @param baseDirName The root directory to delete
+     * @throws CachePersistenceException
+     */
+    private void clear(String baseDirName) throws CachePersistenceException {
+        File baseDir = new File(baseDirName);
+        File[] fileList = baseDir.listFiles();
+
+        try {
+            if (fileList != null) {
+                // Loop through all the files and directory to delete them
+                for (int count = 0; count < fileList.length; count++) {
+                    if (fileList[count].isFile()) {
+                        fileList[count].delete();
+                    } else {
+                        // Make a recursive call to delete the directory
+                        clear(fileList[count].toString());
+                        fileList[count].delete();
+                    }
+                }
+            }
+
+            // Delete the root directory
+            baseDir.delete();
+        } catch (Exception e) {
+            throw new CachePersistenceException("Unable to clear the cache directory");
+        }
+    }
+
+    /**
+     * Retrives a serialized object from the supplied file, or returns
+     * <code>null</code> if the file does not exist.
+     *
+     * @param file The file to deserialize
+     * @return The deserialized object
+     * @throws CachePersistenceException
+     */
+    private Object retrieve(File file) throws CachePersistenceException {
+        Object readContent = null;
+        boolean fileExist;
+
+        try {
+            fileExist = file.exists();
+        } catch (Exception e) {
+            throw new CachePersistenceException("Unable to verify if " + file + " exists: " + e);
+        }
+
+        // Read the file if it exists
+        if (fileExist) {
+            BufferedInputStream in = null;
+            ObjectInputStream oin = null;
+
+            try {
+                in = new BufferedInputStream(new FileInputStream(file));
+                oin = new ObjectInputStream(in);
+                readContent = oin.readObject();
+            } catch (Exception e) {
+                // We expect this exception to occur.
+                // This is when the item will be invalidated (written or deleted)
+                // during read.
+                // The cache has the logic to retry reading.
+                throw new CachePersistenceException("Unable to read '" + file.getAbsolutePath() + "' from the cache: " + e);
+            } finally {
+                try {
+                    oin.close();
+                } catch (Exception ex) {
+                }
+
+                try {
+                    in.close();
+                } catch (Exception ex) {
+                }
+            }
+        }
+
+        return readContent;
+    }
+}

src/plugins/diskpersistence/test/com/opensymphony/oscache/plugins/diskpersistence/TestDiskPersistenceListener.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.oscache.plugins.diskpersistence;
+
+import com.opensymphony.oscache.base.CacheEntry;
+import com.opensymphony.oscache.base.Config;
+import com.opensymphony.oscache.base.persistence.CachePersistenceException;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+import java.io.File;
+import java.io.FilenameFilter;
+
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * Test all the public methods of the disk persistance listener and assert the
+ * return values
+ *
+ * $Id$
+ * @version        $Revision$
+ * @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
+ */
+public final class TestDiskPersistenceListener extends TestCase {
+    /**
+     * Cache dir to persist to
+     */
+    private static final String CACHEDIR = "build/test/diskcache";
+
+    /**
+     * The persistance listener used for the tests
+     */
+    private DiskPersistenceListener listener = null;
+
+    /**
+     * Object content
+     */
+    private final String CONTENT = "Disk persistance content";
+
+    /**
+     * Cache group
+     */
+    private final String GROUP = "test group";
+
+    /**
+     * Object key
+     */
+    private final String KEY = "Test disk persistance listener key";
+    private CacheFileFilter cacheFileFilter = new CacheFileFilter();
+
+    public TestDiskPersistenceListener(String str) {
+        super(str);
+    }
+
+    /**
+     * This methods returns the name of this test class to JUnit
+     * <p>
+     * @return The test for this class
+     */
+    public static Test suite() {
+        return new TestSuite(TestDiskPersistenceListener.class);
+    }
+
+    /**
+     * This method is invoked before each testXXXX methods of the
+     * class. It set ups the variables required for each tests.
+     */
+    public void setUp() {
+        // At first invocation, create a listener
+        listener = new DiskPersistenceListener();
+
+        Properties p = new Properties();
+        p.setProperty("cache.path", CACHEDIR);
+        p.setProperty("cache.memory", "false");
+        p.setProperty("cache.persistence.class", "com.opensymphony.oscache.plugins.diskpersistence.DiskPersistenceListener");
+        listener.configure(new Config(p));
+    }
+
+    /**
+     * Test the cache directory removal
+     */
+    public void testClear() {
+        // Create an new element since we removed it at the last test
+        testStoreRetrieve();
+
+        // Remove the directory, and assert that we have no more entry
+        try {
+            listener.clear();
+            assertTrue(!listener.isStored(KEY));
+        } catch (CachePersistenceException cpe) {
+            cpe.printStackTrace();
+            fail("Exception thrown in test clear!");
+        }
+    }
+
+    /**
+     * Test that the previouly created file exists
+     */
+    public void testIsStored() {
+        try {
+            listener.store(KEY, CONTENT);
+
+            // Retrieve the previously created file
+            assertTrue(listener.isStored(KEY));
+
+            // Check that the fake key returns false
+            assertTrue(!listener.isStored(KEY + "fake"));
+        } catch (Exception e) {
+            e.printStackTrace();
+            fail("testIsStored raised an exception");
+        }
+    }
+
+    /**
+     * Test the cache removal
+     */
+    public void testRemove() {
+        // Create an entry if it doesn't exists
+        try {
+            if (!listener.isStored(KEY)) {
+                listener.store(KEY, CONTENT);
+            }
+
+            // Remove the previously created file
+            listener.remove(KEY);
+        } catch (CachePersistenceException cpe) {
+            cpe.printStackTrace();
+            fail("Exception thrown in test remove!");
+        }
+    }
+
+    /**
+     * Test the disk store and retrieve
+     */
+    public void testStoreRetrieve() {
+        // Create a cache entry and store it
+        CacheEntry entry = new CacheEntry(KEY);
+        entry.setContent(CONTENT);
+
+        try {
+            listener.store(KEY, entry);
+
+            // Retrieve our entry and validate the values
+            CacheEntry newEntry = (CacheEntry) listener.retrieve(KEY);
+            assertTrue(entry.getContent().equals(newEntry.getContent()));
+            assertEquals(entry.getCreated(), newEntry.getCreated());
+            assertTrue(entry.getKey().equals(newEntry.getKey()));
+
+            // Try to retrieve a non-existent object
+            assertNull(listener.retrieve("doesn't exist"));
+        } catch (Exception ex) {
+            ex.printStackTrace();
+            fail("Exception raised!");
+        }
+    }
+
+    /**
+     * Test the storing and retrieving of groups
+     */
+    public void testStoreRetrieveGroups() {
+        // Store a group
+        Set groupSet = new HashSet();
+        groupSet.add("1");
+        groupSet.add("2");
+
+        try {
+            listener.storeGroup(GROUP, groupSet);
+
+            // Retrieve it and validate its contents
+            groupSet = listener.retrieveGroup(GROUP);
+            assertNotNull(groupSet);
+
+            assertTrue(groupSet.contains("1"));
+            assertTrue(groupSet.contains("2"));
+            assertFalse(groupSet.contains("3"));
+
+            // Try to retrieve a non-existent group
+            assertNull(listener.retrieveGroup("abc"));
+        } catch (Exception ex) {
+            ex.printStackTrace();
+            fail("Exception raised!");
+        }
+    }
+
+    protected void tearDown() throws Exception {
+        listener.clear();
+        assertTrue("Cache not cleared", new File(CACHEDIR).list(cacheFileFilter).length == 0);
+    }
+
+    private static class CacheFileFilter implements FilenameFilter {
+        public boolean accept(File dir, String name) {
+            return !"__groups__".equals(name);
+        }
+    }
+}

src/webapp/WEB-INF/classes/com/opensymphony/oscache/web/OscacheServlet.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.oscache.web;
+
+import com.opensymphony.oscache.base.NeedsRefreshException;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.jsp.PageContext;
+
+/**
+ * Servlet used to test the web portion of osCache. It performs the operations
+ * received by parameter
+ *
+ * $Id$
+ * @version        $Revision$
+ * @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
+ * @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
+ */
+public class OscacheServlet extends HttpServlet {
+    /** Output content type */
+    private static final String CONTENT_TYPE = "text/html";
+
+    /** Clean up resources */
+    public void destroy() {
+    }
+
+    /**
+     * Process the HTTP Get request
+     * <p>
+     * @param request The HTTP request
+     * @param response The servlet response
+     * @throws ServletException
+     * @throws IOException
+     */
+    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        boolean varForceRefresh = false;
+        int refreshPeriod = 0;
+        int scope = PageContext.APPLICATION_SCOPE;
+        String forceCacheUse = null;
+        String key = null;
+
+        // Cache item
+        Long item;
+
+        // Get the admin
+        ServletCacheAdministrator admin = ServletCacheAdministrator.getInstance(getServletContext());
+
+        // Translate parameters
+        try {
+            String paramValue = request.getParameter("forceRefresh");
+
+            if ((paramValue != null) && (paramValue.length() > 0)) {
+                varForceRefresh = Boolean.valueOf(paramValue).booleanValue();
+            }
+
+            paramValue = request.getParameter("scope");
+
+            if ((paramValue != null) && (paramValue.length() > 0)) {
+                scope = getScope(paramValue);
+            }
+
+            paramValue = request.getParameter("refreshPeriod");
+
+            if ((paramValue != null) && (paramValue.length() > 0)) {
+                refreshPeriod = Integer.valueOf(paramValue).intValue();
+            }
+
+            forceCacheUse = request.getParameter("forcecacheuse");
+            key = request.getParameter("key");
+        } catch (Exception e) {
+            getServletContext().log("Error while retrieving the servlet parameters: " + e.toString());
+        }
+
+        // Check if all the items should be flushed
+        if (varForceRefresh) {
+            admin.flushAll();
+        }
+
+        try {
+            // Get the data from the cache
+            item = (Long) admin.getFromCache(scope, request, key, refreshPeriod);
+        } catch (NeedsRefreshException nre) {
+            // Check if we want to force the use of an item already in cache
+            if ("yes".equals(forceCacheUse)) {
+                admin.cancelUpdate(scope, request, key);
+                item = (Long) nre.getCacheContent();
+            } else {
+                item = new Long(System.currentTimeMillis());
+                admin.putInCache(scope, request, key, item);
+            }
+        }
+
+        // Generate the output
+        response.setContentType(CONTENT_TYPE);
+
+        PrintWriter out = response.getWriter();
+        out.println("<html>");
+        out.println("<head><title>OscacheServlet</title></head>");
+        out.println("<body>");
+        out.println("<b>This is some cache content </b>: " + item.toString() + "<br>");
+        out.println("<b>Cache key</b>: " + admin.getCacheKey() + "<br>");
+        out.println("<b>Entry key</b>: " + admin.generateEntryKey("Test_key", request, scope) + "<br>");
+        out.println("</body></html>");
+    }
+
+    /**Initialize global variables*/
+    public void init(ServletConfig config) throws ServletException {
+        super.init(config);
+    }
+
+    /**
+     * Return the scope number corresponding to it's string name
+     */
+    private int getScope(String value) {
+        if ((value != null) && (value.equalsIgnoreCase("session"))) {
+            return PageContext.SESSION_SCOPE;
+        } else {
+            return PageContext.APPLICATION_SCOPE;
+        }
+    }
+}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.