package org.jboss.cache.passivation;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.CacheException;
import org.jboss.cache.DataNode;
import org.jboss.cache.Fqn;
import org.jboss.cache.Modification;
import org.jboss.cache.TreeCache;
import org.jboss.cache.loader.CacheLoader;
import org.jboss.cache.loader.SamplePojo;
import org.jboss.cache.lock.IsolationLevel;
import org.jboss.cache.transaction.DummyTransactionManager;
import org.jboss.cache.xml.XmlHelper;
import org.w3c.dom.Element;

import javax.transaction.NotSupportedException;
import javax.transaction.Transaction;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Base tests for passivation using any of the cache loaders
 * 
 * @author <a href="mailto:{hmesha@novell.com}">{Hany Mesha}</a>
 * @version $Id: PassivationTestsBase.java 3090 2006-12-05 15:27:52Z msurtani $
 */
abstract public class PassivationTestsBase extends TestCase {

   Log log = LogFactory.getLog(getClass());

   //Cache Loader fields
   TreeCache        cache;
   CacheLoader      loader=null;
   Transaction      tx=null;
   static final Fqn FQN = new Fqn("key");


    protected Element getCacheLoaderConfig(String preload, String cacheloaderClass, String properties, boolean async, boolean fetchPersistentState) throws Exception
    {
        String xml = "<config>\n" +
                "<passivation>true</passivation>\n" +
                "<preload>" + preload + "</preload>\n" +
                "<cacheloader>\n" +
                "<class>" + cacheloaderClass + "</class>\n" +
                "<properties>" + properties + "</properties>\n" +
                "<async>" + async + "</async>\n" +
                "<fetchPersistentState>" + fetchPersistentState + "</fetchPersistentState>\n" +
                "</cacheloader>\n" +
                "</config>";
        return XmlHelper.stringToElement(xml);
    }

   protected void setUp() throws Exception {
      super.setUp();
      log.debug("Testing " + getName());
      log.debug("");
      cache=new TreeCache();
      cache.setCacheMode("local");
      configureCache();
      // cache.setCacheLoaderPreload("/1/2/3/4/5/d");
      cache.setTransactionManagerLookupClass("org.jboss.cache.DummyTransactionManagerLookup");
      cache.setIsolationLevel(IsolationLevel.SERIALIZABLE);
      cache.createService();
      cache.startService();
      loader=cache.getCacheLoader();
   }

   abstract protected void configureCache() throws Exception;


   protected void tearDown() throws Exception {
      super.tearDown();
      if(tx != null) {
         try {
            tx.commit();
         }
         catch(Throwable e) {
            e.printStackTrace();
         }
      }
      cache.remove("/");
      loader.remove(Fqn.fromString("/"));
      cache.stopService();
      cache.destroyService();
   }


   protected void addDelay()
   {
      ; // returns immediately in this case.  Subclasses may override where a delay is needed.
   }

   public void testPrintPassivation() throws Exception {
      final Fqn NODE=Fqn.fromString("/test");
      final String KEY="key";
      loader.remove(NODE);
      cache.put(NODE, KEY, new Integer(10));
      cache.evict(NODE);
      assertTrue(loader.exists(NODE));
      addDelay();
      log.info("print node " + NODE);
      String ret=cache.print(NODE);
      assertNotNull(ret);
      log.info("loader exists " + NODE);
      assertTrue(!loader.exists(NODE));
      cache.get(NODE, KEY);
      assertFalse(loader.exists(NODE));
   }

   public void testPutPassivation() throws Exception {
      final String NODE="/test";
      final String KEY="key";
      Object retval=null;
      cache.remove(NODE); // nothing to remove
      addDelay();
      retval=cache.put(NODE, KEY, new Integer(10)); // put in memory
      assertNull(retval);
      retval=cache.put(NODE, KEY, new Integer(20)); // put in memory
      addDelay();
      assertEquals(new Integer(10), retval); // get from memory
      cache.evict(Fqn.fromString(NODE)); // passivate node
      addDelay();
      log.debug("______________");
      retval=cache.put(NODE, KEY, new Integer(30)); // activate node then does put in memory
      assertFalse(loader.exists(Fqn.fromString(NODE)));
      assertEquals(new Integer(20), retval);
   }

   public void testPut2Passivation() throws CacheException {
      final String NODE="/a/b/c";
      final String KEY="key";
      Object retval=null;
      cache.remove(NODE); // nothing to remove
      addDelay();
      retval=cache.put(NODE, KEY, new Integer(10)); // put in memory
      assertNull(retval);
      addDelay();
      retval=cache.put(NODE, KEY, new Integer(20)); // put in memory
      assertEquals(new Integer(10), retval);
      cache.evict(Fqn.fromString(NODE)); // passivate node
      cache.evict(Fqn.fromString("/a/b")); // passivate parent node
      cache.evict(Fqn.fromString("/a")); // passivate parent node
      addDelay();
      try{
         assertTrue(loader.exists(Fqn.fromString("/a/b/c")));
      }
      catch(Exception e){
         fail(e.toString());
      }
      retval=cache.put(NODE, KEY, new Integer(30)); // activate node, put in memory new value
      try{
          assertFalse(loader.exists(Fqn.fromString(NODE)));
      }
      catch(Exception e){
         fail(e.toString());
      }
      assertEquals(new Integer(20), retval);
   }


   public void testSerializationPassivation() throws CacheException {
      SamplePojo pojo=new SamplePojo(39, "Hany");
      pojo.getHobbies().add("Running");
      pojo.getHobbies().add("Beerathlon");
      pojo.getHobbies().add("Triathlon");
      cache.put("/mypojo", new Integer(322649), pojo); // put in memory
      addDelay();
      assertNotNull(cache.get("/mypojo", new Integer(322649))); // get from memory
      cache.evict(Fqn.fromString("/mypojo")); // passivate node
      try{
         assertTrue(loader.exists(Fqn.fromString("/mypojo")));
      }
      catch(Exception e) {
         fail(e.toString());
      }
      SamplePojo pojo2=(SamplePojo)cache.get("/mypojo", new Integer(322649)); // activate node
      try{
         assertFalse(loader.exists(Fqn.fromString("/mypojo")));
      }
      catch(Exception e) {
         fail(e.toString());
      }
      assertNotNull(pojo2);
      assertEquals(39, pojo2.getAge());
      assertEquals("Hany", pojo2.getName());
      assertEquals(3, pojo2.getHobbies().size());
   }

   /** Just adds some data that wil be later retrieved. This test has to be run first */
   public void testPopulate() {
      try {
         Map m=new HashMap();
         for(int i=0; i < 10; i++)
           m.put("key" + i, "val" + i);
         cache.put("/a/b/c", m);
         cache.load("/1/2/3/4/5");
         cache.put("/1/2/3/4/5", null);
         cache.put("/1/2/3/4/5/a", null);
         cache.put("/1/2/3/4/5/b", null);
         cache.put("/1/2/3/4/5/c", null);
         cache.put("/1/2/3/4/5/d", null);
         cache.put("/1/2/3/4/5/e", null);
         cache.put("/1/2/3/4/5/d/one", null);
         cache.put("/1/2/3/4/5/d/two", null);
         cache.put("/1/2/3/4/5/d/three", null);
         // cache.put("/a/b/c", "newKey", "newValue");
         System.out.println("cache: " + cache);

         assertTrue(cache.exists("/1/2/3/4"));
         assertTrue(cache.exists("/a/b/c"));
         assertFalse(cache.exists("/a/b/c/d"));
      }
      catch(Exception e) {
         fail(e.toString());
      }
   }


   public void testPreloadingPassivation() throws Exception {
      cache.remove("/"); // remove nothing
      cache.put("1/2/3/4/5/d", "key", "val"); // put in memory
      cache.evict(Fqn.fromString("1/2/3/4/5/d")); // passivate node
      System.out.println("-- checking for 1/2/3/4/5/d");
      addDelay();
      try{
         assertTrue(loader.exists(Fqn.fromString("1/2/3/4/5/d")));
      }
      catch(Exception e) {
         fail(e.toString());
      }
      cache.get("1/2/3/4/5/d"); // get from loader but doesn't load attributes
      assertEquals(false, loader.exists(Fqn.fromString("1/2/3/4/5/d")));
      assertTrue(cache.exists("1/2/3/4/5/d"));
      System.out.println("-- 1/2/3/4/5/d exists");
      cache.get("1/2/3/4/5/d", "key"); // activate node
      assertEquals(false, loader.exists(Fqn.fromString("1/2/3/4/5/d")));
   }



   public void testCacheLoading2() throws CacheException {
      Set keys=null;
      cache.put("/a/b/c", "key", "val");
      try {
         keys=cache.getKeys("/a/b/c");
         assertNotNull(keys);
         assertEquals(1, keys.size());
      }
      catch(Exception e) {
         fail(e.toString());
      }

      try {
         keys.add("myKey");
      }
      catch(UnsupportedOperationException ex) {
         fail("unsupported operation: " + ex);
      }
   }


   public void testExists() throws Exception {
      cache.put("/eins/zwei/drei", "key1", "val1");
      assertTrue(cache.exists("/eins/zwei/drei"));
      assertTrue(cache.exists("/eins/zwei/drei", "key1"));
      assertFalse(cache.exists("/eins/zwei/drei", "key2"));
      assertFalse(cache.exists("/uno/due/tre"));
      assertFalse(cache.exists("/une/due/tre", "key1"));
   }

   public void testGetChildren() throws Exception {
      cache.put("/d/one", (Map)null);
      cache.put("/d/two", (Map)null);
      cache.put("/d/three", (Map)null);
      cache.get("/d");
      Set children=cache.getChildrenNames("/d");
      assertNotNull(children);
      assertEquals(3, children.size());
      assertTrue(children.contains("one"));
      assertTrue(children.contains("two"));
      assertTrue(children.contains("three"));
   }


   public void testGetChildrenWithEvictionPassivation() throws CacheException {
       cache.put("/a/b/c/1", null);
       cache.put("/a/b/c/2", null);
       cache.put("/a/b/c/3", null);
       cache.evict(Fqn.fromString("/a/b/c/1")); // passivate node
       cache.evict(Fqn.fromString("/a/b/c/2")); // passivate node
       cache.evict(Fqn.fromString("/a/b/c/3")); // passivate node
       cache.evict(Fqn.fromString("/a/b/c"));   // passivate node
       cache.evict(Fqn.fromString("/a/b"));     // passivate node
       cache.evict(Fqn.fromString("/a"));       // passivate node
       cache.evict(Fqn.fromString("/"));        // passivate node
       addDelay();
       Set children=cache.getChildrenNames("/a/b/c"); // load node children names
       assertNotNull(children);
       assertEquals(3, children.size());
       assertTrue(children.contains("1"));
       assertTrue(children.contains("2"));
       assertTrue(children.contains("3"));
       try{
          assertTrue(loader.exists(Fqn.fromString("/a/b/c")));
       }
       catch(Exception e) {
          fail(e.toString());
       }
       cache.get("/a/b/c/1", "test"); // load child
       cache.get("/a/b/c/2", "test"); // load child
       cache.get("/a/b/c/3", "test"); // load child
       cache.get("/a/b/c", "test");   // load attributes
       try{
          assertFalse(loader.exists(Fqn.fromString("/a/b/c/1")));
          assertFalse(loader.exists(Fqn.fromString("/a/b/c/2")));
          assertFalse(loader.exists(Fqn.fromString("/a/b/c/3")));
          assertFalse(loader.exists(Fqn.fromString("/a/b/c")));
       }
       catch(Exception e) {
          fail(e.toString());
       }
   }

   public void testGetChildren2() {
      try {
         cache.put("/1", null);
         cache.put("a", null);
         Set children=cache.getChildrenNames("/"); // get root node children names
         assertNotNull(children);
         assertEquals(2, children.size());
         assertTrue(children.contains("1"));
         assertTrue(children.contains("a"));
      }
      catch(Exception e) {
         fail(e.toString());
      }
   }

   public void testGetChildren3() {
      try {
         cache.put("/1", null);
         cache.put("a", null);
         Set children=cache.getChildrenNames(""); // get children from root node
         assertNotNull(children);
         assertEquals(2, children.size());
         assertTrue(children.contains("1"));
         assertTrue(children.contains("a"));
      }
      catch(Exception e) {
         fail(e.toString());
      }
   }

   public void testGetChildren4() {
      try {
         if(!cache.exists("/a/b/c"))
            cache.put("/a/b/c", null);
         Set children=cache.getChildrenNames((Fqn)null); // get "null* node children names
         assertNull(children);
      }
      catch(Exception e) {
         fail(e.toString());
      }
   }


   public void testGetChildren5() {
      try {
         cache.put("/a/1", null);
         cache.put("/a/2", null);
         cache.put("/a/3", null);
         System.out.println("cache is " + cache.printLockInfo());

         DataNode n=cache.get("/a");
         assertNotNull(n);

         Set children=cache.getChildrenNames("/a");
         assertNotNull(children);
         assertEquals(3, children.size());
      }
      catch(Exception e) {
         fail(e.toString());
      }
   }


    public void testGetChildren6Passivation() {
       try {
          cache.put("/a/1", null); // put node in memory
          cache.put("/a/2", null); // put node in memory
          cache.put("/a/3", null); // put node in memory
          System.out.println("cache is " + cache.printLockInfo());
          cache.evict(Fqn.fromString("/a/1")); // passivate node
          cache.evict(Fqn.fromString("/a/2")); // passivate node
          cache.evict(Fqn.fromString("/a/3")); // passivate node
          cache.evict(Fqn.fromString("/a"));   // passivate node
          assertTrue(loader.exists(Fqn.fromString("/a")));
          System.out.println("cache is " + cache.printLockInfo());
          addDelay();
          assertNotNull(cache.get("/a")); // load node
          assertTrue(loader.exists(Fqn.fromString("/a"))); // children haven't been loaded
          Set children=cache.getChildrenNames("/a");
          assertNotNull("No children were loaded", children);
          System.out.println("children: " + children);
          assertEquals("3 children weren't loaded", 3, children.size());
          cache.get("/a/1", "test"); // activate node
          assertFalse(loader.exists(Fqn.fromString("/a/1")));
          cache.get("/a/2", "test"); // activate node
          assertFalse(loader.exists(Fqn.fromString("/a/2")));
          cache.get("/a/3", "test"); // activate node
          assertFalse(loader.exists(Fqn.fromString("/a/3")));
          cache.get("/a", "test"); // activate node
          assertFalse(loader.exists(Fqn.fromString("/a")));
       }
       catch(Exception e) {
          fail(e.toString());
       }
    }

   public void testGetChildren7Passivation() {
      try {
         cache.put("/a/1", null);
         cache.put("/a/2", null);
         cache.put("/a/3", null);
         cache.put("/a", "test", "test");
         System.out.println("cache is " + cache.printLockInfo());
         cache.evict(Fqn.fromString("/a/1")); // passivate node
         cache.evict(Fqn.fromString("/a/2")); // passivate node
         cache.evict(Fqn.fromString("/a/3")); // passivate node
         cache.evict(Fqn.fromString("/a"));   // passivate node
         assertTrue(loader.exists(Fqn.fromString("/a")));
         System.out.println("cache is " + cache.printLockInfo());
         addDelay();
         Object val=cache.get("/a", "test"); // load node's attributes but not children
         assertEquals("attributes weren't loaded", "test", val);

         Set children=cache.getChildrenNames("/a"); // get node's children names
         assertNotNull("No children were loaded", children);
         System.out.println("children: " + children);
         assertEquals("3 children weren't loaded", 3, children.size());
         cache.get("/a/1", "test"); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/1")));
         cache.get("/a/2", "test"); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/2")));
         cache.get("/a/3", "test"); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/3")));
         assertTrue(loader.exists(Fqn.fromString("/a")));
      }
      catch(Exception e) {
         fail(e.toString());
      }
   }

   public void testGetChildren8Passivation() {
      try {
         cache.put("/a/1", null);
         cache.put("/a/2", null);
         cache.put("/a/3", null);
         System.out.println("cache is " + cache.printLockInfo());
         cache.evict(Fqn.fromString("/a/1")); // passivate node
         cache.evict(Fqn.fromString("/a/2")); // passivate node
         cache.evict(Fqn.fromString("/a/3")); // passivate node
         cache.evict(Fqn.fromString("/a"));   // passivate node
         
         System.out.println("cache is " + cache.printLockInfo());
         addDelay();
         assertNull(cache.get("/a", "test")); // load attributes only
         assertTrue(loader.exists(Fqn.fromString("/a"))); // loaded attibutes but not children

         assertNull(cache.get("/a/1", "test")); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/1"))); // loaded attributes and has no children
         Set children=cache.getChildrenNames("/a"); // load children names
         assertNotNull("No children were loaded", children);
         System.out.println("children: " + children);
         assertEquals("3 children weren't loaded", 3, children.size());
         assertTrue(loader.exists(Fqn.fromString("/a"))); //loaded children but didn't initalizae them
      }
      catch(Exception e) {
         fail(e.toString());
      }
   }

   public void testGetChildren9Passivation() {
      try {
         cache.put("/a/1", null);
         cache.put("/a/2", null);
         cache.put("/a/3", null);
         System.out.println("cache is " + cache.printLockInfo());
         cache.evict(Fqn.fromString("/a/1")); // passivate node
         cache.evict(Fqn.fromString("/a/2")); // passivate node
         cache.evict(Fqn.fromString("/a/3")); // passivate node
         cache.evict(Fqn.fromString("/a"));   // passivate node
         assertTrue(loader.exists(Fqn.fromString("/a")));
         System.out.println("cache is " + cache.printLockInfo());
         addDelay();
         
         cache.get("/a/1", "test"); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/1")));
         cache.get("/a/2", "test"); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/2")));
         cache.get("/a/3", "test"); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/3")));
         Set children=cache.getChildrenNames("/a"); // get node's children names
         assertNotNull("No children were loaded", children);
         System.out.println("children: " + children);
         assertEquals("3 children weren't loaded", 3, children.size());
         assertNull(cache.get("/a", "test")); // load attributes and has no children by now, activation
         assertFalse(loader.exists(Fqn.fromString("/a")));

         cache.evict(Fqn.fromString("/a/1")); // passivate node
         cache.evict(Fqn.fromString("/a/2")); // passivate node
         cache.evict(Fqn.fromString("/a/3")); // passivate node
         cache.evict(Fqn.fromString("/a"));   // passivate node
         System.out.println("cache is " + cache.printLockInfo());
         assertTrue(loader.exists(Fqn.fromString("/a")));

         cache.get("/a/1", "test"); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/1")));
         cache.get("/a/2", "test"); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/2")));
         cache.get("/a/3", "test"); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/3")));
         children=cache.getChildrenNames("/a"); // get children names
         assertNotNull("No children were loaded", children);
         System.out.println("children: " + children);
         assertEquals("3 children weren't loaded", 3, children.size());
         assertNull(cache.get("/a", "test")); // load attributes and has no children by now, activation
         assertFalse(loader.exists(Fqn.fromString("/a")));
      }
      catch(Exception e) {
         fail(e.toString());
      }
   }


    public void testGetChildren10Passivation() {
      try {
         cache.put("/a/1", null); 
         cache.put("/a/2", null);
         cache.put("/a/3", null);
         System.out.println("cache is " + cache.printLockInfo());
         cache.evict(Fqn.fromString("/a/1")); // passivate node
         cache.evict(Fqn.fromString("/a/2")); // passivate node
         cache.evict(Fqn.fromString("/a/3")); // passivate node
         cache.evict(Fqn.fromString("/a"));   // passivate node
         System.out.println("cache is " + cache.printLockInfo());
         addDelay();
         assertTrue(loader.exists(Fqn.fromString("/a")));
         assertNull(cache.get("/a", "test")); // load attributes from loader

         cache.get("/a/1", "test"); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/1")));
         cache.get("/a/2", "test"); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a/2")));
         cache.get("/a/3", "test"); // passivate node
         assertFalse(loader.exists(Fqn.fromString("/a/3")));
         Set children=cache.getChildrenNames("/a");
         assertNotNull("No children were loaded", children);
         System.out.println("children: " + children);
         assertEquals("3 children weren't loaded", 3, children.size());

         assertNull(cache.get("/a", "test")); // activate node
         assertFalse(loader.exists(Fqn.fromString("/a")));
      }
      catch(Exception e) {
         fail(e.toString());
      }
   }

   public void testRemoveData() throws Exception {
      String key="/x/y/z/";
      cache.put(key, "keyA", "valA");
      cache.put(key, "keyB", "valB");
      cache.put(key, "keyC", "valC");
      assertEquals(3, cache.getKeys(key).size());
      cache.removeData(key);
      Set keys=cache.getKeys(key);
      assertEquals(0, keys.size());
      cache.remove("/x");
      Object val=cache.get(key, "keyA");
      assertNull(val);
   }


   public void testRemoveData2Passivation() throws Exception {
      Set keys;
      Fqn key=Fqn.fromString("/x/y/z/");
      cache.put(key, "keyA", "valA");
      cache.put(key, "keyB", "valB");
      cache.put(key, "keyC", "valC");
      addDelay();
      keys=cache.getKeys(key);
      assertEquals(3, keys.size());
      cache.removeData(key);
      cache.evict(key); // passivate node
      
      addDelay();
      keys=cache.getKeys(key); // activate node
      try{
          assertFalse(loader.exists(key));
      }
      catch(Exception e) {
         fail(e.toString());
      }
      assertEquals(0, keys.size());
   }

   public void testRemoveData3Passivation() throws Exception {
      Set keys;
      Fqn key=Fqn.fromString("/x/y/z/");
      cache.put(key, "keyA", "valA");
      cache.put(key, "keyB", "valB");
      cache.put(key, "keyC", "valC");
      keys=cache.getKeys(key);
      assertEquals(3, keys.size());
      cache.evict(key); // passivate node
      assertTrue(loader.exists(key));
      cache.removeData(key);
      keys=cache.getKeys(key); // activate node
      assertFalse(loader.exists(key));
      assertEquals(0, keys.size());
   }

   public void testRemoveKey() throws Exception {
      String key="/x/y/z/";
      cache.put(key, "keyA", "valA");
      cache.put(key, "keyB", "valB");
      cache.put(key, "keyC", "valC");
      cache.remove(key, "keyA");
      assertEquals(2, cache.getKeys(key).size());
      cache.remove("/x");
   }


   public void testRemoveKey2() throws CacheException {
      final String NODE="/test";
      final String KEY="key";
      Object retval=null;
      cache.remove(NODE);
      retval=cache.put(NODE, KEY, new Integer(10));
      assertNull(retval);
      addDelay();
      retval=cache.remove(NODE, KEY);
      assertEquals(new Integer(10), retval);
      addDelay();
      retval=cache.remove(NODE, KEY);
      assertNull(retval);
   }

   public void testRemoveKey3Passivation() throws CacheException {
      final String NODE="/test";
      final String KEY="key";
      Object retval=null;
      cache.remove(NODE);
      retval=cache.put(NODE, KEY, new Integer(10));
      assertNull(retval);

      cache.evict(Fqn.fromString(NODE)); // passivate node
      addDelay();
      try{
         assertTrue(loader.exists(Fqn.fromString(NODE)));
         assertEquals(new Integer(10), loader.get(Fqn.fromString(NODE)).get(KEY));
      }
      catch(Exception e) {
         fail(e.toString());
      }
      retval=cache.remove(NODE, KEY); // activate node
      assertEquals(new Integer(10), retval);
      try{
         assertFalse(loader.exists(Fqn.fromString(NODE)));
      }
      catch(Exception e) {
         fail(e.toString());
      }
      
      cache.evict(Fqn.fromString(NODE)); // passiave node
      addDelay();
      retval=cache.remove(NODE, KEY); // activate node
      try{
         assertFalse(loader.exists(Fqn.fromString(NODE)));
      }
      catch(Exception e) {
         fail(e.toString());
      }
      assertNull(retval);
   }


   public void testRemove() throws Exception {
      String key="/x/y/z/";
      cache.put(key, "keyA", "valA");
      cache.put(key, "keyB", "valB");
      cache.put(key, "keyC", "valC");
      cache.remove("/x");
      assertNull(cache.get(key, "keyA"));
      addDelay();
      Set keys=cache.getKeys(key);
      assertNull(keys);
      cache.remove("/x");
   }


   public void testRemoveRoot() throws Exception {
      assertEquals(0, cache.getKeys("/").size());
      cache.put("/1/2/3/4/5", null);
      cache.put("uno/due/tre", null);
      cache.put("1/2/3/a", null);
      cache.put("/eins/zwei/drei", null);
      cache.put("/one/two/three", null);
      cache.remove("/");
      assertEquals(0, cache.getKeys("/").size());
   }


   public void testEvictionWithCacheLoaderPassivation() throws Exception {
      cache.put("/first/second", "key1", "val1");        
      cache.put("/first/second/third", "key2", "val2"); 
      cache.evict(Fqn.fromString("/first/second"));      // pasivate node to cache loader
      addDelay();
      assertTrue(loader.exists(Fqn.fromString("/first/second")));
      assertTrue(cache.exists("/first"));
      String val=(String)cache.get("/first/second", "key1"); // loads attributes only
      assertTrue(loader.exists(Fqn.fromString("/first/second")));
      assertEquals("val1", val);
      String val2=(String)cache.get("/first/second/third", "key2"); // activate node
      assertFalse(loader.exists(Fqn.fromString("/first/second/third")));
      assertEquals("val2", val2);
      assertTrue(cache.exists("/first/second/third"));
      assertTrue(cache.exists("/first/second"));
      assertTrue(cache.exists("/first"));
   }


   public void testEvictionWithCacheLoaderPassivation2() throws Exception {
      cache.put("/first/second/third", "key1", "val1");   // stored in cache loader
      cache.evict(Fqn.fromString("/first/second/third"));  // passivate node, note: it has no children
      addDelay();
      try{
         assertTrue(loader.exists(Fqn.fromString("/first/second/third")));
      }
      catch(Exception e){
         fail(e.toString());
      }
      assertTrue(cache.exists("/first/second"));
      assertTrue(cache.exists("/first"));
      String val=(String)cache.get("/first/second/third", "key1"); // activate node
      try{
         assertFalse(loader.exists(Fqn.fromString("/first/second/third")));
      }
      catch(Exception e){
         fail(e.toString());
      }
      assertEquals("val1", val);
      assertTrue(cache.exists("/first/second/third"));
      assertTrue(cache.exists("/first/second"));
      assertTrue(cache.exists("/first"));
   }



   public void testEvictionWithGetChildrenNamesPassivation() throws Exception {
      cache.put("/a/1", null);
      cache.put("/a/2", null);
      cache.put("/a/3", null);
      cache.evict(Fqn.fromString("/a/1")); // passivate node
      assertTrue(loader.exists(Fqn.fromString("/a/1")));
      cache.evict(Fqn.fromString("/a/2")); // passivate node
      assertTrue(loader.exists(Fqn.fromString("/a/2")));
      cache.evict(Fqn.fromString("/a/3")); // passivate node
      assertTrue(loader.exists(Fqn.fromString("/a/3")));
      cache.evict(Fqn.fromString("/a")); // passivate node
      assertTrue(loader.exists(Fqn.fromString("/a")));
      addDelay();
      DummyTransactionManager mgr=DummyTransactionManager.getInstance();
      mgr.begin();
      tx=mgr.getTransaction();
      Set children=cache.getChildrenNames("/a");
      assertEquals(3, children.size());
      assertTrue(children.contains("1"));
      assertTrue(children.contains("2"));
      assertTrue(children.contains("3"));
      assertEquals(5, cache.getNumberOfLocksHeld());
      tx.commit();
   }



   public void testTxPutCommit() throws Exception, NotSupportedException {
      
      DummyTransactionManager mgr=DummyTransactionManager.getInstance();
      mgr.begin();
      tx=mgr.getTransaction();
      
      cache.put("/one/two/three", "key1", "val1");
      cache.put("/one/two/three/four", "key2", "val2");
      
      tx.commit();

      assertNotNull(cache.getKeys("/one/two/three"));
      assertEquals("val1", cache.get(Fqn.fromString("/one/two/three"), "key1"));
      mgr.begin();
      tx=mgr.getTransaction();
      
      cache.evict(Fqn.fromString("/one/two/three"));
      cache.evict(Fqn.fromString("/one/two/three/four"));
      
      tx.commit();
      assertTrue(loader.exists(Fqn.fromString("/one/two/three")));
      assertTrue(loader.exists(Fqn.fromString("/one/two/three/four")));
      assertNotNull(cache.getKeys("/one/two/three"));
      Set children=cache.getChildrenNames("/one");
      assertEquals(1, children.size());
      cache.remove("/");
   }


   public void testTxPutRollback() throws Exception, NotSupportedException {
      DummyTransactionManager mgr=DummyTransactionManager.getInstance();

      cache.remove("/one");
      addDelay();
      mgr.begin();
      tx=mgr.getTransaction();

      cache.put("/one/two/three", "key1", "val1");
      cache.put("/one/two/three/four", "key2", "val2");
      tx.rollback();
      addDelay();
      assertNull(cache.getKeys("/one/two/three"));
      Set children=cache.getChildrenNames("/one");
      assertNull(children);
      assertFalse(loader.exists(Fqn.fromString("/one/two/three")));
      assertFalse(loader.exists(Fqn.fromString("/one/two/three/four")));
   }



   public void testPassivationAndActivation() throws Exception {
      Object val, val2;
      Fqn NODE=Fqn.fromString("/test");
      loader.remove(Fqn.fromString("/"));
      cache.put(NODE, "key", "val");
      //val=loader.get(NODE).get("key");
      assertNull("value cannot be passivated yet (only on eviction)", loader.get(NODE));
      cache.evict(NODE);
      assertEquals(0, cache.getNumberOfNodes());
      assertEquals(0, cache.getNumberOfAttributes());
      val=loader.get(NODE).get("key");
      assertNotNull("value must have been passivated on evict()", val);
      assertEquals(val, "val");
      val2=cache.get(NODE, "key");
      assertNotNull(val2);
      assertEquals(val, val2);
//      val=loader.get(NODE).get("key");
      assertNull("value should have been deleted from store on activation", loader.get(NODE));
   }




   /**
    * Tests basic operations without a transaction.
    */
   public void testBasicOperations()
      throws Exception {

      doTestBasicOperations();
   }

   /**
    * Tests basic operations with a transaction.
    */
   public void testBasicOperationsTransactional()
      throws Exception {

      DummyTransactionManager mgr=DummyTransactionManager.getInstance();
      mgr.begin();
      tx=mgr.getTransaction();
      doTestBasicOperations();
      tx.commit();
   }

   /**
    * Tests basic operations.
    */
   private void doTestBasicOperations() throws Exception {

      /* One FQN only. */
      doPutTests(new Fqn("key"));
      doRemoveTests(new Fqn("key"));
      // assertEquals(0, loader.loadEntireState().length);

      /* Add three FQNs, middle FQN last. */
      doPutTests(new Fqn("key1"));
      doPutTests(new Fqn("key3"));
      doPutTests(new Fqn("key2"));
      assertEquals(4, loader.get(new Fqn("key1")).size());
      assertEquals(4, loader.get(new Fqn("key2")).size());
      assertEquals(4, loader.get(new Fqn("key3")).size());

      /* Remove middle FQN first, then the others. */
      doRemoveTests(new Fqn("key2"));
      doRemoveTests(new Fqn("key3"));
      doRemoveTests(new Fqn("key1"));
      assertEquals(null, loader.get(new Fqn("key1")));
      assertEquals(null, loader.get(new Fqn("key2")));
      assertEquals(null, loader.get(new Fqn("key3")));
      // assertEquals(0, loader.loadEntireState().length);
   }

   /**
    * Do basic put tests for a given FQN.
    */
   private void doPutTests(Fqn fqn)
      throws Exception {

      assertTrue(!loader.exists(fqn));

      /* put(Fqn,Object,Object) and get(Fqn,Object) */
      Object oldVal;
      oldVal = loader.put(fqn, "one", "two");
      assertNull(oldVal);
      addDelay();
      oldVal = loader.put(fqn, "three", "four");
      assertNull(oldVal);
      addDelay();
      assertEquals("two", loader.get(fqn).get("one"));
      assertEquals("four", loader.get(fqn).get("three"));
      addDelay();
      oldVal = loader.put(fqn, "one", "xxx");
      assertEquals("two", oldVal);
      addDelay();
      oldVal = loader.put(fqn, "one", "two");
      assertEquals("xxx", oldVal);

      /* get(Fqn) */
      addDelay();
      Map map = loader.get(fqn);
      assertEquals(2, map.size());
      assertEquals("two", map.get("one"));
      assertEquals("four", map.get("three"));

      /* put(Fqn,Map) */
      map.put("five", "six");
      map.put("seven", "eight");
      loader.put(fqn, map);
      addDelay();
      assertEquals("six", loader.get(fqn).get("five"));
      assertEquals("eight", loader.get(fqn).get("seven"));
      assertEquals(map, loader.get(fqn));
      assertEquals(4, map.size());

      assertTrue(loader.exists(fqn));
   }

   /**
    * Do basic remove tests for a given FQN.
    */
   private void doRemoveTests(Fqn fqn)
      throws Exception {

      /* remove(Fqn,Object) */
      Object oldVal;
      oldVal = loader.remove(fqn, "one");
      assertEquals("two", oldVal);
      addDelay();
      oldVal = loader.remove(fqn, "five");
      assertEquals("six", oldVal);
      addDelay();
      assertEquals(null, loader.get(fqn).get("one"));
      assertEquals(null, loader.get(fqn).get("five"));
      assertEquals("four", loader.get(fqn).get("three"));
      assertEquals("eight", loader.get(fqn).get("seven"));
      Map map = loader.get(fqn);
      assertEquals(2, map.size());
      assertEquals("four", map.get("three"));
      assertEquals("eight", map.get("seven"));

      /* remove(Fqn) */
      assertTrue(loader.exists(fqn));
      loader.remove(fqn);
      addDelay();
      assertNull(loader.get(fqn));
      assertTrue(!loader.exists(fqn));
   }

   /**
    * Tests creating implicit intermediate nodes when a leaf node is created,
    * and tests removing subtrees.
    */
   public void testMultiLevelTree()
      throws Exception {

      /* Create top level node implicitly. */
      assertTrue(!loader.exists(new Fqn("key0")));
      loader.put(Fqn.fromString("/key0/level1/level2"), null);
      addDelay();
      assertTrue(loader.exists(Fqn.fromString("/key0/level1/level2")));
      assertTrue(loader.exists(Fqn.fromString("/key0/level1")));
      assertTrue(loader.exists(new Fqn("key0")));

      /* Remove leaf, leaving implicitly created middle level. */
      loader.put(Fqn.fromString("/key0/x/y"), null);
      addDelay();
      assertTrue(loader.exists(Fqn.fromString("/key0/x/y")));
      assertTrue(loader.exists(Fqn.fromString("/key0/x")));
      loader.remove(Fqn.fromString("/key0/x/y"));
      addDelay();
      assertTrue(!loader.exists(Fqn.fromString("/key0/x/y")));
      assertTrue(loader.exists(Fqn.fromString("/key0/x")));

      /* Delete top level to delete everything. */
      loader.remove(new Fqn("key0"));
      addDelay();
      assertTrue(!loader.exists(new Fqn("key0")));
      assertTrue(!loader.exists(Fqn.fromString("/key0/level1/level2")));
      assertTrue(!loader.exists(Fqn.fromString("/key0/level1")));
      assertTrue(!loader.exists(Fqn.fromString("/key0/x")));

      /* Add three top level nodes as context. */
      loader.put(new Fqn("key1"), null);
      loader.put(new Fqn("key2"), null);
      loader.put(new Fqn("key3"), null);
      addDelay();
      assertTrue(loader.exists(new Fqn("key1")));
      assertTrue(loader.exists(new Fqn("key2")));
      assertTrue(loader.exists(new Fqn("key3")));

      /* Put /key3/level1/level2.  level1 should be implicitly created. */
      assertTrue(!loader.exists(Fqn.fromString("/key3/level1")));
      assertTrue(!loader.exists(Fqn.fromString("/key3/level1/level2")));
      loader.put(Fqn.fromString("/key3/level1/level2"), null);
      addDelay();
      assertTrue(loader.exists(Fqn.fromString("/key3/level1/level2")));
      assertTrue(loader.exists(Fqn.fromString("/key3/level1")));

      /* Context nodes should still be intact. */
      assertTrue(loader.exists(new Fqn("key1")));
      assertTrue(loader.exists(new Fqn("key2")));
      assertTrue(loader.exists(new Fqn("key3")));

      /* Remove middle level only. */
      loader.remove(Fqn.fromString("/key3/level1"));
      addDelay();
      assertTrue(!loader.exists(Fqn.fromString("/key3/level1/level2")));
      assertTrue(!loader.exists(Fqn.fromString("/key3/level1")));

      /* Context nodes should still be intact. */
      assertTrue(loader.exists(new Fqn("key1")));
      assertTrue(loader.exists(new Fqn("key2")));
      assertTrue(loader.exists(new Fqn("key3")));

      /* Delete first root, leaving other roots. */
      loader.remove(new Fqn("key1"));
      addDelay();
      assertTrue(!loader.exists(new Fqn("key1")));
      assertTrue(loader.exists(new Fqn("key2")));
      assertTrue(loader.exists(new Fqn("key3")));

      /* Delete last root, leaving other roots. */
      loader.remove(new Fqn("key3"));
      addDelay();
      assertTrue(loader.exists(new Fqn("key2")));
      assertTrue(!loader.exists(new Fqn("key3")));

      /* Delete final root, leaving none. */
      loader.remove(new Fqn("key2"));
      addDelay();
      assertTrue(!loader.exists(new Fqn("key0")));
      assertTrue(!loader.exists(new Fqn("key1")));
      assertTrue(!loader.exists(new Fqn("key2")));
      assertTrue(!loader.exists(new Fqn("key3")));

      /* Repeat all tests above using put(Fqn,Object,Object) and get(Fqn) */

      assertNull(loader.get(new Fqn("key0")));
      loader.put(Fqn.fromString("/key0/level1/level2"), "a", "b");
      addDelay();
      assertNotNull(loader.get(Fqn.fromString("/key0/level1/level2")));
      assertNotNull(loader.get(Fqn.fromString("/key0/level1")));
      assertTrue(loader.get(Fqn.fromString("/key0/level1")).isEmpty());
      assertNotNull(loader.get(new Fqn("key0")));
      assertTrue(loader.get(Fqn.fromString("/key0")).isEmpty());

      loader.put(Fqn.fromString("/key0/x/y"), "a", "b");
      addDelay();
      assertNotNull(loader.get(Fqn.fromString("/key0/x/y")));
      assertNotNull(loader.get(Fqn.fromString("/key0/x")));
      assertTrue(loader.get(Fqn.fromString("/key0/x")).isEmpty());
      loader.remove(Fqn.fromString("/key0/x/y"));
      addDelay();
      assertNull(loader.get(Fqn.fromString("/key0/x/y")));
      assertNotNull(loader.get(Fqn.fromString("/key0/x")));
      assertTrue(loader.get(Fqn.fromString("/key0/x")).isEmpty());

      loader.remove(new Fqn("key0"));
      addDelay();
      assertNull(loader.get(new Fqn("key0")));
      assertNull(loader.get(Fqn.fromString("/key0/level1/level2")));
      assertNull(loader.get(Fqn.fromString("/key0/level1")));
      assertNull(loader.get(Fqn.fromString("/key0/x")));

      loader.put(new Fqn("key1"), "a", "b");
      loader.put(new Fqn("key2"), "a", "b");
      loader.put(new Fqn("key3"), "a", "b");
      addDelay();
      assertNotNull(loader.get(new Fqn("key1")));
      assertNotNull(loader.get(new Fqn("key2")));
      assertNotNull(loader.get(new Fqn("key3")));

      assertNull(loader.get(Fqn.fromString("/key3/level1")));
      assertNull(loader.get(Fqn.fromString("/key3/level1/level2")));
      loader.put(Fqn.fromString("/key3/level1/level2"), "a", "b");
      addDelay();
      assertNotNull(loader.get(Fqn.fromString("/key3/level1/level2")));
      assertNotNull(loader.get(Fqn.fromString("/key3/level1")));
      assertTrue(loader.get(Fqn.fromString("/key3/level1")).isEmpty());

      assertNotNull(loader.get(new Fqn("key1")));
      assertNotNull(loader.get(new Fqn("key2")));
      assertNotNull(loader.get(new Fqn("key3")));

      loader.remove(Fqn.fromString("/key3/level1"));
      addDelay();
      assertNull(loader.get(Fqn.fromString("/key3/level1/level2")));
      assertNull(loader.get(Fqn.fromString("/key3/level1")));

      assertNotNull(loader.get(new Fqn("key1")));
      assertNotNull(loader.get(new Fqn("key2")));
      assertNotNull(loader.get(new Fqn("key3")));

      loader.remove(new Fqn("key1"));
      addDelay();
      assertNull(loader.get(new Fqn("key1")));
      assertNotNull(loader.get(new Fqn("key2")));
      assertNotNull(loader.get(new Fqn("key3")));

      loader.remove(new Fqn("key3"));
      addDelay();
      assertNotNull(loader.get(new Fqn("key2")));
      assertNull(loader.get(new Fqn("key3")));

      loader.remove(new Fqn("key2"));
      addDelay();
      assertNull(loader.get(new Fqn("key0")));
      assertNull(loader.get(new Fqn("key1")));
      assertNull(loader.get(new Fqn("key2")));
      assertNull(loader.get(new Fqn("key3")));
   }

   /**
    * Tests the getChildrenNames() method.
    */
   public void testGetChildrenNames()
      throws Exception {

      checkChildren(new Fqn(), null);
      checkChildren(Fqn.fromString("/key0"), null);

      loader.put(Fqn.fromString("/key0"), null);
      addDelay();
      checkChildren(new Fqn(), new String[] { "key0" });

      loader.put(Fqn.fromString("/key1/x"), null);
      addDelay();
      checkChildren(new Fqn(), new String[] { "key0", "key1" });
      checkChildren(Fqn.fromString("/key1"), new String[] { "x" });

      loader.remove(Fqn.fromString("/key1/x"));
      addDelay();
      checkChildren(new Fqn(), new String[] { "key0", "key1" });
      checkChildren(Fqn.fromString("/key0"), null);
      checkChildren(Fqn.fromString("/key1"), null);

      loader.put(Fqn.fromString("/key0/a"), null);
      loader.put(Fqn.fromString("/key0/ab"), null);
      loader.put(Fqn.fromString("/key0/abc"), null);
      addDelay();
      checkChildren(Fqn.fromString("/key0"),
                    new String[] { "a", "ab", "abc" });

      loader.put(Fqn.fromString("/key0/xxx"), null);
      loader.put(Fqn.fromString("/key0/xx"), null);
      loader.put(Fqn.fromString("/key0/x"), null);
      addDelay();
      checkChildren(Fqn.fromString("/key0"),
                    new String[] { "a", "ab", "abc", "x", "xx", "xxx" });

      loader.put(Fqn.fromString("/key0/a/1"), null);
      loader.put(Fqn.fromString("/key0/a/2"), null);
      loader.put(Fqn.fromString("/key0/a/2/1"), null);
      addDelay();
      checkChildren(Fqn.fromString("/key0/a/2"), new String[] { "1" });
      checkChildren(Fqn.fromString("/key0/a"), new String[] { "1", "2" });
      checkChildren(Fqn.fromString("/key0"),
                    new String[] { "a", "ab", "abc", "x", "xx", "xxx" });
//
//      loader.put(Fqn.fromString("/key0/\u0000"), null);
//      loader.put(Fqn.fromString("/key0/\u0001"), null);
//      checkChildren(Fqn.fromString("/key0"),
//                    new String[] { "a", "ab", "abc", "x", "xx", "xxx",
//                                   "\u0000", "\u0001"});
//
//      loader.put(Fqn.fromString("/\u0001"), null);
//      checkChildren(new Fqn(), new String[] { "key0", "key1", "\u0001" });
//
//      loader.put(Fqn.fromString("/\u0001/\u0001"), null);
//      checkChildren(Fqn.fromString("/\u0001"), new String[] { "\u0001" });
//
//      loader.put(Fqn.fromString("/\u0001/\uFFFF"), null);
//      checkChildren(Fqn.fromString("/\u0001"),
//                    new String[] { "\u0001", "\uFFFF" });
//
//      loader.put(Fqn.fromString("/\u0001/\uFFFF/\u0001"), null);
//      checkChildren(Fqn.fromString("/\u0001/\uFFFF"),
//                    new String[] { "\u0001" });
   }

   /**
    * Checks that the given list of children part names is returned.
    */
   private void checkChildren(Fqn fqn, String[] names)
      throws Exception {

      Set set = loader.getChildrenNames(fqn);
      if (names != null) {
         assertEquals(names.length, set.size());
         for (int i = 0; i < names.length; i += 1) {
            assertTrue(set.contains(names[i]));
         }
      } else {
         assertNull(set);
      }
   }

   /**
    * Tests basic operations without a transaction.
    */
   public void testModifications()
      throws Exception {

      doTestModifications();
   }

   /**
    * Tests basic operations with a transaction.
    */
   public void testModificationsTransactional()
      throws Exception {
      DummyTransactionManager mgr=DummyTransactionManager.getInstance();
      mgr.begin();
      tx=mgr.getTransaction();
      doTestModifications();
      tx.commit();
   }

   /**
    * Tests modifications.
    */
   private void doTestModifications()
      throws Exception {

      /* PUT_KEY_VALUE, PUT_DATA */
      List list = createUpdates();
      loader.put(list);
      addDelay();
      checkModifications(list);

      /* REMOVE_KEY_VALUE */
      list = new ArrayList();
      Modification mod = new Modification();
      mod.setType(Modification.REMOVE_KEY_VALUE);
      mod.setFqn(FQN);
      mod.setKey("one");
      list.add(mod);
      loader.put(list);
      addDelay();
      checkModifications(list);

      /* REMOVE_NODE */
      list = new ArrayList();
      mod = new Modification();
      mod.setType(Modification.REMOVE_NODE);
      mod.setFqn(FQN);
      list.add(mod);
      loader.put(list);
      addDelay();
      checkModifications(list);
      assertEquals(null, loader.get(FQN));

      /* REMOVE_DATA */
      loader.put(FQN, "one", "two");
      list = new ArrayList();
      mod = new Modification();
      mod.setType(Modification.REMOVE_DATA);
      mod.setFqn(FQN);
      list.add(mod);
      loader.put(list);
      addDelay();
      checkModifications(list);
   }

   /**
    * Tests a one-phase transaction.
    */
   public void testOnePhaseTransaction()
      throws Exception {
      List mods = createUpdates();
      loader.prepare(null, mods, true);
      checkModifications(mods);
   }

   /**
    * Tests a two-phase transaction.
    */
   public void testTwoPhaseTransactionPassivation()
      throws Exception {

      Object txnKey = new Object();
      List mods = createUpdates();
      loader.prepare(txnKey, mods, false);
//      try {
//         checkModifications(mods);
//         // fail("Expected lock timeout");
//      } catch (DeadlockException expected) {}
      loader.commit(txnKey);
      addDelay();
      checkModifications(mods);
   }

   /**
    * Tests rollback of a two-phase transaction.
    */
   public void testTransactionRollbackPassivation()
      throws Exception {

      loader.remove(Fqn.fromString("/"));

      int num=loader.loadEntireState().length;

      Object txnKey = new Object();
      List mods = createUpdates();
      loader.prepare(txnKey, mods, false);
      loader.rollback(txnKey);
      assertEquals(num, loader.loadEntireState().length);
   }

   /**
    * Creates a set of update (PUT_KEY_VALUE, PUT_DATA) modifications.
    */
   private List createUpdates() {

      List list = new ArrayList();

      Modification mod = new Modification();
      mod.setType(Modification.PUT_KEY_VALUE);
      mod.setFqn(FQN);
      mod.setKey("one");
      mod.setValue("two");
      list.add(mod);

      mod = new Modification();
      mod.setType(Modification.PUT_KEY_VALUE);
      mod.setFqn(FQN);
      mod.setKey("three");
      mod.setValue("four");
      list.add(mod);

      Map map = new HashMap();
      map.put("five", "six");
      map.put("seven", "eight");
      mod = new Modification();
      mod.setType(Modification.PUT_DATA);
      mod.setFqn(FQN);
      mod.setData(map);
      list.add(mod);

      return list;
   }

   /**
    * Checks that a list of modifications was applied.
    */
   private void checkModifications(List list)
      throws Exception {

      for (int i = 0; i < list.size(); i += 1) {
         Modification mod = (Modification) list.get(i);
         Fqn fqn = mod.getFqn();
         switch (mod.getType()) {
         case Modification.PUT_KEY_VALUE:
            assertEquals(mod.getValue(), loader.get(fqn).get(mod.getKey()));
            break;
         case Modification.PUT_DATA:
            Map map = mod.getData();
            for (Iterator iter = map.keySet().iterator(); iter.hasNext();) {
               Object key = iter.next();
               assertEquals(map.get(key), loader.get(fqn).get(key));
            }
            break;
         case Modification.REMOVE_KEY_VALUE:
            assertEquals(null, loader.get(fqn).get(mod.getKey()));
            break;
         case Modification.REMOVE_DATA:
            map = loader.get(fqn);
            assertNotNull(map);
            assertTrue(map.isEmpty());
            break;
         case Modification.REMOVE_NODE:
            assertEquals(null, loader.get(fqn));
            break;
         default:
            fail("unknown type: " + mod);
            break;
         }
      }
   }







   /**
    * Tests that null keys and values work as for a standard Java Map.
    */
   public void testNullKeysAndValues()
      throws Exception {

      loader.put(FQN, null, "x");
      addDelay();
      assertEquals("x", loader.get(FQN).get(null));
      Map map = loader.get(FQN);
      assertEquals(1, map.size());
      assertEquals("x", map.get(null));

      loader.put(FQN, "y", null);
      addDelay();
      assertEquals(null, loader.get(FQN).get("y"));
      map = loader.get(FQN);
      assertEquals(2, map.size());
      assertEquals("x", map.get(null));
      assertEquals(null, map.get("y"));

      loader.remove(FQN, null);
      addDelay();
      assertEquals(null, loader.get(FQN).get(null));
      assertEquals(1, loader.get(FQN).size());

      loader.remove(FQN, "y");
      addDelay();
      assertNotNull(loader.get(FQN));
      assertNull(loader.get(FQN).get("y"));
      assertEquals(0, loader.get(FQN).size());

      map = new HashMap();
      map.put(null, null);
      loader.put(FQN, map);
      addDelay();
      assertEquals(map, loader.get(FQN));

      loader.remove(FQN);
      addDelay();
      assertNull(loader.get(FQN));

      map = new HashMap();
      map.put("xyz", null);
      map.put(null, "abc");
      loader.put(FQN, map);
      addDelay();
      assertEquals(map, loader.get(FQN));

      loader.remove(FQN);
      addDelay();
      assertNull(loader.get(FQN));
   }

   /**
    * Test non-default database name.
    */
   public void testDatabaseNamePassivation()
      throws Exception {

      loader.put(FQN, "one", "two");
      addDelay();
      assertEquals("two", loader.get(FQN).get("one"));
   }

   /**
    * Test load/store state.
    */
   public void testLoadAndStore()
      throws Exception {

      /* Empty state. */
      loader.remove(Fqn.fromString("/"));
      // assertEquals(0, loader.loadEntireState().length);
//      loader.storeEntireState(new byte[0]);
//      assertEquals(0, loader.loadEntireState().length);
//      loader.storeEntireState(null);
//      assertEquals(0, loader.loadEntireState().length);
//      assertEquals(null, loader.get(FQN));

      /* Use a complex object to ensure that the class catalog is used. */
      Complex c1 = new Complex();
      Complex c2 = new Complex(c1);

      /* Add objects. */
      loader.put(FQN, new Integer(1), c1);
      loader.put(FQN, new Integer(2), c2);
      addDelay();
      assertEquals(c1, loader.get(FQN).get(new Integer(1)));
      assertEquals(c2, loader.get(FQN).get(new Integer(2)));
      assertEquals(2, loader.get(FQN).size());

      /* Save state. */
      byte[] state = loader.loadEntireState();
      assertTrue(state.length > 0);


      /* Restore state. */
      loader.storeEntireState(state);
      addDelay();
      assertEquals(c1, loader.get(FQN).get(new Integer(1)));
      assertEquals(c2, loader.get(FQN).get(new Integer(2)));
      assertEquals(2, loader.get(FQN).size());
   }





   /**
    * Complex object whose class description is stored in the class catalog.
    */
   private static class Complex implements Serializable {

      Complex nested;

      Complex() {
         this(null);
      }

      Complex(Complex nested) {
         this.nested = nested;
      }

      public boolean equals(Object o) {
         try {
            Complex x = (Complex) o;
            return (nested != null) ? nested.equals(x.nested)
                                    : (x.nested == null);
         } catch (ClassCastException e) {
            return false;
         }
      }

      public int hashCode() {
         if(nested == null)
            return super.hashCode();
         else
            return 13 + nested.hashCode();
      }
   }


   public static Test suite() {
      return new TestSuite(PassivationTestsBase.class);
   }

   public static void main(String[] args) {
      junit.textui.TestRunner.run(suite());
   }

}
