/* * 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.config; import static org.forgerock.opendj.ldap.Entries.unmodifiableEntry; import static org.opends.messages.ConfigMessages.*; import static org.opends.server.config.ConfigConstants.*; import static org.opends.server.extensions.ExtensionsConstants.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; 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.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.LocalizableMessageDescriptor.Arg4; 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.Attribute; 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; import org.forgerock.opendj.ldap.LdapResultHandler; import org.forgerock.opendj.ldap.LinkedAttribute; import org.forgerock.opendj.ldap.LinkedHashMapEntry; import org.forgerock.opendj.ldap.MemoryBackend; import org.forgerock.opendj.ldap.RequestContext; 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; import org.forgerock.opendj.ldap.schema.Schema; import org.forgerock.opendj.ldap.schema.SchemaBuilder; import org.forgerock.opendj.ldif.EntryReader; import org.forgerock.opendj.ldif.LDIF; import org.forgerock.opendj.ldif.LDIFChangeRecordReader; import org.forgerock.opendj.ldif.LDIFEntryReader; import org.forgerock.opendj.ldif.LDIFEntryWriter; import org.forgerock.util.Utils; import org.opends.server.api.AlertGenerator; import org.opends.server.core.DirectoryServer; import org.opends.server.core.SearchOperation; import org.opends.server.core.ServerContext; import org.opends.server.schema.GeneralizedTimeSyntax; 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.util.LDIFException; import org.opends.server.util.TimeThread; /** * Responsible for managing configuration, including listeners on configuration entries. *
* 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. *
* 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. *
* The handler also maintains an up-to-date archive of configuration files.
*/
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 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;
/** The config root entry. */
private Entry rootEntry;
/** The add/delete/change listeners on configuration entries. */
private final ConcurrentHashMap
* The returned ConfigurationHandler is initialized with a partial schema and must be later
* re-initialized with the full schema by calling {@link #reinitializeWithFullSchema(Schema)}
* method once the schema has been fully loaded.
*
* @param serverContext
* The server context.
* @return the configuration handler
* @throws InitializationException
* If an error occurs during bootstrapping.
*/
public static ConfigurationHandler bootstrapConfiguration(ServerContext serverContext)
throws InitializationException {
final ConfigurationFramework configFramework = ConfigurationFramework.getInstance();
try
{
if (!configFramework.isInitialized())
{
configFramework.initialize(serverContext.getServerRoot(), serverContext.getInstanceRoot());
}
configFramework.setIsClient(false);
}
catch (ConfigException e)
{
LocalizableMessage msg = ERR_CANNOT_INITIALIZE_CONFIGURATION_FRAMEWORK.get(stackTraceToSingleLineString(e));
throw new InitializationException(msg, e);
}
final ConfigurationHandler configHandler = new ConfigurationHandler(serverContext);
configHandler.initializeWithPartialSchema();
return configHandler;
}
/**
* Initializes the configuration with an incomplete schema.
*
* 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.
*
* @throws InitializationException
* If an error occurs.
*/
private void initializeWithPartialSchema() throws InitializationException
{
File configFileToUse = preInitialization();
Schema configEnabledSchema = loadSchemaWithConfigurationEnabled();
loadConfiguration(configFileToUse, configEnabledSchema);
}
/**
* Re-initializes the configuration handler with a fully initialized schema.
*
* 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
* 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 = retrieveParentDNForAdd(entryDN);
// If the entry contains any expressions then these must be evaluated before passing to listeners.
final Entry evaluatedEntry = evaluateEntry(entry, ERR_CONFIG_FILE_ADD_REJECTED_DUE_TO_EVALUATION_FAILURE);
// Iterate through add listeners to make sure the new entry is acceptable.
final List
* 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)
{
throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
ERR_CONFIG_BACKEND_CANNOT_DELETE_ENTRY.get(stackTraceToSingleLineString(e)), e);
}
final DN parentDN = retrieveParentDNForDelete(dn);
// Iterate through delete listeners to make sure the deletion is acceptable.
final List
* 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 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);
}
if (!Entries.getStructuralObjectClass(oldEntry, serverContext.getSchema()).equals(
Entries.getStructuralObjectClass(newEntry, serverContext.getSchema())))
{
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
ERR_CONFIG_FILE_MODIFY_STRUCTURAL_CHANGE_NOT_ALLOWED.get(oldEntry.getName()));
}
// If the entry contains any expressions then these must be evaluated before passing to listeners.
final Entry evaluatedNewEntry = evaluateEntry(newEntry, ERR_CONFIG_FILE_MODIFY_REJECTED_DUE_TO_EVALUATION_FAILURE);
// Iterate through change listeners to make sure the change is acceptable.
final List
* This method must not be called if configuration can't be correctly initialized.
*
* 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
* 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
* It is the responsibility of the caller to ensure that reader is closed after usage.
*
* @param configFile
* LDIF file containing the configuration entries.
* @param schema
* Schema to validate entries when reading the config file.
* @return the LDIF reader
* @throws InitializationException
* If an error occurs.
*/
private EntryReader getLDIFReader(final File configFile, final Schema schema) throws InitializationException
{
try
{
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.getLocalizedMessage()), e);
}
}
/**
* Returns the entry listeners attached to the provided DN.
*
* If no listener exist for the provided DN, then a new set of empty listeners is created and
* returned.
*
* @param dn
* DN of a configuration entry.
* @return the listeners attached to the corresponding configuration entry.
*/
private EntryListeners getEntryListeners(final DN dn)
{
EntryListeners entryListeners = listeners.get(dn);
if (entryListeners == null)
{
entryListeners = new EntryListeners();
final EntryListeners previousListeners = listeners.putIfAbsent(dn, entryListeners);
if (previousListeners != null)
{
entryListeners = previousListeners;
}
}
return entryListeners;
}
/**
* Returns the parent DN of the configuration entry corresponding to the provided DN.
*
* @param entryDN
* DN of entry to retrieve the parent from.
* @return the parent DN
* @throws DirectoryException
* If entry has no parent or parent entry does not exist.
*/
private DN retrieveParentDNForAdd(final DN entryDN) throws DirectoryException
{
final DN parentDN = entryDN.parent();
if (parentDN == null)
{
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_ADD_NO_PARENT_DN.get(entryDN));
}
if (!backend.contains(parentDN))
{
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_ADD_NO_PARENT.get(entryDN, parentDN),
getMatchedDN(parentDN), null);
}
return parentDN;
}
/**
* Returns the parent DN of the configuration entry corresponding to the provided DN.
*
* @param entryDN
* DN of entry to retrieve the parent from.
* @return the parent DN
* @throws DirectoryException
* If entry has no parent or parent entry does not exist.
*/
private DN retrieveParentDNForDelete(final DN entryDN) throws DirectoryException
{
final DN parentDN = entryDN.parent();
if (parentDN == null)
{
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_DELETE_NO_PARENT_DN.get(entryDN));
}
if (!backend.contains(parentDN))
{
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_DELETE_NO_PARENT.get(entryDN),
getMatchedDN(parentDN), null);
}
return parentDN;
}
/** Returns the matched DN that is available in the configuration for the provided DN. */
private DN getMatchedDN(final DN dn)
{
DN matchedDN = null;
DN parentDN = dn.parent();
while (parentDN != null)
{
if (backend.contains(parentDN))
{
matchedDN = parentDN;
break;
}
parentDN = parentDN.parent();
}
return matchedDN;
}
/**
* Examines the provided result and logs a message if appropriate.
*
*
*
* 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
{
final String tempFilePath = changesFile.getAbsolutePath() + ".tmp";
try (final LDIFEntryReader sourceReader = new LDIFEntryReader(new FileReader(sourceFile));
final LDIFChangeRecordReader changeRecordReader = new LDIFChangeRecordReader(new FileReader(changesFile));
final LDIFEntryWriter ldifWriter = new LDIFEntryWriter(new FileWriter(tempFilePath)))
{
LDIF.copyTo(LDIF.patch(sourceReader, changeRecordReader), ldifWriter);
}
catch (final IOException e)
{
throw new LDIFException(ERR_CONFIG_UNABLE_TO_APPLY_CHANGES_FILE.get(e.getLocalizedMessage()));
}
// 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(tempFilePath).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.
*
*
*
* @param result
* The config change result object that
* @param entryDN
* The DN of the entry that was added, deleted, or modified.
* @param className
* The name of the class for the object that generated the provided result.
* @param methodName
* The name of the method that generated the provided result.
*/
private void handleConfigChangeResult(ConfigChangeResult result, DN entryDN, String className, String methodName)
{
if (result == null)
{
logger.error(ERR_CONFIG_CHANGE_NO_RESULT, className, methodName, entryDN);
return;
}
final ResultCode resultCode = result.getResultCode();
final boolean adminActionRequired = result.adminActionRequired();
final String messages = Utils.joinAsString(" ", result.getMessages());
if (resultCode != ResultCode.SUCCESS)
{
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, messages);
}
else if (!messages.isEmpty())
{
logger.debug(INFO_CONFIG_CHANGE_RESULT_MESSAGES, className, methodName, entryDN, messages);
}
}
private static Entry evaluateEntryIfPossible(final Entry entry)
{
try
{
return evaluateEntry(entry, ERR_CONFIG_FILE_READ_FAILED_DUE_TO_EVALUATION_FAILURE);
}
catch (final DirectoryException e)
{
// The entry contained an invalid expression. Fall-back to returning the original entry.
logger.traceException(e);
return entry;
}
}
private static Entry evaluateEntry(final Entry entry, final Arg4