From 76ceb213fc96fcbe6c967a2fe4eccc3fb2fdb713 Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Sun, 23 Sep 2007 02:38:12 +0000
Subject: [PATCH] Add support for an LDIF backend.  Entries in this backend will be held in memory, and all read operations will be served from memory, but the underlying data will be stored in an LDIF file on disk and any write operation will cause that LDIF file to be updated.

---
 opendj-sdk/opends/src/messages/messages/backend.properties                                               |   66 +
 opendj-sdk/opends/src/server/org/opends/server/backends/LDIFBackend.java                                 | 1525 ++++++++++++++++++++++++++++++++++
 opendj-sdk/opends/src/server/org/opends/server/types/DN.java                                             |   93 +
 opendj-sdk/opends/tests/unit-tests-testng/resource/config-changes.ldif                                   |   12 
 opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java                                 |   22 
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/backends/LDIFBackendTestCase.java |  814 ++++++++++++++++++
 opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/LDIFBackendConfiguration.xml                |   84 +
 opendj-sdk/opends/resource/schema/02-config.ldif                                                         |    6 
 8 files changed, 2,596 insertions(+), 26 deletions(-)

diff --git a/opendj-sdk/opends/resource/schema/02-config.ldif b/opendj-sdk/opends/resource/schema/02-config.ldif
index 9e6eb03..6b8a0f7 100644
--- a/opendj-sdk/opends/resource/schema/02-config.ldif
+++ b/opendj-sdk/opends/resource/schema/02-config.ldif
@@ -1678,6 +1678,9 @@
   NAME 'ds-cfg-invoke-for-internal-operations'
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE
   X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.496 NAME 'ds-cfg-ldif-file'
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
+  X-ORIGIN 'OpenDS Directory Server' )
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
   NAME 'ds-cfg-access-control-handler' SUP top STRUCTURAL
   MUST ( cn $ ds-cfg-acl-handler-class $ ds-cfg-acl-handler-enabled )
@@ -2512,4 +2515,7 @@
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.174
   NAME 'ds-cfg-memory-usage-monitor-provider' SUP ds-cfg-monitor-provider
   STRUCTURAL X-ORIGIN 'OpenDS Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.26027.1.2.175 NAME 'ds-cfg-ldif-backend'
+  SUP ds-cfg-backend STRUCTURAL MUST ds-cfg-ldif-file
+  X-ORIGIN 'OpenDS Directory Server' )
 
diff --git a/opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/LDIFBackendConfiguration.xml b/opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/LDIFBackendConfiguration.xml
new file mode 100644
index 0000000..6f11e61
--- /dev/null
+++ b/opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/LDIFBackendConfiguration.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+! CDDL HEADER START
+!
+! The contents of this file are subject to the terms of the
+! Common Development and Distribution License, Version 1.0 only
+! (the "License").  You may not use this file except in compliance
+! with the License.
+!
+! You can obtain a copy of the license at
+! trunk/opends/resource/legal-notices/OpenDS.LICENSE
+! or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+! See the License for the specific language governing permissions
+! and limitations under the License.
+!
+! When distributing Covered Code, include this CDDL HEADER in each
+! file and include the License file at
+! trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+! add the following below this CDDL HEADER, with the fields enclosed
+! by brackets "[]" replaced with your own identifying information:
+!      Portions Copyright [yyyy] [name of copyright owner]
+!
+! CDDL HEADER END
+!
+!
+!      Portions Copyright 2007 Sun Microsystems, Inc.
+! -->
+
+<adm:managed-object name="ldif-backend" plural-name="ldif-backends"
+  package="org.opends.server.admin.std"
+  extends="backend"
+  xmlns:adm="http://www.opends.org/admin"
+  xmlns:ldap="http://www.opends.org/admin-ldap">
+
+  <adm:synopsis>
+    The LDIF backend provides a mechanism for interacting with data stored in
+    an LDIF file.  All basic LDAP operations are supported in the LDIF backend,
+    although it has minimal support for custom controls.
+  </adm:synopsis>
+
+  <adm:profile name="ldap">
+    <ldap:object-class>
+      <ldap:oid>1.3.6.1.4.1.26027.1.2.175</ldap:oid>
+      <ldap:name>ds-cfg-ldif-backend</ldap:name>
+      <ldap:superior>ds-cfg-backend</ldap:superior>
+    </ldap:object-class>
+  </adm:profile>
+
+  <adm:property-override name="backend-class">
+    <adm:requires-admin-action>
+      <adm:component-restart />
+    </adm:requires-admin-action>
+    <adm:default-behavior>
+      <adm:defined>
+        <adm:value>
+          org.opends.server.backends.LDIFBackend
+        </adm:value>
+      </adm:defined>
+    </adm:default-behavior>
+  </adm:property-override>
+
+  <adm:property name="ldif-file"
+  mandatory="true"
+  multi-valued="false">
+    <adm:synopsis>
+      This specifies the path to the LDIF file containing the data for this
+      backend.
+    </adm:synopsis>
+    <adm:requires-admin-action>
+      <adm:component-restart />
+    </adm:requires-admin-action>
+    <adm:syntax>
+      <adm:string />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:oid>1.3.6.1.4.1.26027.1.1.496</ldap:oid>
+        <ldap:name>ds-cfg-ldif-file</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+
+</adm:managed-object>
+
diff --git a/opendj-sdk/opends/src/messages/messages/backend.properties b/opendj-sdk/opends/src/messages/messages/backend.properties
index ce4afa7..8e1fc52 100644
--- a/opendj-sdk/opends/src/messages/messages/backend.properties
+++ b/opendj-sdk/opends/src/messages/messages/backend.properties
@@ -964,3 +964,69 @@
  certificate %s from the trust store file %s: %s
 SEVERE_ERR_TRUSTSTORE_CERTIFICATE_NOT_FOUND_338=Unable to retrieve entry %s \
  from the trust store backend because the certificate %s does not exist
+SEVERE_ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS_339=The LDIF backend defined in \
+ configuration entry %s only supports a single base DN, but was configured \
+ for use with multiple base DNs
+SEVERE_ERR_LDIF_BACKEND_ERROR_OPENING_FILE_340=An error occurred while \
+ attempting to open LDIF file %s for use by the LDIF backend defined in \
+ configuration entry %s:  %s
+SEVERE_ERR_LDIF_BACKEND_ERROR_READING_ENTRY_341=An error occurred while \
+ attempting to read data from LDIF file %s into the LDIF backend defined in \
+ configuration entry %s: %s
+MILD_ERR_LDIF_BACKEND_DUPLICATE_ENTRY_342=LDIF file %s configured for use \
+ with the LDIF backend defined in configuration entry %s has multiple entries \
+ with a DN of %s
+MILD_ERR_LDIF_BACKEND_ENTRY_OUT_OF_SCOPE_343=LDIF file %s configured for use \
+ with the LDIF backend defined in configuration entry %s includes entry %s \
+ which is not below the base DN defined for that backend
+MILD_ERR_LDIF_BACKEND_MISSING_PARENT_344=LDIF file %s configured for use with \
+ the LDIF backend defined in configuration entry %s contains entry %s but \
+ its parent entry has not yet been read
+SEVERE_ERR_LDIF_BACKEND_ERROR_CREATING_FILE_345=An error occurred while \
+ trying to create file %s to write an updated version of the data for the \
+ LDIF backend defined in configuration entry %s:  %s
+SEVERE_ERR_LDIF_BACKEND_ERROR_WRITING_FILE_346=An error occurred while \
+ trying to write updated data to file %s for the LDIF backend defined in \
+ configuration entry %s:  %s
+SEVERE_ERR_LDIF_BACKEND_ERROR_RENAMING_FILE_347=An error occurred while \
+ attempting to rename file %s to %s while writing updated data for the LDIF \
+ backend defined in configuration entry %s:  %s
+MILD_ERR_LDIF_BACKEND_ADD_ALREADY_EXISTS_348=Entry %s already exists in the \
+ LDIF backend
+MILD_ERR_LDIF_BACKEND_ADD_MISSING_PARENT_349=The parent for entry %s does not \
+ exist
+MILD_ERR_LDIF_BACKEND_DELETE_NO_SUCH_ENTRY_350=Entry %s does not exist
+MILD_ERR_LDIF_BACKEND_DELETE_NONLEAF_351=Entry %s has one or more subordinate \
+ entries and cannot be deleted until all of its subordinate entries are \
+ removed first
+MILD_ERR_LDIF_BACKEND_MODIFY_NO_SUCH_ENTRY_352=Entry %s does not exist
+MILD_ERR_LDIF_BACKEND_MODDN_NO_SUCH_SOURCE_ENTRY_353=Source entry %s does not \
+ exist
+MILD_ERR_LDIF_BACKEND_MODDN_TARGET_ENTRY_ALREADY_EXISTS_354=Target entry %s \
+ already exists
+MILD_ERR_LDIF_BACKEND_MODDN_NEW_PARENT_DOESNT_EXIST_355=The new parent DN %s \
+ does not exist
+MILD_ERR_LDIF_BACKEND_SEARCH_NO_SUCH_BASE_356=Entry %s specified as the \
+ search base DN does not exist
+SEVERE_ERR_LDIF_BACKEND_CANNOT_CREATE_LDIF_WRITER_357=An error occurred while \
+ trying to create the writer for the LDIF export operation:  %s
+SEVERE_ERR_LDIF_BACKEND_CANNOT_WRITE_ENTRY_TO_LDIF_358=An error occurred \
+ while trying to write entry %s during the LDIF export:  %s
+SEVERE_ERR_LDIF_BACKEND_CANNOT_CREATE_LDIF_READER_359=An error occurred while \
+ trying to create the reader for the LDIF import operation:  %s
+SEVERE_ERR_LDIF_BACKEND_ERROR_READING_LDIF_360=An unrecoverable error \
+ occurred while attempting to read data from the import file:  %s.  The LDIF \
+ import cannot continue
+MILD_ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED_361=The LDIF backend \
+ currently does not provide a backup or restore mechanism.  Use LDIF import \
+ and export operations instead
+MILD_ERR_LDIF_BACKEND_LDIF_DOESNT_EXIST_362=LDIF file %s referenced in LDIF \
+ backend configuration entry %s does not exist
+INFO_LDIF_BACKEND_LDIF_FILE_CHANGED_363=The change to the LDIF file path \
+ will not take effect until the backend is disabled and re-enabled
+INFO_LDIF_BACKEND_BASE_DN_CHANGED_364=The change to the LDIF backend base DN \
+ will not take effect until the backend is disabled and re-enabled
+MILD_ERR_LDIF_BACKEND_HAS_SUBORDINATES_NO_SUCH_ENTRY_365=The target entry %s \
+ does not exist
+MILD_ERR_LDIF_BACKEND_NUM_SUBORDINATES_NO_SUCH_ENTRY_366=The target entry %s \
+ does not exist
diff --git a/opendj-sdk/opends/src/server/org/opends/server/backends/LDIFBackend.java b/opendj-sdk/opends/src/server/org/opends/server/backends/LDIFBackend.java
new file mode 100644
index 0000000..6b29124
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/backends/LDIFBackend.java
@@ -0,0 +1,1525 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.backends;
+
+
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.opends.messages.Message;
+import org.opends.server.admin.Configuration;
+import org.opends.server.admin.server.ConfigurationChangeListener;
+import org.opends.server.admin.std.server.LDIFBackendCfg;
+import org.opends.server.api.AlertGenerator;
+import org.opends.server.api.Backend;
+import org.opends.server.config.ConfigException;
+import org.opends.server.core.AddOperation;
+import org.opends.server.core.DeleteOperation;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ModifyOperation;
+import org.opends.server.core.ModifyDNOperation;
+import org.opends.server.core.SearchOperation;
+import org.opends.server.loggers.debug.DebugTracer;
+import org.opends.server.types.BackupConfig;
+import org.opends.server.types.BackupDirectory;
+import org.opends.server.types.ConditionResult;
+import org.opends.server.types.ConfigChangeResult;
+import org.opends.server.types.Control;
+import org.opends.server.types.DebugLogLevel;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.ExistingFileBehavior;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.LDIFExportConfig;
+import org.opends.server.types.LDIFImportConfig;
+import org.opends.server.types.LDIFImportResult;
+import org.opends.server.types.RestoreConfig;
+import org.opends.server.types.ResultCode;
+import org.opends.server.types.SearchFilter;
+import org.opends.server.types.SearchScope;
+import org.opends.server.util.LDIFException;
+import org.opends.server.util.LDIFReader;
+import org.opends.server.util.LDIFWriter;
+import org.opends.server.util.Validator;
+
+import static org.opends.messages.BackendMessages.*;
+import static org.opends.server.loggers.ErrorLogger.*;
+import static org.opends.server.loggers.debug.DebugLogger.*;
+import static org.opends.server.util.ServerConstants.*;
+import static org.opends.server.util.StaticUtils.*;
+
+
+
+/**
+ * This class provides a backend implementation that stores the underlying data
+ * in an LDIF file.  When the backend is initialized, the contents of the
+ * backend are read into memory and all read operations are performed purely
+ * from memory.  Write operations cause the underlying LDIF file to be
+ * re-written on disk.
+ */
+public class LDIFBackend
+       extends Backend
+       implements ConfigurationChangeListener<LDIFBackendCfg>, AlertGenerator
+{
+  /**
+   * The tracer object for the debug logger.
+   */
+  private static final DebugTracer TRACER = getTracer();
+
+
+
+  // The base DNs for this backend.
+  private DN[] baseDNs;
+
+  // The mapping between parent DNs and their immediate children.
+  private HashMap<DN,HashSet<DN>> childDNs;
+
+  // The base DNs for this backend, in a hash set.
+  private HashSet<DN> baseDNSet;
+
+  // The set of supported controls for this backend.
+  private HashSet<String> supportedControls;
+
+  // The set of supported features for this backend.
+  private HashSet<String> supportedFeatures;
+
+  // The current configuration for this backend.
+  private LDIFBackendCfg currentConfig;
+
+  // The mapping between entry DNs and the corresponding entries.
+  private LinkedHashMap<DN,Entry> entryMap;
+
+  // A read-write lock used to protect access to this backend.
+  private ReentrantReadWriteLock backendLock;
+
+  // The path to the LDIF file containing the data for this backend.
+  private String ldifFilePath;
+
+
+
+  /**
+   * Creates a new backend with the provided information.  All backend
+   * implementations must implement a default constructor that use
+   * <CODE>super()</CODE> to invoke this constructor.
+   */
+  public LDIFBackend()
+  {
+    super();
+
+    entryMap = new LinkedHashMap<DN,Entry>();
+    childDNs = new HashMap<DN,HashSet<DN>>();
+
+    boolean useFairLocking =
+         DirectoryServer.getEnvironmentConfig().getLockManagerFairOrdering();
+    backendLock = new ReentrantReadWriteLock(useFairLocking);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void initializeBackend()
+         throws ConfigException, InitializationException
+  {
+    // We won't support anything other than exactly one base DN in this
+    // implementation.  If we were to add such support in the future, we would
+    // likely want to separate the data for each base DN into a separate entry
+    // map.
+    if ((baseDNs == null) || (baseDNs.length != 1))
+    {
+      Message message = ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(
+                             currentConfig.dn().toString());
+      throw new ConfigException(message);
+    }
+
+    for (DN dn : baseDNs)
+    {
+      try
+      {
+        DirectoryServer.registerBaseDN(dn, this, false, false);
+      }
+      catch (Exception e)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugCaught(DebugLogLevel.ERROR, e);
+        }
+
+        Message message = ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(
+            dn.toString(), getExceptionMessage(e));
+        throw new InitializationException(message, e);
+      }
+    }
+
+    DirectoryServer.registerAlertGenerator(this);
+
+    readLDIF();
+  }
+
+
+
+  /**
+   * Reads the contents of the LDIF backing file into memory.
+   *
+   * @throws  InitializationException  If a problem occurs while reading the
+   *                                   LDIF file.
+   */
+  private void readLDIF()
+          throws InitializationException
+  {
+    File ldifFile = getFileForPath(ldifFilePath);
+    if (! ldifFile.exists())
+    {
+      // This is fine.  We will just start with an empty backend.
+      if (debugEnabled())
+      {
+        TRACER.debugInfo("LDIF backend starting empty because LDIF file " +
+                         ldifFilePath + " does not exist");
+      }
+
+      entryMap.clear();
+      childDNs.clear();
+      return;
+    }
+
+
+    try
+    {
+      importLDIF(new LDIFImportConfig(ldifFile.getAbsolutePath()), false);
+    }
+    catch (DirectoryException de)
+    {
+      throw new InitializationException(de.getMessageObject(), de);
+    }
+  }
+
+
+
+  /**
+   * Writes the current set of entries to the target LDIF file.  The new LDIF
+   * will first be created as a temporary file and then renamed into place.  The
+   * caller must either hold the write lock for this backend, or must ensure
+   * that it's in some other state that guarantees exclusive access to the data.
+   *
+   * @throws  DirectoryException  If a problem occurs that prevents the updated
+   *                              LDIF from being written.
+   */
+  private void writeLDIF()
+          throws DirectoryException
+  {
+    File ldifFile = getFileForPath(ldifFilePath);
+    File tempFile = new File(ldifFile.getAbsolutePath() + ".new");
+    File oldFile  = new File(ldifFile.getAbsolutePath() + ".old");
+
+
+    // Write the new data to a temporary file.
+    LDIFWriter writer;
+    try
+    {
+      LDIFExportConfig exportConfig =
+           new LDIFExportConfig(tempFile.getAbsolutePath(),
+                                ExistingFileBehavior.OVERWRITE);
+      writer = new LDIFWriter(exportConfig);
+    }
+    catch (Exception e)
+    {
+      if (debugEnabled())
+      {
+        TRACER.debugCaught(DebugLogLevel.ERROR, e);
+      }
+
+      Message m = ERR_LDIF_BACKEND_ERROR_CREATING_FILE.get(
+                       tempFile.getAbsolutePath(),
+                       currentConfig.dn().toString(),
+                       stackTraceToSingleLineString(e));
+      DirectoryServer.sendAlertNotification(this,
+                           ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
+      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
+                                   m, e);
+    }
+
+
+    for (Entry entry : entryMap.values())
+    {
+      try
+      {
+        writer.writeEntry(entry);
+      }
+      catch (Exception e)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugCaught(DebugLogLevel.ERROR, e);
+        }
+
+        try
+        {
+          writer.close();
+        } catch (Exception e2) {}
+
+        Message m = ERR_LDIF_BACKEND_ERROR_WRITING_FILE.get(
+                         tempFile.getAbsolutePath(),
+                         currentConfig.dn().toString(),
+                         stackTraceToSingleLineString(e));
+        DirectoryServer.sendAlertNotification(this,
+                             ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
+        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
+                                     m, e);
+      }
+    }
+
+    try
+    {
+      writer.close();
+    } catch (Exception e) {}
+
+
+    // Rename the existing "live" file out of the way and move the new file
+    // into place.
+    try
+    {
+      if (oldFile.exists())
+      {
+        oldFile.delete();
+      }
+    } catch (Exception e) {}
+
+    try
+    {
+      if (ldifFile.exists())
+      {
+        ldifFile.renameTo(oldFile);
+      }
+    }
+    catch (Exception e)
+    {
+      if (debugEnabled())
+      {
+        TRACER.debugCaught(DebugLogLevel.ERROR, e);
+      }
+    }
+
+    try
+    {
+      tempFile.renameTo(ldifFile);
+    }
+    catch (Exception e)
+    {
+      if (debugEnabled())
+      {
+        TRACER.debugCaught(DebugLogLevel.ERROR, e);
+      }
+
+      Message m = ERR_LDIF_BACKEND_ERROR_RENAMING_FILE.get(
+                       tempFile.getAbsolutePath(),
+                       ldifFile.getAbsolutePath(),
+                       currentConfig.dn().toString(),
+                       stackTraceToSingleLineString(e));
+      DirectoryServer.sendAlertNotification(this,
+                           ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
+      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
+                                   m, e);
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void finalizeBackend()
+  {
+    backendLock.writeLock().lock();
+
+    try
+    {
+      currentConfig.removeLDIFChangeListener(this);
+      DirectoryServer.deregisterAlertGenerator(this);
+
+      for (DN dn : baseDNs)
+      {
+        try
+        {
+          DirectoryServer.deregisterBaseDN(dn, false);
+        }
+        catch (Exception e)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, e);
+          }
+        }
+      }
+    }
+    finally
+    {
+      backendLock.writeLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public DN[] getBaseDNs()
+  {
+    return baseDNs;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public long getEntryCount()
+  {
+    backendLock.readLock().lock();
+
+    try
+    {
+      if (entryMap != null)
+      {
+        return entryMap.size();
+      }
+
+      return -1;
+    }
+    finally
+    {
+      backendLock.readLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean isLocal()
+  {
+    return true;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public ConditionResult hasSubordinates(DN entryDN)
+         throws DirectoryException
+  {
+    backendLock.readLock().lock();
+
+    try
+    {
+      HashSet<DN> childDNSet = childDNs.get(entryDN);
+      if ((childDNSet == null) || childDNSet.isEmpty())
+      {
+        // It could be that the entry doesn't exist, in which case we should
+        // throw an exception.
+        if (entryMap.containsKey(entryDN))
+        {
+          return ConditionResult.FALSE;
+        }
+        else
+        {
+          Message m = ERR_LDIF_BACKEND_HAS_SUBORDINATES_NO_SUCH_ENTRY.get(
+                           String.valueOf(entryDN));
+          throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m);
+        }
+      }
+      else
+      {
+        return ConditionResult.TRUE;
+      }
+    }
+    finally
+    {
+      backendLock.readLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public long numSubordinates(DN entryDN)
+         throws DirectoryException
+  {
+    backendLock.readLock().lock();
+
+    try
+    {
+      HashSet<DN> childDNSet = childDNs.get(entryDN);
+      if ((childDNSet == null) || childDNSet.isEmpty())
+      {
+        // It could be that the entry doesn't exist, in which case we should
+        // throw an exception.
+        if (entryMap.containsKey(entryDN))
+        {
+          return 0L;
+        }
+        else
+        {
+          Message m = ERR_LDIF_BACKEND_NUM_SUBORDINATES_NO_SUCH_ENTRY.get(
+                           String.valueOf(entryDN));
+          throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m);
+        }
+      }
+      else
+      {
+        return childDNSet.size();
+      }
+    }
+    finally
+    {
+      backendLock.readLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public Entry getEntry(DN entryDN)
+  {
+    backendLock.readLock().lock();
+
+    try
+    {
+      return entryMap.get(entryDN);
+    }
+    finally
+    {
+      backendLock.readLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean entryExists(DN entryDN)
+  {
+    backendLock.readLock().lock();
+
+    try
+    {
+      return entryMap.containsKey(entryDN);
+    }
+    finally
+    {
+      backendLock.readLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void addEntry(Entry entry, AddOperation addOperation)
+         throws DirectoryException
+  {
+    backendLock.writeLock().lock();
+
+    try
+    {
+      // Make sure that the target entry does not already exist, but that its
+      // parent does exist (or that the entry being added is the base DN).
+      DN entryDN = entry.getDN();
+      if (entryMap.containsKey(entryDN))
+      {
+        Message m = ERR_LDIF_BACKEND_ADD_ALREADY_EXISTS.get(entryDN.toString());
+        throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, m);
+      }
+
+      if (baseDNSet.contains(entryDN))
+      {
+        entryMap.put(entryDN, entry.duplicate(false));
+        writeLDIF();
+        return;
+      }
+      else
+      {
+        DN parentDN = entryDN.getParentDNInSuffix();
+        if ((parentDN != null) && entryMap.containsKey(parentDN))
+        {
+          entryMap.put(entryDN, entry.duplicate(false));
+
+          HashSet<DN> childDNSet = childDNs.get(parentDN);
+          if (childDNSet == null)
+          {
+            childDNSet = new HashSet<DN>();
+            childDNs.put(parentDN, childDNSet);
+          }
+          childDNSet.add(entryDN);
+          writeLDIF();
+          return;
+        }
+        else
+        {
+          DN matchedDN = null;
+          while (true)
+          {
+            parentDN = parentDN.getParentDNInSuffix();
+            if (parentDN == null)
+            {
+              break;
+            }
+
+            if (entryMap.containsKey(parentDN))
+            {
+              matchedDN = parentDN;
+              break;
+            }
+          }
+
+          Message m =
+               ERR_LDIF_BACKEND_ADD_MISSING_PARENT.get(entryDN.toString());
+          throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN,
+                                       null);
+        }
+      }
+    }
+    finally
+    {
+      backendLock.writeLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void deleteEntry(DN entryDN, DeleteOperation deleteOperation)
+         throws DirectoryException
+  {
+    backendLock.writeLock().lock();
+
+    try
+    {
+      // Get the DN of the target entry's parent, if it exists.  We'll need to
+      // also remove the reference to the target entry from the parent's set of
+      // children.
+      DN parentDN = entryDN.getParentDNInSuffix();
+
+      // Make sure that the target entry exists.  If not, then fail.
+      if (! entryMap.containsKey(entryDN))
+      {
+        DN matchedDN = null;
+        while (parentDN != null)
+        {
+          if (entryMap.containsKey(parentDN))
+          {
+            matchedDN = parentDN;
+            break;
+          }
+
+          parentDN = parentDN.getParentDNInSuffix();
+        }
+
+        Message m =
+             ERR_LDIF_BACKEND_DELETE_NO_SUCH_ENTRY.get(entryDN.toString());
+        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN,
+                                     null);
+      }
+
+
+      // See if the target entry has any children.  If so, then we'll only
+      // delete it if the request contains the subtree delete control (in
+      // which case we'll delete the entire subtree).
+      HashSet<DN> childDNSet = childDNs.get(entryDN);
+      if ((childDNSet == null) || childDNSet.isEmpty())
+      {
+        entryMap.remove(entryDN);
+        childDNs.remove(entryDN);
+
+        if (parentDN != null)
+        {
+          HashSet<DN> parentChildren = childDNs.get(parentDN);
+          if (parentChildren != null)
+          {
+            parentChildren.remove(entryDN);
+            if (parentChildren.isEmpty())
+            {
+              childDNs.remove(parentDN);
+            }
+          }
+        }
+
+        writeLDIF();
+        return;
+      }
+      else
+      {
+        boolean subtreeDelete = false;
+        for (Control c : deleteOperation.getRequestControls())
+        {
+          if (c.getOID().equals(OID_SUBTREE_DELETE_CONTROL))
+          {
+            subtreeDelete = true;
+            break;
+          }
+        }
+
+        if (! subtreeDelete)
+        {
+          Message m = ERR_LDIF_BACKEND_DELETE_NONLEAF.get(entryDN.toString());
+          throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF, m);
+        }
+
+        entryMap.remove(entryDN);
+        childDNs.remove(entryDN);
+
+        if (parentDN != null)
+        {
+          HashSet<DN> parentChildren = childDNs.get(parentDN);
+          if (parentChildren != null)
+          {
+            parentChildren.remove(entryDN);
+            if (parentChildren.isEmpty())
+            {
+              childDNs.remove(parentDN);
+            }
+          }
+        }
+
+        for (DN childDN : childDNSet)
+        {
+          subtreeDelete(childDN);
+        }
+
+        writeLDIF();
+        return;
+      }
+    }
+    finally
+    {
+      backendLock.writeLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * Removes the specified entry and any subordinates that it may have from
+   * the backend.  This method assumes that the caller holds the backend write
+   * lock.
+   *
+   * @param  entryDN  The DN of the entry to remove, along with all of its
+   *                  subordinate entries.
+   */
+  private void subtreeDelete(DN entryDN)
+  {
+    entryMap.remove(entryDN);
+    HashSet<DN> childDNSet = childDNs.remove(entryDN);
+    if (childDNSet != null)
+    {
+      for (DN childDN : childDNSet)
+      {
+        subtreeDelete(childDN);
+      }
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void replaceEntry(Entry entry, ModifyOperation modifyOperation)
+         throws DirectoryException
+  {
+    backendLock.writeLock().lock();
+
+    try
+    {
+      // Make sure that the target entry exists.  If not, then fail.
+      DN entryDN = entry.getDN();
+      if (! entryMap.containsKey(entryDN))
+      {
+        DN matchedDN = null;
+        DN parentDN = entryDN.getParentDNInSuffix();
+        while (parentDN != null)
+        {
+          if (entryMap.containsKey(parentDN))
+          {
+            matchedDN = parentDN;
+            break;
+          }
+
+          parentDN = parentDN.getParentDNInSuffix();
+        }
+
+        Message m =
+             ERR_LDIF_BACKEND_MODIFY_NO_SUCH_ENTRY.get(entryDN.toString());
+        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN,
+                                     null);
+      }
+
+      entryMap.put(entryDN, entry.duplicate(false));
+      writeLDIF();
+      return;
+    }
+    finally
+    {
+      backendLock.writeLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void renameEntry(DN currentDN, Entry entry,
+                          ModifyDNOperation modifyDNOperation)
+         throws DirectoryException
+  {
+    backendLock.writeLock().lock();
+
+    try
+    {
+      // Make sure that the original entry exists and that the new entry doesn't
+      // exist but its parent does.
+      DN newDN = entry.getDN();
+      if (! entryMap.containsKey(currentDN))
+      {
+        DN matchedDN = null;
+        DN parentDN = currentDN.getParentDNInSuffix();
+        while (parentDN != null)
+        {
+          if (entryMap.containsKey(parentDN))
+          {
+            matchedDN = parentDN;
+            break;
+          }
+
+          parentDN = parentDN.getParentDNInSuffix();
+        }
+
+        Message m = ERR_LDIF_BACKEND_MODDN_NO_SUCH_SOURCE_ENTRY.get(
+                         currentDN.toString());
+        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN,
+                                     null);
+      }
+
+      if (entryMap.containsKey(newDN))
+      {
+        Message m = ERR_LDIF_BACKEND_MODDN_TARGET_ENTRY_ALREADY_EXISTS.get(
+                         newDN.toString());
+        throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, m);
+      }
+
+      DN parentDN = newDN.getParentDNInSuffix();
+      if (! entryMap.containsKey(parentDN))
+      {
+        Message m = ERR_LDIF_BACKEND_MODDN_NEW_PARENT_DOESNT_EXIST.get(
+                         String.valueOf(parentDN));
+        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m);
+      }
+
+
+      // If the entry has children, then we'll need to work on the whole
+      // subtree.  Otherwise, just work on the target entry.
+      Set<DN> childDNSet = childDNs.remove(currentDN);
+      if ((childDNSet == null) || childDNSet.isEmpty())
+      {
+        entryMap.remove(currentDN);
+        entryMap.put(newDN, entry.duplicate(false));
+        writeLDIF();
+        return;
+      }
+      else
+      {
+        entryMap.remove(currentDN);
+        entryMap.put(newDN, entry.duplicate(false));
+        for (DN childDN : childDNSet)
+        {
+          subtreeRename(childDN, newDN);
+        }
+        writeLDIF();
+        return;
+      }
+    }
+    finally
+    {
+      backendLock.writeLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * Moves the specified entry and all of its children so that they are
+   * appropriately placed below the given new parent DN.  This method assumes
+   * that the caller holds the backend write lock.
+   *
+   * @param  entryDN      The DN of the entry to move/rename.
+   * @param  newParentDN  The DN of the new parent under which the entry should
+   *                      be placed.
+   */
+  private void subtreeRename(DN entryDN, DN newParentDN)
+  {
+    Set<DN> childDNSet = childDNs.remove(entryDN);
+    DN newEntryDN = new DN(entryDN.getRDN(), newParentDN);
+
+    Entry oldEntry = entryMap.remove(entryDN);
+    if (oldEntry == null)
+    {
+      // This should never happen.
+      if (debugEnabled())
+      {
+        TRACER.debugWarning("Subtree rename encountered entry DN " +
+                            entryDN.toString() + " for nonexistent entry.");
+      }
+      return;
+    }
+
+    Entry newEntry = oldEntry.duplicate(false);
+    newEntry.setDN(newEntryDN);
+    entryMap.put(newEntryDN, newEntry);
+
+    HashSet<DN> parentChildren = childDNs.get(newParentDN);
+    if (parentChildren == null)
+    {
+      parentChildren = new HashSet<DN>();
+      childDNs.put(newParentDN, parentChildren);
+    }
+    parentChildren.add(newEntryDN);
+
+    if (childDNSet != null)
+    {
+      for (DN childDN : childDNSet)
+      {
+        subtreeRename(childDN, newEntryDN);
+      }
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void search(SearchOperation searchOperation)
+         throws DirectoryException
+  {
+    backendLock.readLock().lock();
+
+    try
+    {
+      // Get the base DN, scope, and filter for the search.
+      DN           baseDN = searchOperation.getBaseDN();
+      SearchScope  scope  = searchOperation.getScope();
+      SearchFilter filter = searchOperation.getFilter();
+
+
+      // Make sure the base entry exists if it's supposed to be in this backend.
+      Entry baseEntry = entryMap.get(baseDN);
+      if ((baseEntry == null) && handlesEntry(baseDN))
+      {
+        DN matchedDN = baseDN.getParentDNInSuffix();
+        while (matchedDN != null)
+        {
+          if (entryMap.containsKey(matchedDN))
+          {
+            break;
+          }
+
+          matchedDN = matchedDN.getParentDNInSuffix();
+        }
+
+        Message m = ERR_LDIF_BACKEND_SEARCH_NO_SUCH_BASE.get(
+                         String.valueOf(baseDN));
+        throw new DirectoryException(
+                ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
+      }
+
+      if (baseEntry != null)
+      {
+        baseEntry = baseEntry.duplicate(true);
+      }
+
+      // If it's a base-level search, then just get that entry and return it if
+      // it matches the filter.
+      if (scope == SearchScope.BASE_OBJECT)
+      {
+        if (filter.matchesEntry(baseEntry))
+        {
+          searchOperation.returnEntry(baseEntry, new LinkedList<Control>());
+        }
+      }
+      else
+      {
+        // Walk through all entries and send the ones that match.
+        for (Entry e : entryMap.values())
+        {
+          e = e.duplicate(true);
+          if (e.matchesBaseAndScope(baseDN, scope) && filter.matchesEntry(e))
+          {
+            searchOperation.returnEntry(e, new LinkedList<Control>());
+          }
+        }
+      }
+    }
+    finally
+    {
+      backendLock.readLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public HashSet<String> getSupportedControls()
+  {
+    return supportedControls;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public HashSet<String> getSupportedFeatures()
+  {
+    return supportedFeatures;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean supportsLDIFExport()
+  {
+    return true;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void exportLDIF(LDIFExportConfig exportConfig)
+         throws DirectoryException
+  {
+    backendLock.readLock().lock();
+
+    try
+    {
+      // Create the LDIF writer.
+      LDIFWriter ldifWriter;
+      try
+      {
+        ldifWriter = new LDIFWriter(exportConfig);
+      }
+      catch (Exception e)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugCaught(DebugLogLevel.ERROR, e);
+        }
+
+        Message m = ERR_LDIF_BACKEND_CANNOT_CREATE_LDIF_WRITER.get(
+                         stackTraceToSingleLineString(e));
+        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
+                                     m, e);
+      }
+
+
+      // Walk through all the entries and write them to LDIF.
+      DN entryDN = null;
+      try
+      {
+        for (Entry entry : entryMap.values())
+        {
+          entryDN = entry.getDN();
+          ldifWriter.writeEntry(entry);
+        }
+      }
+      catch (Exception e)
+      {
+        Message m = ERR_LDIF_BACKEND_CANNOT_WRITE_ENTRY_TO_LDIF.get(
+                         String.valueOf(entryDN),
+                         stackTraceToSingleLineString(e));
+        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
+                                     m, e);
+      }
+      finally
+      {
+        try
+        {
+          ldifWriter.close();
+        }
+        catch (Exception e)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, e);
+          }
+        }
+      }
+    }
+    finally
+    {
+      backendLock.readLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean supportsLDIFImport()
+  {
+    return true;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public LDIFImportResult importLDIF(LDIFImportConfig importConfig)
+         throws DirectoryException
+  {
+    return importLDIF(importConfig, true);
+  }
+
+
+
+  /**
+   * Processes an LDIF import operation, optionally writing the resulting LDIF
+   * to disk.
+   *
+   * @param  importConfig  The LDIF import configuration.
+   * @param  writeLDIF     Indicates whether the LDIF backing file for this
+   *                       backend should be updated when the import is
+   *                       complete.  This should only be {@code false} when
+   *                       reading the LDIF as the backend is coming online.
+   */
+  private LDIFImportResult importLDIF(LDIFImportConfig importConfig,
+                                     boolean writeLDIF)
+         throws DirectoryException
+  {
+    backendLock.writeLock().lock();
+
+    try
+    {
+      LDIFReader reader;
+      try
+      {
+        reader = new LDIFReader(importConfig);
+      }
+      catch (Exception e)
+      {
+        Message m = ERR_LDIF_BACKEND_CANNOT_CREATE_LDIF_READER.get(
+                         stackTraceToSingleLineString(e));
+        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
+                                     m, e);
+      }
+
+      entryMap.clear();
+      childDNs.clear();
+
+
+      try
+      {
+        while (true)
+        {
+          Entry e = null;
+          try
+          {
+            e = reader.readEntry();
+            if (e == null)
+            {
+              break;
+            }
+          }
+          catch (LDIFException le)
+          {
+            if (! le.canContinueReading())
+            {
+              Message m = ERR_LDIF_BACKEND_ERROR_READING_LDIF.get(
+                               stackTraceToSingleLineString(le));
+              throw new DirectoryException(
+                             DirectoryServer.getServerErrorResultCode(), m, le);
+            }
+            else
+            {
+              continue;
+            }
+          }
+
+          // Make sure that we don't already have an entry with the same DN.  If
+          // a duplicate is encountered, then log a message and continue.
+          DN entryDN = e.getDN();
+          if (entryMap.containsKey(entryDN))
+          {
+            Message m = ERR_LDIF_BACKEND_DUPLICATE_ENTRY.get(ldifFilePath,
+                             currentConfig.dn().toString(), entryDN.toString());
+            logError(m);
+            reader.rejectLastEntry(m);
+            continue;
+          }
+
+
+          // If the entry DN is a base DN, then add it with no more processing.
+          if (baseDNSet.contains(entryDN))
+          {
+            entryMap.put(entryDN, e);
+            continue;
+          }
+
+
+          // Make sure that the parent exists.  If not, then reject the entry.
+          boolean isBelowBaseDN = false;
+          for (DN baseDN : baseDNs)
+          {
+            if (baseDN.isAncestorOf(entryDN))
+            {
+              isBelowBaseDN = true;
+              break;
+            }
+          }
+
+          if (! isBelowBaseDN)
+          {
+            Message m = ERR_LDIF_BACKEND_ENTRY_OUT_OF_SCOPE.get(ldifFilePath,
+                             currentConfig.dn().toString(), entryDN.toString());
+            logError(m);
+            reader.rejectLastEntry(m);
+            continue;
+          }
+
+          DN parentDN = entryDN.getParentDNInSuffix();
+          if ((parentDN == null) || (! entryMap.containsKey(parentDN)))
+          {
+            Message m = ERR_LDIF_BACKEND_MISSING_PARENT.get(ldifFilePath,
+                             currentConfig.dn().toString(), entryDN.toString());
+            logError(m);
+            reader.rejectLastEntry(m);
+            continue;
+          }
+
+
+          // The entry does not exist but its parent does, so add it and update
+          // the set of children for the parent.
+          entryMap.put(entryDN, e);
+
+          HashSet<DN> childDNSet = childDNs.get(parentDN);
+          if (childDNSet == null)
+          {
+            childDNSet = new HashSet<DN>();
+            childDNs.put(parentDN, childDNSet);
+          }
+
+          childDNSet.add(entryDN);
+        }
+
+
+        if (writeLDIF)
+        {
+          writeLDIF();
+        }
+
+        return new LDIFImportResult(reader.getEntriesRead(),
+                                    reader.getEntriesRejected(),
+                                    reader.getEntriesIgnored());
+      }
+      catch (DirectoryException de)
+      {
+        throw de;
+      }
+      catch (Exception e)
+      {
+        Message m = ERR_LDIF_BACKEND_ERROR_READING_LDIF.get(
+                         stackTraceToSingleLineString(e));
+        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
+                                     m, e);
+      }
+      finally
+      {
+        reader.close();
+      }
+    }
+    finally
+    {
+      backendLock.writeLock().unlock();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean supportsBackup()
+  {
+    // This backend does not provide a backup/restore mechanism.
+    return false;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean supportsBackup(BackupConfig backupConfig,
+                                StringBuilder unsupportedReason)
+  {
+    // This backend does not provide a backup/restore mechanism.
+    return false;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void createBackup(BackupConfig backupConfig)
+         throws DirectoryException
+  {
+    Message message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get();
+    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void removeBackup(BackupDirectory backupDirectory, String backupID)
+         throws DirectoryException
+  {
+    Message message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get();
+    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean supportsRestore()
+  {
+    // This backend does not provide a backup/restore mechanism.
+    return false;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void restoreBackup(RestoreConfig restoreConfig)
+         throws DirectoryException
+  {
+    Message message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get();
+    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void configureBackend(Configuration config)
+         throws ConfigException
+  {
+    if (config != null)
+    {
+      Validator.ensureTrue(config instanceof LDIFBackendCfg);
+      currentConfig = (LDIFBackendCfg) config;
+      currentConfig.addLDIFChangeListener(this);
+
+      baseDNs = new DN[currentConfig.getBackendBaseDN().size()];
+      currentConfig.getBackendBaseDN().toArray(baseDNs);
+      if (baseDNs.length != 1)
+      {
+        throw new ConfigException(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(
+                                       currentConfig.dn().toString()));
+      }
+
+      baseDNSet = new HashSet<DN>();
+      for (DN dn : baseDNs)
+      {
+        baseDNSet.add(dn);
+      }
+
+      supportedControls = new HashSet<String>(1);
+      supportedControls.add(OID_SUBTREE_DELETE_CONTROL);
+
+      supportedFeatures = new HashSet<String>(0);
+
+      ldifFilePath = currentConfig.getLDIFFile();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public boolean isConfigurationChangeAcceptable(LDIFBackendCfg configuration,
+                      List<Message> unacceptableReasons)
+  {
+    boolean configAcceptable = true;
+
+    // Make sure that there is only a single base DN.
+    if (configuration.getBackendBaseDN().size() != 1)
+    {
+      unacceptableReasons.add(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(
+                                   configuration.dn().toString()));
+      configAcceptable = false;
+    }
+
+    return configAcceptable;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public ConfigChangeResult applyConfigurationChange(
+                                 LDIFBackendCfg configuration)
+  {
+    // We don't actually need to do anything in response to this.  However, if
+    // the base DNs or LDIF file are different from what we're currently using
+    // then indicate that admin action is required.
+    boolean adminActionRequired = false;
+    LinkedList<Message> messages = new LinkedList<Message>();
+
+    if (ldifFilePath != null)
+    {
+      File currentLDIF = getFileForPath(ldifFilePath);
+      File newLDIF     = getFileForPath(configuration.getLDIFFile());
+      if (! currentLDIF.equals(newLDIF))
+      {
+        messages.add(INFO_LDIF_BACKEND_LDIF_FILE_CHANGED.get());
+        adminActionRequired = true;
+      }
+    }
+
+    if (baseDNSet != null)
+    {
+      if (! baseDNSet.equals(configuration.getBackendBaseDN()))
+      {
+        messages.add(INFO_LDIF_BACKEND_BASE_DN_CHANGED.get());
+        adminActionRequired = true;
+      }
+    }
+
+    currentConfig = configuration;
+    return new ConfigChangeResult(ResultCode.SUCCESS, adminActionRequired,
+                                  messages);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public DN getComponentEntryDN()
+  {
+    return currentConfig.dn();
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public String getClassName()
+  {
+    return LDIFBackend.class.getName();
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public LinkedHashMap<String,String> getAlerts()
+  {
+    LinkedHashMap<String,String> alerts = new LinkedHashMap<String,String>();
+
+    alerts.put(ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE,
+               ALERT_DESCRIPTION_LDIF_BACKEND_CANNOT_WRITE_UPDATE);
+
+    return alerts;
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/types/DN.java b/opendj-sdk/opends/src/server/org/opends/server/types/DN.java
index d5ca252..0e5ec70 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/types/DN.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/types/DN.java
@@ -41,6 +41,7 @@
 import static org.opends.server.config.ConfigConstants.*;
 import static org.opends.server.loggers.debug.DebugLogger.*;
 import static org.opends.server.util.StaticUtils.*;
+import static org.opends.server.util.Validator.*;
 
 
 
@@ -91,17 +92,17 @@
 
 
   // The number of RDN components that comprise this DN.
-  private int numComponents;
+  private final int numComponents;
 
   // The set of RDN components that comprise this DN, arranged with
   // the suffix as the last element.
-  private RDN[] rdnComponents;
+  private final RDN[] rdnComponents;
 
   // The string representation of this DN.
   private String dnString;
 
   // The normalized string representation of this DN.
-  private String normalizedDN;
+  private final String normalizedDN;
 
 
 
@@ -136,7 +137,7 @@
 
     numComponents = this.rdnComponents.length;
     dnString      = null;
-    normalizedDN  = toNormalizedString();
+    normalizedDN  = normalize(this.rdnComponents);
   }
 
 
@@ -162,7 +163,37 @@
 
     numComponents = this.rdnComponents.length;
     dnString      = null;
-    normalizedDN  = toNormalizedString();
+    normalizedDN  = normalize(this.rdnComponents);
+  }
+
+
+
+  /**
+   * Creates a new DN with the given RDN below the specified parent.
+   *
+   * @param  rdn       The RDN to use for the new DN.  It must not be
+   *                   {@code null}.
+   * @param  parentDN  The DN of the entry below which the new DN
+   *                   should exist. It must not be {@code null}.
+   */
+  public DN(RDN rdn, DN parentDN)
+  {
+    ensureNotNull(rdn, parentDN);
+    if (parentDN.isNullDN())
+    {
+      rdnComponents = new RDN[] { rdn };
+    }
+    else
+    {
+      rdnComponents = new RDN[parentDN.numComponents + 1];
+      rdnComponents[0] = rdn;
+      System.arraycopy(parentDN.rdnComponents, 0, rdnComponents, 1,
+                       parentDN.numComponents);
+    }
+
+    numComponents = this.rdnComponents.length;
+    dnString      = null;
+    normalizedDN  = normalize(this.rdnComponents);
   }
 
 
@@ -2808,33 +2839,43 @@
 
 
   /**
+   * Retrieves a normalized representation of the DN with the provided
+   * components.
+   *
+   * @param  rdnComponents  The RDN components for which to obtain the
+   *                        normalized string representation.
+   *
+   * @return  The normalized string representation of the provided RDN
+   *          components.
+   */
+  private static final String normalize(RDN[] rdnComponents)
+  {
+    if (rdnComponents.length == 0)
+    {
+      return "";
+    }
+
+    StringBuilder buffer = new StringBuilder();
+    rdnComponents[0].toNormalizedString(buffer);
+
+    for (int i=1; i < rdnComponents.length; i++)
+    {
+      buffer.append(',');
+      rdnComponents[i].toNormalizedString(buffer);
+    }
+
+    return buffer.toString();
+  }
+
+
+
+  /**
    * Retrieves a normalized string representation of this DN.
    *
    * @return  A normalized string representation of this DN.
    */
   public String toNormalizedString()
   {
-    if (normalizedDN == null)
-    {
-      if (numComponents == 0)
-      {
-        normalizedDN = "";
-      }
-      else
-      {
-        StringBuilder buffer = new StringBuilder();
-        rdnComponents[0].toNormalizedString(buffer);
-
-        for (int i=1; i < numComponents; i++)
-        {
-          buffer.append(',');
-          rdnComponents[i].toNormalizedString(buffer);
-        }
-
-        normalizedDN = buffer.toString();
-      }
-    }
-
     return normalizedDN;
   }
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java b/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java
index a202534..b018028 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java
@@ -1676,6 +1676,28 @@
 
   /**
    * The description for the alert type that will be used for the alert
+   * notification generated when the LDIF backend cannot write an updated LDIF
+   * file.
+   */
+  public static final String
+       ALERT_DESCRIPTION_LDIF_BACKEND_CANNOT_WRITE_UPDATE =
+            "This alert type will be used to provide notification that an " +
+            "LDIF backend was unable to store an updated copy of the LDIF " +
+            "file after processing a write operation.";
+
+
+
+  /**
+   * The alert type string that will be used for the alert notification
+   * generated when the LDIF backend cannot write an updated LDIF file.
+   */
+  public static final String ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE =
+       "org.opends.server.LDIFBackendCannotWriteUupdate";
+
+
+
+  /**
+   * The description for the alert type that will be used for the alert
    * notification generated when the LDIF connection handler is unable to
    * process the contents of a file as valid LDIF.
    */
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/resource/config-changes.ldif b/opendj-sdk/opends/tests/unit-tests-testng/resource/config-changes.ldif
index 7491871..5241c68 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/resource/config-changes.ldif
+++ b/opendj-sdk/opends/tests/unit-tests-testng/resource/config-changes.ldif
@@ -855,3 +855,15 @@
 ds-cfg-password-storage-scheme-class: org.opends.server.extensions.RC4PasswordStorageScheme
 ds-cfg-password-storage-scheme-enabled: true
 
+dn: ds-cfg-backend-id=ldifRoot,cn=Backends,cn=config
+changetype: add
+objectClass: top
+objectClass: ds-cfg-backend
+objectClass: ds-cfg-ldif-backend
+ds-cfg-backend-id: ldifRoot
+ds-cfg-backend-enabled: true
+ds-cfg-backend-class: org.opends.server.backends.LDIFBackend
+ds-cfg-backend-writability-mode: enabled
+ds-cfg-backend-base-dn: o=ldif
+ds-cfg-ldif-file: config/ldif-backend.ldif
+
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/backends/LDIFBackendTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/backends/LDIFBackendTestCase.java
new file mode 100644
index 0000000..fa38b98
--- /dev/null
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/backends/LDIFBackendTestCase.java
@@ -0,0 +1,814 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.backends;
+
+
+
+import java.io.File;
+import java.util.UUID;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.opends.server.TestCaseUtils;
+import org.opends.server.api.Backend;
+import org.opends.server.backends.LDIFBackend;
+import org.opends.server.backends.task.Task;
+import org.opends.server.backends.task.TaskState;
+import org.opends.server.core.AddOperation;
+import org.opends.server.core.CompareOperation;
+import org.opends.server.core.DeleteOperation;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ModifyDNOperation;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.protocols.internal.InternalSearchOperation;
+import org.opends.server.tasks.LdifFileWriter;
+import org.opends.server.tasks.TasksTestCase;
+import org.opends.server.tools.LDAPModify;
+import org.opends.server.tools.LDAPSearch;
+import org.opends.server.tools.LDIFDiff;
+import org.opends.server.types.ConditionResult;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.LDIFImportConfig;
+import org.opends.server.types.ResultCode;
+import org.opends.server.types.SearchScope;
+
+import static org.testng.Assert.*;
+
+import static org.opends.server.util.StaticUtils.*;
+
+
+
+/**
+ * A set of test cases for the LDIF backend.
+ */
+public class LDIFBackendTestCase
+       extends BackendTestCase
+{
+  /**
+   * Ensures that the Directory Server is running and that the LDIF backend
+   * is populated with sample data.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @BeforeClass()
+  public void setUp()
+         throws Exception
+  {
+    TestCaseUtils.startServer();
+
+    String templateFilePath = TestCaseUtils.createTempFile(
+      "define suffix=o=ldif",
+      "define numusers=25",
+      "",
+      "branch: [suffix]",
+      "",
+      "branch: ou=People,[suffix]",
+      "subordinateTemplate: person:[numusers]",
+      "",
+      "template: person",
+      "rdnAttr: uid",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "givenName: <random:alpha:6>",
+      "sn: <random:alpha:6>",
+      "cn: {givenName} {sn}",
+      "uid: user.<sequential:1>",
+      "userPassword: password");
+
+    // Create a temporary test LDIF file.
+    File ldifFile = File.createTempFile("import-test", ".ldif");
+    String resourcePath = DirectoryServer.getServerRoot() + File.separator +
+         "config" + File.separator + "MakeLDIF";
+    LdifFileWriter.makeLdif(ldifFile.getPath(), resourcePath, templateFilePath);
+
+    String taskDN = "ds-task-id=" + UUID.randomUUID() +
+                    ",cn=Scheduled Tasks,cn=Tasks";
+
+    TestCaseUtils.addEntry(
+      "dn: " + taskDN,
+      "objectclass: top",
+      "objectclass: ds-task",
+      "objectclass: ds-task-import",
+      "ds-task-class-name: org.opends.server.tasks.ImportTask",
+      "ds-task-import-backend-id: ldifRoot",
+      "ds-task-import-ldif-file: " + ldifFile.getAbsolutePath());
+
+    Task t = TasksTestCase.getCompletedTask(DN.decode(taskDN));
+    assertNotNull(t);
+    assertEquals(t.getTaskState(), TaskState.COMPLETED_SUCCESSFULLY);
+  }
+
+
+
+  /**
+   * Tests to ensure that add and delete operations (including subtree delete)
+   * work as expected.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testAddAndDelete()
+         throws Exception
+  {
+    // Add a number of entries to the server.
+    int resultCode = TestCaseUtils.applyModifications(
+      "dn: ou=dummy,o=ldif",
+      "changetype: add",
+      "objectClass: top",
+      "objectClass: organizationalUnit",
+      "ou: dummy",
+      "",
+      "dn: ou=sub1,ou=dummy,o=ldif",
+      "changetype: add",
+      "objectClass: top",
+      "objectClass: organizationalUnit",
+      "ou: sub1",
+      "",
+      "dn: ou=sub2,ou=dummy,o=ldif",
+      "changetype: add",
+      "objectClass: top",
+      "objectClass: organizationalUnit",
+      "ou: sub2",
+      "",
+      "dn: ou=sub3,ou=dummy,o=ldif",
+      "changetype: add",
+      "objectClass: top",
+      "objectClass: organizationalUnit",
+      "ou: sub3",
+      "",
+      "dn: ou=sub4,ou=dummy,o=ldif",
+      "changetype: add",
+      "objectClass: top",
+      "objectClass: organizationalUnit",
+      "ou: sub4",
+      "",
+      "dn: ou=sub5,ou=dummy,o=ldif",
+      "changetype: add",
+      "objectClass: top",
+      "objectClass: organizationalUnit",
+      "ou: sub5");
+    assertEquals(resultCode, 0);
+
+
+    // Verify that we can delete a single leaf entry.
+    resultCode = TestCaseUtils.applyModifications(
+      "dn: ou=sub5,ou=dummy,o=ldif",
+      "changetype: delete");
+    assertEquals(resultCode, 0);
+
+
+    // Verify that a default attempt to delete a non-leaf entry will fail.
+    String subtreeDeletePath = TestCaseUtils.createTempFile(
+      "dn: ou=dummy,o=ldif",
+      "changetype: delete");
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "-f", subtreeDeletePath
+    };
+    resultCode = LDAPModify.mainModify(args, false, System.out, System.err);
+    assertEquals(resultCode, ResultCode.NOT_ALLOWED_ON_NONLEAF.getIntValue());
+
+
+    // Verify that the subtree delete will succeed if we include the subtree
+    // delete control in the request.
+    args = new String[]
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "-J", "subtreeDelete",
+      "-f", subtreeDeletePath
+    };
+    resultCode = LDAPModify.mainModify(args, false, System.out, System.err);
+    assertEquals(resultCode, 0);
+  }
+
+
+
+  /**
+   * Tests an attempt to add an entry to the LDIF backend when an entry with
+   * the same DN already exists.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testAddAlreadyExists()
+         throws Exception
+  {
+    Entry e = TestCaseUtils.makeEntry(
+      "dn: ou=People,o=ldif",
+      "objectClass: top",
+      "objectClass: organizationalUnit",
+      "ou: People");
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation = conn.processAdd(e);
+    assertEquals(addOperation.getResultCode(), ResultCode.ENTRY_ALREADY_EXISTS);
+  }
+
+
+
+  /**
+   * Tests an attempt to add an entry to the LDIF backend when the parent for
+   * the new entry doesn't exist.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testAddNoParent()
+         throws Exception
+  {
+    Entry e = TestCaseUtils.makeEntry(
+      "dn: ou=test,ou=doesntexist,o=ldif",
+      "objectClass: top",
+      "objectClass: organizationalUnit",
+      "ou: test");
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation = conn.processAdd(e);
+    assertEquals(addOperation.getResultCode(), ResultCode.NO_SUCH_OBJECT);
+    assertEquals(addOperation.getMatchedDN(), DN.decode("o=ldif"));
+  }
+
+
+
+  /**
+   * Tests the ability to add the base entry to the backend.  This will first
+   * perform a subtree delete to get rid of everything, then add the base entry,
+   * and then restore the original content.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testAddBaseEntry()
+         throws Exception
+  {
+    assertTrue(DirectoryServer.entryExists(DN.decode("o=ldif")));
+    assertTrue(DirectoryServer.entryExists(
+                   DN.decode("uid=user.1,ou=People,o=ldif")));
+
+    String path = TestCaseUtils.createTempFile(
+      "dn: o=ldif",
+      "changetype: delete");
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "-J", "subtreeDelete",
+      "-f", path
+    };
+    assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), 0);
+    assertFalse(DirectoryServer.entryExists(DN.decode("o=ldif")));
+    assertFalse(DirectoryServer.entryExists(
+                    DN.decode("uid=user.1,ou=People,o=ldif")));
+
+    Entry e = TestCaseUtils.makeEntry(
+      "dn: o=ldif",
+      "objectClass: top",
+      "objectClass: organization",
+      "o: ldif");
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation = conn.processAdd(e);
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+    assertTrue(DirectoryServer.entryExists(DN.decode("o=ldif")));
+    assertFalse(DirectoryServer.entryExists(
+                    DN.decode("uid=user.1,ou=People,o=ldif")));
+
+    setUp();
+    assertTrue(DirectoryServer.entryExists(DN.decode("o=ldif")));
+    assertTrue(DirectoryServer.entryExists(
+                   DN.decode("uid=user.1,ou=People,o=ldif")));
+  }
+
+
+
+  /**
+   * Tests to ensure that we can bind as a user contained in an LDIF backend.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testBind()
+         throws Exception
+  {
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "uid=user.1,ou=People,o=ldif",
+      "-w", "password",
+      "-b", "o=ldif",
+      "-s", "base",
+      "(objectClass=*)"
+    };
+
+    assertEquals(LDAPSearch.mainSearch(args, false, System.out, System.err), 0);
+  }
+
+
+
+  /**
+   * Tests to ensure that we can perform a compare against entries in an LDIF
+   * backend.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testCompare()
+         throws Exception
+  {
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    CompareOperation compareOperation =
+         conn.processCompare("uid=user.1,ou=People,o=ldif", "uid", "user.1");
+    assertEquals(compareOperation.getResultCode(), ResultCode.COMPARE_TRUE);
+  }
+
+
+
+  /**
+   * Tests to ensure that we can modify entries in the LDIF backend.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testModify()
+         throws Exception
+  {
+    String path = TestCaseUtils.createTempFile(
+      "dn: o=ldif",
+      "changetype: modify",
+      "replace: description",
+      "description: foo");
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "-f", path
+    };
+    assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), 0);
+  }
+
+
+
+  /**
+   * Tests a simple modify DN operation that targets a single leaf entry.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSimpleModifyDN()
+         throws Exception
+  {
+    TestCaseUtils.addEntry(
+      "dn: ou=leaf before,o=ldif",
+      "objectClass: top",
+      "objectClass: organizationalUnit",
+      "ou: leaf before");
+
+    DN beforeDN = DN.decode("ou=leaf before,o=ldif");
+    DN afterDN  = DN.decode("ou=leaf after,o=ldif");
+
+    assertTrue(DirectoryServer.entryExists(beforeDN));
+    assertFalse(DirectoryServer.entryExists(afterDN));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyDNOperation modifyDNOperation =
+      conn.processModifyDN("ou=leaf before,o=ldif", "ou=leaf after", true);
+    assertEquals(modifyDNOperation.getResultCode(), ResultCode.SUCCESS);
+
+    assertFalse(DirectoryServer.entryExists(beforeDN));
+    assertTrue(DirectoryServer.entryExists(afterDN));
+
+    DeleteOperation deleteOperation = conn.processDelete(afterDN);
+    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
+    assertFalse(DirectoryServer.entryExists(afterDN));
+  }
+
+
+
+  /**
+   * Tests a modify DN operation in which the target entry already exists.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testModifyDNTargetAlreadyExists()
+         throws Exception
+  {
+    TestCaseUtils.addEntry(
+      "dn: ou=new entry,o=ldif",
+      "objectClass: top",
+      "objectClass: organizationalUnit",
+      "ou: new entry");
+    assertTrue(DirectoryServer.entryExists(DN.decode("ou=new entry,o=ldif")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyDNOperation modifyDNOperation =
+         conn.processModifyDN("ou=new entry,o=ldif", "ou=People", true);
+    assertEquals(modifyDNOperation.getResultCode(),
+                 ResultCode.ENTRY_ALREADY_EXISTS);
+    assertTrue(DirectoryServer.entryExists(DN.decode("ou=new entry,o=ldif")));
+
+    DeleteOperation deleteOperation = conn.processDelete("ou=new entry,o=ldif");
+    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
+    assertFalse(DirectoryServer.entryExists(DN.decode("ou=new entry,o=ldif")));
+  }
+
+
+
+  /**
+   * Tests a modify DN operation that targets a single leaf entry and provides a
+   * new superior DN.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testModifyDNWithNewSuperior()
+         throws Exception
+  {
+    TestCaseUtils.addEntry(
+      "dn: ou=leaf before,o=ldif",
+      "objectClass: top",
+      "objectClass: organizationalUnit",
+      "ou: leaf before");
+
+    DN beforeDN = DN.decode("ou=leaf before,o=ldif");
+    DN afterDN  = DN.decode("ou=leaf after,ou=People,o=ldif");
+
+    assertTrue(DirectoryServer.entryExists(beforeDN));
+    assertFalse(DirectoryServer.entryExists(afterDN));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyDNOperation modifyDNOperation =
+      conn.processModifyDN("ou=leaf before,o=ldif", "ou=leaf after", true,
+                           "ou=People,o=ldif");
+    assertEquals(modifyDNOperation.getResultCode(), ResultCode.SUCCESS);
+
+    assertFalse(DirectoryServer.entryExists(beforeDN));
+    assertTrue(DirectoryServer.entryExists(afterDN));
+
+    DeleteOperation deleteOperation = conn.processDelete(afterDN);
+    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
+    assertFalse(DirectoryServer.entryExists(afterDN));
+  }
+
+
+
+  /**
+   * Tests a modify DN operation that involves a subtree rename operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testModifyDNSubtreeRename()
+         throws Exception
+  {
+    DN beforeDN      = DN.decode("ou=People,o=ldif");
+    DN afterDN       = DN.decode("ou=Users,o=ldif");
+    DN childBeforeDN = DN.decode("uid=user.1,ou=People,o=ldif");
+    DN childAfterDN  = DN.decode("uid=user.1,ou=Users,o=ldif");
+
+    assertTrue(DirectoryServer.entryExists(beforeDN));
+    assertFalse(DirectoryServer.entryExists(afterDN));
+
+    assertTrue(DirectoryServer.entryExists(childBeforeDN));
+    assertFalse(DirectoryServer.entryExists(childAfterDN));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyDNOperation modifyDNOperation =
+      conn.processModifyDN("ou=People,o=ldif", "ou=Users", true);
+    assertEquals(modifyDNOperation.getResultCode(), ResultCode.SUCCESS);
+
+    assertFalse(DirectoryServer.entryExists(beforeDN));
+    assertTrue(DirectoryServer.entryExists(afterDN));
+
+    assertFalse(DirectoryServer.entryExists(childBeforeDN));
+    assertTrue(DirectoryServer.entryExists(childAfterDN));
+
+    modifyDNOperation =
+         conn.processModifyDN("ou=Users,o=ldif", "ou=People", true);
+
+    assertTrue(DirectoryServer.entryExists(beforeDN));
+    assertFalse(DirectoryServer.entryExists(afterDN));
+
+    assertTrue(DirectoryServer.entryExists(childBeforeDN));
+    assertFalse(DirectoryServer.entryExists(childAfterDN));
+  }
+
+
+
+  /**
+   * Tests to ensure that a base-level search works as expected.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testBaseSearch()
+         throws Exception
+  {
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    InternalSearchOperation searchOperation =
+         conn.processSearch("o=ldif", SearchScope.BASE_OBJECT,
+                         "(objectClass=*)");
+    assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
+    assertEquals(searchOperation.getSearchEntries().size(), 1);
+  }
+
+
+
+  /**
+   * Tests to ensure that a base-level search works as expected when the filter
+   * doesn't match the target entry.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testBaseSearchNonMatchingFilter()
+         throws Exception
+  {
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    InternalSearchOperation searchOperation =
+         conn.processSearch("o=ldif", SearchScope.BASE_OBJECT,
+                         "(o=not ldif)");
+    assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
+    assertEquals(searchOperation.getSearchEntries().size(), 0);
+  }
+
+
+
+  /**
+   * Tests to ensure that a base-level search works as expected when the target
+   * entry does not exist.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testBaseSearchNoSuchEntry()
+         throws Exception
+  {
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    InternalSearchOperation searchOperation =
+         conn.processSearch("o=nonexistent2,o=nonexistent1,o=ldif",
+                            SearchScope.BASE_OBJECT, "(objectClass=*)");
+    assertEquals(searchOperation.getResultCode(), ResultCode.NO_SUCH_OBJECT);
+    assertEquals(searchOperation.getMatchedDN(), DN.decode("o=ldif"));
+  }
+
+
+
+  /**
+   * Tests to ensure that a single-level search works as expected.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSingleLevelSearch()
+         throws Exception
+  {
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    InternalSearchOperation searchOperation =
+         conn.processSearch("o=ldif", SearchScope.SINGLE_LEVEL,
+                         "(objectClass=*)");
+    assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
+    assertEquals(searchOperation.getSearchEntries().size(), 1);
+  }
+
+
+
+  /**
+   * Tests to ensure that subtree search operations work as expected.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSubtreeSearch()
+         throws Exception
+  {
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    InternalSearchOperation searchOperation =
+         conn.processSearch("o=ldif", SearchScope.WHOLE_SUBTREE,
+                            "(uid=user.1)");
+    assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
+    assertEquals(searchOperation.getSearchEntries().size(), 1);
+  }
+
+
+
+  /**
+   * Tests to ensure that subordinate subtree search operations work as
+   * expected.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSubordinateSubtreeSearch()
+         throws Exception
+  {
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    InternalSearchOperation searchOperation =
+         conn.processSearch("o=ldif", SearchScope.SUBORDINATE_SUBTREE,
+                            "(uid=user.1)");
+    assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
+    assertEquals(searchOperation.getSearchEntries().size(), 1);
+  }
+
+
+
+  /**
+   * Tests the {@code hasSubordinates} method.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testHasSubordinates()
+         throws Exception
+  {
+    Backend b = DirectoryServer.getBackend("ldifRoot");
+    assertNotNull(b);
+    assertTrue(b instanceof LDIFBackend);
+
+    assertEquals(b.hasSubordinates(DN.decode("o=ldif")), ConditionResult.TRUE);
+    assertEquals(b.hasSubordinates(DN.decode("uid=user.1,ou=People,o=ldif")),
+                 ConditionResult.FALSE);
+
+    try
+    {
+      b.hasSubordinates(DN.decode("ou=nonexistent,o=ldif"));
+      fail("Expected an exception when calling hasSubordinates on a " +
+           "non-existent entry");
+    }
+    catch (DirectoryException de)
+    {
+      assertEquals(de.getResultCode(), ResultCode.NO_SUCH_OBJECT);
+    }
+  }
+
+
+
+  /**
+   * Tests the {@code numSubordinates} method.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testNumSubordinates()
+         throws Exception
+  {
+    Backend b = DirectoryServer.getBackend("ldifRoot");
+    assertNotNull(b);
+    assertTrue(b instanceof LDIFBackend);
+
+    assertEquals(b.numSubordinates(DN.decode("o=ldif")), 1);
+    assertEquals(b.numSubordinates(DN.decode("uid=user.1,ou=People,o=ldif")),
+                 0);
+
+    try
+    {
+      b.numSubordinates(DN.decode("ou=nonexistent,o=ldif"));
+      fail("Expected an exception when calling numSubordinates on a " +
+           "non-existent entry");
+    }
+    catch (DirectoryException de)
+    {
+      assertEquals(de.getResultCode(), ResultCode.NO_SUCH_OBJECT);
+    }
+  }
+
+
+
+  /**
+   * Tests LDIF export functionality.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testLDIFExport()
+         throws Exception
+  {
+    Backend b = DirectoryServer.getBackend("ldifRoot");
+    assertNotNull(b);
+    assertTrue(b instanceof LDIFBackend);
+    assertTrue(b.supportsLDIFExport());
+
+    String tempFilePath = TestCaseUtils.createTempFile();
+
+    String taskDN = "ds-task-id=" + UUID.randomUUID() +
+                    ",cn=Scheduled Tasks,cn=Tasks";
+    TestCaseUtils.addEntry(
+      "dn: " + taskDN,
+      "objectclass: top",
+      "objectclass: ds-task",
+      "objectclass: ds-task-export",
+      "ds-task-class-name: org.opends.server.tasks.ExportTask",
+      "ds-task-export-backend-id: ldifRoot",
+      "ds-task-export-ldif-file: " + tempFilePath);
+
+    Task t = TasksTestCase.getCompletedTask(DN.decode(taskDN));
+    assertNotNull(t);
+    assertEquals(t.getTaskState(), TaskState.COMPLETED_SUCCESSFULLY);
+  }
+
+
+
+  /**
+   * Tests a number of miscellaneous backend methods.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testMiscellaneousBackendMethods()
+         throws Exception
+  {
+    Backend b = DirectoryServer.getBackend("ldifRoot");
+    assertNotNull(b);
+    assertTrue(b instanceof LDIFBackend);
+
+    assertTrue(b.getEntryCount() > 0);
+
+    assertTrue(b.isLocal());
+
+    assertFalse(b.supportsBackup());
+    assertFalse(b.supportsBackup(null, null));
+
+    try
+    {
+      b.createBackup(null);
+      fail("Expected an exception when calling createBackup");
+    } catch (DirectoryException de) {}
+
+    try
+    {
+      b.removeBackup(null, null);
+      fail("Expected an exception when calling removeBackup");
+    } catch (DirectoryException de) {}
+
+    assertFalse(b.supportsRestore());
+
+    try
+    {
+      b.restoreBackup(null);
+      fail("Expected an exception when calling restoreBackup");
+    } catch (DirectoryException de) {}
+
+    LDIFBackend ldifBackend = (LDIFBackend) b;
+    assertNotNull(ldifBackend.getClassName());
+    assertNotNull(ldifBackend.getAlerts());
+    assertFalse(ldifBackend.getAlerts().isEmpty());
+  }
+}
+

--
Gitblit v1.10.0