mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

neil_a_wilson
23.38.2007 6c344c0b005235e10fae64bad1571fe386b8e118
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.

This backend supports all major operations, including moving/renaming non-leaf
entries with the modify DN operation, and also supports the subtree delete
control and LDIF import and export operations. Backup and restore operations
are not currently supported.
3 files added
5 files modified
2622 ■■■■■ changed files
opends/resource/schema/02-config.ldif 6 ●●●●● patch | view | raw | blame | history
opends/src/admin/defn/org/opends/server/admin/std/LDIFBackendConfiguration.xml 84 ●●●●● patch | view | raw | blame | history
opends/src/messages/messages/backend.properties 66 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/backends/LDIFBackend.java 1525 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/types/DN.java 93 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/ServerConstants.java 22 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/resource/config-changes.ldif 12 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/backends/LDIFBackendTestCase.java 814 ●●●●● patch | view | raw | blame | history
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' )
opends/src/admin/defn/org/opends/server/admin/std/LDIFBackendConfiguration.xml
New file
@@ -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>
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
opends/src/server/org/opends/server/backends/LDIFBackend.java
New file
@@ -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;
  }
}
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;
  }
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.
   */
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
opends/tests/unit-tests-testng/src/server/org/opends/server/backends/LDIFBackendTestCase.java
New file
@@ -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());
  }
}