/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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
 *
 *
 *      Copyright 2007-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.server.backends;

import static org.forgerock.util.Reject.*;
import static org.opends.messages.BackendMessages.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;

import java.io.File;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
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.controls.SubtreeDeleteControl;
import org.opends.server.core.*;
import org.opends.server.types.*;
import org.opends.server.util.LDIFException;
import org.opends.server.util.LDIFReader;
import org.opends.server.util.LDIFWriter;
import 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<LDIFBackendCfg>
       implements ConfigurationChangeListener<LDIFBackendCfg>, AlertGenerator
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();



  /** The base DNs for this backend. */
  private DN[] baseDNs;

  /** The mapping between parent DNs and their immediate children. */
  private final Map<DN, Set<DN>> childDNs = new HashMap<>();

  /** The base DNs for this backend, in a hash set. */
  private Set<DN> baseDNSet;

  /** The set of supported controls for this backend. */
  private final Set<String> supportedControls =
      Collections.singleton(OID_SUBTREE_DELETE_CONTROL);

  /** The current configuration for this backend. */
  private LDIFBackendCfg currentConfig;

  /** The mapping between entry DNs and the corresponding entries. */
  private final Map<DN, Entry> entryMap = new LinkedHashMap<>();

  /** A read-write lock used to protect access to this backend. */
  private final ReentrantReadWriteLock backendLock = new ReentrantReadWriteLock();

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

  /** {@inheritDoc} */
  @Override
  public void openBackend()
         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)
    {
      throw new ConfigException(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(currentConfig.dn()));
    }

    for (DN dn : baseDNs)
    {
      try
      {
        DirectoryServer.registerBaseDN(dn, this,
                                       currentConfig.isIsPrivateBackend());
      }
      catch (Exception e)
      {
        logger.traceException(e);

        LocalizableMessage message = ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(
            dn, 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 (logger.isTraceEnabled())
      {
        logger.trace("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)
    {
      logger.traceException(e);

      LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_CREATING_FILE.get(
                       tempFile.getAbsolutePath(),
                       currentConfig.dn(),
                       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)
      {
        logger.traceException(e);

        StaticUtils.close(writer);

        LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_WRITING_FILE.get(
                         tempFile.getAbsolutePath(),
                         currentConfig.dn(),
                         stackTraceToSingleLineString(e));
        DirectoryServer.sendAlertNotification(this,
                             ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     m, e);
      }
    }

    // On Linux the final write() on a file can actually fail but not throw an Exception.
    // The close() will throw an Exception in this case so we MUST check for Exceptions
    // here.
    try
    {
        writer.close();
    }
    catch (Exception e)
    {
      logger.traceException(e);
      LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_CLOSING_FILE.get(
                       tempFile.getAbsolutePath(),
                       currentConfig.dn(),
                       stackTraceToSingleLineString(e));
      DirectoryServer.sendAlertNotification(this,
                           ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   m, e);
    }

    // Extra sanity check
    if (!entryMap.isEmpty() && tempFile.exists() && tempFile.length() == 0)
    {
      LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_EMPTY_FILE.get(
                       tempFile.getAbsolutePath(),
                       currentConfig.dn());
      DirectoryServer.sendAlertNotification(this,
                           ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), m);
    }

    if (tempFile.exists())
    {
      // Rename the existing "live" file out of the way and move the new file
      // into place.
      try
      {
        oldFile.delete();
      }
      catch (Exception e)
      {
        logger.traceException(e);
      }
    }

    try
    {
      if (ldifFile.exists())
      {
        ldifFile.renameTo(oldFile);
      }
    }
    catch (Exception e)
    {
      logger.traceException(e);
    }

    try
    {
      tempFile.renameTo(ldifFile);
    }
    catch (Exception e)
    {
      logger.traceException(e);

      LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_RENAMING_FILE.get(
                       tempFile.getAbsolutePath(),
                       ldifFile.getAbsolutePath(),
                       currentConfig.dn(),
                       stackTraceToSingleLineString(e));
      DirectoryServer.sendAlertNotification(this,
                           ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   m, e);
    }
  }

  /** {@inheritDoc} */
  @Override
  public void closeBackend()
  {
    backendLock.writeLock().lock();

    try
    {
      currentConfig.removeLDIFChangeListener(this);
      DirectoryServer.deregisterAlertGenerator(this);

      for (DN dn : baseDNs)
      {
        try
        {
          DirectoryServer.deregisterBaseDN(dn);
        }
        catch (Exception e)
        {
          logger.traceException(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 isIndexed(AttributeType attributeType, IndexType indexType)
  {
    // All searches in this backend will always be considered indexed.
    return true;
  }

  /** {@inheritDoc} */
  @Override
  public ConditionResult hasSubordinates(DN entryDN)
         throws DirectoryException
  {
    backendLock.readLock().lock();

    try
    {
      Set<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
        {
          throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
              ERR_LDIF_BACKEND_HAS_SUBORDINATES_NO_SUCH_ENTRY.get(entryDN));
        }
      }
      else
      {
        return ConditionResult.TRUE;
      }
    }
    finally
    {
      backendLock.readLock().unlock();
    }
  }

  /** {@inheritDoc} */
  @Override
  public long getNumberOfChildren(DN parentDN) throws DirectoryException
  {
    checkNotNull(parentDN, "parentDN must not be null");
    return getNumberOfSubordinates(parentDN, false);
  }

  /** {@inheritDoc} */
  @Override
  public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException
  {
    checkNotNull(baseDN, "baseDN must not be null");
    if (!Arrays.asList(baseDNs).contains(baseDN))
    {
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_LDIF_BACKEND_NUM_SUBORDINATES_NO_SUCH_ENTRY
          .get(baseDN));
    }
    final int baseDNIfExists = childDNs.containsKey(baseDN) ? 1 : 0;
    return getNumberOfSubordinates(baseDN, true) + baseDNIfExists;
  }

  private long getNumberOfSubordinates(DN entryDN, boolean includeSubtree) throws DirectoryException
  {
    backendLock.readLock().lock();

    try
    {
      Set<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;
        }
        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_LDIF_BACKEND_NUM_SUBORDINATES_NO_SUCH_ENTRY
            .get(entryDN));
      }

      if (!includeSubtree)
      {
        return childDNSet.size();
      }

      long count = 0;
      for (DN childDN : childDNSet)
      {
        count += getNumberOfSubordinates(childDN, true);
        count++;
      }
      return count;
    }
    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.getName();
      if (entryMap.containsKey(entryDN))
      {
        LocalizableMessage m = ERR_LDIF_BACKEND_ADD_ALREADY_EXISTS.get(entryDN);
        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));

          Set<DN> childDNSet = childDNs.get(parentDN);
          if (childDNSet == null)
          {
            childDNSet = new HashSet<>();
            childDNs.put(parentDN, childDNSet);
          }
          childDNSet.add(entryDN);
          writeLDIF();
          return;
        }
        else
        {
          DN matchedDN = null;
          if (parentDN != null)
          {
            while (true)
            {
              parentDN = parentDN.getParentDNInSuffix();
              if (parentDN == null)
              {
                break;
              }

              if (entryMap.containsKey(parentDN))
              {
                matchedDN = parentDN;
                break;
              }
            }
          }

          LocalizableMessage m = ERR_LDIF_BACKEND_ADD_MISSING_PARENT.get(entryDN);
          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();
        }

        LocalizableMessage m = ERR_LDIF_BACKEND_DELETE_NO_SUCH_ENTRY.get(entryDN);
        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).
      Set<DN> childDNSet = childDNs.get(entryDN);
      if (childDNSet == null || childDNSet.isEmpty())
      {
        entryMap.remove(entryDN);
        childDNs.remove(entryDN);

        if (parentDN != null)
        {
          Set<DN> parentChildren = childDNs.get(parentDN);
          if (parentChildren != null)
          {
            parentChildren.remove(entryDN);
            if (parentChildren.isEmpty())
            {
              childDNs.remove(parentDN);
            }
          }
        }
      }
      else
      {
        boolean subtreeDelete = deleteOperation != null
            && deleteOperation
                .getRequestControl(SubtreeDeleteControl.DECODER) != null;

        if (! subtreeDelete)
        {
          LocalizableMessage m = ERR_LDIF_BACKEND_DELETE_NONLEAF.get(entryDN);
          throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF, m);
        }

        entryMap.remove(entryDN);
        childDNs.remove(entryDN);

        if (parentDN != null)
        {
          Set<DN> parentChildren = childDNs.get(parentDN);
          if (parentChildren != null)
          {
            parentChildren.remove(entryDN);
            if (parentChildren.isEmpty())
            {
              childDNs.remove(parentDN);
            }
          }
        }

        for (DN childDN : childDNSet)
        {
          subtreeDelete(childDN);
        }
      }

      writeLDIF();
    }
    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);
    Set<DN> childDNSet = childDNs.remove(entryDN);
    if (childDNSet != null)
    {
      for (DN childDN : childDNSet)
      {
        subtreeDelete(childDN);
      }
    }
  }

  /** {@inheritDoc} */
  @Override
  public void replaceEntry(Entry oldEntry, Entry newEntry,
      ModifyOperation modifyOperation) throws DirectoryException
  {
    backendLock.writeLock().lock();

    try
    {
      // Make sure that the target entry exists.  If not, then fail.
      DN entryDN = newEntry.getName();
      if (! entryMap.containsKey(entryDN))
      {
        DN matchedDN = null;
        DN parentDN = entryDN.getParentDNInSuffix();
        while (parentDN != null)
        {
          if (entryMap.containsKey(parentDN))
          {
            matchedDN = parentDN;
            break;
          }

          parentDN = parentDN.getParentDNInSuffix();
        }

        LocalizableMessage m = ERR_LDIF_BACKEND_MODIFY_NO_SUCH_ENTRY.get(entryDN);
        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
      }

      entryMap.put(entryDN, newEntry.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.getName();
      if (! entryMap.containsKey(currentDN))
      {
        DN matchedDN = null;
        DN parentDN = currentDN.getParentDNInSuffix();
        while (parentDN != null)
        {
          if (entryMap.containsKey(parentDN))
          {
            matchedDN = parentDN;
            break;
          }

          parentDN = parentDN.getParentDNInSuffix();
        }

        LocalizableMessage m = ERR_LDIF_BACKEND_MODDN_NO_SUCH_SOURCE_ENTRY.get(currentDN);
        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
      }

      if (entryMap.containsKey(newDN))
      {
        LocalizableMessage m = ERR_LDIF_BACKEND_MODDN_TARGET_ENTRY_ALREADY_EXISTS.get(newDN);
        throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, m);
      }

      DN newParentDN = newDN.getParentDNInSuffix();
      if (! entryMap.containsKey(newParentDN))
      {
        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
            ERR_LDIF_BACKEND_MODDN_NEW_PARENT_DOESNT_EXIST.get(newParentDN));
      }

      // Remove the entry from the list of children for the old parent and
      // add the new entry DN to the set of children for the new parent.
      DN oldParentDN = currentDN.getParentDNInSuffix();
      Set<DN> parentChildDNs = childDNs.get(oldParentDN);
      if (parentChildDNs != null)
      {
        parentChildDNs.remove(currentDN);
        if (parentChildDNs.isEmpty()
            && modifyDNOperation.getNewSuperior() != null)
        {
          childDNs.remove(oldParentDN);
        }
      }

      parentChildDNs = childDNs.get(newParentDN);
      if (parentChildDNs == null)
      {
        parentChildDNs = new HashSet<>();
        childDNs.put(newParentDN, parentChildDNs);
      }
      parentChildDNs.add(newDN);


      // 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);
      entryMap.remove(currentDN);
      entryMap.put(newDN, entry.duplicate(false));
      if (childDNSet != null && !childDNSet.isEmpty())
      {
        for (DN childDN : childDNSet)
        {
          subtreeRename(childDN, newDN);
        }
      }
      writeLDIF();
    }
    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.rdn(), newParentDN);

    Entry oldEntry = entryMap.remove(entryDN);
    if (oldEntry == null)
    {
      // This should never happen.
      if (logger.isTraceEnabled())
      {
        logger.trace("Subtree rename encountered entry DN " +
                            entryDN + " for nonexistent entry.");
      }
      return;
    }

    Entry newEntry = oldEntry.duplicate(false);
    newEntry.setDN(newEntryDN);
    entryMap.put(newEntryDN, newEntry);

    Set<DN> parentChildren = childDNs.get(newParentDN);
    if (parentChildren == null)
    {
      parentChildren = new HashSet<>();
      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();
        }

        LocalizableMessage m = ERR_LDIF_BACKEND_SEARCH_NO_SUCH_BASE.get(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 Set<String> getSupportedControls()
  {
    return supportedControls;
  }

  /** {@inheritDoc} */
  @Override
  public Set<String> getSupportedFeatures()
  {
    return Collections.emptySet();
  }

  /** {@inheritDoc} */
  @Override
  public boolean supports(BackendOperation backendOperation)
  {
    switch (backendOperation)
    {
    case LDIF_EXPORT:
    case LDIF_IMPORT:
      return true;

    default:
      return false;
    }
  }

  /** {@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)
      {
        logger.traceException(e);

        LocalizableMessage 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.getName();
          ldifWriter.writeEntry(entry);
        }
      }
      catch (Exception e)
      {
        LocalizableMessage m = ERR_LDIF_BACKEND_CANNOT_WRITE_ENTRY_TO_LDIF.get(
            entryDN, stackTraceToSingleLineString(e));
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     m, e);
      }
      finally
      {
        StaticUtils.close(ldifWriter);
      }
    }
    finally
    {
      backendLock.readLock().unlock();
    }
  }

  /** {@inheritDoc} */
  @Override
  public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext serverContext)
      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)
      {
        LocalizableMessage 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())
            {
              LocalizableMessage 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.getName();
          if (entryMap.containsKey(entryDN))
          {
            LocalizableMessage m =
                ERR_LDIF_BACKEND_DUPLICATE_ENTRY.get(ldifFilePath, currentConfig.dn(), entryDN);
            logger.error(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)
          {
            LocalizableMessage m = ERR_LDIF_BACKEND_ENTRY_OUT_OF_SCOPE.get(
                ldifFilePath, currentConfig.dn(), entryDN);
            logger.error(m);
            reader.rejectLastEntry(m);
            continue;
          }

          DN parentDN = entryDN.getParentDNInSuffix();
          if (parentDN == null || !entryMap.containsKey(parentDN))
          {
            LocalizableMessage m = ERR_LDIF_BACKEND_MISSING_PARENT.get(
                ldifFilePath, currentConfig.dn(), entryDN);
            logger.error(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);

          Set<DN> childDNSet = childDNs.get(parentDN);
          if (childDNSet == null)
          {
            childDNSet = new HashSet<>();
            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)
      {
        LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_READING_LDIF.get(
                         stackTraceToSingleLineString(e));
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     m, e);
      }
      finally
      {
        StaticUtils.close(reader);
      }
    }
    finally
    {
      backendLock.writeLock().unlock();
    }
  }

  /** {@inheritDoc} */
  @Override
  public void createBackup(BackupConfig backupConfig)
         throws DirectoryException
  {
    LocalizableMessage 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
  {
    LocalizableMessage message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get();
    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
  }

  /** {@inheritDoc} */
  @Override
  public void restoreBackup(RestoreConfig restoreConfig)
         throws DirectoryException
  {
    LocalizableMessage message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get();
    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
  }

  /** {@inheritDoc} */
  @Override
  public void configureBackend(LDIFBackendCfg config, ServerContext serverContext) throws ConfigException
  {
    if (config != null)
    {
      currentConfig = config;
      currentConfig.addLDIFChangeListener(this);

      baseDNs = new DN[currentConfig.getBaseDN().size()];
      currentConfig.getBaseDN().toArray(baseDNs);
      if (baseDNs.length != 1)
      {
        throw new ConfigException(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(currentConfig.dn()));
      }

      baseDNSet = new HashSet<>();
      Collections.addAll(baseDNSet, baseDNs);

      ldifFilePath = currentConfig.getLDIFFile();
    }
  }

  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationChangeAcceptable(LDIFBackendCfg configuration,
                      List<LocalizableMessage> unacceptableReasons)
  {
    boolean configAcceptable = true;

    // Make sure that there is only a single base DN.
    if (configuration.getBaseDN().size() != 1)
    {
      unacceptableReasons.add(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(configuration.dn()));
      configAcceptable = false;
    }

    return configAcceptable;
  }

  /** {@inheritDoc} */
  @Override
  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.
    final ConfigChangeResult ccr = new ConfigChangeResult();

    if (ldifFilePath != null)
    {
      File currentLDIF = getFileForPath(ldifFilePath);
      File newLDIF     = getFileForPath(configuration.getLDIFFile());
      if (! currentLDIF.equals(newLDIF))
      {
        ccr.addMessage(INFO_LDIF_BACKEND_LDIF_FILE_CHANGED.get());
        ccr.setAdminActionRequired(true);
      }
    }

    if (baseDNSet != null && !baseDNSet.equals(configuration.getBaseDN()))
    {
      ccr.addMessage(INFO_LDIF_BACKEND_BASE_DN_CHANGED.get());
      ccr.setAdminActionRequired(true);
    }

    currentConfig = configuration;
    return ccr;
  }

  /** {@inheritDoc} */
  @Override
  public DN getComponentEntryDN()
  {
    return currentConfig.dn();
  }

  /** {@inheritDoc} */
  @Override
  public String getClassName()
  {
    return LDIFBackend.class.getName();
  }

  /** {@inheritDoc} */
  @Override
  public Map<String,String> getAlerts()
  {
    Map<String,String> alerts = new LinkedHashMap<>();
    alerts.put(ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE,
               ALERT_DESCRIPTION_LDIF_BACKEND_CANNOT_WRITE_UPDATE);
    return alerts;
  }
}
