package org.jboss.cache.loader;

import com.sleepycat.je.*;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.jboss.cache.Fqn;
import org.jboss.cache.Modification;
import org.jboss.cache.TreeCache;

import java.io.File;
import java.io.FileFilter;
import java.io.Serializable;
import java.util.*;

/**
 * Tests BdbjeCacheLoader directly via the CacheLoader interface.
 *
 * <p>Run this test case with the current directory set to the JE environment
 * directory.  Any scratch directory will do, but beware that all files in
 * the directory will be deleted by setUp().</p>
 *
 * @version $Revision: 1054 $
 */
public class BdbjeTest extends TestCase {

   private static final String envHome = ".";
   private static final Fqn FQN = new Fqn("key");

   private TreeCache treeCache;
   private CacheLoader loader;

   /**
    * Creates a test case.
    */
   public BdbjeTest(String name) {
      super(name);
   }

   /**
    * Deletes all files in the environment directory.
    */
   public void setUp() throws Exception {
      super.setUp();

      File dir = new File(envHome);

      class MyFilter implements FileFilter {
         public boolean accept(File file) {
            return file.getName().endsWith(".jdb");
         }
      }

      File[] files=dir.listFiles(new MyFilter());

      if (files != null) {
         for (int i = 0; i < files.length; i += 1) {
            File file = files[i];
            if (file.isFile()) {
               if (!file.delete()) {
                  System.err.println("Unable to delete: " + file);
               }
            }
         }
      }
   }

   /**
    * Release all resources and ignore exceptions, to shutdown gracefully
    * when an assertion fires.
    */
   public void tearDown() throws Exception {
      super.tearDown();

      if (loader != null) {
         try {
            loader.stop();
         } catch (Exception ignored) {}
         loader = null;
      }
      if (treeCache != null) {
         try {
            treeCache.stopService();
         } catch (Exception ignored) {}
         treeCache = null;
      }
   }

   /**
    * Creates and starts a loader.
    * @param transactional whether to set the TransactionManagerLookupClass
    * property.
    * @param dbName a database name, or null to default to the cluster name.
    */
   private void startLoader(boolean transactional, String dbName)
      throws Exception {

      /*
       * Create a dummy TreeCache object.  This is used for setting the cluster
       * name and TransactionManagerLookupClass (transactional) propertes only.
       * the TreeCache object is not used otherwise during testing.
       */
      treeCache = new TreeCache();
      treeCache.setClusterName("myCluster");
      if (transactional) {
         treeCache.setTransactionManagerLookupClass(
            "org.jboss.cache.DummyTransactionManagerLookup");
      }
      treeCache.startService();

      /* Derive the config string. */
      String configStr;
      if (dbName != null) {
         configStr = envHome + '#' + dbName;
      } else {
         configStr = envHome;
         dbName = "myCluster";
      }

      instantiateLoader();

      /* Initialize and start the loader. */
      loader.setCache(treeCache);
      Properties props=new Properties();
      props.setProperty("location", configStr);
      loader.setConfig(props);
      loader.create();
      loader.start();

      /* Verify the database name by trying to open it. */
      Environment env = new Environment(new File(envHome), null);
      DatabaseConfig dbConfig = new DatabaseConfig();
      dbConfig.setTransactional(transactional);
      Database db = env.openDatabase(null, dbName, dbConfig);
      db.close();
      env.close();
   }

   /**
    * Creates the loader instance.
    */
   private void instantiateLoader()
      throws Exception {

      /* Create the cache loader as TreeCache would. */
      Class cls =
         Class.forName("org.jboss.cache.loader.bdbje.BdbjeCacheLoader");
      loader = (CacheLoader) cls.newInstance();
   }

   /**
    * Stops and destroys the loader.
    */
   private void stopLoader()
      throws Exception {

      loader.stop();
      loader.destroy();
   }

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

      doTestBasicOperations(false);
   }

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

      doTestBasicOperations(true);
   }

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

      startLoader(transactional, null);

      /* 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);

      stopLoader();
   }

   /**
    * 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);
      oldVal = loader.put(fqn, "three", "four");
      assertNull(oldVal);
      assertEquals("two", loader.get(fqn).get("one"));
      assertEquals("four", loader.get(fqn).get("three"));
      oldVal = loader.put(fqn, "one", "xxx");
      assertEquals("two", oldVal);
      oldVal = loader.put(fqn, "one", "two");
      assertEquals("xxx", oldVal);

      /* get(Fqn) */
      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);
      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);
      oldVal = loader.remove(fqn, "five");
      assertEquals("six", oldVal);
      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);
      assertNull("Null expected", 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 {

      startLoader(false, null);

      /* Create top level node implicitly. */
      assertTrue(!loader.exists(new Fqn("key0")));
      loader.put(Fqn.fromString("/key0/level1/level2"), null);
      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);
      assertTrue(loader.exists(Fqn.fromString("/key0/x/y")));
      assertTrue(loader.exists(Fqn.fromString("/key0/x")));
      loader.remove(Fqn.fromString("/key0/x/y"));
      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"));
      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);
      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);
      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"));
      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"));
      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"));
      assertTrue(loader.exists(new Fqn("key2")));
      assertTrue(!loader.exists(new Fqn("key3")));

      /* Delete final root, leaving none. */
      loader.remove(new Fqn("key2"));
      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");
      assertNotNull(loader.get(Fqn.fromString("/key0/level1/level2")));
       assertNotNull(loader.get(Fqn.fromString("/key0/level1")));
       assertNotNull(loader.get(new Fqn("key0")));
       assertEquals(0, loader.get(Fqn.fromString("/key0/level1")).size());
       assertEquals(0, loader.get(new Fqn("key0")).size());


       loader.put(Fqn.fromString("/key0/x/y"), "a", "b");
      assertNotNull(loader.get(Fqn.fromString("/key0/x/y")));
      assertNotNull(loader.get(Fqn.fromString("/key0/x")));
      assertEquals(0, loader.get(Fqn.fromString("/key0/x")).size());
      loader.remove(Fqn.fromString("/key0/x/y"));
      assertNull(loader.get(Fqn.fromString("/key0/x/y")));
      assertNotNull(loader.get(Fqn.fromString("/key0/x")));
      assertEquals(0, loader.get(Fqn.fromString("/key0/x")).size());

      loader.remove(new Fqn("key0"));
      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");
      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");
      assertNotNull(loader.get(Fqn.fromString("/key3/level1/level2")));
      assertNotNull(loader.get(Fqn.fromString("/key3/level1")));
      assertEquals(0, loader.get(Fqn.fromString("/key3/level1")).size());

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

      loader.remove(Fqn.fromString("/key3/level1"));
      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"));
      assertNull(loader.get(new Fqn("key1")));
      assertNotNull(loader.get(new Fqn("key2")));
      assertNotNull(loader.get(new Fqn("key3")));

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

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

      stopLoader();
   }

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

      startLoader(false, null);

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

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

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

      loader.remove(Fqn.fromString("/key1/x"));
      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);
      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);
      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);
      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" });

      stopLoader();
   }

   /**
    * 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(false);
   }

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

      doTestModifications(true);
   }

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

      startLoader(transactional, null);

      /* PUT_KEY_VALUE, PUT_DATA */
      List list = createUpdates();
      loader.put(list);
      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);
      checkModifications(list);

      /* REMOVE_NODE */
      list = new ArrayList();
      mod = new Modification();
      mod.setType(Modification.REMOVE_NODE);
      mod.setFqn(FQN);
      list.add(mod);
      loader.put(list);
      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);
      checkModifications(list);
      assertNotNull(loader.get(FQN)); 
      assertEquals(0, loader.get(FQN).size());

      stopLoader();
   }

   /**
    * Tests a one-phase transaction.
    */
   public void testOnePhaseTransaction()
      throws Exception {

      startLoader(true, null);

      List mods = createUpdates();
      loader.prepare(null, mods, true);
      checkModifications(mods);

      stopLoader();
   }

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

      startLoader(true, null);

      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);
      checkModifications(mods);

      stopLoader();
   }

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

      startLoader(true, null);

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

      stopLoader();
   }

   /**
    * 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:
            assertTrue(loader.exists(fqn));
            assertNotNull(loader.get(fqn));
            assertEquals(0, loader.get(fqn).size());
            break;
         case Modification.REMOVE_NODE:
            assertTrue(!loader.exists(fqn));
            assertEquals(null, loader.get(fqn));
            break;
         default:
            fail("unknown type: " + mod);
            break;
         }
      }
   }

   /**
    * Test exception cases and create/destroy/create/destroy sequence.
    */
//   public void testBasicExceptions()
//      throws Exception {
//
//      instantiateLoader();
//      checkPreCreateExceptions();
//
//      startLoader(false, null);
//      checkPostCreateExceptions();
//      loader.put(FQN, "one", "two");
//      assertEquals("two", loader.get(FQN).get("one"));
//
//      stopLoader();
//      checkPreCreateExceptions();
//
//      startLoader(false, null);
//      checkPostCreateExceptions();
//      loader.put(FQN, "one", "two");
//      assertEquals("two", loader.get(FQN).get("one"));
//
//      stopLoader();
//      checkPreCreateExceptions();
//   }

   /**
    * Check exception cases that occur before create().
    */
   private void checkPreCreateExceptions()
      throws Exception {

      loader.setCache(new TreeCache());
      loader.setConfig(null);
      try {
         loader.start();
         // fail();
      } catch (IllegalStateException expected) {}

      // loader.setCache(null);
      Properties props=new Properties();
      props.setProperty("location", "xyz");
      loader.setConfig(props);
      try {
         loader.start();
         fail();
      } catch (IllegalStateException expected) {}

      loader.setCache(new TreeCache());
      props=new Properties();
      props.setProperty("location", "directory_that_does_not_exist");
      loader.setConfig(props);
      try {
         loader.start();
         fail();
      } catch (DatabaseException expected) {}

      try {
         loader.put(FQN, "one", "two");
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.put(FQN, new HashMap());
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.put(new ArrayList());
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.get(FQN).get("one");
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.get(FQN);
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.remove(FQN);
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.remove(FQN, "one");
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.prepare(new Object(), new ArrayList(), false);
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.commit(new Object());
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.rollback(new Object());
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.loadEntireState();
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.storeEntireState(new byte[0]);
         fail();
      } catch (IllegalStateException expected) {}

      /* Redundant stop and destroy are allowed. */
      loader.stop();
      loader.destroy();
   }

   /**
    * Check exception cases that occur after create().
    */
   private void checkPostCreateExceptions()
      throws Exception {

      try {
         loader.create();
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.start();
         fail();
      } catch (IllegalStateException expected) {}

      try {
         loader.put(null, "one", "two");
         fail();
      } catch (NullPointerException expected) {}

      try {
         loader.put(null, new HashMap());
         fail();
      } catch (NullPointerException expected) {}

      try {
         loader.put(null);
         fail();
      } catch (NullPointerException expected) {}

      try {
         loader.get(null).get("one");
         fail();
      } catch (NullPointerException expected) {}

      try {
         loader.get(null);
         fail();
      } catch (NullPointerException expected) {}

      try {
         loader.remove(null);
         fail();
      } catch (NullPointerException expected) {}

      try {
         loader.remove(null, "one");
         fail();
      } catch (NullPointerException expected) {}

      try {
         loader.prepare(null, new ArrayList(), false);
         fail();
      } catch (NullPointerException expected) {}
      try {
         loader.prepare(new Object(), null, false);
         fail();
      } catch (NullPointerException expected) {}

      try {
         loader.commit(null);
         fail();
      } catch (NullPointerException expected) {}

      try {
         loader.rollback(null);
         fail();
      } catch (NullPointerException expected) {}
   }

   /**
    * Tests a non-transactional prepare.
    */
   public void testTransactionExceptions()
      throws Exception {

      List mods = createUpdates();

      /* A non-transactional cache loader should not allow prepare(). */
      startLoader(false, null);
      try {
         loader.prepare(new Object(), mods, false);
         fail();
      } catch (UnsupportedOperationException expected) {}
      stopLoader();

      startLoader(true, null);

      /* Commit and rollback a non-prepared transaction. */
      try {
         loader.commit(new Object());
         fail();
      } catch (IllegalArgumentException expected) {}
      try {
         loader.rollback(new Object());
         fail();
      } catch (IllegalArgumentException expected) {}

      /* Commit and rollback after commit. */
      loader.storeEntireState(null);
      Object txnKey = new Object();
      loader.prepare(txnKey, mods, false);
      loader.commit(txnKey);
      try {
         loader.commit(txnKey);
         fail();
      } catch (IllegalArgumentException expected) {}
      try {
         loader.rollback(txnKey);
         fail();
      } catch (IllegalArgumentException expected) {}

      /* Commit and rollback after rollback. */
      loader.storeEntireState(null);
      txnKey = new Object();
      loader.prepare(txnKey, mods, false);
      loader.rollback(txnKey);
      try {
         loader.rollback(txnKey);
         fail();
      } catch (IllegalArgumentException expected) {}
      try {
         loader.rollback(txnKey);
         fail();
      } catch (IllegalArgumentException expected) {}

      stopLoader();
   }

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

      startLoader(false, null);

      loader.put(FQN, null, "x");
      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);
      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);
      assertEquals(null, loader.get(FQN).get(null));
      assertEquals(1, loader.get(FQN).size());

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

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

      loader.remove(FQN);
      assertNull("Should be null", loader.get(FQN));

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

      loader.remove(FQN);
      assertEquals(null, loader.get(FQN));

      stopLoader();
   }

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

      startLoader(false, "nonDefaultDbName");
      loader.put(FQN, "one", "two");
      assertEquals("two", loader.get(FQN).get("one"));
      stopLoader();
   }

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

      startLoader(false, null);

      /* Empty state. */
      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);
      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);

      /* Clear state. */
      loader.storeEntireState(null);
      assertEquals(0, loader.loadEntireState().length);
      assertEquals(null, loader.get(FQN));

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

      stopLoader();
   }

   public static Test suite()
      throws Exception {

      return new TestSuite(BdbjeTest.class);
   }

   public static void main(String[] args)
      throws Exception {

      junit.textui.TestRunner.run(suite());
   }

   /**
    * 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;
         }
      }
   }
}
