From 3f1ed4284d2b06b0bc85659c2017e40989b232db Mon Sep 17 00:00:00 2001
From: Nicolas Capponi <nicolas.capponi@forgerock.com>
Date: Mon, 04 Apr 2016 13:38:33 +0000
Subject: [PATCH] Add/Update ConfigurationBackend and ConfigurationHandler classes.

---
 opendj-config/src/main/java/org/forgerock/opendj/config/server/ConfigChangeResult.java      |   28 
 opendj-server-legacy/src/messages/org/opends/messages/config.properties                     |    6 
 opendj-server-legacy/src/test/java/org/opends/server/core/ConfigurationHandlerTestCase.java |   18 
 opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationHandler.java         | 1979 ++++++++++++++++++++++++++++++++++-------------
 opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationBackend.java         |  480 +++++++++++
 5 files changed, 1,946 insertions(+), 565 deletions(-)

diff --git a/opendj-config/src/main/java/org/forgerock/opendj/config/server/ConfigChangeResult.java b/opendj-config/src/main/java/org/forgerock/opendj/config/server/ConfigChangeResult.java
index e13e998..9dd19c4 100644
--- a/opendj-config/src/main/java/org/forgerock/opendj/config/server/ConfigChangeResult.java
+++ b/opendj-config/src/main/java/org/forgerock/opendj/config/server/ConfigChangeResult.java
@@ -12,16 +12,16 @@
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
  * Copyright 2006-2008 Sun Microsystems, Inc.
- * Portions copyright 2015 ForgeRock AS.
+ * Portions copyright 2015-2016 ForgeRock AS.
  */
 package org.forgerock.opendj.config.server;
 
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.util.Utils;
 
 /**
  * This class defines a data structure that can be used to hold information
@@ -85,6 +85,19 @@
     }
 
     /**
+     * Aggregates the results from the provided config change result.
+     *
+     * @param other
+     *          The config change result to aggregate
+     */
+    public void aggregate(ConfigChangeResult other) {
+        if (other.getResultCode() != ResultCode.SUCCESS) {
+            setResultCodeIfSuccess(other.getResultCode());
+            messages.addAll(other.getMessages());
+        }
+    }
+
+    /**
      * Indicates whether administrative action is required before one or more of
      * the changes will take effect.
      *
@@ -156,16 +169,7 @@
         buffer.append(", adminActionRequired=");
         buffer.append(adminActionRequired);
         buffer.append(", messages={");
-
-        if (!messages.isEmpty()) {
-            final Iterator<LocalizableMessage> iterator = messages.iterator();
-            buffer.append(iterator.next());
-            while (iterator.hasNext()) {
-                buffer.append(",");
-                buffer.append(iterator.next());
-            }
-        }
-
+        Utils.joinAsString(buffer, ",", messages);
         buffer.append("})");
     }
 }
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationBackend.java b/opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationBackend.java
new file mode 100644
index 0000000..b503864
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationBackend.java
@@ -0,0 +1,480 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.opends.server.core;
+
+import static org.opends.messages.ConfigMessages.*;
+import static org.opends.server.config.ConfigConstants.ATTR_DEFAULT_ROOT_PRIVILEGE_NAME;
+import static org.opends.server.config.ConfigConstants.CONFIG_ARCHIVE_DIR_NAME;
+import static org.opends.server.util.StaticUtils.getExceptionMessage;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.adapter.server3x.Converters;
+import org.forgerock.opendj.config.server.ConfigException;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.opends.server.admin.std.server.ConfigFileHandlerBackendCfg;
+import org.opends.server.api.Backend;
+import org.opends.server.api.Backupable;
+import org.opends.server.api.ClientConnection;
+import org.opends.server.types.BackupConfig;
+import org.opends.server.types.BackupDirectory;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.Entry;
+import org.opends.server.types.IndexType;
+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.Modification;
+import org.opends.server.types.Privilege;
+import org.opends.server.types.RestoreConfig;
+import org.opends.server.util.BackupManager;
+import org.opends.server.util.StaticUtils;
+
+/** Back-end responsible for management of configuration entries. */
+public class ConfigurationBackend extends Backend<ConfigFileHandlerBackendCfg> implements Backupable
+{
+  /**
+   * The backend ID for the configuration backend.
+   * <p>
+   * Try to avoid potential conflict with user backend identifiers.
+   */
+  public static final String CONFIG_BACKEND_ID = "__config.ldif__";
+
+  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+  /** The set of supported control OIDs for this backend. */
+  private static final Set<String> SUPPORTED_CONTROLS = new HashSet<>(0);
+  /** The set of supported feature OIDs for this backend. */
+  private static final Set<String> SUPPORTED_FEATURES = new HashSet<>(0);
+
+  /** The privilege array containing both the CONFIG_READ and CONFIG_WRITE privileges. */
+  private static final Privilege[] CONFIG_READ_AND_WRITE =
+  {
+    Privilege.CONFIG_READ,
+    Privilege.CONFIG_WRITE
+  };
+
+  /** Handles the configuration entries and their storage in files. */
+  private final ConfigurationHandler configurationHandler;
+
+  /** The reference to the configuration root entry. */
+  private final Entry configRootEntry;
+
+  /** The set of base DNs for this config handler backend. */
+  private DN[] baseDNs;
+
+  /**
+   * The write lock used to ensure that only one thread can apply a
+   * configuration update at any given time.
+   */
+  private final Object configLock = new Object();
+
+  /**
+   * Creates and initializes a new instance of this backend.
+   *
+   * @param serverContext
+   *            The server context.
+   * @param configurationHandler
+   *            Contains the configuration entries.
+   * @throws InitializationException
+   *            If an errors occurs.
+   */
+  public ConfigurationBackend(ServerContext serverContext, ConfigurationHandler configurationHandler)
+      throws InitializationException
+  {
+    this.configurationHandler = configurationHandler;
+    this.configRootEntry = Converters.to(configurationHandler.getRootEntry());
+    baseDNs = new DN[] { configRootEntry.getName() };
+
+    setBackendID(CONFIG_BACKEND_ID);
+  }
+
+  @Override
+  public void closeBackend()
+  {
+    try
+    {
+      DirectoryServer.deregisterBaseDN(configRootEntry.getName());
+    }
+    catch (Exception e)
+    {
+      logger.traceException(e, "Error when deregistering base DN: " + configRootEntry.getName());
+    }
+  }
+
+  @Override
+  public void configureBackend(ConfigFileHandlerBackendCfg cfg,  ServerContext serverContext) throws ConfigException
+  {
+    // No action is required.
+  }
+
+  @Override
+  public void openBackend() throws InitializationException
+  {
+    DN baseDN = configRootEntry.getName();
+    try
+    {
+      DirectoryServer.registerBaseDN(baseDN, this, true);
+    }
+    catch (DirectoryException e)
+    {
+      logger.traceException(e);
+      throw new InitializationException(
+          ERR_CONFIG_CANNOT_REGISTER_AS_PRIVATE_SUFFIX.get(baseDN, getExceptionMessage(e)), e);
+    }
+  }
+
+  @Override
+  public DN[] getBaseDNs()
+  {
+    return baseDNs;
+  }
+
+  @Override
+  public Entry getEntry(DN entryDN)
+  {
+    try
+    {
+      org.forgerock.opendj.ldap.Entry entry = configurationHandler.getEntry(entryDN);
+      if (entry != null)
+      {
+        Entry serverEntry = Converters.to(entry);
+        serverEntry.processVirtualAttributes();
+        return serverEntry;
+      }
+    }
+    catch (ConfigException e)
+    {
+      // should never happen
+    }
+    return null;
+  }
+
+  @Override
+  public long getEntryCount()
+  {
+    try
+    {
+      return getNumberOfEntriesInBaseDN(configRootEntry.getName());
+    }
+    catch (DirectoryException e)
+    {
+      logger.traceException(e, "Unable to count entries of configuration backend");
+      return -1;
+    }
+  }
+
+  @Override
+  public File getDirectory()
+  {
+    return configurationHandler.getConfigurationFile().getParentFile();
+  }
+
+  @Override
+  public long getNumberOfChildren(DN parentDN) throws DirectoryException
+  {
+    try {
+      return configurationHandler.numSubordinates(parentDN, false);
+    }
+    catch (ConfigException e)
+    {
+      throw new DirectoryException(ResultCode.UNDEFINED, e.getMessageObject());
+    }
+  }
+
+  @Override
+  public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException
+  {
+    try
+    {
+      return configurationHandler.numSubordinates(baseDN, true) + 1;
+    }
+    catch (ConfigException e)
+    {
+      throw new DirectoryException(ResultCode.UNDEFINED, e.getMessageObject());
+    }
+  }
+
+  @Override
+  public Set<String> getSupportedControls()
+  {
+    return SUPPORTED_CONTROLS;
+  }
+
+  @Override
+  public Set<String> getSupportedFeatures()
+  {
+    return SUPPORTED_FEATURES;
+  }
+
+  @Override
+  public ConditionResult hasSubordinates(DN entryDN) throws DirectoryException
+  {
+    long ret = getNumberOfChildren(entryDN);
+    if(ret < 0)
+    {
+      return ConditionResult.UNDEFINED;
+    }
+    return ConditionResult.valueOf(ret != 0);
+  }
+
+  @Override
+  public boolean isIndexed(AttributeType attributeType, IndexType indexType)
+  {
+    // All searches in this backend will always be considered indexed.
+    return true;
+  }
+
+  @Override
+  public boolean entryExists(DN entryDN) throws DirectoryException
+  {
+    try
+    {
+      return configurationHandler.hasEntry(entryDN);
+    }
+    catch (ConfigException e)
+    {
+      throw new DirectoryException(ResultCode.UNDEFINED, e.getMessageObject(), e);
+    }
+  }
+
+  @Override
+  public boolean supports(BackendOperation backendOperation)
+  {
+    switch (backendOperation)
+    {
+    case BACKUP:
+    case RESTORE:
+    case LDIF_EXPORT:
+      return true;
+    default:
+      return false;
+    }
+  }
+
+  @Override
+  public void search(SearchOperation searchOperation) throws DirectoryException
+  {
+    // Make sure that the associated user has the CONFIG_READ privilege.
+    ClientConnection clientConnection = searchOperation.getClientConnection();
+    if (! clientConnection.hasPrivilege(Privilege.CONFIG_READ, searchOperation))
+    {
+      LocalizableMessage message = ERR_CONFIG_FILE_SEARCH_INSUFFICIENT_PRIVILEGES.get();
+      throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
+    }
+
+    configurationHandler.search(searchOperation);
+  }
+
+  @Override
+  public void addEntry(Entry entry, AddOperation addOperation)
+         throws DirectoryException
+  {
+    // Make sure that the associated user has
+    // both the CONFIG_READ and CONFIG_WRITE privileges.
+    if (addOperation != null)
+    {
+      ClientConnection clientConnection = addOperation.getClientConnection();
+      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, addOperation))
+      {
+        LocalizableMessage message = ERR_CONFIG_FILE_ADD_INSUFFICIENT_PRIVILEGES.get();
+        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
+      }
+    }
+
+    // Only one configuration update may be in progress at any given time.
+    synchronized (configLock)
+    {
+      configurationHandler.addEntry(Converters.from(copyWithoutVirtualAttributes(entry)));
+    }
+  }
+
+  private Entry copyWithoutVirtualAttributes(Entry entry) {
+    return entry.duplicate(false);
+  }
+
+  @Override
+  public void deleteEntry(DN entryDN, DeleteOperation deleteOperation)
+         throws DirectoryException
+  {
+    // Make sure that the associated user
+    // has both the CONFIG_READ and CONFIG_WRITE privileges.
+    if (deleteOperation != null)
+    {
+      ClientConnection clientConnection = deleteOperation.getClientConnection();
+      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, deleteOperation))
+      {
+        LocalizableMessage message = ERR_CONFIG_FILE_DELETE_INSUFFICIENT_PRIVILEGES.get();
+        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
+      }
+    }
+
+    // Only one configuration update may be in progress at any given time.
+    synchronized (configLock)
+    {
+      if (configRootEntry.getName().equals(entryDN))
+      {
+        LocalizableMessage message = ERR_CONFIG_FILE_DELETE_NO_PARENT.get(entryDN);
+        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
+      }
+      configurationHandler.deleteEntry(entryDN);
+    }
+  }
+
+  @Override
+  public void replaceEntry(Entry oldEntry, Entry newEntry, ModifyOperation modifyOperation) throws DirectoryException
+  {
+    // Make sure that the associated user has both the CONFIG_READ and CONFIG_WRITE privileges.
+    // Also, if the operation targets the set of root privileges
+    // then make sure the user has the PRIVILEGE_CHANGE privilege.
+    if (modifyOperation != null)
+    {
+      ClientConnection clientConnection = modifyOperation.getClientConnection();
+      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, modifyOperation))
+      {
+        LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_INSUFFICIENT_PRIVILEGES.get();
+        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
+      }
+
+      for (Modification m : modifyOperation.getModifications())
+      {
+        if (m.getAttribute().getAttributeDescription().getAttributeType().hasName(ATTR_DEFAULT_ROOT_PRIVILEGE_NAME))
+        {
+          if (!clientConnection.hasPrivilege(Privilege.PRIVILEGE_CHANGE, modifyOperation))
+          {
+            LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_PRIVS_INSUFFICIENT_PRIVILEGES.get();
+            throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
+          }
+
+          break;
+        }
+      }
+    }
+
+    // Only one configuration update may be in progress at any given time.
+    synchronized (configLock)
+    {
+      configurationHandler.replaceEntry(
+          Converters.from(copyWithoutVirtualAttributes(oldEntry)),
+          Converters.from(copyWithoutVirtualAttributes(newEntry)));
+    }
+  }
+
+  @Override
+  public void renameEntry(DN currentDN, Entry entry, ModifyDNOperation modifyDNOperation) throws DirectoryException
+  {
+    // Make sure that the associated
+    // user has both the CONFIG_READ and CONFIG_WRITE privileges.
+    if (modifyDNOperation != null)
+    {
+      ClientConnection clientConnection = modifyDNOperation.getClientConnection();
+      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, modifyDNOperation))
+      {
+        LocalizableMessage message = ERR_CONFIG_FILE_MODDN_INSUFFICIENT_PRIVILEGES.get();
+        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
+      }
+    }
+
+    // Modify DN operations will not be allowed in the configuration, so this
+    // will always throw an exception.
+    LocalizableMessage message = ERR_CONFIG_FILE_MODDN_NOT_ALLOWED.get();
+    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
+  }
+
+  @Override
+  public void exportLDIF(LDIFExportConfig exportConfig) throws DirectoryException
+  {
+    configurationHandler.writeLDIF(exportConfig);
+  }
+
+  @Override
+  public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext serverContext)
+         throws DirectoryException
+  {
+    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_CONFIG_FILE_UNWILLING_TO_IMPORT.get());
+  }
+
+  @Override
+  public void createBackup(BackupConfig backupConfig) throws DirectoryException
+  {
+    new BackupManager(getBackendID()).createBackup(this, backupConfig);
+  }
+
+  @Override
+  public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException
+  {
+    new BackupManager(getBackendID()).removeBackup(backupDirectory, backupID);
+  }
+
+  @Override
+  public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException
+  {
+    new BackupManager(getBackendID()).restoreBackup(this, restoreConfig);
+  }
+
+  @Override
+  public ListIterator<Path> getFilesToBackup()
+  {
+    final List<Path> files = new ArrayList<>();
+
+    File configFile = configurationHandler.getConfigurationFile();
+    files.add(configFile.toPath());
+
+    // the files in archive directory
+    File archiveDirectory = new File(getDirectory(), CONFIG_ARCHIVE_DIR_NAME);
+    if (archiveDirectory.exists())
+    {
+      for (File archiveFile : archiveDirectory.listFiles())
+      {
+        files.add(archiveFile.toPath());
+      }
+    }
+
+    return files.listIterator();
+  }
+
+  @Override
+  public boolean isDirectRestore()
+  {
+    return true;
+  }
+
+  @Override
+  public Path beforeRestore() throws DirectoryException
+  {
+    // save current config files to a save directory
+    return BackupManager.saveCurrentFilesToDirectory(this, getBackendID());
+  }
+
+  @Override
+  public void afterRestore(Path restoreDirectory, Path saveDirectory) throws DirectoryException
+  {
+    // restore was successful, delete the save directory
+    StaticUtils.recursiveDelete(saveDirectory.toFile());
+  }
+}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationHandler.java
index c89865d..9ded23e 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationHandler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationHandler.java
@@ -15,32 +15,62 @@
  */
 package org.opends.server.core;
 
-import static org.forgerock.util.Utils.*;
+import static org.opends.messages.ConfigMessages.ERR_CONFIG_FILE_ADD_NO_PARENT;
+import static org.opends.messages.ConfigMessages.ERR_CONFIG_FILE_ADD_NO_PARENT_DN;
 import static org.opends.messages.ConfigMessages.*;
 import static org.opends.server.config.ConfigConstants.*;
+import static org.opends.server.util.ServerConstants.ALERT_DESCRIPTION_CANNOT_WRITE_CONFIGURATION;
+import static org.opends.server.util.ServerConstants.ALERT_DESCRIPTION_MANUAL_CONFIG_EDIT_HANDLED;
+import static org.opends.server.util.ServerConstants.ALERT_DESCRIPTION_MANUAL_CONFIG_EDIT_LOST;
+import static org.opends.server.util.ServerConstants.ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED;
+import static org.opends.server.util.ServerConstants.ALERT_TYPE_MANUAL_CONFIG_EDIT_LOST;
+import static org.opends.server.util.ServerConstants.ALERT_TYPE_CANNOT_WRITE_CONFIGURATION;
+import static org.opends.server.util.StaticUtils.getExceptionMessage;
+import static org.opends.server.util.StaticUtils.renameFile;
+import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
+import static org.opends.server.extensions.ExtensionsConstants.MESSAGE_DIGEST_ALGORITHM_SHA_1;
 
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
 
 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.ConfigurationFramework;
 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.ByteString;
 import org.forgerock.opendj.ldap.CancelRequestListener;
 import org.forgerock.opendj.ldap.CancelledResultException;
 import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entries;
 import org.forgerock.opendj.ldap.Entry;
 import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.LdapException;
@@ -50,7 +80,9 @@
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.SearchResultHandler;
 import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
 import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
 import org.forgerock.opendj.ldap.responses.Result;
 import org.forgerock.opendj.ldap.responses.SearchResultEntry;
 import org.forgerock.opendj.ldap.responses.SearchResultReference;
@@ -58,29 +90,66 @@
 import org.forgerock.opendj.ldap.schema.SchemaBuilder;
 import org.forgerock.opendj.ldif.EntryReader;
 import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
 import org.forgerock.util.Utils;
+import org.forgerock.util.annotations.VisibleForTesting;
+import org.opends.server.api.AlertGenerator;
+import org.opends.server.schema.GeneralizedTimeSyntax;
+import org.opends.server.tools.LDIFModify;
 import org.opends.server.types.DirectoryEnvironmentConfig;
 import org.opends.server.types.DirectoryException;
+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.util.ActivateOnceSDKSchemaIsUsed;
+import org.opends.server.util.LDIFException;
+import org.opends.server.util.LDIFReader;
+import org.opends.server.util.LDIFWriter;
+import org.opends.server.util.TimeThread;
 
 /**
- * Responsible for managing configuration entries and listeners on these
- * entries.
+ * Responsible for managing configuration, including listeners on configuration entries.
+ * <p>
+ * Configuration is represented by configuration entries, persisted on the file system.
+ * Configuration entries are initially read from configuration file ("config/config.ldif" by default), then stored
+ * in a {@code MemoryBackend} during server uptime.
+ * <p>
+ * The handler allows to register and unregister some listeners on any configuration entry
+ * (add, change or delete listener).
+ * Configuration entries can be added, replaced or deleted to the handler.
+ * Any change of a configuration entry will trigger the listeners registered for this entry, and will also
+ * trigger an update of configuration file.
+ * <p>
+ * The handler also maintains an up-to-date archive of configuration files.
  */
-public class ConfigurationHandler implements ConfigurationRepository
+public class ConfigurationHandler implements ConfigurationRepository, AlertGenerator
 {
   private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
 
   private static final String CONFIGURATION_FILE_NAME = "02-config.ldif";
+  private static final String CLASS_NAME = ConfigurationHandler.class.getName();
 
   private final ServerContext serverContext;
 
-  /** The complete path to the configuration file to use. */
+  /** The complete path to the default configuration file. */
   private File configFile;
 
   /** Indicates whether to start using the last known good configuration. */
   private boolean useLastKnownGoodConfig;
 
+  /** Indicates whether to maintain a configuration archive. */
+  private boolean maintainConfigArchive;
+
+  /** The maximum config archive size to maintain. */
+  private int maxConfigArchiveSize;
+
+  /**
+   * A SHA-1 digest of the last known configuration. This should only be incorrect if the server
+   * configuration file has been manually edited with the server online, which is a bad thing.
+   */
+  private byte[] configurationDigest;
+
   /** Backend containing the configuration entries. */
   private MemoryBackend backend;
 
@@ -90,9 +159,6 @@
   /** 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.
    *
@@ -105,24 +171,831 @@
   }
 
   /**
-   * Initialize the configuration.
+   * Bootstraps the server configuration.
+   * <p>
+   * The returned ConfigurationHandler is initialized with a partial schema and must be later
+   * re-itinialized with the full schema by calling {@code reInitializeWithFullSchema()} method
+   * once the schema has been fully loaded.
    *
+   * @param serverContext
+   *            The server context.
+   * @param configClass
+   *            The actual configuration class to use.
+   * @return the configuration handler
    * @throws InitializationException
-   *            If an error occurs during the initialization.
+   *            If an error occurs during bootstrapping.
    */
-  public void initialize() throws InitializationException
+  public static ConfigurationHandler bootstrapConfiguration(ServerContext serverContext,
+      Class<ConfigurationHandler> configClass) throws InitializationException {
+    final ConfigurationFramework configFramework = ConfigurationFramework.getInstance();
+    try
+    {
+      if (!configFramework.isInitialized())
+      {
+        configFramework.initialize();
+      }
+    }
+    catch (ConfigException e)
+    {
+      // TODO : fix the message
+      throw new InitializationException(LocalizableMessage.raw("Cannot initialize configuration framework"), e);
+    }
+
+    final ConfigurationHandler configHandler = new ConfigurationHandler(serverContext);
+    configHandler.initializeWithPartialSchema();
+    return configHandler;
+  }
+
+  /**
+   * Initializes the configuration with an incomplete schema.
+   * <p>
+   * As configuration contains schema-related items, the initialization of the configuration
+   * can only be performed with an incomplete schema before a complete schema is available.
+   * Once a complete schema is available, the {@link #reinitializeWithFullSchema(Schema)} method
+   * should be called to have a fully validated configuration.
+   */
+  @VisibleForTesting
+  void initializeWithPartialSchema() throws InitializationException
+  {
+    File configFileToUse = preInitialization();
+    Schema configEnabledSchema = loadSchemaWithConfigurationEnabled();
+    loadConfiguration(configFileToUse, configEnabledSchema);
+  }
+
+  /**
+   * Re-initializes the configuration handler with a fully initialized schema.
+   * <p>
+   * Previously registered listeners are preserved.
+   *
+   * @param schema
+   *            The server schema, fully initialized.
+   * @throws InitializationException
+   *            If an error occurs.
+   */
+  public void reinitializeWithFullSchema(Schema schema) throws InitializationException
+  {
+    final Map<String, EntryListeners> exportedListeners = exportListeners();
+    finalize();
+    File configFileToUse = preInitialization();
+    loadConfiguration(configFileToUse, schema);
+    importListeners(exportedListeners, schema);
+  }
+
+  /** Finalizes the configuration handler. */
+  @Override
+  public void finalize()
+  {
+    listeners.clear();
+    backend.clear();
+  }
+
+  /**
+   * Prepares the initialization of the handler, returning the up-to-date configuration file to use
+   * to load the configuration.
+   *
+   * @return the file containing the configuration
+   * @throws InitializationException
+   *            If an error occurs.
+   */
+  private File preInitialization() throws InitializationException
   {
     final DirectoryEnvironmentConfig environment = serverContext.getEnvironment();
     useLastKnownGoodConfig = environment.useLastKnownGoodConfiguration();
-    configFile = findConfigFileToUse(environment.getConfigFile());
-
-    configEnabledSchema = loadConfigEnabledSchema();
-    loadConfiguration(configFile, configEnabledSchema);
+    configFile = environment.getConfigFile();
+    File configFileToUse = findConfigFileToUse(configFile);
+    ensureArchiveExistsAndIsUpToDate(environment, configFileToUse);
+    applyConfigChangesIfNeeded(configFileToUse);
+    return configFileToUse;
   }
 
-  /** Holds add, change and delete listeners for a given configuration entry. */
-  private static class EntryListeners {
+  /**
+   * Returns a copy of the listeners with DN as strings.
+   * Use strings to avoid holding copies on the old schema.
+   */
+  private Map<String, EntryListeners> exportListeners()
+  {
+    final Map<String, EntryListeners> listenersCopy = new HashMap<>();
+    for (Map.Entry<DN, EntryListeners> entry : listeners.entrySet())
+    {
+      listenersCopy.put(entry.getKey().toString(), entry.getValue());
+    }
+    return listenersCopy;
+  }
 
+  /** Imports the provided listeners into the configuration handler. */
+  private void importListeners(Map<String, EntryListeners> listenersCopy, Schema schema)
+  {
+    for (Map.Entry<String, EntryListeners> entry : listenersCopy.entrySet())
+    {
+      listeners.put(DN.valueOf(entry.getKey(), schema), entry.getValue());
+    }
+  }
+
+  @Override
+  public Map<String, String> getAlerts()
+  {
+    Map<String, String> alerts = new LinkedHashMap<>();
+
+    alerts.put(ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, ALERT_DESCRIPTION_CANNOT_WRITE_CONFIGURATION);
+    alerts.put(ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED, ALERT_DESCRIPTION_MANUAL_CONFIG_EDIT_HANDLED);
+    alerts.put(ALERT_TYPE_MANUAL_CONFIG_EDIT_LOST, ALERT_DESCRIPTION_MANUAL_CONFIG_EDIT_LOST);
+
+    return alerts;
+  }
+
+  @Override
+  public Set<DN> getChildren(DN dn) throws ConfigException
+  {
+    final ConfigLdapResultHandler resultHandler = new ConfigLdapResultHandler();
+    final CollectorSearchResultHandler searchHandler = new CollectorSearchResultHandler();
+
+    SearchRequest searchRequest = Requests.newSearchRequest(dn, SearchScope.SINGLE_LEVEL, Filter.alwaysTrue());
+    backend.handleSearch(UNCANCELLABLE_REQUEST_CONTEXT, searchRequest, null, searchHandler, resultHandler);
+
+    if (resultHandler.hasCompletedSuccessfully())
+    {
+      final Set<DN> children = new HashSet<>();
+      for (final Entry entry : searchHandler.getEntries())
+      {
+        children.add(entry.getName());
+      }
+      return children;
+    }
+    // TODO : fix message
+    throw new ConfigException(LocalizableMessage.raw("Unable to retrieve children of configuration entry : %s", dn),
+        resultHandler.getResultError());
+  }
+
+  @Override
+  public String getClassName()
+  {
+    return CLASS_NAME;
+  }
+
+  @Override
+  public DN getComponentEntryDN()
+  {
+    return rootEntry.getName();
+  }
+
+  /**
+   * Returns the configuration file containing all configuration entries.
+   *
+   * @return the configuration file
+   */
+  public File getConfigurationFile()
+  {
+    return configFile;
+  }
+
+  @Override
+  public Entry getEntry(final DN dn) throws ConfigException
+  {
+    Entry entry = backend.get(dn);
+    if (entry != null)
+    {
+      entry = Entries.unmodifiableEntry(entry);
+    }
+    return entry;
+  }
+
+  /**
+   * Returns the configuration root entry.
+   *
+   * @return the root entry
+   */
+  public Entry getRootEntry()
+  {
+    return rootEntry;
+  }
+
+  @Override
+  public List<ConfigAddListener> getAddListeners(final DN dn)
+  {
+    return getEntryListeners(dn).getAddListeners();
+  }
+
+  @Override
+  public List<ConfigChangeListener> getChangeListeners(final DN dn)
+  {
+    return getEntryListeners(dn).getChangeListeners();
+  }
+
+  @Override
+  public List<ConfigDeleteListener> getDeleteListeners(final DN dn)
+  {
+    return getEntryListeners(dn).getDeleteListeners();
+  }
+
+  @Override
+  public boolean hasEntry(final DN dn) throws ConfigException
+  {
+    return backend.get(dn) != null;
+  }
+
+  /**
+   * Search the configuration entries.
+   *
+   * @param searchOperation
+   *          Defines the search to perform
+   */
+  public void search(SearchOperation searchOperation)
+  {
+    // Leave all filtering to the SearchResultHandlerAdapter
+    SearchRequest request = Requests.newSearchRequest(
+        searchOperation.getBaseDN(), searchOperation.getScope(), Filter.alwaysTrue(), "*", "+");
+
+    LdapResultHandlerAdapter resultHandler = new LdapResultHandlerAdapter(searchOperation);
+    SearchResultHandler entryHandler = new SearchResultHandlerAdapter(searchOperation, resultHandler);
+    backend.handleSearch(UNCANCELLABLE_REQUEST_CONTEXT, request, null, entryHandler, resultHandler);
+  }
+
+  /**
+   * 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 ConfigLdapResultHandler resultHandler = new ConfigLdapResultHandler();
+    final CollectorSearchResultHandler searchHandler = new CollectorSearchResultHandler();
+    final SearchScope scope = subtree ? SearchScope.SUBORDINATES : SearchScope.SINGLE_LEVEL;
+    final SearchRequest searchRequest = Requests.newSearchRequest(entryDN, scope, Filter.alwaysTrue());
+    backend.handleSearch(UNCANCELLABLE_REQUEST_CONTEXT, searchRequest, null, searchHandler, resultHandler);
+
+    if (resultHandler.hasCompletedSuccessfully())
+    {
+      return searchHandler.getEntries().size();
+    }
+    // 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 ConfigLdapResultHandler resultHandler = new ConfigLdapResultHandler();
+    backend.handleAdd(UNCANCELLABLE_REQUEST_CONTEXT, Requests.newAddRequest(entry), null, resultHandler);
+
+    if (!resultHandler.hasCompletedSuccessfully())
+    {
+      // TODO fix the message : error when adding config entry
+      LdapException ex = resultHandler.getResultError();
+      throw new DirectoryException(ex.getResult().getResultCode(),
+          ERR_CONFIG_FILE_ADD_FAILED.get(entryDN, parentDN, ex.getLocalizedMessage()), ex);
+    }
+    writeUpdatedConfig();
+
+    // Notify all the add listeners to apply the new configuration entry.
+    final ConfigChangeResult ccr = new ConfigChangeResult();
+    for (final ConfigAddListener listener : addListeners)
+    {
+      final ConfigChangeResult result = listener.applyConfigurationAdd(entry);
+      ccr.aggregate(result);
+      handleConfigChangeResult(result, entry.getName(), listener.getClass().getName(), "applyConfigurationAdd");
+    }
+
+    if (ccr.getResultCode() != ResultCode.SUCCESS)
+    {
+      final String reasons = Utils.joinAsString(".  ", ccr.getMessages());
+      throw new DirectoryException(ccr.getResultCode(), 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), 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 : i18n
+      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
+          LocalizableMessage.raw("Backend config error when trying to delete an entry: %s",
+              stackTraceToSingleLineString(e)), 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_DELETE_REJECTED_BY_LISTENER.get(entry, parentDN, unacceptableReason));
+      }
+    }
+
+    // Delete the entry and all listeners on the entry
+    final ConfigLdapResultHandler resultHandler = new ConfigLdapResultHandler();
+    backend.handleDelete(UNCANCELLABLE_REQUEST_CONTEXT, Requests.newDeleteRequest(dn), null, resultHandler);
+    listeners.remove(dn);
+
+    if (!resultHandler.hasCompletedSuccessfully())
+    {
+      LdapException ex = resultHandler.getResultError();
+      throw new DirectoryException(ex.getResult().getResultCode(),
+          ERR_CONFIG_FILE_DELETE_FAILED.get(dn, parentDN, ex.getLocalizedMessage()), ex);
+    }
+    writeUpdatedConfig();
+
+    // Notify all the delete listeners that the entry has been removed.
+    final ConfigChangeResult ccr = new ConfigChangeResult();
+    for (final ConfigDeleteListener listener : deleteListeners)
+    {
+      final ConfigChangeResult result = listener.applyConfigurationDelete(entry);
+      ccr.aggregate(result);
+      handleConfigChangeResult(result, dn, listener.getClass().getName(), "applyConfigurationDelete");
+    }
+
+    if (ccr.getResultCode() != ResultCode.SUCCESS)
+    {
+      final String reasons = Utils.joinAsString(".  ", ccr.getMessages());
+      throw new DirectoryException(ccr.getResultCode(), 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.
+   */
+  @ActivateOnceSDKSchemaIsUsed("uncomment code down below in this method")
+  public void replaceEntry(final Entry oldEntry, final Entry newEntry) throws DirectoryException
+  {
+    final DN newEntryDN = newEntry.getName();
+    if (!backend.contains(newEntryDN))
+    {
+      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
+          ERR_CONFIG_FILE_MODIFY_NO_SUCH_ENTRY.get(oldEntry), getMatchedDN(newEntryDN), 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(newEntryDN);
+    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(newEntryDN, unacceptableReason));
+      }
+    }
+
+    // Replace the old entry with new entry.
+    ModifyRequest modifyRequest = Entries.diffEntries(oldEntry, newEntry, Entries.diffOptions().attributes("*", "+"));
+    final ConfigLdapResultHandler resultHandler = new ConfigLdapResultHandler();
+    backend.handleModify(UNCANCELLABLE_REQUEST_CONTEXT, modifyRequest, null, resultHandler);
+
+    if (!resultHandler.hasCompletedSuccessfully())
+    {
+      LdapException ex = resultHandler.getResultError();
+      throw new DirectoryException(ex.getResult().getResultCode(),
+          ERR_CONFIG_FILE_MODIFY_FAILED.get(newEntryDN, newEntryDN, ex.getLocalizedMessage()), ex);
+    }
+    writeUpdatedConfig();
+
+    // Notify all the change listeners of the update.
+    final ConfigChangeResult ccr = new ConfigChangeResult();
+    for (final ConfigChangeListener listener : changeListeners)
+    {
+      if (!changeListeners.contains(listener))
+      {
+        // some listeners may have de-registered themselves due to previous changes, ignore them
+        continue;
+      }
+      final ConfigChangeResult result = listener.applyConfigurationChange(newEntry);
+      ccr.aggregate(result);
+      handleConfigChangeResult(result, newEntryDN, listener.getClass().getName(), "applyConfigurationChange");
+    }
+
+    if (ccr.getResultCode() != ResultCode.SUCCESS)
+    {
+      String reasons = Utils.joinAsString(".  ", ccr.getMessages());
+      throw new DirectoryException(ccr.getResultCode(), ERR_CONFIG_FILE_MODIFY_APPLY_FAILED.get(reasons));
+    }
+  }
+
+  @Override
+  public void registerAddListener(final DN dn, final ConfigAddListener listener)
+  {
+    getEntryListeners(dn).registerAddListener(listener);
+  }
+
+  @Override
+  public void registerDeleteListener(final DN dn, final ConfigDeleteListener listener)
+  {
+    getEntryListeners(dn).registerDeleteListener(listener);
+  }
+
+  @Override
+  public void registerChangeListener(final DN dn, final ConfigChangeListener listener)
+  {
+    getEntryListeners(dn).registerChangeListener(listener);
+  }
+
+  @Override
+  public void deregisterAddListener(final DN dn, final ConfigAddListener listener)
+  {
+    getEntryListeners(dn).deregisterAddListener(listener);
+  }
+
+  @Override
+  public void deregisterDeleteListener(final DN dn, final ConfigDeleteListener listener)
+  {
+    getEntryListeners(dn).deregisterDeleteListener(listener);
+  }
+
+  @Override
+  public boolean deregisterChangeListener(final DN dn, final ConfigChangeListener listener)
+  {
+    return getEntryListeners(dn).deregisterChangeListener(listener);
+  }
+
+  /**
+   * Writes the current configuration to LDIF with the provided export configuration.
+   *
+   * @param exportConfig
+   *          The configuration to use for the export.
+   * @throws DirectoryException
+   *           If a problem occurs while writing the LDIF.
+   */
+  public void writeLDIF(LDIFExportConfig exportConfig) throws DirectoryException
+  {
+    try (LDIFEntryWriter writer = new LDIFEntryWriter(exportConfig.getWriter()))
+    {
+      writer.writeComment(INFO_CONFIG_FILE_HEADER.get().toString());
+      for (Entry entry : new ArrayList<Entry>(backend.getAll()))
+      {
+        try
+        {
+          writer.writeEntry(entry);
+        }
+        catch (IOException e)
+        {
+          logger.traceException(e);
+          LocalizableMessage message = ERR_CONFIG_FILE_WRITE_ERROR.get(entry.getName(), e);
+          throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
+        }
+      }
+    }
+    catch (IOException e)
+    {
+      logger.traceException(e);
+      LocalizableMessage message = ERR_CONFIG_LDIF_WRITE_ERROR.get(e);
+      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
+    }
+  }
+
+  /**
+   * Generates a configuration file with the ".startok" suffix, representing a configuration
+   * file that has a successful start.
+   * <p>
+   * This method must not be called if configuration can't be correctly initialized.
+   * <p>
+   * The actual generation is skipped if last known good configuration is used.
+   */
+  public void writeSuccessfulStartupConfig()
+  {
+    if (useLastKnownGoodConfig)
+    {
+      // The server was started with the "last known good" configuration, so we
+      // shouldn't overwrite it with something that is probably bad.
+      return;
+    }
+
+    String startOKFilePath = configFile + ".startok";
+    String tempFilePath = startOKFilePath + ".tmp";
+    String oldFilePath = startOKFilePath + ".old";
+
+    // Copy the current config file to a temporary file.
+    File tempFile = new File(tempFilePath);
+    try (FileInputStream inputStream = new FileInputStream(configFile))
+    {
+      try (FileOutputStream outputStream = new FileOutputStream(tempFilePath, false))
+      {
+        try
+        {
+          byte[] buffer = new byte[8192];
+          while (true)
+          {
+            int bytesRead = inputStream.read(buffer);
+            if (bytesRead < 0)
+            {
+              break;
+            }
+
+            outputStream.write(buffer, 0, bytesRead);
+          }
+        }
+        catch (IOException e)
+        {
+          logger.traceException(e);
+          logger.error(ERR_STARTOK_CANNOT_WRITE, configFile, tempFilePath, getExceptionMessage(e));
+          return;
+        }
+      }
+      catch (FileNotFoundException e)
+      {
+        logger.traceException(e);
+        logger.error(ERR_STARTOK_CANNOT_OPEN_FOR_WRITING, tempFilePath, getExceptionMessage(e));
+        return;
+      }
+      catch (IOException e)
+      {
+        logger.traceException(e);
+      }
+    }
+    catch (FileNotFoundException e)
+    {
+      logger.traceException(e);
+      logger.error(ERR_STARTOK_CANNOT_OPEN_FOR_READING, configFile, getExceptionMessage(e));
+      return;
+    }
+    catch (IOException e)
+    {
+      logger.traceException(e);
+    }
+
+    // If a ".startok" file already exists, then move it to an ".old" file.
+    File oldFile = new File(oldFilePath);
+    try
+    {
+      if (oldFile.exists())
+      {
+        oldFile.delete();
+      }
+    }
+    catch (Exception e)
+    {
+      logger.traceException(e);
+    }
+
+    File startOKFile = new File(startOKFilePath);
+    try
+    {
+      if (startOKFile.exists())
+      {
+        startOKFile.renameTo(oldFile);
+      }
+    }
+    catch (Exception e)
+    {
+      logger.traceException(e);
+    }
+
+    // Rename the temp file to the ".startok" file.
+    try
+    {
+      tempFile.renameTo(startOKFile);
+    }
+    catch (Exception e)
+    {
+      logger.traceException(e);
+      logger.error(ERR_STARTOK_CANNOT_RENAME, tempFilePath, startOKFilePath, getExceptionMessage(e));
+      return;
+    }
+
+    // Remove the ".old" file if there is one.
+    try
+    {
+      if (oldFile.exists())
+      {
+        oldFile.delete();
+      }
+    }
+    catch (Exception e)
+    {
+      logger.traceException(e);
+    }
+  }
+
+  private void writeUpdatedConfig() throws DirectoryException
+  {
+    // FIXME -- This needs support for encryption.
+
+    // Calculate an archive for the current server configuration file and see if
+    // it matches what we expect. If not, then the file has been manually
+    // edited with the server online which is a bad thing. In that case, we'll
+    // copy the current config off to the side before writing the new config
+    // so that the manual changes don't get lost but also don't get applied.
+    // Also, send an admin alert notifying administrators about the problem.
+    if (maintainConfigArchive)
+    {
+      try
+      {
+        byte[] currentDigest = calculateConfigDigest();
+        if (!Arrays.equals(configurationDigest, currentDigest))
+        {
+          File existingCfg = configFile;
+          File newConfigFile =
+              new File(existingCfg.getParent(), "config.manualedit-" + TimeThread.getGMTTime() + ".ldif");
+          int counter = 2;
+          while (newConfigFile.exists())
+          {
+            newConfigFile = new File(newConfigFile.getAbsolutePath() + "." + counter);
+          }
+
+          try (FileInputStream inputStream = new FileInputStream(existingCfg);
+              FileOutputStream outputStream = new FileOutputStream(newConfigFile))
+          {
+            byte[] buffer = new byte[8192];
+            while (true)
+            {
+              int bytesRead = inputStream.read(buffer);
+              if (bytesRead < 0)
+              {
+                break;
+              }
+              outputStream.write(buffer, 0, bytesRead);
+            }
+          }
+
+          LocalizableMessage message =
+              WARN_CONFIG_MANUAL_CHANGES_DETECTED.get(configFile, newConfigFile.getAbsolutePath());
+          logger.warn(message);
+
+          DirectoryServer.sendAlertNotification(this, ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED, message);
+        }
+      }
+      catch (Exception e)
+      {
+        logger.traceException(e);
+
+        LocalizableMessage message = ERR_CONFIG_MANUAL_CHANGES_LOST.get(configFile, stackTraceToSingleLineString(e));
+        logger.error(message);
+
+        DirectoryServer.sendAlertNotification(this, ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED, message);
+      }
+    }
+
+    // Write the new configuration to a temporary file.
+    String tempConfig = configFile + ".tmp";
+    try
+    {
+      LDIFExportConfig exportConfig = new LDIFExportConfig(tempConfig, ExistingFileBehavior.OVERWRITE);
+
+      // FIXME -- Add all the appropriate configuration options.
+      writeLDIF(exportConfig);
+    }
+    catch (Exception e)
+    {
+      logger.traceException(e);
+
+      LocalizableMessage message =
+          ERR_CONFIG_FILE_WRITE_CANNOT_EXPORT_NEW_CONFIG.get(tempConfig, stackTraceToSingleLineString(e));
+      logger.error(message);
+
+      DirectoryServer.sendAlertNotification(this, ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message);
+      return;
+    }
+
+    // Delete the previous version of the configuration and rename the new one.
+    try
+    {
+      File actualConfig = configFile;
+      File tmpConfig = new File(tempConfig);
+      renameFile(tmpConfig, actualConfig);
+    }
+    catch (Exception e)
+    {
+      logger.traceException(e);
+
+      LocalizableMessage message =
+          ERR_CONFIG_FILE_WRITE_CANNOT_RENAME_NEW_CONFIG.get(tempConfig, configFile, stackTraceToSingleLineString(e));
+      logger.error(message);
+
+      DirectoryServer.sendAlertNotification(this, ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message);
+      return;
+    }
+
+    configurationDigest = calculateConfigDigest();
+
+    // Try to write the archive for the new configuration.
+    if (maintainConfigArchive)
+    {
+      writeConfigArchive();
+    }
+  }
+
+  /** Request context to be used when requesting the internal backend. */
+  private static final RequestContext UNCANCELLABLE_REQUEST_CONTEXT = new RequestContext()
+  {
+    @Override
+    public void removeCancelRequestListener(final CancelRequestListener listener)
+    {
+      // nothing to do
+    }
+
+    @Override
+    public int getMessageID()
+    {
+      return -1;
+    }
+
+    @Override
+    public void checkIfCancelled(final boolean signalTooLate) throws CancelledResultException
+    {
+      // nothing to do
+    }
+
+    @Override
+    public void addCancelRequestListener(final CancelRequestListener listener)
+    {
+      // nothing to do
+    }
+  };
+
+  /** 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. */
@@ -174,46 +1047,10 @@
     {
       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
+  /** Handler for search results collecting all received entries. */
+  private static final class CollectorSearchResultHandler implements SearchResultHandler
   {
     private final Set<Entry> entries = new HashSet<>();
 
@@ -222,14 +1059,12 @@
       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)
     {
@@ -238,9 +1073,49 @@
     }
   }
 
-  /** Handler for LDAP operations. */
-  private static final class ConfigResultHandler implements LdapResultHandler<Result> {
+  /** Handler for search results redirecting to a SearchOperation. */
+  private static final class SearchResultHandlerAdapter implements SearchResultHandler
+  {
+    private final SearchOperation searchOperation;
+    private final LdapResultHandlerAdapter resultHandler;
 
+    private SearchResultHandlerAdapter(SearchOperation searchOperation, LdapResultHandlerAdapter resultHandler)
+    {
+      this.searchOperation = searchOperation;
+      this.resultHandler = resultHandler;
+    }
+
+    @Override
+    public boolean handleReference(SearchResultReference reference)
+    {
+      throw new UnsupportedOperationException("Search references are not supported for configuration entries.");
+    }
+
+    @Override
+    public boolean handleEntry(SearchResultEntry entry)
+    {
+      org.opends.server.types.Entry serverEntry = Converters.to(entry);
+      serverEntry.processVirtualAttributes();
+      return !filterMatchesEntry(serverEntry) || searchOperation.returnEntry(serverEntry, null);
+    }
+
+    private boolean filterMatchesEntry(org.opends.server.types.Entry serverEntry)
+    {
+      try
+      {
+        return searchOperation.getFilter().matchesEntry(serverEntry);
+      }
+      catch (DirectoryException e)
+      {
+        resultHandler.handleException(LdapException.newLdapException(ResultCode.UNWILLING_TO_PERFORM, e));
+        return false;
+      }
+    }
+  }
+
+  /** Handler for LDAP operations. */
+  private static final class ConfigLdapResultHandler implements LdapResultHandler<Result>
+  {
     private LdapException resultError;
 
     LdapException getResultError()
@@ -248,18 +1123,17 @@
       return resultError;
     }
 
-    boolean hasCompletedSuccessfully() {
+    boolean hasCompletedSuccessfully()
+    {
       return resultError == null;
     }
 
-    /** {@inheritDoc} */
     @Override
     public void handleResult(Result result)
     {
       // nothing to do
     }
 
-    /** {@inheritDoc} */
     @Override
     public void handleException(LdapException exception)
     {
@@ -267,438 +1141,118 @@
     }
   }
 
-  /**
-   * 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
+  /** Handler for LDAP operations redirecting to a SearchOperation. */
+  private static final class LdapResultHandlerAdapter implements LdapResultHandler<Result>
   {
-    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);
+    private final SearchOperation searchOperation;
 
-    if (resultHandler.hasCompletedSuccessfully())
+    LdapResultHandlerAdapter(SearchOperation searchOperation)
     {
-      return searchHandler.getEntries().size();
+      this.searchOperation = searchOperation;
     }
-    else {
-      // TODO : fix the message
-      throw new ConfigException(
-          LocalizableMessage.raw("Unable to retrieve children of configuration entry : %s", entryDN),
-          resultHandler.getResultError());
+
+    @Override
+    public void handleResult(Result result)
+    {
+      searchOperation.setResultCode(result.getResultCode());
+    }
+
+    @Override
+    public void handleException(LdapException exception)
+    {
+      searchOperation.setResultCode(exception.getResult().getResultCode());
+      searchOperation.setErrorMessage(
+          new LocalizableMessageBuilder(LocalizableMessage.raw(exception.getLocalizedMessage())));
+      String matchedDNString = exception.getResult().getMatchedDN();
+      if (matchedDNString != null)
+      {
+        searchOperation.setMatchedDN(DN.valueOf(matchedDNString));
+      }
     }
   }
 
   /**
-   * 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.
+   * Find the actual configuration file to use to load configuration, given the standard
+   * configuration file.
    *
-   * @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
+   * @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.
    */
-  public void deleteEntry(final DN dn) throws DirectoryException
+  private File findConfigFileToUse(final File standardConfigFile) throws InitializationException
   {
-    // Entry must exist.
-    if (!backend.contains(dn))
+    File fileToUse;
+    if (useLastKnownGoodConfig)
     {
-      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
-          ERR_CONFIG_FILE_DELETE_NO_SUCH_ENTRY.get(dn), getMatchedDN(dn), null);
+      fileToUse = new File(standardConfigFile.getPath() + ".startok");
+      if (fileToUse.exists())
+      {
+        logger.info(NOTE_CONFIG_FILE_USING_STARTOK_FILE, fileToUse.getAbsolutePath(), standardConfigFile);
+      }
+      else
+      {
+        logger.warn(WARN_CONFIG_FILE_NO_STARTOK_FILE, fileToUse.getAbsolutePath(), standardConfigFile);
+        useLastKnownGoodConfig = false;
+        fileToUse = standardConfigFile;
+      }
+    }
+    else
+    {
+      fileToUse = standardConfigFile;
     }
 
-    // Entry must not have children.
+    boolean fileExists = false;
     try
     {
-      if (!getChildren(dn).isEmpty())
-      {
-        throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF,
-            ERR_CONFIG_FILE_DELETE_HAS_CHILDREN.get(dn));
-      }
+      fileExists = fileToUse.exists();
     }
-    catch (ConfigException e)
+    catch (Exception e)
     {
-      // TODO : fix message = ERROR BACKEND CONFIG
-      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
-          ERR_CONFIG_FILE_DELETE_HAS_CHILDREN.get(dn), e);
+      logger.traceException(e);
+      throw new InitializationException(ERR_CONFIG_FILE_CANNOT_VERIFY_EXISTENCE.get(fileToUse.getAbsolutePath(), 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 (!fileExists)
     {
-      if (!listener.configDeleteIsAcceptable(entry, unacceptableReason))
-      {
-        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
-            ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get(entry, parentDN, unacceptableReason));
-      }
+      throw new InitializationException(ERR_CONFIG_FILE_DOES_NOT_EXIST.get(fileToUse.getAbsolutePath()));
     }
+    return fileToUse;
+  }
 
-    // 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)
+  /** Load the configuration-enabled schema that will allow to read the configuration file. */
+  private Schema loadSchemaWithConfigurationEnabled() throws InitializationException
+  {
+    final File schemaDir = serverContext.getEnvironment().getSchemaDirectory();
+    try (LDIFEntryReader reader = new LDIFEntryReader(new FileReader(new File(schemaDir, CONFIGURATION_FILE_NAME))))
     {
-      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), 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 Schema schema = Schema.getDefaultSchema();
+      reader.setSchema(schema);
       final Entry entry = reader.readEntry();
-      return new SchemaBuilder(Schema.getDefaultSchema()).addSchema(entry, false).toSchema();
+      return new SchemaBuilder(schema).addSchema(entry, false).toSchema().asNonStrictSchema();
     }
     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.
+   *          LDIF file with configuration entries.
    * @param schema
    *          Schema to validate entries when reading the config file.
    * @throws InitializationException
-   *            If an errors occurs.
+   *           If an errors occurs.
    */
-  private void loadConfiguration(final File configFile, final Schema schema)
-      throws InitializationException
+  private void loadConfiguration(final File configFile, final Schema schema) throws InitializationException
   {
-    EntryReader reader = null;
-    try
+    try (EntryReader reader = getLDIFReader(configFile, schema))
     {
-      reader = getLDIFReader(configFile, schema);
       backend = new MemoryBackend(schema, reader);
     }
     catch (IOException e)
@@ -706,26 +1260,415 @@
       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));
+      throw new InitializationException(
+          ERR_CONFIG_FILE_INVALID_BASE_DN.get(configFile.getAbsolutePath(), "", DN_CONFIG_ROOT));
+    }
+  }
+
+  /**
+   * Ensure there is an-up-to-date configuration archive.
+   * <p>
+   * Check to see if a configuration archive exists. If not, then create one.
+   * If so, then check whether the current configuration matches the last
+   * configuration in the archive. If it doesn't, then archive it.
+   */
+  private void ensureArchiveExistsAndIsUpToDate(DirectoryEnvironmentConfig environment, File configFileToUse)
+      throws InitializationException
+  {
+    maintainConfigArchive = environment.maintainConfigArchive();
+    maxConfigArchiveSize = environment.getMaxConfigArchiveSize();
+    if (maintainConfigArchive && !useLastKnownGoodConfig)
+    {
+      try
+      {
+        configurationDigest = calculateConfigDigest();
+      }
+      catch (DirectoryException e)
+      {
+        throw new InitializationException(e.getMessageObject(), e.getCause());
+      }
+
+      File archiveDirectory = new File(configFileToUse.getParent(), CONFIG_ARCHIVE_DIR_NAME);
+      if (archiveDirectory.exists())
+      {
+        try
+        {
+          byte[] lastDigest = getLastConfigDigest(archiveDirectory);
+          if (!Arrays.equals(configurationDigest, lastDigest))
+          {
+            writeConfigArchive();
+          }
+        }
+        catch (DirectoryException e)
+        {
+          throw new InitializationException(e.getMessageObject(), e.getCause());
+        }
+      }
+      else
+      {
+        writeConfigArchive();
+      }
+    }
+  }
+
+  /** Writes the current configuration to the configuration archive. This will be a best-effort attempt. */
+  private void writeConfigArchive()
+  {
+    if (!maintainConfigArchive)
+    {
+      return;
+    }
+    File archiveDirectory = new File(configFile.getParentFile(), CONFIG_ARCHIVE_DIR_NAME);
+    try
+    {
+      createArchiveDirectoryIfNeeded(archiveDirectory);
+      File archiveFile = getNewArchiveFile(archiveDirectory);
+      copyCurrentConfigFileToArchiveFile(archiveFile);
+      removeOldArchiveFilesIfNeeded(archiveDirectory);
+    }
+    catch (DirectoryException e)
+    {
+      LocalizableMessage message = e.getMessageObject();
+      logger.error(message);
+      DirectoryServer.sendAlertNotification(this, ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message);
+    }
+  }
+
+  private void createArchiveDirectoryIfNeeded(File archiveDirectory) throws DirectoryException
+  {
+    if (!archiveDirectory.exists())
+    {
+      try
+      {
+        if (!archiveDirectory.mkdirs())
+        {
+          throw new DirectoryException(ResultCode.UNDEFINED,
+              ERR_CONFIG_FILE_CANNOT_CREATE_ARCHIVE_DIR_NO_REASON.get(archiveDirectory.getAbsolutePath()));
+        }
+      }
+      catch (Exception e)
+      {
+        logger.traceException(e);
+        throw new DirectoryException(ResultCode.UNDEFINED,
+            ERR_CONFIG_FILE_CANNOT_CREATE_ARCHIVE_DIR.get(archiveDirectory.getAbsolutePath(),
+            stackTraceToSingleLineString(e)), e);
+      }
+    }
+  }
+
+  private File getNewArchiveFile(File archiveDirectory) throws DirectoryException
+  {
+    try
+    {
+      String timestamp = TimeThread.getGMTTime();
+      File archiveFile = new File(archiveDirectory, "config-" + timestamp + ".gz");
+      if (archiveFile.exists())
+      {
+        int counter = 1;
+        do
+        {
+          counter++;
+          archiveFile = new File(archiveDirectory, "config-" + timestamp + "-" + counter + ".gz");
+        }
+        while (archiveFile.exists());
+      }
+      return archiveFile;
+    }
+    catch (Exception e)
+    {
+      logger.traceException(e);
+      throw new DirectoryException(ResultCode.UNDEFINED,
+          ERR_CONFIG_FILE_CANNOT_WRITE_CONFIG_ARCHIVE.get(stackTraceToSingleLineString(e)));
+    }
+  }
+
+  /** Copy the current configuration file to the archive configuration file. */
+  private void copyCurrentConfigFileToArchiveFile(File archiveFile) throws DirectoryException
+  {
+    byte[] buffer = new byte[8192];
+    try(FileInputStream inputStream = new FileInputStream(configFile);
+        GZIPOutputStream outputStream = new GZIPOutputStream(new FileOutputStream(archiveFile)))
+    {
+      int bytesRead = inputStream.read(buffer);
+      while (bytesRead > 0)
+      {
+        outputStream.write(buffer, 0, bytesRead);
+        bytesRead = inputStream.read(buffer);
+      }
+    }
+    catch (IOException e)
+    {
+      logger.traceException(e);
+      throw new DirectoryException(ResultCode.UNDEFINED,
+          ERR_CONFIG_FILE_CANNOT_WRITE_CONFIG_ARCHIVE.get(stackTraceToSingleLineString(e)));
+    }
+  }
+
+  /** Deletes old archives files if we should enforce a maximum number of archived configurations. */
+  private void removeOldArchiveFilesIfNeeded(File archiveDirectory)
+  {
+    if (maxConfigArchiveSize > 0)
+    {
+      String[] archivedFileList = archiveDirectory.list();
+      int numToDelete = archivedFileList.length - maxConfigArchiveSize;
+      if (numToDelete > 0)
+      {
+        Set<String> archiveSet = new TreeSet<>();
+        for (String name : archivedFileList)
+        {
+          if (!name.startsWith("config-"))
+          {
+            continue;
+          }
+          // Simply ordering by filename should work, even when there are
+          // timestamp conflicts, because the dash comes before the period in
+          // the ASCII character set.
+          archiveSet.add(name);
+        }
+        Iterator<String> iterator = archiveSet.iterator();
+        for (int i = 0; i < numToDelete && iterator.hasNext(); i++)
+        {
+          File archive = new File(archiveDirectory, iterator.next());
+          try
+          {
+            archive.delete();
+          }
+          catch (Exception e)
+          {
+            // do nothing
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Looks at the existing archive directory, finds the latest archive file, and calculates a SHA-1
+   * digest of that file.
+   *
+   * @return The calculated digest of the most recent archived configuration file.
+   * @throws DirectoryException
+   *           If a problem occurs while calculating the digest.
+   */
+  private byte[] getLastConfigDigest(File archiveDirectory) throws DirectoryException
+  {
+    int latestCounter = 0;
+    long latestTimestamp = -1;
+    String latestFileName = null;
+    for (String name : archiveDirectory.list())
+    {
+      if (!name.startsWith("config-"))
+      {
+        continue;
+      }
+      int dotPos = name.indexOf('.', 7);
+      if (dotPos < 0)
+      {
+        continue;
+      }
+      int dashPos = name.indexOf('-', 7);
+      if (dashPos < 0)
+      {
+        try
+        {
+          ByteString ts = ByteString.valueOfUtf8(name.substring(7, dotPos));
+          long timestamp = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(ts);
+          if (timestamp > latestTimestamp)
+          {
+            latestFileName = name;
+            latestTimestamp = timestamp;
+            latestCounter = 0;
+            continue;
+          }
+        }
+        catch (Exception e)
+        {
+          continue;
+        }
+      }
+      else
+      {
+        try
+        {
+          ByteString ts = ByteString.valueOfUtf8(name.substring(7, dashPos));
+          long timestamp = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(ts);
+          int counter = Integer.parseInt(name.substring(dashPos + 1, dotPos));
+
+          if (timestamp > latestTimestamp
+              || (timestamp == latestTimestamp && counter > latestCounter))
+          {
+            latestFileName = name;
+            latestTimestamp = timestamp;
+            latestCounter = counter;
+            continue;
+          }
+        }
+        catch (Exception e)
+        {
+          continue;
+        }
+      }
+    }
+
+    if (latestFileName == null)
+    {
+      return null;
+    }
+
+    File latestFile = new File(archiveDirectory, latestFileName);
+    try (GZIPInputStream inputStream = new GZIPInputStream(new FileInputStream(latestFile)))
+    {
+      return calculateDigest(inputStream);
+    }
+    catch (Exception e)
+    {
+      LocalizableMessage message =
+          ERR_CONFIG_CANNOT_CALCULATE_DIGEST.get(latestFile.getAbsolutePath(), stackTraceToSingleLineString(e));
+      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
+    }
+  }
+
+  /**
+   * Calculates a SHA-1 digest of the current configuration file.
+   *
+   * @return The calculated configuration digest.
+   * @throws DirectoryException
+   *           If a problem occurs while calculating the digest.
+   */
+  private byte[] calculateConfigDigest() throws DirectoryException
+  {
+    try (InputStream inputStream = new FileInputStream(configFile))
+    {
+      return calculateDigest(inputStream);
+    }
+    catch (Exception e)
+    {
+      LocalizableMessage message = ERR_CONFIG_CANNOT_CALCULATE_DIGEST.get(configFile, stackTraceToSingleLineString(e));
+      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
+    }
+  }
+
+  private byte[] calculateDigest(InputStream inputStream) throws NoSuchAlgorithmException, IOException
+  {
+    MessageDigest sha1Digest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1);
+    byte[] buffer = new byte[8192];
+    while (true)
+    {
+      int bytesRead = inputStream.read(buffer);
+      if (bytesRead < 0)
+      {
+        break;
+      }
+      sha1Digest.update(buffer, 0, bytesRead);
+    }
+    return sha1Digest.digest();
+  }
+
+  /**
+   * Applies the updates in the provided changes file to the content in the specified source file.
+   * The result will be written to a temporary file, the current source file will be moved out of
+   * place, and then the updated file will be moved into the place of the original file. The changes
+   * file will also be renamed so it won't be applied again. <BR>
+   * <BR>
+   * If any problems are encountered, then the config initialization process will be aborted.
+   *
+   * @param sourceFile
+   *          The LDIF file containing the source data.
+   * @param changesFile
+   *          The LDIF file containing the changes to apply.
+   * @throws IOException
+   *           If a problem occurs while performing disk I/O.
+   * @throws LDIFException
+   *           If a problem occurs while trying to interpret the data.
+   */
+  private void applyChangesFile(File sourceFile, File changesFile) throws IOException, LDIFException
+  {
+    // Create the appropriate LDIF readers and writer.
+    LDIFImportConfig sourceImportCfg = new LDIFImportConfig(sourceFile.getAbsolutePath());
+    sourceImportCfg.setValidateSchema(false);
+
+    LDIFImportConfig changesImportCfg = new LDIFImportConfig(changesFile.getAbsolutePath());
+    changesImportCfg.setValidateSchema(false);
+
+    String tempFile = changesFile.getAbsolutePath() + ".tmp";
+    LDIFExportConfig exportConfig = new LDIFExportConfig(tempFile, ExistingFileBehavior.OVERWRITE);
+
+    List<LocalizableMessage> errorList = new LinkedList<>();
+    boolean successful;
+    try (LDIFReader sourceReader = new LDIFReader(sourceImportCfg);
+        LDIFReader changesReader = new LDIFReader(changesImportCfg);
+        LDIFWriter targetWriter = new LDIFWriter(exportConfig))
+    {
+      // Apply the changes and make sure there were no errors.
+      successful = LDIFModify.modifyLDIF(sourceReader, changesReader, targetWriter, errorList);
+    }
+
+    if (!successful)
+    {
+      // FIXME -- Log each error message and throw an exception.
+      for (LocalizableMessage s : errorList)
+      {
+        logger.error(ERR_CONFIG_ERROR_APPLYING_STARTUP_CHANGE, s);
+      }
+
+      LocalizableMessage message = ERR_CONFIG_UNABLE_TO_APPLY_CHANGES_FILE.get();
+      throw new LDIFException(message);
+    }
+
+    // Move the current config file out of the way and replace it with the updated version.
+    File oldSource = new File(sourceFile.getAbsolutePath() + ".prechanges");
+    if (oldSource.exists())
+    {
+      oldSource.delete();
+    }
+    sourceFile.renameTo(oldSource);
+    new File(tempFile).renameTo(sourceFile);
+
+    // Move the changes file out of the way so it doesn't get applied again.
+    File newChanges = new File(changesFile.getAbsolutePath() + ".applied");
+    if (newChanges.exists())
+    {
+      newChanges.delete();
+    }
+    changesFile.renameTo(newChanges);
+  }
+
+  private void applyConfigChangesIfNeeded(File configFileToUse) throws InitializationException
+  {
+    // See if there is a config changes file. If there is, then try to apply
+    // the changes contained in it.
+    File changesFile = new File(configFileToUse.getParent(), CONFIG_CHANGES_NAME);
+    try
+    {
+      if (changesFile.exists())
+      {
+        applyChangesFile(configFileToUse, changesFile);
+        if (maintainConfigArchive)
+        {
+          configurationDigest = calculateConfigDigest();
+          writeConfigArchive();
+        }
+      }
+    }
+    catch (Exception e)
+    {
+      logger.traceException(e);
+
+      LocalizableMessage message = ERR_CONFIG_UNABLE_TO_APPLY_STARTUP_CHANGES.get(changesFile.getAbsolutePath(), e);
+      throw new InitializationException(message, e);
     }
   }
 
   /**
    * Returns the LDIF reader on configuration entries.
    * <p>
-   * It is the responsability of the caller to ensure that reader
-   * is closed after usage.
+   * It is the responsibility of the caller to ensure that reader is closed after usage.
    *
    * @param configFile
    *          LDIF file containing the configuration entries.
@@ -735,39 +1678,40 @@
    * @throws InitializationException
    *           If an error occurs.
    */
-  private EntryReader getLDIFReader(final File configFile, final Schema schema)
-      throws InitializationException
+  private EntryReader getLDIFReader(final File configFile, final Schema schema) throws InitializationException
   {
-    LDIFEntryReader reader = null;
     try
     {
-      reader = new LDIFEntryReader(new FileReader(configFile));
+      LDIFEntryReader reader = new LDIFEntryReader(new FileReader(configFile));
       reader.setSchema(schema);
+      return reader;
     }
     catch (Exception e)
     {
       throw new InitializationException(
-          ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get(configFile.getAbsolutePath(), e), e);
+          ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get(configFile.getAbsolutePath(), e.getLocalizedMessage()), 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.
+   * 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) {
+  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) {
+      if (previousListeners != null)
+      {
         entryListeners = previousListeners;
       }
     }
@@ -775,8 +1719,7 @@
   }
 
   /**
-   * Returns the parent DN of the configuration entry corresponding to the
-   * provided DN.
+   * Returns the parent DN of the configuration entry corresponding to the provided DN.
    *
    * @param entryDN
    *          DN of entry to retrieve the parent from.
@@ -787,25 +1730,19 @@
   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), getMatchedDN(parentDN), null);
+      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_ADD_NO_PARENT.get(entryDN, parentDN),
+          getMatchedDN(parentDN), null);
     }
     return parentDN;
   }
 
-  /**
-   * Returns the matched DN that is available in the configuration for the
-   * provided DN.
-   */
+  /** Returns the matched DN that is available in the configuration for the provided DN. */
   private DN getMatchedDN(final DN dn)
   {
     DN matchedDN = null;
@@ -823,68 +1760,20 @@
   }
 
   /**
-   * 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.
+   * Examines the provided result and logs a message if appropriate.
+   * <p>
+   * <ul>
+   * <li>If the result code is anything other than {@code SUCCESS}, then it will log an error message.</li>
+   * <li>If the operation was successful but admin action is required, then it will log a warning message.</li>
+   * <li>If no action is required but messages were generated, then it will log an informational message.</li>
+   * </ul>
    *
    * @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.
+   *          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.
    */
@@ -898,22 +1787,20 @@
 
     final ResultCode resultCode = result.getResultCode();
     final boolean adminActionRequired = result.adminActionRequired();
-    final List<LocalizableMessage> messages = result.getMessages();
+    final String messages = Utils.joinAsString("  ", 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);
+      logger.error(ERR_CONFIG_CHANGE_RESULT_ERROR, className, methodName, entryDN, resultCode, adminActionRequired,
+          messages);
     }
     else if (adminActionRequired)
     {
-      logger.warn(WARN_CONFIG_CHANGE_RESULT_ACTION_REQUIRED, className, methodName, entryDN, messageBuffer);
+      logger.warn(WARN_CONFIG_CHANGE_RESULT_ACTION_REQUIRED, className, methodName, entryDN, messages);
     }
-    else if (messageBuffer.length() > 0)
+    else if (!messages.isEmpty())
     {
-      logger.debug(INFO_CONFIG_CHANGE_RESULT_MESSAGES, className, methodName, entryDN, messageBuffer);
+      logger.debug(INFO_CONFIG_CHANGE_RESULT_MESSAGES, className, methodName, entryDN, messages);
     }
   }
-
 }
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/config.properties b/opendj-server-legacy/src/messages/org/opends/messages/config.properties
index d009c32..2df734f 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/config.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/config.properties
@@ -363,6 +363,8 @@
 ERR_CONFIG_SASL_INITIALIZATION_FAILED_277=An error occurred while trying \
  to initialize an instance of class %s as a SASL mechanism handler as defined \
  in configuration entry %s: %s
+ERR_CONFIG_FILE_DELETE_NO_PARENT_DN_278=Entry %s cannot be removed from the \
+ Directory Server configuration because that DN does not have a parent
 ERR_CONFIG_FILE_ADD_ALREADY_EXISTS_280=Entry %s cannot be added to the \
  Directory Server configuration because another configuration entry already \
  exists with that DN
@@ -384,7 +386,7 @@
 ERR_CONFIG_FILE_DELETE_NO_PARENT_287=Entry %s cannot be removed from the \
  Directory Server configuration because the entry does not have a parent and \
  removing the configuration root entry is not allowed
-ERR_CONFIG_FILE_DELETE_REJECTED_288=Entry %s cannot be removed from the \
+ERR_CONFIG_FILE_DELETE_REJECTED_BY_LISTENER_288=Entry %s cannot be removed from the \
  Directory Server configuration because one of the delete listeners registered \
  with the parent entry %s rejected this change with the message: %s
 ERR_CONFIG_FILE_DELETE_FAILED_289=An unexpected error occurred while \
@@ -394,6 +396,8 @@
 ERR_CONFIG_FILE_MODIFY_REJECTED_BY_CHANGE_LISTENER_291=Entry %s cannot \
  be modified because one of the configuration change listeners registered for \
  that entry rejected the change: %s
+ERR_CONFIG_FILE_MODIFY_FAILED_292=An unexpected error occurred while \
+ attempting to modify configuration entry %s as a child of entry %s: %s
 ERR_CONFIG_FILE_SEARCH_NO_SUCH_BASE_293=The search operation cannot be \
  processed because base entry %s does not exist
 ERR_CONFIG_FILE_SEARCH_INVALID_SCOPE_294=The search operation cannot be \
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/core/ConfigurationHandlerTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/core/ConfigurationHandlerTestCase.java
index 82d6237..ba98e87 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/core/ConfigurationHandlerTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/core/ConfigurationHandlerTestCase.java
@@ -56,17 +56,17 @@
         build();
 
     final ConfigurationHandler configHandler = new ConfigurationHandler(context);
-    configHandler.initialize();
+    configHandler.initializeWithPartialSchema();
     return configHandler;
   }
 
   @Test
-  public void testInitializeConfiguration() throws Exception
-  {
-    ConfigurationHandler configHandler = getConfigurationHandler();
+    public void testInitializeWithPartialSchemaConfiguration() throws Exception
+    {
+      ConfigurationHandler configHandler = getConfigurationHandler();
 
-    assertTrue(configHandler.hasEntry(DN_CONFIG));
-  }
+      assertTrue(configHandler.hasEntry(DN_CONFIG));
+    }
 
   @Test
   public void testGetEntry() throws Exception
@@ -330,6 +330,12 @@
   }
 
   @Test
+  public void testChangeListenerIsDeletedWhenConfigEntryIsDeleted()
+  {
+    // TODO
+  }
+
+  @Test
   public void testChangeListenerWithReplaceEntry() throws Exception
   {
     ConfigurationHandler configHandler = getConfigurationHandler();

--
Gitblit v1.10.0