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

Nicolas Capponi
25.38.2016 3f1ed4284d2b06b0bc85659c2017e40989b232db
Add/Update ConfigurationBackend and ConfigurationHandler classes.

ConfigurationBackend does not compile because referring to new config
framework.

These two classes are a replacement for ConfigFileHandler class that
will be deleted.
4 files modified
1 files added
2511 ■■■■ changed files
opendj-config/src/main/java/org/forgerock/opendj/config/server/ConfigChangeResult.java 28 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationBackend.java 480 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationHandler.java 1979 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/config.properties 6 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/core/ConfigurationHandlerTestCase.java 18 ●●●●● patch | view | raw | blame | history
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("})");
    }
}
opendj-server-legacy/src/main/java/org/opends/server/core/ConfigurationBackend.java
New file
@@ -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());
  }
}
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);
    }
  }
}
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 \
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();