/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2007-2008 Sun Microsystems, Inc.
* Portions copyright 2015 ForgeRock AS
*/
package org.forgerock.opendj.config.server;
import static com.forgerock.opendj.ldap.config.AdminMessages.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import com.forgerock.opendj.util.StaticUtils;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.forgerock.opendj.config.AbsoluteInheritedDefaultBehaviorProvider;
import org.forgerock.opendj.config.AbstractManagedObjectDefinition;
import org.forgerock.opendj.config.AliasDefaultBehaviorProvider;
import org.forgerock.opendj.config.Configuration;
import org.forgerock.opendj.config.Constraint;
import org.forgerock.opendj.config.DecodingException;
import org.forgerock.opendj.config.DefaultBehaviorProvider;
import org.forgerock.opendj.config.DefaultBehaviorProviderVisitor;
import org.forgerock.opendj.config.DefinedDefaultBehaviorProvider;
import org.forgerock.opendj.config.ManagedObjectDefinition;
import org.forgerock.opendj.config.ManagedObjectPath;
import org.forgerock.opendj.config.PropertyDefinition;
import org.forgerock.opendj.config.RelativeInheritedDefaultBehaviorProvider;
import org.forgerock.opendj.config.UndefinedDefaultBehaviorProvider;
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.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.ResultCode;
/**
* An adaptor class which converts {@link ConfigChangeListener} call-backs to
* {@link ServerManagedObjectChangeListener} call-backs.
*
* @param <S>
* The type of server configuration handled by the change listener.
*/
final class ConfigChangeListenerAdaptor<S extends Configuration> extends AbstractConfigListenerAdaptor implements
ConfigChangeListener {
private static final Logger debugLogger = LoggerFactory.getLogger(ConfigChangeListenerAdaptor.class);
private static final LocalizedLogger adminLogger = LocalizedLogger
.getLocalizedLogger(ERR_ADMIN_MANAGED_OBJECT_DOES_NOT_EXIST.get("").resourceName());
/**
* A default behavior visitor used for determining the set of dependencies.
*
* @param <T>
* The type of property.
*/
private static final class Visitor<T> implements DefaultBehaviorProviderVisitor<T, Void, ManagedObjectPath<?, ?>> {
/**
* Finds the dependencies associated with the provided property
* definition.
*
* @param <T> The type of property definition.
* @param path
* The current base path used for relative name resolution.
* @param pd
* The property definition.
* @param dependencies
* Add dependencies names to this collection.
*/
public static <T> void find(ManagedObjectPath<?, ?> path, PropertyDefinition<T> pd,
Collection<DN> dependencies) {
Visitor<T> v = new Visitor<>(dependencies);
DefaultBehaviorProvider<T> db = pd.getDefaultBehaviorProvider();
db.accept(v, path);
}
/** The names of entries that this change listener depends on. */
private final Collection<DN> dependencies;
/** Prevent instantiation. */
private Visitor(Collection<DN> dependencies) {
this.dependencies = dependencies;
}
/** {@inheritDoc} */
public Void visitAbsoluteInherited(AbsoluteInheritedDefaultBehaviorProvider<T> d, ManagedObjectPath<?, ?> p) {
ManagedObjectPath<?, ?> next = d.getManagedObjectPath();
dependencies.add(DNBuilder.create(next));
// If the dependent property uses inherited defaults then
// recursively get those as well.
String propertyName = d.getPropertyName();
AbstractManagedObjectDefinition<?, ?> mod = d.getManagedObjectDefinition();
PropertyDefinition<?> pd = mod.getPropertyDefinition(propertyName);
find(next, pd, dependencies);
return null;
}
/** {@inheritDoc} */
public Void visitAlias(AliasDefaultBehaviorProvider<T> d, ManagedObjectPath<?, ?> p) {
return null;
}
/** {@inheritDoc} */
public Void visitDefined(DefinedDefaultBehaviorProvider<T> d, ManagedObjectPath<?, ?> p) {
return null;
}
/** {@inheritDoc} */
public Void visitRelativeInherited(RelativeInheritedDefaultBehaviorProvider<T> d, ManagedObjectPath<?, ?> p) {
ManagedObjectPath<?, ?> next = d.getManagedObjectPath(p);
dependencies.add(DNBuilder.create(next));
// If the dependent property uses inherited defaults then
// recursively get those as well.
String propertyName = d.getPropertyName();
AbstractManagedObjectDefinition<?, ?> mod = d.getManagedObjectDefinition();
PropertyDefinition<?> pd = mod.getPropertyDefinition(propertyName);
find(next, pd, dependencies);
return null;
}
/** {@inheritDoc} */
public Void visitUndefined(UndefinedDefaultBehaviorProvider<T> d, ManagedObjectPath<?, ?> p) {
return null;
}
}
/** Cached managed object between accept/apply call-backs. */
private ServerManagedObject<? extends S> cachedManagedObject;
/**
* The delete listener which is used to remove this listener and any
* dependencies.
*/
private final ConfigDeleteListener cleanerListener;
/** The names of entries that this change listener depends on. */
private final Set<DN> dependencies;
/**
* The listener used to notify this listener when dependency entries are
* modified.
*/
private final ConfigChangeListener dependencyListener;
/** The DN associated with this listener. */
private final DN dn;
/** The underlying change listener. */
private final ServerManagedObjectChangeListener<? super S> listener;
/** The managed object path. */
private final ManagedObjectPath<?, S> path;
/** Repository of configuration entries. */
private final ConfigurationRepository configRepository;
private final ServerManagementContext serverContext;
/**
* Create a new configuration change listener adaptor.
*
* @param serverContext
* The server context.
* @param path
* The managed object path.
* @param listener
* The underlying change listener.
*/
public ConfigChangeListenerAdaptor(final ServerManagementContext serverContext,
final ManagedObjectPath<?, S> path, final ServerManagedObjectChangeListener<? super S> listener) {
this.serverContext = serverContext;
configRepository = serverContext.getConfigRepository();
this.path = path;
this.dn = DNBuilder.create(path);
this.listener = listener;
this.cachedManagedObject = null;
// This change listener should be notified when dependent entries
// are modified. Determine the dependencies and register change
// listeners against them.
this.dependencies = new HashSet<>();
this.dependencyListener = new ConfigChangeListener() {
public ConfigChangeResult applyConfigurationChange(Entry configEntry) {
Entry dependentConfigEntry = getConfigEntry(dn);
if (dependentConfigEntry != null) {
return ConfigChangeListenerAdaptor.this.applyConfigurationChange(dependentConfigEntry);
} else {
// The dependent entry was not found.
configRepository.deregisterChangeListener(configEntry.getName(), this);
return new ConfigChangeResult();
}
}
public boolean configChangeIsAcceptable(Entry configEntry, LocalizableMessageBuilder unacceptableReason) {
Entry dependentConfigEntry = getConfigEntry(dn);
if (dependentConfigEntry != null) {
return ConfigChangeListenerAdaptor.this.configChangeIsAcceptable(dependentConfigEntry,
unacceptableReason, configEntry);
} else {
// The dependent entry was not found.
configRepository.deregisterChangeListener(configEntry.getName(), this);
return true;
}
}
};
AbstractManagedObjectDefinition<?, ?> d = path.getManagedObjectDefinition();
for (PropertyDefinition<?> pd : d.getAllPropertyDefinitions()) {
Visitor.find(path, pd, dependencies);
}
for (DN entryDN : dependencies) {
// Be careful not to register listeners against the dependent
// entry itself.
if (!entryDN.equals(dn)) {
Entry configEntry = getConfigEntry(entryDN);
if (configEntry != null) {
configRepository.registerChangeListener(configEntry.getName(), dependencyListener);
}
}
}
// Register a delete listener against the parent which will
// finalize this change listener when the monitored configuration
// entry is removed.
this.cleanerListener = new ConfigDeleteListener() {
public ConfigChangeResult applyConfigurationDelete(Entry configEntry) {
// Perform finalization if the deleted entry is the monitored
// entry.
if (configEntry.getName().equals(dn)) {
finalizeChangeListener();
}
return new ConfigChangeResult();
}
public boolean configDeleteIsAcceptable(Entry configEntry, LocalizableMessageBuilder unacceptableReason) {
// Always acceptable.
return true;
}
};
DN parent = dn.parent();
if (parent != null) {
Entry configEntry = getConfigEntry(dn.parent());
if (configEntry != null) {
configRepository.registerDeleteListener(configEntry.getName(), cleanerListener);
}
}
}
/** {@inheritDoc} */
public ConfigChangeResult applyConfigurationChange(Entry configEntry) {
// Looking at the ConfigFileHandler implementation reveals
// that this ConfigEntry will actually be a different object to
// the one passed in the previous call-back (it will have the same
// content though). This configuration entry has the correct
// listener lists.
cachedManagedObject.setConfigDN(configEntry.getName());
ConfigChangeResult result = listener.applyConfigurationChange(cachedManagedObject);
// Now apply post constraint call-backs.
if (result.getResultCode() == ResultCode.SUCCESS) {
ManagedObjectDefinition<?, ?> d = cachedManagedObject.getManagedObjectDefinition();
for (Constraint constraint : d.getAllConstraints()) {
for (ServerConstraintHandler handler : constraint.getServerConstraintHandlers()) {
try {
handler.performPostModify(cachedManagedObject);
} catch (ConfigException e) {
debugLogger.trace("Unable to perform post modify", e);
}
}
}
}
return result;
}
/** {@inheritDoc} */
public boolean configChangeIsAcceptable(Entry configEntry, LocalizableMessageBuilder unacceptableReason) {
return configChangeIsAcceptable(configEntry, unacceptableReason, configEntry);
}
/**
* Indicates whether the configuration entry that will result from a
* proposed modification is acceptable to this change listener.
*
* @param configEntry
* The configuration entry that will result from the requested
* update.
* @param unacceptableReason
* A buffer to which this method can append a human-readable
* message explaining why the proposed change is not acceptable.
* @param newConfigEntry
* The configuration entry that caused the notification (will be
* different from <code>configEntry</code> if a dependency was
* modified).
* @return <CODE>true</CODE> if the proposed entry contains an acceptable
* configuration, or <CODE>false</CODE> if it does not.
*/
public boolean configChangeIsAcceptable(Entry configEntry, LocalizableMessageBuilder unacceptableReason,
Entry newConfigEntry) {
try {
cachedManagedObject = serverContext.decode(path, configEntry, newConfigEntry);
} catch (DecodingException e) {
unacceptableReason.append(e.getMessageObject());
return false;
}
// Give up immediately if a constraint violation occurs.
try {
cachedManagedObject.ensureIsUsable();
} catch (ConstraintViolationException e) {
generateUnacceptableReason(e.getMessages(), unacceptableReason);
return false;
}
// Let the change listener decide.
List<LocalizableMessage> reasons = new LinkedList<>();
if (listener.isConfigurationChangeAcceptable(cachedManagedObject, reasons)) {
return true;
} else {
generateUnacceptableReason(reasons, unacceptableReason);
return false;
}
}
/**
* Finalizes this configuration change listener adaptor. This method must be
* called before this change listener is removed.
*/
public void finalizeChangeListener() {
// Remove the dependency listeners.
for (DN dependency : dependencies) {
Entry listenerConfigEntry = getConfigEntry(dependency);
if (listenerConfigEntry != null) {
configRepository.deregisterChangeListener(listenerConfigEntry.getName(), dependencyListener);
}
}
// Now remove the cleaner listener as it will no longer be
// needed.
Entry parentConfigEntry = getConfigEntry(dn.parent());
if (parentConfigEntry != null) {
configRepository.deregisterDeleteListener(parentConfigEntry.getName(), cleanerListener);
}
}
/**
* Get the server managed object change listener associated with this
* adaptor.
*
* @return Returns the server managed object change listener associated with
* this adaptor.
*/
ServerManagedObjectChangeListener<? super S> getServerManagedObjectChangeListener() {
return listener;
}
/**
* Returns the named configuration entry or null if it could not be
* retrieved.
*/
private Entry getConfigEntry(DN dn) {
try {
if (configRepository.hasEntry(dn)) {
return configRepository.getEntry(dn);
} else {
adminLogger.error(ERR_ADMIN_MANAGED_OBJECT_DOES_NOT_EXIST, String.valueOf(dn));
}
} catch (ConfigException e) {
debugLogger.trace("The dependent entry could not be retrieved", e);
adminLogger.error(ERR_ADMIN_CANNOT_GET_MANAGED_OBJECT, String.valueOf(dn),
StaticUtils.getExceptionMessage(e));
}
return null;
}
}