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