/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.cache.loader.bdbje;

import com.sleepycat.bind.serial.SerialBinding;
import com.sleepycat.bind.serial.StoredClassCatalog;
import com.sleepycat.bind.tuple.TupleBinding;
import com.sleepycat.bind.tuple.TupleInput;
import com.sleepycat.bind.tuple.TupleOutput;
import com.sleepycat.je.*;
import com.sleepycat.je.util.DbDump;
import com.sleepycat.je.util.DbLoad;
import org.jboss.cache.Fqn;
import org.jboss.cache.Modification;
import org.jboss.cache.TreeCache;
import org.jboss.cache.loader.CacheLoader;

import java.io.*;
import java.util.*;


/**
 * A persistent <code>CacheLoader</code> based on Berkeley DB Java Edition.
 *
 * <p>The configuration string format is:</p>
 * <pre>environmentDirectoryName[#databaseName]</pre>
 * <p>where databaseName, if omitted, defaults to the ClusterName property
 * of the TreeCache.</p>
 *
 * <p>A je.properties file may optionally be placed in the JE environment
 * directory and used to customize the default JE configuration.</p>
 *
 * @author Mark Hayes May 16, 2004
 * @author Bela Ban
 * @version $Id: BdbjeCacheLoader.java,v 1.13.4.4 2005/12/01 19:48:42 dimitris Exp $
 */
public class BdbjeCacheLoader implements CacheLoader {

   private static final int MAX_TXN_RETRIES = 10;
   private static final char LOWEST_UTF_CHAR = '\u0001';

   private String configStr;
   private TreeCache treeCache;
   private Environment env;
   private String cacheDbName;
   private String catalogDbName;
   private Database cacheDb;
   private Database catalogDb;
   private StoredClassCatalog catalog;
   private SerialBinding serialBinding;
   private Map txnMap;
   private boolean transactional;

   /*
    * Service implementation -- lifecycle methods.
    * Note that setConfig() and setCache() are called before create().
    */

   /**
    * Does nothing since start() does all the work.
    */
   public void create() throws Exception {
      String license="\n*************************************************************************************\n" +
            "Berkeley DB Java Edition version: " + JEVersion.CURRENT_VERSION.toString() + "\n" +
            "JBossCache can use Berkeley DB Java Edition from Sleepycat Software \n" +
            "(http://www.sleepycat.com/jeforjbosscache)\n" +
            "for persistent, reliable and transaction-protected data storage.\n" +
            "If you choose to use Berkeley DB Java Edition with JBossCache, you must comply with the terms\n" +
            "of Sleepycat's public license, included in the file LICENSE.txt.\n" +
            "If you prefer not to release the source code for your own application in order to comply\n" +
            "with the Sleepycat public license, you may purchase a different license for use of\n" +
            "Berkeley DB Java Edition with JBossCache.\n" +
            "Contact Sleepycat Software at info@sleepycat.com for pricing and terms for that license\n" +
            "*************************************************************************************";
      System.out.println(license);

      checkNotOpen();
   }

   /**
    * Does nothing since stop() does all the work.
    */
   public void destroy() {
   }

   /**
    * Opens the JE environment and the database specified by the configuration
    * string.  The environment and databases are created if necessary.
    */
   public void start()
      throws Exception {

      checkNotOpen();

      if (treeCache == null) {
         throw new IllegalStateException(
            "A non-null Cache property (TreeCache object) is required");
      }
      if (configStr == null) {
         throw new IllegalStateException(
            "A non-null Config property (configuration string) is required");
      }

      /* Parse config string. */
      File homeDir;
      int offset = configStr.indexOf('#');
      if (offset >= 0 && offset < configStr.length() - 1) {
         homeDir = new File(configStr.substring(0, offset));
         cacheDbName = configStr.substring(offset + 1);
      } else {
         homeDir = new File(configStr);
         cacheDbName = treeCache.getClusterName();
      }
      catalogDbName = cacheDbName + "_class_catalog";

      /*
       * If the TreeCache is transactional, we will create transactional
       * databases.  However, we always create a transactional environment
       * since it may be shared by transactional and non-transactional caches.
       */
      transactional = treeCache.getTransactionManager() != null;

      try {
         /* Open the environment, creating it if it doesn't exist. */
         EnvironmentConfig envConfig = new EnvironmentConfig();
         envConfig.setAllowCreate(true);
         envConfig.setTransactional(true);
         env = new Environment(homeDir, envConfig);

         /* Open cache and catalog databases. */
         openDatabases();
      } catch (Exception e) {
         destroy();
         throw e;
      }
   }

   /**
    * Opens all databases and initializes database related information.
    */
   private void openDatabases()
      throws Exception {

      /* Use a generic database config, with no duplicates allowed. */
      DatabaseConfig dbConfig  = new DatabaseConfig();
      dbConfig.setAllowCreate(true);
      dbConfig.setTransactional(transactional);

      /* Create/open the cache database and associated catalog database. */
      cacheDb = env.openDatabase(null, cacheDbName, dbConfig);
      catalogDb = env.openDatabase(null, catalogDbName, dbConfig);

      /* Use the catalog for the serial binding. */
      catalog = new StoredClassCatalog(catalogDb);
      serialBinding = new SerialBinding(catalog, null);

      /* Start with a fresh transaction map. */
      txnMap = new HashMap();
   }

   /**
    * Closes all databases, ignoring exceptions, and nulls references to all
    * database related information.
    */
   private void closeDatabases() {

      if (cacheDb != null) {
         try {
            cacheDb.close();
         } catch (Exception shouldNotOccur) {
            shouldNotOccur.printStackTrace();
         }
      }
      if (catalogDb != null) {
         try {
            catalogDb.close();
         } catch (Exception shouldNotOccur) {
            shouldNotOccur.printStackTrace();
         }
      }
      cacheDb = null;
      catalogDb = null;
      catalog = null;
      serialBinding = null;
      txnMap = null;
   }

   /**
    * Closes the JE databases and environment, and nulls references to them.
    * The environment and databases are not removed from the file system.
    * Exceptions during close are ignored.
    */
   public void stop() {

      closeDatabases();

      if (env != null) {
         try {
            env.close();
         } catch (Exception shouldNotOccur) {
            shouldNotOccur.printStackTrace();
         }
      }
      env = null;
   }

   /*
    * CacheLoader implementation.
    */

   /**
    * Sets the configuration string for this cache loader.
    */
   public void setConfig(Properties props) {
      checkNotOpen();
      String location=props != null? props.getProperty("location") : null;
      configStr = location;
   }

   /**
    * Sets the TreeCache owner of this cache loader.
    */
   public void setCache(TreeCache c) {
      checkNotOpen();
      treeCache = c;
   }

   /**
    * Returns an unmodifiable set of relative children names (strings), or
    * returns null if the parent node is not found or if no children are found.
    * This is a fairly expensive operation, and is assumed to be performed by
    * browser applications.  Calling this method as part of a run-time
    * transaction is not recommended.
    */
   public Set getChildrenNames(Fqn name)
      throws Exception {

      checkOpen();
      checkNonNull(name, "name");

      DatabaseEntry prefixEntry = makeKeyEntry(name);
      DatabaseEntry dataEntry = new DatabaseEntry();
      dataEntry.setPartial(0, 0, true);

      String namePart = "";
      int namePartIndex = name.size();
      Set set = null;

      Cursor cursor = cacheDb.openCursor(null, null);
      try {
         while (true) {
            DatabaseEntry keyEntry = makeKeyEntry(prefixEntry, namePart);
            OperationStatus status =
               cursor.getSearchKeyRange(keyEntry, dataEntry, null);
            if (status != OperationStatus.SUCCESS ||
                !startsWith(keyEntry, prefixEntry)) {
               break;
            }
            if (set == null) {
               set = new HashSet();
            }
            Fqn childName = makeKeyObject(keyEntry);
            namePart = childName.get(namePartIndex).toString();
            set.add(namePart);
            namePart += LOWEST_UTF_CHAR;
         }
      } finally {
         cursor.close();
      }
      if (set != null) {
         return Collections.unmodifiableSet(set);
      } else {
         return null;
      }
   }

   /**
    * Returns a map containing all key-value pairs for the given FQN, or null
    * if the node is not present.
    * This operation is always non-transactional, even in a transactional
    * environment.
    */
   public Map get(Fqn name)
      throws Exception {

      checkOpen();
      checkNonNull(name, "name");

      DatabaseEntry keyEntry = makeKeyEntry(name);
      DatabaseEntry foundData = new DatabaseEntry();
      OperationStatus status = cacheDb.get(null, keyEntry, foundData, null);
      if (status == OperationStatus.SUCCESS) {
         return makeDataObject(foundData, false);
      } else {
         return null;
      }
   }

   /**
    * Returns the data object stored under the given FQN and key, or null if
    * the FQN and key are not present.
    * This operation is always non-transactional, even in a transactional
    * environment.
    */
   public Object get(Fqn name, Object key)
      throws Exception {

      Map map = get(name);
      if (map != null) {
         return map.get(key);
      } else {
         return null;
      }
   }

   /**
    * Returns whether the given node exists.
    */
   public boolean exists(Fqn name)
      throws Exception {

      checkOpen();
      checkNonNull(name, "name");

      DatabaseEntry keyEntry = makeKeyEntry(name);
      DatabaseEntry foundData = new DatabaseEntry();
      foundData.setPartial(0, 0, true);
      OperationStatus status = cacheDb.get(null, keyEntry, foundData, null);
      return (status == OperationStatus.SUCCESS);
   }


   /**
    * Stores a single FQN-key-value record.
    * Intended to be used in a non-transactional environment, but will use
    * auto-commit in a transactional environment.
    */
   public Object put(Fqn name, Object key, Object value) throws Exception {

      checkOpen();
      checkNonNull(name, "name");

      Object oldVal;
      if (transactional) {
         Modification mod =
            new Modification(Modification.PUT_KEY_VALUE, name, key, value);
         commitModification(mod);
         oldVal = mod.getOldValue();
      } else {
         oldVal = doPut(null, name, key, value);
      }
      return oldVal;
   }


   /**
    * Internal version of store(String,Object,Object) that allows passing a
    * transaction.
    */
   private Object doPut(Transaction txn, Fqn name, Object key, Object value)
      throws Exception {

      Object oldVal = null;
      /* To update-or-insert, try putNoOverwrite first, then a RMW cycle. */
      Map map = new HashMap();
      map.put(key, value);
      DatabaseEntry dataEntry = makeDataEntry(map);
      DatabaseEntry keyEntry = makeKeyEntry(name);
      Cursor cursor = cacheDb.openCursor(txn, null);
      try {
         OperationStatus status=cursor.putNoOverwrite(keyEntry, dataEntry);
         if(status == OperationStatus.SUCCESS) {
            createParentNodes(cursor, name);
         }
         else {
            DatabaseEntry foundData=new DatabaseEntry();
            status=cursor.getSearchKey(keyEntry, foundData, LockMode.RMW);
            if(status == OperationStatus.SUCCESS) {
               map=makeDataObject(foundData, true);
               oldVal = map.put(key, value);
               cursor.putCurrent(makeDataEntry(map));
            }
         }
      }
      finally {
         cursor.close();
      }
      return oldVal;
   }



   /**
    * Stores a map of key-values for a given FQN, but does not delete existing
    * key-value pairs (that is, it does not erase).
    * Intended to be used in a non-transactional environment, but will use
    * auto-commit in a transactional environment.
    */
   public void put(Fqn name, Map values)
      throws Exception {

      checkOpen();
      checkNonNull(name, "name");

      if (transactional) {
         commitModification(
            new Modification(Modification.PUT_DATA, name, values));
      } else {
         doPut(null, name, values);
      }
   }

   /**
    * Internal version of put(Fqn,Map) that allows passing a
    * transaction.
    */
   private void doPut(Transaction txn, Fqn name, Map values)
      throws Exception {

      /* To update-or-insert, try putNoOverwrite first, then a RMW cycle. */
      DatabaseEntry dataEntry = makeDataEntry(values);
      DatabaseEntry keyEntry = makeKeyEntry(name);
      Cursor cursor = cacheDb.openCursor(txn, null);
      try {
         OperationStatus status = cursor.putNoOverwrite(keyEntry, dataEntry);
         if (status == OperationStatus.SUCCESS) {
            createParentNodes(cursor, name);
         } else {
            DatabaseEntry foundData = new DatabaseEntry();
            status = cursor.getSearchKey(keyEntry, foundData, LockMode.RMW);
            if (status == OperationStatus.SUCCESS) {
               Map map = makeDataObject(foundData, true);
               if(values != null)
                  map.putAll(values);
               cursor.putCurrent(makeDataEntry(map));
            }
         }
      } finally {
         cursor.close();
      }
   }

   /**
    * Internal version of put(Fqn,Map) that allows passing a
    * transaction and erases existing data.
    */
   private void doPutErase(Transaction txn, Fqn name, Map values)
      throws Exception {

      DatabaseEntry dataEntry = makeDataEntry(values);
      DatabaseEntry keyEntry = makeKeyEntry(name);
      Cursor cursor = cacheDb.openCursor(txn, null);
      try {
         cursor.put(keyEntry, dataEntry);
         createParentNodes(cursor, name);
      } finally {
         cursor.close();
      }
   }

   /**
    * Applies the given modifications.
    * Intended to be used in a non-transactional environment, but will use
    * auto-commit in a transactional environment.
    */
   public void put(List modifications)
      throws Exception {

      checkOpen();
      checkNonNull(modifications, "modifications");

      if (transactional) {
         commitModifications(modifications);
      } else {
         doPut(null, modifications);
      }
   }

   /**
    * Internal version of put(List) that allows passing a transaction.
    */
   private void doPut(Transaction txn, List modifications)
      throws Exception {

      /* This could be optimized by grouping modifications by Fqn, and
       * performing a single database operation for each Fqn (record). */

      for (Iterator i = modifications.iterator(); i.hasNext();) {
         Modification mod = (Modification) i.next();
         Fqn name = mod.getFqn();
         Object oldVal;
         switch (mod.getType()) {
            case Modification.PUT_KEY_VALUE:
               oldVal = doPut(txn, name, mod.getKey(), mod.getValue());
               mod.setOldValue(oldVal);
               break;
            case Modification.PUT_DATA:
               doPut(txn, name, mod.getData());
               break;
            case Modification.PUT_DATA_ERASE:
               doPutErase(txn, name, mod.getData());
               break;
            case Modification.REMOVE_KEY_VALUE:
               oldVal = doRemove(txn, name, mod.getKey());
               mod.setOldValue(oldVal);
               break;
            case Modification.REMOVE_NODE:
               doRemove(txn, name);
               break;
            case Modification.REMOVE_DATA:
               doRemoveData(txn, name);
               break;
            default:
               throw new IllegalArgumentException(
                     "Unknown Modification type: " + mod.getType());
         }
      }
   }

   /**
    * Creates parent nodes of the given Fqn, moving upward until an existing
    * node is found.
    */
   private void createParentNodes(Cursor cursor, Fqn name)
      throws Exception {

      DatabaseEntry dataEntry = makeDataEntry(null);
      for (int nParts = name.size() - 1; nParts >= 1; nParts -= 1) {
         DatabaseEntry keyEntry = makeKeyEntry(name, nParts);
         OperationStatus status = cursor.putNoOverwrite(keyEntry, dataEntry);
         if (status != OperationStatus.SUCCESS) {
            break;
         }
      }
   }

   /**
    * Deletes the node for a given FQN and all its descendent nodes.
    * Intended to be used in a non-transactional environment, but will use
    * auto-commit in a transactional environment.
    */
   public void remove(Fqn name)
      throws Exception {

      checkOpen();
      checkNonNull(name, "name");

      if (transactional) {
         commitModification(
            new Modification(Modification.REMOVE_NODE, name));
      } else {
         doRemove(null, name);
      }
   }

   /**
    * Internal version of remove(Fqn) that allows passing a transaction.
    */
   private void doRemove(Transaction txn, Fqn name)
      throws Exception {

      DatabaseEntry keyEntry = makeKeyEntry(name);
      DatabaseEntry foundKey = new DatabaseEntry();
      DatabaseEntry foundData = new DatabaseEntry();
      foundData.setPartial(0, 0, true);
      Cursor cursor = cacheDb.openCursor(txn, null);
      try {
         OperationStatus status =
            cursor.getSearchKey(keyEntry, foundData, LockMode.RMW);
         while (status == OperationStatus.SUCCESS) {
            cursor.delete();
            status = cursor.getNext(foundKey, foundData, LockMode.RMW);
            if (status == OperationStatus.SUCCESS &&
                !startsWith(foundKey, keyEntry)) {
               status = OperationStatus.NOTFOUND;
            }
         }
      } finally {
         cursor.close();
      }
   }

   /**
    * Deletes a single FQN-key-value record.
    * Intended to be used in a non-transactional environment, but will use
    * auto-commit in a transactional environment.
    */
   public Object remove(Fqn name, Object key)
      throws Exception {

      checkOpen();
      checkNonNull(name, "name");

      Object oldVal;
      if (transactional) {
         Modification mod =
            new Modification(Modification.REMOVE_KEY_VALUE, name, key);
         commitModification(mod);
         oldVal = mod.getOldValue();
      } else {
         oldVal = doRemove(null, name, key);
      }
      return oldVal;
   }

   /**
    * Internal version of remove(String,Object) that allows passing a
    * transaction.
    */
   private Object doRemove(Transaction txn, Fqn name, Object key)
      throws Exception {

      Object oldVal = null;
      DatabaseEntry keyEntry = makeKeyEntry(name);
      DatabaseEntry foundData = new DatabaseEntry();
      Cursor cursor = cacheDb.openCursor(txn, null);
      try {
         OperationStatus status =
            cursor.getSearchKey(keyEntry, foundData, LockMode.RMW);
         if (status == OperationStatus.SUCCESS) {
            Map map = makeDataObject(foundData, true);
            oldVal = map.remove(key);
            cursor.putCurrent(makeDataEntry(map));
         }
      } finally {
         cursor.close();
      }
      return oldVal;
   }

   /**
    * Clears the map for the given node, but does not remove the node.
    */
   public void removeData(Fqn name)
      throws Exception {

      checkOpen();
      checkNonNull(name, "name");

      if (transactional) {
         commitModification(
            new Modification(Modification.REMOVE_DATA, name));
      } else {
         doRemoveData(null, name);
      }
   }

   /**
    * Internal version of removeData(Fqn) that allows passing a transaction.
    */
   private void doRemoveData(Transaction txn, Fqn name)
      throws Exception {

      DatabaseEntry dataEntry = new DatabaseEntry();
      dataEntry.setPartial(0, 0, true);
      DatabaseEntry keyEntry = makeKeyEntry(name);
      Cursor cursor = cacheDb.openCursor(txn, null);
      try {
         OperationStatus status =
            cursor.getSearchKey(keyEntry, dataEntry, LockMode.RMW);
         if (status == OperationStatus.SUCCESS) {
            cursor.putCurrent(makeDataEntry(null));
         }
      } finally {
         cursor.close();
      }
   }

   /**
    * Begins a transaction and applies the given modifications.
    * 
    * <p>If onePhase is true, commits the transaction; otherwise, associates
    * the txn value with the transaction and expects commit() or rollback() to
    * be called later with the same tx value.  Performs retries if necessary to
    * resolve deadlocks.</p>
    */
   public void prepare(Object tx, List modifications, boolean onePhase)
      throws Exception {

      checkOpen();
      checkNonNull(modifications, "modifications");
      if (!onePhase) {
         checkNonNull(tx, "tx");
      }
      if (!transactional) {
         throw new UnsupportedOperationException(
            "prepare() not allowed with a non-transactional cache loader");
      }
      Transaction txn = performTransaction(modifications);
      if (onePhase) {
         txn.commit();
      } else {
         txnMap.put(tx, txn);
      }
   }

   /**
    * Performs and commits a single modification.  The loader must be
    * transactional. Commits the transaction if successful, or aborts the
    * transaction and throws an exception if not successful.
    */
   private void commitModification(Modification mod)
      throws Exception {

      commitModifications(Collections.singletonList(mod));
   }

   /**
    * Performs and commits a list of modifications.  The loader must be
    * transactional. Commits the transaction if successful, or aborts the
    * transaction and throws an exception if not successful.
    */
   private void commitModifications(List mods)
      throws Exception {

      if (!transactional) throw new IllegalStateException();
      Transaction txn = performTransaction(mods);
      txn.commit();
   }

   /**
    * Performs the given operation, starting a transaction and performing
    * retries.  Returns the transaction if successful; aborts the transaction
    * and throws an exception if not successful.
    */
   private Transaction performTransaction(List modifications)
      throws Exception {

      /*
       * Note that we can't use TransactionRunner here since if onePhase=false
       * in the call to prepare(), we do not want to commit.  TransactionRunner
       * always commits or aborts.
       */

      int retries = MAX_TXN_RETRIES;
      while (true) {
         Transaction txn = env.beginTransaction(null, null);
         try {
            doPut(txn, modifications);
            return txn;
         } catch (Exception e) {
            txn.abort();
            if (e instanceof DeadlockException && retries > 0) {
               retries -= 1;
               continue;
            } else {
               throw e;
            }
         }
      }
   }

   /**
    * Commits the given transaction, or throws IllegalArgumentException if the
    * given key is not associated with an uncommited transaction.
    */
   public void commit(Object tx)
      throws Exception {

      checkOpen();
      checkNonNull(tx, "tx");

      Transaction txn = (Transaction) txnMap.remove(tx);
      if (txn != null) {
         txn.commit();
      } else if (transactional) {
         throw new IllegalArgumentException("Unknown txn key: " + tx);
      }
   }

   /**
    * Commits the given transaction, or throws IllegalArgumentException if the
    * given key is not associated with an uncommited transaction.
    */
   public void rollback(Object tx) {

      checkOpen();
      checkNonNull(tx, "tx");

      Transaction txn = (Transaction) txnMap.remove(tx);
      if (txn != null) {
         try {
            txn.abort();
         } catch (Exception ignored) {}
      } else if (transactional) {
         throw new IllegalArgumentException("Unknown txn key: " + tx);
      }
   }

   /**
    * Export the contents of the databases as a byte array.
    * If the databases are empty a zero-lenth array is returned.
    * The export format is two consecutive DbDumps.
    */
   public byte[] loadEntireState()
      throws Exception {

      checkOpen();

      boolean isEmpty;
      Cursor cursor = cacheDb.openCursor(null, null);
      try {
         isEmpty = cursor.getFirst(new DatabaseEntry(),
                                   new DatabaseEntry(),
                                   null) != OperationStatus.SUCCESS;
      } finally {
         cursor.close();
      }
      if (isEmpty) {
         return new byte[0];
      } else {
         ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
         PrintStream ps = new PrintStream(baos, false, "US-ASCII");
         dumpDatabase(cacheDbName, ps);
         dumpDatabase(catalogDbName, ps);
         ps.flush();
         return baos.toByteArray();
      }
   }

   /**
    * Dumps one database in ASCII format and adds a DATA=END marker line.
    */
   private void dumpDatabase(String dbName, PrintStream ps)
      throws Exception {

      DbDump dumper = new DbDump(env, dbName, ps, false);
      dumper.dump();
   }

   /**
    * Replace the contents of the databases with the given exported data.
    * If state is null or zero-length, the databases will be cleared.
    * The export format is two consecutive DbDumps.
    */
   public void storeEntireState(byte[] state)
      throws Exception {

      checkOpen();

      closeDatabases();
      env.removeDatabase(null, cacheDbName);
      env.removeDatabase(null, catalogDbName);
      if (state != null && state.length > 0) {
         BufferedReader reader = new BufferedReader
            (new InputStreamReader(new ByteArrayInputStream(state)));
         loadDatabase(cacheDbName, reader);
         loadDatabase(catalogDbName, reader);
      }
      openDatabases();
   }

   /**
    * Loads one database in ASCII format, stopping at the DATA=END marker line
    * or EOF.
    */
   private void loadDatabase(String dbName, BufferedReader reader)
      throws Exception {

      DbLoad loader = new DbLoad();
      loader.setEnv(env);
      loader.setDbName(dbName);
      loader.setInputReader(reader);
      loader.setNoOverwrite(false);
      loader.setTextFileMode(false);
      loader.setIgnoreUnknownConfig(true);
      loader.load();
   }

   /**
    * Returns whether the given entry starts with the given prefix bytes.
    * Used to determine whether a database key starts with a given FQN.
    */
   private boolean startsWith(DatabaseEntry entry,
                              DatabaseEntry prefix) {
      int size = prefix.getSize();
      if (size > entry.getSize()) {
         return false;
      }
      byte[] d1 = entry.getData();
      byte[] d2 = prefix.getData();
      int o1 = entry.getOffset();
      int o2 = prefix.getOffset();
      for (int i = 0; i < size; i += 1) {
         if (d1[o1 + i] != d2[o2 + i]) {
            return false;
         }
      }
      return true;
   }

   /**
    * Converts a database entry to an Fqn.
    */
   private Fqn makeKeyObject(DatabaseEntry entry) {

      Fqn name = new Fqn();
      TupleInput tupleInput = TupleBinding.entryToInput(entry);
      while (tupleInput.available() > 0) {
         String part = tupleInput.readString();
         name=new Fqn(name, part);
      }
      return name;
   }

   /**
    * Converts an Fqn to a database entry.
    */
   private DatabaseEntry makeKeyEntry(Fqn name) {
      
      return makeKeyEntry(name, name.size());
   }

   /**
    * Converts an Fqn to a database entry, outputing the given number of name
    * parts.
    */
   private DatabaseEntry makeKeyEntry(Fqn name, int nParts) {

      /* Write the sequence of name parts. */
      TupleOutput tupleOutput = new TupleOutput();
      for (int i = 0; i < nParts; i += 1) {
         tupleOutput.writeString(name.get(i).toString());
      }

      /* Return the tuple as an entry. */
      DatabaseEntry entry = new DatabaseEntry();
      TupleBinding.outputToEntry(tupleOutput, entry);
      return entry;
   }

   /**
    * Creates a key database entry from a parent database entry (prefix) and
    * a child name part.
    */
   private DatabaseEntry makeKeyEntry(DatabaseEntry prefix, String namePart) {

      /* Write the bytes of the prefix followed by the child name. */
      TupleOutput tupleOutput = new TupleOutput();
      tupleOutput.writeFast(prefix.getData(),
                            prefix.getOffset(),
                            prefix.getSize());
      tupleOutput.writeString(namePart);

      /* Return the tuple as an entry. */
      DatabaseEntry entry = new DatabaseEntry();
      TupleBinding.outputToEntry(tupleOutput, entry);
      return entry;
   }

   /**
    * Converts a database entry to a Map.
    */
   private Map makeDataObject(DatabaseEntry entry, boolean createIfNull) {
      Map map = (Map) serialBinding.entryToObject(entry);
      if (createIfNull && map == null) {
         map = new HashMap();
      }
      return map;
   }

   /**
    * Converts a Map to a database entry.
    */
   private DatabaseEntry makeDataEntry(Map map) {

      if (map != null) {
         if (map.size() == 0) {
            map = null;
         } else if (!(map instanceof Serializable)) {
            map = new HashMap(map);
         }
      }
      DatabaseEntry entry = new DatabaseEntry();
      serialBinding.objectToEntry(map, entry);
      return entry;
   }

   /**
    * Throws an exception if the environment is not open.
    */
   private void checkOpen() {
      if (env == null) {
         throw new IllegalStateException(
               "Operation not allowed before calling create()");
      }
   }

   /**
    * Throws an exception if the environment is not open.
    */
   private void checkNotOpen() {
      if (env != null) {
         throw new IllegalStateException(
               "Operation not allowed after calling create()");
      }
   }

   /**
    * Throws an exception if the parameter is null.
    */
   private void checkNonNull(Object param, String paramName) {
      if (param ==  null) {
         throw new NullPointerException(
               "Parameter must not be null: " + paramName);
      }
   }
}
