/*
 * 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 2014-2015 ForgeRock AS
 */
package org.opends.server.core;

import static org.forgerock.util.Utils.*;
import static org.opends.messages.ConfigMessages.*;
import static org.opends.server.config.ConfigConstants.*;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.adapter.server3x.Converters;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.config.server.spi.ConfigAddListener;
import org.forgerock.opendj.config.server.spi.ConfigChangeListener;
import org.forgerock.opendj.config.server.spi.ConfigDeleteListener;
import org.forgerock.opendj.config.server.spi.ConfigurationRepository;
import org.forgerock.opendj.ldap.CancelRequestListener;
import org.forgerock.opendj.ldap.CancelledResultException;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.MemoryBackend;
import org.forgerock.opendj.ldap.RequestContext;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.LdapResultHandler;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.ldap.responses.SearchResultReference;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldap.schema.SchemaBuilder;
import org.forgerock.opendj.ldif.EntryReader;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.forgerock.util.Utils;
import org.opends.server.types.DirectoryEnvironmentConfig;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;

/**
 * Responsible for managing configuration entries and listeners on these
 * entries.
 */
public class ConfigurationHandler implements ConfigurationRepository
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  private static final String CONFIGURATION_FILE_NAME = "02-config.ldif";

  private final ServerContext serverContext;

  /** The complete path to the configuration file to use. */
  private File configFile;

  /** Indicates whether to start using the last known good configuration. */
  private boolean useLastKnownGoodConfig;

  /** Backend containing the configuration entries. */
  private MemoryBackend backend;

  /** The config root entry. */
  private Entry rootEntry;

  /** The add/delete/change listeners on configuration entries. */
  private final ConcurrentHashMap<DN, EntryListeners> listeners = new ConcurrentHashMap<>();

  /** Schema with configuration-related elements. */
  private Schema configEnabledSchema;

  /**
   * Creates a new instance.
   *
   * @param serverContext
   *          The server context.
   */
  public ConfigurationHandler(final ServerContext serverContext)
  {
    this.serverContext = serverContext;
  }

  /**
   * Initialize the configuration.
   *
   * @throws InitializationException
   *            If an error occurs during the initialization.
   */
  public void initialize() throws InitializationException
  {
    final DirectoryEnvironmentConfig environment = serverContext.getEnvironment();
    useLastKnownGoodConfig = environment.useLastKnownGoodConfiguration();
    configFile = findConfigFileToUse(environment.getConfigFile());

    configEnabledSchema = loadConfigEnabledSchema();
    loadConfiguration(configFile, configEnabledSchema);
  }

  /** Holds add, change and delete listeners for a given configuration entry. */
  private static class EntryListeners {

    /** The set of add listeners that have been registered with this entry. */
    private final CopyOnWriteArrayList<ConfigAddListener> addListeners = new CopyOnWriteArrayList<>();
    /** The set of change listeners that have been registered with this entry. */
    private final CopyOnWriteArrayList<ConfigChangeListener> changeListeners = new CopyOnWriteArrayList<>();
    /** The set of delete listeners that have been registered with this entry. */
    private final CopyOnWriteArrayList<ConfigDeleteListener> deleteListeners = new CopyOnWriteArrayList<>();

    CopyOnWriteArrayList<ConfigChangeListener> getChangeListeners()
    {
      return changeListeners;
    }

    void registerChangeListener(final ConfigChangeListener listener)
    {
      changeListeners.add(listener);
    }

    boolean deregisterChangeListener(final ConfigChangeListener listener)
    {
      return changeListeners.remove(listener);
    }

    CopyOnWriteArrayList<ConfigAddListener> getAddListeners()
    {
      return addListeners;
    }

    void registerAddListener(final ConfigAddListener listener)
    {
      addListeners.addIfAbsent(listener);
    }

    void deregisterAddListener(final ConfigAddListener listener)
    {
      addListeners.remove(listener);
    }

    CopyOnWriteArrayList<ConfigDeleteListener> getDeleteListeners()
    {
      return deleteListeners;
    }

    void registerDeleteListener(final ConfigDeleteListener listener)
    {
      deleteListeners.addIfAbsent(listener);
    }

    void deregisterDeleteListener(final ConfigDeleteListener listener)
    {
      deleteListeners.remove(listener);
    }

  }

  /** Request context to be used when requesting the internal backend. */
  private static final RequestContext UNCANCELLABLE_REQUEST_CONTEXT =
      new RequestContext()
      {
        /** {@inheritDoc} */
        @Override
        public void removeCancelRequestListener(final CancelRequestListener listener)
        {
          // nothing to do
        }

        /** {@inheritDoc} */
        @Override
        public int getMessageID()
        {
          return -1;
        }

        /** {@inheritDoc} */
        @Override
        public void checkIfCancelled(final boolean signalTooLate)
            throws CancelledResultException
        {
          // nothing to do
        }

        /** {@inheritDoc} */
        @Override
        public void addCancelRequestListener(final CancelRequestListener listener)
        {
          // nothing to do

        }
      };

  /** Handler for search results.  */
  private static final class ConfigSearchHandler implements SearchResultHandler
  {
    private final Set<Entry> entries = new HashSet<>();

    Set<Entry> getEntries()
    {
      return entries;
    }

    /** {@inheritDoc} */
    @Override
    public boolean handleReference(SearchResultReference reference)
    {
      throw new UnsupportedOperationException("Search references are not supported for configuration entries.");
    }

    /** {@inheritDoc} */
    @Override
    public boolean handleEntry(SearchResultEntry entry)
    {
      entries.add(entry);
      return true;
    }
  }

  /** Handler for LDAP operations. */
  private static final class ConfigResultHandler implements LdapResultHandler<Result> {

    private LdapException resultError;

    LdapException getResultError()
    {
      return resultError;
    }

    boolean hasCompletedSuccessfully() {
      return resultError == null;
    }

    /** {@inheritDoc} */
    @Override
    public void handleResult(Result result)
    {
      // nothing to do
    }

    /** {@inheritDoc} */
    @Override
    public void handleException(LdapException exception)
    {
      resultError = exception;
    }
  }

  /**
   * Returns the configuration root entry.
   *
   * @return the root entry
   */
  public Entry getRootEntry() {
    return rootEntry;
  }

  /** {@inheritDoc} */
  @Override
  public Entry getEntry(final DN dn) throws ConfigException {
    Entry entry = backend.get(dn);
    if (entry == null)
    {
      // TODO : fix message
      LocalizableMessage message = LocalizableMessage.raw("Unable to retrieve the configuration entry %s", dn);
      throw new ConfigException(message);
    }
    return entry;
  }

  /** {@inheritDoc} */
  @Override
  public boolean hasEntry(final DN dn) throws ConfigException {
    return backend.get(dn) != null;
  }

  /** {@inheritDoc} */
  @Override
  public Set<DN> getChildren(DN dn) throws ConfigException {
    final ConfigResultHandler resultHandler = new ConfigResultHandler();
    final ConfigSearchHandler searchHandler = new ConfigSearchHandler();

    backend.handleSearch(
        UNCANCELLABLE_REQUEST_CONTEXT,
        Requests.newSearchRequest(dn, SearchScope.SINGLE_LEVEL, Filter.objectClassPresent()),
        null, searchHandler, resultHandler);

    if (resultHandler.hasCompletedSuccessfully())
    {
      final Set<DN> children = new HashSet<>();
      for (final Entry entry : searchHandler.getEntries())
      {
        children.add(entry.getName());
      }
      return children;
    }
    else {
      // TODO : fix message
      throw new ConfigException(
          LocalizableMessage.raw("Unable to retrieve children of configuration entry : %s", dn),
          resultHandler.getResultError());
    }
  }

  /**
   * Retrieves the number of subordinates for the requested entry.
   *
   * @param entryDN
   *          The distinguished name of the entry.
   * @param subtree
   *          {@code true} to include all entries from the requested entry
   *          to the lowest level in the tree or {@code false} to only
   *          include the entries immediately below the requested entry.
   * @return The number of subordinate entries
   * @throws ConfigException
   *           If a problem occurs while trying to retrieve the entry.
   */
  public long numSubordinates(final DN entryDN, final boolean subtree) throws ConfigException
  {
    final ConfigResultHandler resultHandler = new ConfigResultHandler();
    final ConfigSearchHandler searchHandler = new ConfigSearchHandler();
    final SearchScope scope = subtree ? SearchScope.SUBORDINATES : SearchScope.SINGLE_LEVEL;
    backend.handleSearch(
        UNCANCELLABLE_REQUEST_CONTEXT,
        Requests.newSearchRequest(entryDN, scope, Filter.objectClassPresent()),
        null, searchHandler, resultHandler);

    if (resultHandler.hasCompletedSuccessfully())
    {
      return searchHandler.getEntries().size();
    }
    else {
      // TODO : fix the message
      throw new ConfigException(
          LocalizableMessage.raw("Unable to retrieve children of configuration entry : %s", entryDN),
          resultHandler.getResultError());
    }
  }

  /**
   * Add a configuration entry
   * <p>
   * The add is performed only if all Add listeners on the parent entry accept
   * the changes. Once the change is accepted, entry is effectively added and
   * all Add listeners are called again to apply the change resulting from this
   * new entry.
   *
   * @param entry
   *          The configuration entry to add.
   * @throws DirectoryException
   *           If an error occurs.
   */
  public void addEntry(final Entry entry) throws DirectoryException
  {
    final DN entryDN = entry.getName();
    if (backend.contains(entryDN))
    {
      throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, ERR_CONFIG_FILE_ADD_ALREADY_EXISTS.get(entryDN));
    }

    final DN parentDN = retrieveParentDN(entryDN);

    // Iterate through add listeners to make sure the new entry is acceptable.
    final List<ConfigAddListener> addListeners = getAddListeners(parentDN);
    final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
    for (final ConfigAddListener listener : addListeners)
    {
      if (!listener.configAddIsAcceptable(entry, unacceptableReason))
      {
        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
            ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get(entryDN, parentDN, unacceptableReason));
      }
    }

    // Add the entry.
    final ConfigResultHandler resultHandler = new ConfigResultHandler();
    backend.handleAdd(UNCANCELLABLE_REQUEST_CONTEXT, Requests.newAddRequest(entry), null, resultHandler);

    if (!resultHandler.hasCompletedSuccessfully()) {
      // TODO fix the message : error when adding config entry
      // use resultHandler.getResultError() to get the error
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
          ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get(entryDN, parentDN, unacceptableReason));
    }

    // Notify all the add listeners to apply the new configuration entry.
    ResultCode resultCode = ResultCode.SUCCESS;
    final List<LocalizableMessage> messages = new LinkedList<>();
    for (final ConfigAddListener listener : addListeners)
    {
      final ConfigChangeResult result = listener.applyConfigurationAdd(entry);
      if (result.getResultCode() != ResultCode.SUCCESS)
      {
        resultCode = resultCode == ResultCode.SUCCESS ? result.getResultCode() : resultCode;
        messages.addAll(result.getMessages());
      }

      handleConfigChangeResult(result, entry.getName(), listener.getClass().getName(), "applyConfigurationAdd");
    }

    if (resultCode != ResultCode.SUCCESS)
    {
      final String reasons = Utils.joinAsString(".  ", messages);
      throw new DirectoryException(resultCode, ERR_CONFIG_FILE_ADD_APPLY_FAILED.get(reasons));
    }
  }

  /**
   * Delete a configuration entry.
   * <p>
   * The delete is performed only if all Delete listeners on the parent entry
   * accept the changes. Once the change is accepted, entry is effectively
   * deleted and all Delete listeners are called again to apply the change
   * resulting from this deletion.
   *
   * @param dn
   *          DN of entry to delete.
   * @throws DirectoryException
   *           If a problem occurs.
   */
  public void deleteEntry(final DN dn) throws DirectoryException
  {
    // Entry must exist.
    if (!backend.contains(dn))
    {
      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
          ERR_CONFIG_FILE_DELETE_NO_SUCH_ENTRY.get(dn), Converters.to(getMatchedDN(dn)), null);
    }

    // Entry must not have children.
    try
    {
      if (!getChildren(dn).isEmpty())
      {
        throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF,
            ERR_CONFIG_FILE_DELETE_HAS_CHILDREN.get(dn));
      }
    }
    catch (ConfigException e)
    {
      // TODO : fix message = ERROR BACKEND CONFIG
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
          ERR_CONFIG_FILE_DELETE_HAS_CHILDREN.get(dn), e);
    }

    // TODO : pass in the localizable message (2)
    final DN parentDN = retrieveParentDN(dn);

    // Iterate through delete listeners to make sure the deletion is acceptable.
    final List<ConfigDeleteListener> deleteListeners = getDeleteListeners(parentDN);
    final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
    final Entry entry = backend.get(dn);
    for (final ConfigDeleteListener listener : deleteListeners)
    {
      if (!listener.configDeleteIsAcceptable(entry, unacceptableReason))
      {
        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
            ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get(entry, parentDN, unacceptableReason));
      }
    }

    // Delete the entry
    final ConfigResultHandler resultHandler = new ConfigResultHandler();
    backend.handleDelete(UNCANCELLABLE_REQUEST_CONTEXT, Requests.newDeleteRequest(dn), null, resultHandler);

    if (!resultHandler.hasCompletedSuccessfully()) {
      // TODO fix message : error when deleting config entry
      // use resultHandler.getResultError() to get the error
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
          ERR_CONFIG_FILE_DELETE_REJECTED.get(dn, parentDN, unacceptableReason));
    }

    // Notify all the delete listeners that the entry has been removed.
    ResultCode resultCode = ResultCode.SUCCESS;
    final List<LocalizableMessage> messages = new LinkedList<>();
    for (final ConfigDeleteListener listener : deleteListeners)
    {
      final ConfigChangeResult result = listener.applyConfigurationDelete(entry);
      if (result.getResultCode() != ResultCode.SUCCESS)
      {
        resultCode = resultCode == ResultCode.SUCCESS ? result.getResultCode() : resultCode;
        messages.addAll(result.getMessages());
      }

      handleConfigChangeResult(result, dn, listener.getClass().getName(), "applyConfigurationDelete");
    }

    if (resultCode != ResultCode.SUCCESS)
    {
      final String reasons = Utils.joinAsString(".  ", messages);
      throw new DirectoryException(resultCode, ERR_CONFIG_FILE_DELETE_APPLY_FAILED.get(reasons));
    }
  }

  /**
   * Replaces the old configuration entry with the new configuration entry
   * provided.
   * <p>
   * The replacement is performed only if all Change listeners on the entry
   * accept the changes. Once the change is accepted, entry is effectively
   * replaced and all Change listeners are called again to apply the change
   * resulting from the replacement.
   *
   * @param oldEntry
   *          The original entry that is being replaced.
   * @param newEntry
   *          The new entry to use in place of the existing entry with the same
   *          DN.
   * @throws DirectoryException
   *           If a problem occurs while trying to replace the entry.
   */
  public void replaceEntry(final Entry oldEntry, final Entry newEntry)
      throws DirectoryException
  {
    final DN entryDN = oldEntry.getName();
    if (!backend.contains(entryDN))
    {
      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
          ERR_CONFIG_FILE_MODIFY_NO_SUCH_ENTRY.get(oldEntry), Converters.to(getMatchedDN(entryDN)), null);
    }

    //TODO : add objectclass and attribute to the config schema in order to get this code run
//    if (!Entries.getStructuralObjectClass(oldEntry, configEnabledSchema)
//        .equals(Entries.getStructuralObjectClass(newEntry, configEnabledSchema)))
//    {
//      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
//          ERR_CONFIG_FILE_MODIFY_STRUCTURAL_CHANGE_NOT_ALLOWED.get(entryDN));
//    }

    // Iterate through change listeners to make sure the change is acceptable.
    final List<ConfigChangeListener> changeListeners = getChangeListeners(entryDN);
    final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
    for (ConfigChangeListener listeners : changeListeners)
    {
      if (!listeners.configChangeIsAcceptable(newEntry, unacceptableReason))
      {
        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
            ERR_CONFIG_FILE_MODIFY_REJECTED_BY_CHANGE_LISTENER.get(entryDN, unacceptableReason));
      }
    }

    // Replace the old entry with new entry.
    final ConfigResultHandler resultHandler = new ConfigResultHandler();
    backend.handleModify(
        UNCANCELLABLE_REQUEST_CONTEXT,
        Requests.newModifyRequest(oldEntry, newEntry),
        null,
        resultHandler);

    if (!resultHandler.hasCompletedSuccessfully())
    {
      // TODO fix message : error when replacing config entry
      // use resultHandler.getResultError() to get the error
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
          ERR_CONFIG_FILE_DELETE_REJECTED.get(entryDN, entryDN, unacceptableReason));
    }

    // Notify all the change listeners of the update.
    ResultCode resultCode = ResultCode.SUCCESS;
    final List<LocalizableMessage> messages = new LinkedList<>();
    for (final ConfigChangeListener listener : changeListeners)
    {
      final ConfigChangeResult result = listener.applyConfigurationChange(newEntry);
      if (result.getResultCode() != ResultCode.SUCCESS)
      {
        resultCode = resultCode == ResultCode.SUCCESS ? result.getResultCode() : resultCode;
        messages.addAll(result.getMessages());
      }

      handleConfigChangeResult(result, entryDN, listener.getClass().getName(), "applyConfigurationChange");
    }

    if (resultCode != ResultCode.SUCCESS)
    {
      throw new DirectoryException(resultCode,
          ERR_CONFIG_FILE_MODIFY_APPLY_FAILED.get(Utils.joinAsString(".  ", messages)));
    }
  }

  /** {@inheritDoc} */
  @Override
  public void registerAddListener(final DN dn, final ConfigAddListener listener)
  {
    getEntryListeners(dn).registerAddListener(listener);
  }

  /** {@inheritDoc} */
  @Override
  public void registerDeleteListener(final DN dn, final ConfigDeleteListener listener)
  {
    getEntryListeners(dn).registerDeleteListener(listener);
  }

  /** {@inheritDoc} */
  @Override
  public void registerChangeListener(final DN dn, final ConfigChangeListener listener)
  {
    getEntryListeners(dn).registerChangeListener(listener);
  }

  /** {@inheritDoc} */
  @Override
  public void deregisterAddListener(final DN dn, final ConfigAddListener listener)
  {
    getEntryListeners(dn).deregisterAddListener(listener);
  }

  /** {@inheritDoc} */
  @Override
  public void deregisterDeleteListener(final DN dn, final ConfigDeleteListener listener)
  {
    getEntryListeners(dn).deregisterDeleteListener(listener);
  }

  /** {@inheritDoc} */
  @Override
  public boolean deregisterChangeListener(final DN dn, final ConfigChangeListener listener)
  {
    return getEntryListeners(dn).deregisterChangeListener(listener);
  }

  /** {@inheritDoc} */
  @Override
  public List<ConfigAddListener> getAddListeners(final DN dn)
  {
    return getEntryListeners(dn).getAddListeners();
  }

  /** {@inheritDoc} */
  @Override
  public List<ConfigDeleteListener> getDeleteListeners(final DN dn)
  {
    return getEntryListeners(dn).getDeleteListeners();
  }

  /** {@inheritDoc} */
  @Override
  public List<ConfigChangeListener> getChangeListeners(final DN dn)
  {
    return getEntryListeners(dn).getChangeListeners();
  }

  /** Load the configuration-enabled schema that will allow to read configuration file. */
  private Schema loadConfigEnabledSchema() throws InitializationException {
    LDIFEntryReader reader = null;
    try
    {
      final File schemaDir = serverContext.getEnvironment().getSchemaDirectory();
      reader = new LDIFEntryReader(new FileReader(new File(schemaDir, CONFIGURATION_FILE_NAME)));
      reader.setSchema(Schema.getDefaultSchema());
      final Entry entry = reader.readEntry();
      return new SchemaBuilder(Schema.getDefaultSchema()).addSchema(entry, false).toSchema();
    }
    catch (Exception e)
    {
      // TODO : fix message
      throw new InitializationException(LocalizableMessage.raw("Unable to load config-enabled schema"), e);
    }
    finally {
      closeSilently(reader);
    }
  }

  /**
   * Read configuration entries from provided configuration file.
   *
   * @param configFile
   *            LDIF file with configuration entries.
   * @param schema
   *          Schema to validate entries when reading the config file.
   * @throws InitializationException
   *            If an errors occurs.
   */
  private void loadConfiguration(final File configFile, final Schema schema)
      throws InitializationException
  {
    EntryReader reader = null;
    try
    {
      reader = getLDIFReader(configFile, schema);
      backend = new MemoryBackend(schema, reader);
    }
    catch (IOException e)
    {
      throw new InitializationException(
          ERR_CONFIG_FILE_GENERIC_ERROR.get(configFile.getAbsolutePath(), e.getCause()), e);
    }
    finally
    {
      closeSilently(reader);
    }

    // Check that root entry is the expected one
    rootEntry = backend.get(DN_CONFIG_ROOT);
    if (rootEntry == null)
    {
      // fix message : we didn't find the expected root in the file
      throw new InitializationException(ERR_CONFIG_FILE_INVALID_BASE_DN.get(
          configFile.getAbsolutePath(), "", DN_CONFIG_ROOT));
    }
  }

  /**
   * Returns the LDIF reader on configuration entries.
   * <p>
   * It is the responsability of the caller to ensure that reader
   * is closed after usage.
   *
   * @param configFile
   *          LDIF file containing the configuration entries.
   * @param schema
   *          Schema to validate entries when reading the config file.
   * @return the LDIF reader
   * @throws InitializationException
   *           If an error occurs.
   */
  private EntryReader getLDIFReader(final File configFile, final Schema schema)
      throws InitializationException
  {
    LDIFEntryReader reader = null;
    try
    {
      reader = new LDIFEntryReader(new FileReader(configFile));
      reader.setSchema(schema);
    }
    catch (Exception e)
    {
      throw new InitializationException(
          ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get(configFile.getAbsolutePath(), e), e);
    }
    return reader;
  }

  /**
   * Returns the entry listeners attached to the provided DN.
   * <p>
   * If no listener exist for the provided DN, then a new set of empty listeners
   * is created and returned.
   *
   * @param dn
   *          DN of a configuration entry.
   * @return the listeners attached to the corresponding configuration entry.
   */
  private EntryListeners getEntryListeners(final DN dn) {
    EntryListeners entryListeners  = listeners.get(dn);
    if (entryListeners == null) {
      entryListeners = new EntryListeners();
      final EntryListeners previousListeners = listeners.putIfAbsent(dn, entryListeners);
      if (previousListeners != null) {
        entryListeners = previousListeners;
      }
    }
    return entryListeners;
  }

  /**
   * Returns the parent DN of the configuration entry corresponding to the
   * provided DN.
   *
   * @param entryDN
   *          DN of entry to retrieve the parent from.
   * @return the parent DN
   * @throws DirectoryException
   *           If entry has no parent or parent entry does not exist.
   */
  private DN retrieveParentDN(final DN entryDN) throws DirectoryException
  {
    final DN parentDN = entryDN.parent();
    // Entry must have a parent.
    if (parentDN == null)
    {
      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_ADD_NO_PARENT_DN.get(entryDN));
    }

    // Parent entry must exist.
    if (!backend.contains(parentDN))
    {
      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
          ERR_CONFIG_FILE_ADD_NO_PARENT.get(entryDN, parentDN), Converters.to(getMatchedDN(parentDN)), null);
    }
    return parentDN;
  }

  /**
   * Returns the matched DN that is available in the configuration for the
   * provided DN.
   */
  private DN getMatchedDN(final DN dn)
  {
    DN matchedDN = null;
    DN parentDN = dn.parent();
    while (parentDN != null)
    {
      if (backend.contains(parentDN))
      {
        matchedDN = parentDN;
        break;
      }
      parentDN = parentDN.parent();
    }
    return matchedDN;
  }

  /**
   * Find the actual configuration file to use to load configuration, given the
   * standard config file.
   *
   * @param standardConfigFile
   *          "Standard" configuration file provided.
   * @return the actual configuration file to use, which is either the standard
   *         config file provided or the config file corresponding to the last
   *         known good configuration
   * @throws InitializationException
   *           If a problem occurs.
   */
  private File findConfigFileToUse(final File standardConfigFile) throws InitializationException
  {
    File configFileToUse = null;
    if (useLastKnownGoodConfig)
    {
      configFileToUse = new File(standardConfigFile + ".startok");
      if (! configFileToUse.exists())
      {
        logger.warn(WARN_CONFIG_FILE_NO_STARTOK_FILE, configFileToUse.getAbsolutePath(), standardConfigFile);
        useLastKnownGoodConfig = false;
        configFileToUse = standardConfigFile;
      }
      else
      {
        logger.info(NOTE_CONFIG_FILE_USING_STARTOK_FILE, configFileToUse.getAbsolutePath(), standardConfigFile);
      }
    }
    else
    {
      configFileToUse = standardConfigFile;
    }

    try
    {
      if (! configFileToUse.exists())
      {
        throw new InitializationException(ERR_CONFIG_FILE_DOES_NOT_EXIST.get(configFileToUse.getAbsolutePath()));
      }
    }
    catch (Exception e)
    {
      throw new InitializationException(
          ERR_CONFIG_FILE_CANNOT_VERIFY_EXISTENCE.get(configFileToUse.getAbsolutePath(), e));
    }
    return configFileToUse;
  }

  /**
   * Examines the provided result and logs a message if appropriate. If the
   * result code is anything other than {@code SUCCESS}, then it will log an
   * error message. If the operation was successful but admin action is
   * required, then it will log a warning message. If no action is required but
   * messages were generated, then it will log an informational message.
   *
   * @param result
   *          The config change result object that
   * @param entryDN
   *          The DN of the entry that was added, deleted, or modified.
   * @param className
   *          The name of the class for the object that generated the provided
   *          result.
   * @param methodName
   *          The name of the method that generated the provided result.
   */
  private void handleConfigChangeResult(ConfigChangeResult result, DN entryDN, String className, String methodName)
  {
    if (result == null)
    {
      logger.error(ERR_CONFIG_CHANGE_NO_RESULT, className, methodName, entryDN);
      return;
    }

    final ResultCode resultCode = result.getResultCode();
    final boolean adminActionRequired = result.adminActionRequired();
    final List<LocalizableMessage> messages = result.getMessages();

    final String messageBuffer = Utils.joinAsString("  ", messages);
    if (resultCode != ResultCode.SUCCESS)
    {
      logger.error(ERR_CONFIG_CHANGE_RESULT_ERROR, className, methodName, entryDN, resultCode,
          adminActionRequired, messageBuffer);
    }
    else if (adminActionRequired)
    {
      logger.warn(WARN_CONFIG_CHANGE_RESULT_ACTION_REQUIRED, className, methodName, entryDN, messageBuffer);
    }
    else if (messageBuffer.length() > 0)
    {
      logger.debug(INFO_CONFIG_CHANGE_RESULT_MESSAGES, className, methodName, entryDN, messageBuffer);
    }
  }

}
