/*
 * 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 2006-2009 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.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.Configuration;
import org.forgerock.opendj.config.Constraint;
import org.forgerock.opendj.config.InstantiableRelationDefinition;
import org.forgerock.opendj.config.ManagedObjectDefinition;
import org.forgerock.opendj.config.ManagedObjectPath;
import org.forgerock.opendj.config.OptionalRelationDefinition;
import org.forgerock.opendj.config.PropertyDefinition;
import org.forgerock.opendj.config.PropertyProvider;
import org.forgerock.opendj.config.RelationDefinition;
import org.forgerock.opendj.config.SetRelationDefinition;
import org.forgerock.opendj.config.SingletonRelationDefinition;
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.DN;
import org.forgerock.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A server-side managed object.
 *
 * @param <S>
 *            The type of server configuration represented by the server managed
 *            object.
 */
public final class ServerManagedObject<S extends Configuration> implements PropertyProvider {

    private static final Logger logger = LoggerFactory.getLogger(ServerManagedObject.class);

    /**
     * The DN of configuration entry associated with this server managed object,
     * which is {@code null} for root.
     */
    private DN configDN;

    private final ServerManagementContext serverContext;

    private final ConfigurationRepository configRepository;

    private final ManagedObjectDefinition<?, S> definition;

    /** The managed object path identifying this managed object's location. */
    private final ManagedObjectPath<?, S> path;

    private final Map<PropertyDefinition<?>, SortedSet<?>> properties;

    /**
     * Creates an new server side managed object.
     *
     * @param path
     *            The managed object path.
     * @param definition
     *            The managed object definition.
     * @param properties
     *            The managed object's properties.
     * @param configDN
     *            The configuration entry associated with the managed object.
     * @param context
     *            The server management context.
     */
    ServerManagedObject(final ManagedObjectPath<?, S> path, final ManagedObjectDefinition<?, S> definition,
            final Map<PropertyDefinition<?>, SortedSet<?>> properties, final DN configDN,
            final ServerManagementContext context) {
        this.definition = definition;
        this.path = path;
        this.properties = properties;
        this.configDN = configDN;
        this.serverContext = context;
        this.configRepository = context.getConfigRepository();
    }

    /**
     * Deregisters an existing configuration add listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The instantiable relation definition.
     * @param listener
     *            The configuration add listener.
     * @throws IllegalArgumentException
     *             If the instantiable relation definition is not associated
     *             with this managed object's definition.
     */
    public <M extends Configuration> void deregisterAddListener(InstantiableRelationDefinition<?, M> d,
            ConfigurationAddListener<M> listener) {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d);
        deregisterAddListener(baseDN, listener);
    }

    /**
     * Deregisters an existing server managed object add listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The instantiable relation definition.
     * @param listener
     *            The server managed object add listener.
     * @throws IllegalArgumentException
     *             If the instantiable relation definition is not associated
     *             with this managed object's definition.
     */
    public <M extends Configuration> void deregisterAddListener(InstantiableRelationDefinition<?, M> d,
            ServerManagedObjectAddListener<M> listener) {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d);
        deregisterAddListener(baseDN, listener);
    }

    /**
     * Deregisters an existing configuration add listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The optional relation definition.
     * @param listener
     *            The configuration add listener.
     * @throws IllegalArgumentException
     *             If the optional relation definition is not associated with
     *             this managed object's definition.
     */
    public <M extends Configuration> void deregisterAddListener(OptionalRelationDefinition<?, M> d,
            ConfigurationAddListener<M> listener) {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d).parent();
        deregisterAddListener(baseDN, listener);
    }

    /**
     * Deregisters an existing server managed object add listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The optional relation definition.
     * @param listener
     *            The server managed object add listener.
     * @throws IllegalArgumentException
     *             If the optional relation definition is not associated with
     *             this managed object's definition.
     */
    public <M extends Configuration> void deregisterAddListener(OptionalRelationDefinition<?, M> d,
            ServerManagedObjectAddListener<M> listener) {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d).parent();
        deregisterAddListener(baseDN, listener);
    }

    /**
     * Deregisters an existing configuration add listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The set relation definition.
     * @param listener
     *            The configuration add listener.
     * @throws IllegalArgumentException
     *             If the set relation definition is not associated with this
     *             managed object's definition.
     */
    public <M extends Configuration> void deregisterAddListener(SetRelationDefinition<?, M> d,
            ConfigurationAddListener<M> listener) {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d);
        deregisterAddListener(baseDN, listener);
    }

    /**
     * Deregisters an existing server managed object add listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The set relation definition.
     * @param listener
     *            The server managed object add listener.
     * @throws IllegalArgumentException
     *             If the set relation definition is not associated with this
     *             managed object's definition.
     */
    public <M extends Configuration> void deregisterAddListener(SetRelationDefinition<?, M> d,
            ServerManagedObjectAddListener<M> listener) {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d);
        deregisterAddListener(baseDN, listener);
    }

    /**
     * Deregisters an existing configuration change listener.
     *
     * @param listener
     *            The configuration change listener.
     */
    public void deregisterChangeListener(ConfigurationChangeListener<? super S> listener) {
        for (ConfigChangeListener l : configRepository.getChangeListeners(configDN)) {
            if (l instanceof ConfigChangeListenerAdaptor) {
                ConfigChangeListenerAdaptor<?> adaptor = (ConfigChangeListenerAdaptor<?>) l;
                ServerManagedObjectChangeListener<?> l2 = adaptor.getServerManagedObjectChangeListener();
                if (l2 instanceof ServerManagedObjectChangeListenerAdaptor<?>) {
                    ServerManagedObjectChangeListenerAdaptor<?> adaptor2 =
                        (ServerManagedObjectChangeListenerAdaptor<?>) l2;
                    if (adaptor2.getConfigurationChangeListener() == listener) {
                        adaptor.finalizeChangeListener();
                        configRepository.deregisterChangeListener(configDN, adaptor);
                    }
                }
            }
        }
    }

    /**
     * Deregisters an existing server managed object change listener.
     *
     * @param listener
     *            The server managed object change listener.
     */
    public void deregisterChangeListener(ServerManagedObjectChangeListener<? super S> listener) {
        for (ConfigChangeListener l : configRepository.getChangeListeners(configDN)) {
            if (l instanceof ConfigChangeListenerAdaptor) {
                ConfigChangeListenerAdaptor<?> adaptor = (ConfigChangeListenerAdaptor<?>) l;
                if (adaptor.getServerManagedObjectChangeListener() == listener) {
                    adaptor.finalizeChangeListener();
                    configRepository.deregisterChangeListener(configDN, adaptor);
                }
            }
        }
    }

    /**
     * Deregisters an existing configuration delete listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The instantiable relation definition.
     * @param listener
     *            The configuration delete listener.
     * @throws IllegalArgumentException
     *             If the instantiable relation definition is not associated
     *             with this managed object's definition.
     */
    public <M extends Configuration> void deregisterDeleteListener(InstantiableRelationDefinition<?, M> d,
            ConfigurationDeleteListener<M> listener) {
        validateRelationDefinition(d);

        DN baseDN = DNBuilder.create(path, d);
        deregisterDeleteListener(baseDN, listener);
    }

    /**
     * Deregisters an existing server managed object delete listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The instantiable relation definition.
     * @param listener
     *            The server managed object delete listener.
     * @throws IllegalArgumentException
     *             If the instantiable relation definition is not associated
     *             with this managed object's definition.
     */
    public <M extends Configuration> void deregisterDeleteListener(InstantiableRelationDefinition<?, M> d,
            ServerManagedObjectDeleteListener<M> listener) {
        validateRelationDefinition(d);

        DN baseDN = DNBuilder.create(path, d);
        deregisterDeleteListener(baseDN, listener);
    }

    /**
     * Deregisters an existing configuration delete listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The optional relation definition.
     * @param listener
     *            The configuration delete listener.
     * @throws IllegalArgumentException
     *             If the optional relation definition is not associated with
     *             this managed object's definition.
     */
    public <M extends Configuration> void deregisterDeleteListener(OptionalRelationDefinition<?, M> d,
            ConfigurationDeleteListener<M> listener) {
        validateRelationDefinition(d);

        DN baseDN = DNBuilder.create(path, d).parent();
        deregisterDeleteListener(baseDN, listener);
    }

    /**
     * Deregisters an existing server managed object delete listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The optional relation definition.
     * @param listener
     *            The server managed object delete listener.
     * @throws IllegalArgumentException
     *             If the optional relation definition is not associated with
     *             this managed object's definition.
     */
    public <M extends Configuration> void deregisterDeleteListener(OptionalRelationDefinition<?, M> d,
            ServerManagedObjectDeleteListener<M> listener) {
        validateRelationDefinition(d);

        DN baseDN = DNBuilder.create(path, d).parent();
        deregisterDeleteListener(baseDN, listener);
    }

    /**
     * Deregisters an existing configuration delete listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The set relation definition.
     * @param listener
     *            The configuration delete listener.
     * @throws IllegalArgumentException
     *             If the set relation definition is not associated with this
     *             managed object's definition.
     */
    public <M extends Configuration> void deregisterDeleteListener(SetRelationDefinition<?, M> d,
            ConfigurationDeleteListener<M> listener) {
        validateRelationDefinition(d);

        DN baseDN = DNBuilder.create(path, d);
        deregisterDeleteListener(baseDN, listener);
    }

    /**
     * Deregisters an existing server managed object delete listener.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The set relation definition.
     * @param listener
     *            The server managed object delete listener.
     * @throws IllegalArgumentException
     *             If the set relation definition is not associated with this
     *             managed object's definition.
     */
    public <M extends Configuration> void deregisterDeleteListener(SetRelationDefinition<?, M> d,
            ServerManagedObjectDeleteListener<M> listener) {
        validateRelationDefinition(d);

        DN baseDN = DNBuilder.create(path, d);
        deregisterDeleteListener(baseDN, listener);
    }

    /**
     * Retrieve an instantiable child managed object.
     *
     * @param <M>
     *            The requested type of the child server managed object
     *            configuration.
     * @param d
     *            The instantiable relation definition.
     * @param name
     *            The name of the child managed object.
     * @return Returns the instantiable child managed object.
     * @throws IllegalArgumentException
     *             If the relation definition is not associated with this
     *             managed object's definition.
     * @throws ConfigException
     *             If the child managed object could not be found or if it could
     *             not be decoded.
     */
    public <M extends Configuration> ServerManagedObject<? extends M> getChild(InstantiableRelationDefinition<?, M> d,
            String name) throws ConfigException {
        validateRelationDefinition(d);
        return serverContext.getManagedObject(path.child(d, name));
    }

    /**
     * Retrieve an optional child managed object.
     *
     * @param <M>
     *            The requested type of the child server managed object
     *            configuration.
     * @param d
     *            The optional relation definition.
     * @return Returns the optional child managed object.
     * @throws IllegalArgumentException
     *             If the optional relation definition is not associated with
     *             this managed object's definition.
     * @throws ConfigException
     *             If the child managed object could not be found or if it could
     *             not be decoded.
     */
    public <M extends Configuration> ServerManagedObject<? extends M> getChild(OptionalRelationDefinition<?, M> d)
            throws ConfigException {
        validateRelationDefinition(d);
        return serverContext.getManagedObject(path.child(d));
    }

    /**
     * Retrieve a set child managed object.
     *
     * @param <M>
     *            The requested type of the child server managed object
     *            configuration.
     * @param d
     *            The set relation definition.
     * @param name
     *            The name of the child managed object.
     * @return Returns the set child managed object.
     * @throws IllegalArgumentException
     *             If the relation definition is not associated with this
     *             managed object's definition or if {@code name} specifies a
     *             managed object definition which is not a sub-type of the
     *             relation's child definition.
     * @throws ConfigException
     *             If the child managed object could not be found or if it could
     *             not be decoded.
     */
    public <M extends Configuration> ServerManagedObject<? extends M> getChild(SetRelationDefinition<?, M> d,
            String name) throws ConfigException {
        validateRelationDefinition(d);

        return serverContext.getManagedObject(path.child(d, name));
    }

    /**
     * Retrieve a singleton child managed object.
     *
     * @param <M>
     *            The requested type of the child server managed object
     *            configuration.
     * @param d
     *            The singleton relation definition.
     * @return Returns the singleton child managed object.
     * @throws IllegalArgumentException
     *             If the relation definition is not associated with this
     *             managed object's definition.
     * @throws ConfigException
     *             If the child managed object could not be found or if it could
     *             not be decoded.
     */
    public <M extends Configuration> ServerManagedObject<? extends M> getChild(SingletonRelationDefinition<?, M> d)
            throws ConfigException {
        validateRelationDefinition(d);
        return serverContext.getManagedObject(path.child(d));
    }

    /**
     * Returns the server management context used by this object.
     *
     * @return the context
     */
    public ServerManagementContext getServerContext() {
        return serverContext;
    }

    /**
     * Creates a server configuration view of this managed object.
     *
     * @return Returns the server configuration view of this managed object.
     */
    public S getConfiguration() {
        return definition.createServerConfiguration(this);
    }

    /**
     * Get the DN of the LDAP entry associated with this server managed object.
     *
     * @return Returns the DN of the LDAP entry associated with this server
     *         managed object, or an null DN if this is the root managed object.
     */
    public DN getDN() {
        if (configDN != null) {
            return configDN;
        }
        return DN.rootDN();
    }

    /**
     * Get the definition associated with this server managed object.
     *
     * @return Returns the definition associated with this server managed
     *         object.
     */
    public ManagedObjectDefinition<?, S> getManagedObjectDefinition() {
        return definition;
    }

    /**
     * Get the path of this server managed object.
     *
     * @return Returns the path of this server managed object.
     */
    public ManagedObjectPath<?, S> getManagedObjectPath() {
        return path;
    }

    /**
     * Get the effective value of the specified property. If the property is
     * multi-valued then just the first value is returned. If the property does
     * not have a value then its default value is returned if it has one, or
     * <code>null</code> indicating that any default behavior is applicable.
     *
     * @param <T>
     *            The type of the property to be retrieved.
     * @param d
     *            The property to be retrieved.
     * @return Returns the property's effective value, or <code>null</code>
     *         indicating that any default behavior is applicable.
     * @throws IllegalArgumentException
     *             If the property definition is not associated with this
     *             managed object's definition.
     */
    public <T> T getPropertyValue(PropertyDefinition<T> d) {
        Set<T> values = getPropertyValues(d);
        if (!values.isEmpty()) {
            return values.iterator().next();
        }
        return null;
    }

    /**
     * Get the effective values of the specified property. If the property does
     * not have any values then its default values are returned if it has any,
     * or an empty set indicating that any default behavior is applicable.
     *
     * @param <T>
     *            The type of the property to be retrieved.
     * @param d
     *            The property to be retrieved.
     * @return Returns an unmodifiable set containing the property's effective
     *         values. An empty set indicates that the property has no default
     *         values defined and any default behavior is applicable.
     * @throws IllegalArgumentException
     *             If the property definition is not associated with this
     *             managed object's definition.
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T> SortedSet<T> getPropertyValues(PropertyDefinition<T> d) {
        if (!properties.containsKey(d)) {
            throw new IllegalArgumentException("Unknown property " + d.getName());
        }
        return Collections.unmodifiableSortedSet((SortedSet<T>) properties.get(d));
    }

    /**
     * Determines whether or not the optional managed object associated with the
     * specified optional relations exists.
     *
     * @param d
     *            The optional relation definition.
     * @return Returns <code>true</code> if the optional managed object exists,
     *         <code>false</code> otherwise.
     * @throws IllegalArgumentException
     *             If the optional relation definition is not associated with
     *             this managed object's definition.
     */
    public boolean hasChild(OptionalRelationDefinition<?, ?> d) {
        validateRelationDefinition(d);
        return serverContext.managedObjectExists(path.child(d));
    }

    /**
     * Lists the child managed objects associated with the specified
     * instantiable relation.
     *
     * @param d
     *            The instantiable relation definition.
     * @return Returns the names of the child managed objects.
     * @throws IllegalArgumentException
     *             If the relation definition is not associated with this
     *             managed object's definition.
     */
    public String[] listChildren(InstantiableRelationDefinition<?, ?> d) {
        validateRelationDefinition(d);
        return serverContext.listManagedObjects(path, d);
    }

    /**
     * Lists the child managed objects associated with the specified set
     * relation.
     *
     * @param d
     *            The set relation definition.
     * @return Returns the names of the child managed objects.
     * @throws IllegalArgumentException
     *             If the relation definition is not associated with this
     *             managed object's definition.
     */
    public String[] listChildren(SetRelationDefinition<?, ?> d) {
        validateRelationDefinition(d);
        return serverContext.listManagedObjects(path, d);
    }

    /**
     * Register to be notified when new child configurations are added beneath
     * an instantiable relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The instantiable relation definition.
     * @param listener
     *            The configuration add listener.
     * @throws IllegalArgumentException
     *             If the instantiable relation definition is not associated
     *             with this managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the instantiable
     *             relation could not be retrieved.
     */
    public <M extends Configuration> void registerAddListener(InstantiableRelationDefinition<?, M> d,
            ConfigurationAddListener<M> listener) throws ConfigException {
        registerAddListener(d, new ServerManagedObjectAddListenerAdaptor<M>(listener));
    }

    /**
     * Register to be notified when new child server managed object are added
     * beneath an instantiable relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The instantiable relation definition.
     * @param listener
     *            The server managed object add listener.
     * @throws IllegalArgumentException
     *             If the instantiable relation definition is not associated
     *             with this managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the instantiable
     *             relation could not be retrieved.
     */
    public <M extends Configuration> void registerAddListener(InstantiableRelationDefinition<?, M> d,
            ServerManagedObjectAddListener<M> listener) throws ConfigException {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d);
        ConfigAddListener adaptor = new ConfigAddListenerAdaptor<>(serverContext, path, d, listener);
        registerAddListener(baseDN, adaptor);
    }

    /**
     * Register to be notified when a new child configurations is added beneath
     * an optional relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The optional relation definition.
     * @param listener
     *            The configuration add listener.
     * @throws IllegalArgumentException
     *             If the optional relation definition is not associated with
     *             this managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the optional
     *             relation could not be retrieved.
     */
    public <M extends Configuration> void registerAddListener(OptionalRelationDefinition<?, M> d,
            ConfigurationAddListener<M> listener) throws ConfigException {
        registerAddListener(d, new ServerManagedObjectAddListenerAdaptor<M>(listener));
    }

    /**
     * Register to be notified when a new child server managed object is added
     * beneath an optional relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The optional relation definition.
     * @param listener
     *            The server managed object add listener.
     * @throws IllegalArgumentException
     *             If the optional relation definition is not associated with
     *             this managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the optional
     *             relation could not be retrieved.
     */
    public <M extends Configuration> void registerAddListener(OptionalRelationDefinition<?, M> d,
            ServerManagedObjectAddListener<M> listener) throws ConfigException {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d).parent();
        ConfigAddListener adaptor = new ConfigAddListenerAdaptor<>(serverContext, path, d, listener);
        registerAddListener(baseDN, adaptor);
    }

    /**
     * Register to be notified when new child configurations are added beneath a
     * set relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The set relation definition.
     * @param listener
     *            The configuration add listener.
     * @throws IllegalArgumentException
     *             If the set relation definition is not associated with this
     *             managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the set relation
     *             could not be retrieved.
     */
    public <M extends Configuration> void registerAddListener(SetRelationDefinition<?, M> d,
            ConfigurationAddListener<M> listener) throws ConfigException {
        registerAddListener(d, new ServerManagedObjectAddListenerAdaptor<M>(listener));
    }

    /**
     * Register to be notified when new child server managed object are added
     * beneath a set relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The set relation definition.
     * @param listener
     *            The server managed object add listener.
     * @throws IllegalArgumentException
     *             If the set relation definition is not associated with this
     *             managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the set relation
     *             could not be retrieved.
     */
    public <M extends Configuration> void registerAddListener(SetRelationDefinition<?, M> d,
            ServerManagedObjectAddListener<M> listener) throws ConfigException {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d);
        ConfigAddListener adaptor = new ConfigAddListenerAdaptor<>(serverContext, path, d, listener);
        registerAddListener(baseDN, adaptor);
    }

    /**
     * Register to be notified when this server managed object is changed.
     *
     * @param listener
     *            The configuration change listener.
     */
    public void registerChangeListener(ConfigurationChangeListener<? super S> listener) {
        registerChangeListener(new ServerManagedObjectChangeListenerAdaptor<S>(listener));
    }

    /**
     * Register to be notified when this server managed object is changed.
     *
     * @param listener
     *            The server managed object change listener.
     */
    public void registerChangeListener(ServerManagedObjectChangeListener<? super S> listener) {
        ConfigChangeListener adaptor = new ConfigChangeListenerAdaptor<>(serverContext, path, listener);
        configRepository.registerChangeListener(configDN, adaptor);

        // TODO : go toward this
        // Entry entry;
        // configBackend.registerChangeListener(entry.getName(), adapter));

        // Change listener registration usually signifies that a managed
        // object has been accepted and added to the server configuration
        // during initialization post-add.

        // FIXME: we should prevent multiple invocations in the case where
        // multiple change listeners are registered for the same object.
        for (Constraint constraint : definition.getAllConstraints()) {
            for (ServerConstraintHandler handler : constraint.getServerConstraintHandlers()) {
                try {
                    handler.performPostAdd(this);
                } catch (ConfigException e) {
                    logger.trace("Unable to perform post add", e);
                }
            }
        }
    }

    /**
     * Register to be notified when existing child configurations are deleted
     * beneath an instantiable relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The instantiable relation definition.
     * @param listener
     *            The configuration delete listener.
     * @throws IllegalArgumentException
     *             If the instantiable relation definition is not associated
     *             with this managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the instantiable
     *             relation could not be retrieved.
     */
    public <M extends Configuration> void registerDeleteListener(InstantiableRelationDefinition<?, M> d,
            ConfigurationDeleteListener<M> listener) throws ConfigException {
        registerDeleteListener(d, new ServerManagedObjectDeleteListenerAdaptor<M>(listener));
    }

    /**
     * Register to be notified when existing child server managed objects are
     * deleted beneath an instantiable relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The instantiable relation definition.
     * @param listener
     *            The server managed objects delete listener.
     * @throws IllegalArgumentException
     *             If the instantiable relation definition is not associated
     *             with this managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the instantiable
     *             relation could not be retrieved.
     */
    public <M extends Configuration> void registerDeleteListener(InstantiableRelationDefinition<?, M> d,
            ServerManagedObjectDeleteListener<M> listener) throws ConfigException {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d);
        ConfigDeleteListener adaptor = new ConfigDeleteListenerAdaptor<>(serverContext, path, d, listener);
        registerDeleteListener(baseDN, adaptor);
    }

    /**
     * Register to be notified when an existing child configuration is deleted
     * beneath an optional relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The optional relation definition.
     * @param listener
     *            The configuration delete listener.
     * @throws IllegalArgumentException
     *             If the optional relation definition is not associated with
     *             this managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the optional
     *             relation could not be retrieved.
     */
    public <M extends Configuration> void registerDeleteListener(OptionalRelationDefinition<?, M> d,
            ConfigurationDeleteListener<M> listener) throws ConfigException {
        registerDeleteListener(d, new ServerManagedObjectDeleteListenerAdaptor<M>(listener));
    }

    /**
     * Register to be notified when an existing child server managed object is
     * deleted beneath an optional relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The optional relation definition.
     * @param listener
     *            The server managed object delete listener.
     * @throws IllegalArgumentException
     *             If the optional relation definition is not associated with
     *             this managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the optional
     *             relation could not be retrieved.
     */
    public <M extends Configuration> void registerDeleteListener(OptionalRelationDefinition<?, M> d,
            ServerManagedObjectDeleteListener<M> listener) throws ConfigException {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d).parent();
        ConfigDeleteListener adaptor = new ConfigDeleteListenerAdaptor<>(serverContext, path, d, listener);
        registerDeleteListener(baseDN, adaptor);
    }

    /**
     * Register to be notified when existing child configurations are deleted
     * beneath a set relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The set relation definition.
     * @param listener
     *            The configuration delete listener.
     * @throws IllegalArgumentException
     *             If the set relation definition is not associated with this
     *             managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the set relation
     *             could not be retrieved.
     */
    public <M extends Configuration> void registerDeleteListener(SetRelationDefinition<?, M> d,
            ConfigurationDeleteListener<M> listener) throws ConfigException {
        registerDeleteListener(d, new ServerManagedObjectDeleteListenerAdaptor<M>(listener));
    }

    /**
     * Register to be notified when existing child server managed objects are
     * deleted beneath a set relation.
     *
     * @param <M>
     *            The type of the child server configuration object.
     * @param d
     *            The set relation definition.
     * @param listener
     *            The server managed objects delete listener.
     * @throws IllegalArgumentException
     *             If the set relation definition is not associated with this
     *             managed object's definition.
     * @throws ConfigException
     *             If the configuration entry associated with the set relation
     *             could not be retrieved.
     */
    public <M extends Configuration> void registerDeleteListener(SetRelationDefinition<?, M> d,
            ServerManagedObjectDeleteListener<M> listener) throws ConfigException {
        validateRelationDefinition(d);
        DN baseDN = DNBuilder.create(path, d);
        ConfigDeleteListener adaptor = new ConfigDeleteListenerAdaptor<>(serverContext, path, d, listener);
        registerDeleteListener(baseDN, adaptor);
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();

        builder.append("{ TYPE=");
        builder.append(definition.getName());
        builder.append(", DN=\"");
        builder.append(getDN());
        builder.append('\"');
        for (Map.Entry<PropertyDefinition<?>, SortedSet<?>> value : properties.entrySet()) {
            builder.append(", ");
            builder.append(value.getKey().getName());
            builder.append('=');
            builder.append(value.getValue());
        }
        builder.append(" }");

        return builder.toString();
    }

    /**
     * Determines whether or not this managed object can be used by the server.
     *
     * @throws ConstraintViolationException
     *             If one or more constraints determined that this managed
     *             object cannot be used by the server.
     */
    void ensureIsUsable() throws ConstraintViolationException {
        // Enforce any constraints.
        boolean isUsable = true;
        List<LocalizableMessage> reasons = new LinkedList<>();
        for (Constraint constraint : definition.getAllConstraints()) {
            for (ServerConstraintHandler handler : constraint.getServerConstraintHandlers()) {
                try {
                    if (!handler.isUsable(this, reasons)) {
                        isUsable = false;
                    }
                } catch (ConfigException e) {
                    LocalizableMessage message = ERR_SERVER_CONSTRAINT_EXCEPTION.get(e.getMessageObject());
                    reasons.add(message);
                    isUsable = false;
                }
            }
        }

        if (!isUsable) {
            throw new ConstraintViolationException(this, reasons);
        }
    }

    /**
     * Update the config DN associated with this server managed object. This
     * is only intended to be used by change listener call backs in order to
     * update the managed object with the correct config DN.
     *
     * @param configDN
     *            The DN of the underlying configuration entry.
     */
    void setConfigDN(DN configDN) {
        this.configDN = configDN;
    }

    /** Deregister an add listener. */
    private <M extends Configuration> void deregisterAddListener(DN baseDN, ConfigurationAddListener<M> listener) {
        try {
            if (configRepository.hasEntry(baseDN)) {
                for (ConfigAddListener configListener : configRepository.getAddListeners(baseDN)) {
                    if (configListener instanceof ConfigAddListenerAdaptor) {
                        ConfigAddListenerAdaptor<?> adaptor = (ConfigAddListenerAdaptor<?>) configListener;
                        ServerManagedObjectAddListener<?> smoListener = adaptor.getServerManagedObjectAddListener();
                        if (smoListener instanceof ServerManagedObjectAddListenerAdaptor<?>) {
                            ServerManagedObjectAddListenerAdaptor<?> adaptor2 =
                                (ServerManagedObjectAddListenerAdaptor<?>) smoListener;
                            if (adaptor2.getConfigurationAddListener() == listener) {
                                configRepository.deregisterAddListener(baseDN, adaptor);
                            }
                        }
                    }
                }
            } else {
                // The relation entry does not exist so check for and deregister
                // delayed add listener.
                deregisterDelayedAddListener(baseDN, listener);
            }
        } catch (ConfigException e) {
            // Ignore the exception since this implies deregistration.
            logger.trace("Unable to deregister add listener", e);
        }
    }

    /** Deregister an add listener. */
    private <M extends Configuration> void deregisterAddListener(DN baseDN,
        ServerManagedObjectAddListener<M> listener) {
        try {
            if (configRepository.hasEntry(baseDN)) {
                for (ConfigAddListener configListener : configRepository.getAddListeners(baseDN)) {
                    if (configListener instanceof ConfigAddListenerAdaptor) {
                        ConfigAddListenerAdaptor<?> adaptor = (ConfigAddListenerAdaptor<?>) configListener;
                        if (adaptor.getServerManagedObjectAddListener() == listener) {
                            configRepository.deregisterAddListener(baseDN, adaptor);
                        }
                    }
                }
            } else {
                // The relation entry does not exist so check for and deregister
                // delayed add listener.
                deregisterDelayedAddListener(baseDN, listener);
            }
        } catch (ConfigException e) {
            // Ignore the exception since this implies deregistration.
            logger.trace("Unable to deregister add listener", e);
        }
    }

    /**
     * Convenience method to retrieve the initial listener and its intermediate
     * adaptor from the provided configListener.
     *
     * @param <T>
     *            Type of the configuration.
     * @param configListener
     *            Listener from wich to extract the initial listener.
     * @return a pair of (intermediate adaptor, intermediate listener) or
     *         {@code Pair.EMPTY} if listener can't be extracted
     */
    // @Checkstyle:off
    static <T extends Configuration> Pair<ConfigAddListenerAdaptor<T>, ConfigurationAddListener<T>>
        extractInitialListener(ConfigAddListener configListener) {
        // @Checkstyle:on
        Pair<ConfigAddListenerAdaptor<T>, ServerManagedObjectAddListener<T>> pair =
                extractIntermediateListener(configListener);
        if (!pair.equals(Pair.EMPTY) && pair.getSecond() instanceof ServerManagedObjectAddListenerAdaptor) {
            ServerManagedObjectAddListenerAdaptor<T> adaptor2 = (ServerManagedObjectAddListenerAdaptor<T>)
                    pair.getSecond();
            return Pair.of(pair.getFirst(), adaptor2.getConfigurationAddListener());
        }
        return Pair.empty();
    }

    /**
     * Convenience method to retrieve the intermediate listener and its
     * intermediate adaptor from the provided configListener.
     *
     * @param <T>
     *            Type of the configuration.
     * @param configListener
     *            Listener from wich to extract the initial listener.
     * @return a pair of (intermediate adaptor, initial listener) or
     *         {@code Pair.EMPTY} if listener can't be extracted
     */
    @SuppressWarnings("unchecked")
    // @Checkstyle:off
    static <T extends Configuration> Pair<ConfigAddListenerAdaptor<T>, ServerManagedObjectAddListener<T>>
        extractIntermediateListener(ConfigAddListener configListener) {
        // @Checkstyle:on
        if (configListener instanceof ConfigAddListenerAdaptor) {
            ConfigAddListenerAdaptor<T> adaptor = (ConfigAddListenerAdaptor<T>) configListener;
            return Pair.of(adaptor, adaptor.getServerManagedObjectAddListener());
        }
        return Pair.empty();
    }

    /** Deregister a delete listener. */
    private <M extends Configuration> void deregisterDeleteListener(DN baseDN,
        ConfigurationDeleteListener<M> listener) {
        try {
            if (configRepository.hasEntry(baseDN)) {
                for (ConfigDeleteListener l : configRepository.getDeleteListeners(baseDN)) {
                    if (l instanceof ConfigDeleteListenerAdaptor) {
                        ConfigDeleteListenerAdaptor<?> adaptor = (ConfigDeleteListenerAdaptor<?>) l;
                        ServerManagedObjectDeleteListener<?> l2 = adaptor.getServerManagedObjectDeleteListener();
                        if (l2 instanceof ServerManagedObjectDeleteListenerAdaptor<?>) {
                            ServerManagedObjectDeleteListenerAdaptor<?> adaptor2 =
                                (ServerManagedObjectDeleteListenerAdaptor<?>) l2;
                            if (adaptor2.getConfigurationDeleteListener() == listener) {
                                configRepository.deregisterDeleteListener(baseDN, adaptor);
                            }
                        }
                    }
                }
            } else {
                // The relation entry does not exist so check for and deregister
                // delayed add listener.
                deregisterDelayedDeleteListener(baseDN, listener);
            }
        } catch (ConfigException e) {
            // Ignore the exception since this implies deregistration.
            logger.trace("Unable to deregister delete listener", e);
        }
    }

    /** Deregister a delete listener. */
    private <M extends Configuration> void deregisterDeleteListener(DN baseDN,
            ServerManagedObjectDeleteListener<M> listener) {
        try {
            if (configRepository.hasEntry(baseDN)) {
                for (ConfigDeleteListener l : configRepository.getDeleteListeners(baseDN)) {
                    if (l instanceof ConfigDeleteListenerAdaptor) {
                        ConfigDeleteListenerAdaptor<?> adaptor = (ConfigDeleteListenerAdaptor<?>) l;
                        if (adaptor.getServerManagedObjectDeleteListener() == listener) {
                            configRepository.deregisterDeleteListener(baseDN, adaptor);
                        }
                    }
                }
            } else {
                // The relation entry does not exist so check for and deregister
                // delayed add listener.
                deregisterDelayedDeleteListener(baseDN, listener);
            }
        } catch (ConfigException e) {
            // Ignore the exception since this implies deregistration.
            logger.trace("Unable to deregister delete listener", e);
        }
    }

    /** Register an instantiable or optional relation add listener. */
    private void registerAddListener(DN baseDN, ConfigAddListener adaptor) throws
        ConfigException {
        if (configRepository.hasEntry(baseDN)) {
            configRepository.registerAddListener(baseDN, adaptor);
        } else {
            // The relation entry does not exist yet
            // so register a delayed add listener.
            ConfigAddListener delayedListener = new DelayedConfigAddListener(baseDN, adaptor, configRepository);
            registerDelayedListener(baseDN, delayedListener);
        }
    }

    /**
     * Register a delayed listener with the nearest existing parent
     * entry to the provided base DN.
     */
    private void registerDelayedListener(DN baseDN, ConfigAddListener delayedListener) throws ConfigException {
        DN currentDN = baseDN.parent();
        DN previousDN = currentDN;
        while (currentDN != null) {
            if (!configRepository.hasEntry(currentDN)) {
                delayedListener = new DelayedConfigAddListener(currentDN, delayedListener, configRepository);
                previousDN = currentDN;
                currentDN = currentDN.parent();
            } else {
                configRepository.registerAddListener(previousDN, delayedListener);
                return;
            }
        }

        // No parent entry could be found.
        LocalizableMessage message = ERR_ADMIN_UNABLE_TO_REGISTER_LISTENER.get(String.valueOf(baseDN));
        throw new ConfigException(message);
    }

    /**
     * Deregister a delayed listener with the nearest existing parent
     * entry to the provided base DN.
     */
    private <M extends Configuration> void deregisterDelayedAddListener(DN baseDN,
        ConfigurationAddListener<M> listener) throws ConfigException {
        DN parentDN = baseDN.parent();
        int delayWrappers = 0;
        while (parentDN != null) {
            if (!configRepository.hasEntry(parentDN)) {
                parentDN = parentDN.parent();
                delayWrappers++;
            } else {
                for (ConfigAddListener configListener : configRepository.getAddListeners(parentDN)) {
                    if (configListener instanceof DelayedConfigAddListener) {
                        DelayedConfigAddListener delayListener = (DelayedConfigAddListener) configListener;
                        ConfigAddListener wrappedListener;

                        int i = delayWrappers;
                        for (; i > 0; i--) {
                            wrappedListener = delayListener.getDelayedAddListener();
                            if (wrappedListener instanceof DelayedConfigAddListener) {
                                delayListener = (DelayedConfigAddListener) configListener;
                            } else {
                                break;
                            }
                        }

                        if (i > 0) {
                            // There are not enough level of wrapping
                            // so this can't be the listener we are looking for.
                            continue;
                        }

                        ConfigAddListener delayedListener = delayListener.getDelayedAddListener();

                        if (delayedListener instanceof ConfigAddListenerAdaptor) {
                            ConfigAddListenerAdaptor<?> adaptor = (ConfigAddListenerAdaptor<?>) delayedListener;
                            ServerManagedObjectAddListener<?> l2 = adaptor.getServerManagedObjectAddListener();
                            if (l2 instanceof ServerManagedObjectAddListenerAdaptor<?>) {
                                ServerManagedObjectAddListenerAdaptor<?> adaptor2 =
                                    (ServerManagedObjectAddListenerAdaptor<?>) l2;
                                if (adaptor2.getConfigurationAddListener() == listener) {
                                    configRepository.deregisterAddListener(parentDN, configListener);
                                }
                            }
                        }
                    }
                }
                return;
            }
        }
    }

    /**
     * Deregister a delayed listener with the nearest existing parent
     * entry to the provided base DN.
     */
    private <M extends Configuration> void deregisterDelayedDeleteListener(DN baseDN,
            ConfigurationDeleteListener<M> listener) throws ConfigException {
        DN parentDN = baseDN.parent();
        int delayWrappers = 0;
        while (parentDN != null) {
            if (!configRepository.hasEntry(parentDN)) {
                parentDN = parentDN.parent();
                delayWrappers++;
            } else {
                for (ConfigAddListener l : configRepository.getAddListeners(parentDN)) {
                    if (l instanceof DelayedConfigAddListener) {
                        DelayedConfigAddListener delayListener = (DelayedConfigAddListener) l;
                        ConfigAddListener wrappedListener;

                        int i = delayWrappers;
                        for (; i > 0; i--) {
                            wrappedListener = delayListener.getDelayedAddListener();
                            if (wrappedListener instanceof DelayedConfigAddListener) {
                                delayListener = (DelayedConfigAddListener) l;
                            } else {
                                break;
                            }
                        }

                        if (i > 0) {
                            // There are not enough level of wrapping
                            // so this can't be the listener we are looking for.
                            continue;
                        }

                        ConfigDeleteListener delayedListener = delayListener.getDelayedDeleteListener();

                        if (delayedListener instanceof ConfigDeleteListenerAdaptor) {
                            ConfigDeleteListenerAdaptor<?> adaptor = (ConfigDeleteListenerAdaptor<?>) delayedListener;
                            ServerManagedObjectDeleteListener<?> l2 = adaptor.getServerManagedObjectDeleteListener();
                            if (l2 instanceof ServerManagedObjectDeleteListenerAdaptor<?>) {
                                ServerManagedObjectDeleteListenerAdaptor<?> adaptor2 =
                                    (ServerManagedObjectDeleteListenerAdaptor<?>) l2;
                                if (adaptor2.getConfigurationDeleteListener() == listener) {
                                    configRepository.deregisterAddListener(parentDN, l);
                                }
                            }
                        }
                    }
                }
                return;
            }
        }
    }

    /**
     * Deregister a delayed listener with the nearest existing parent
     * entry to the provided base DN.
     */
    private <M extends Configuration> void deregisterDelayedAddListener(DN baseDN,
            ServerManagedObjectAddListener<M> listener) throws ConfigException {
        DN parentDN = baseDN.parent();
        int delayWrappers = 0;
        while (parentDN != null) {
            if (!configRepository.hasEntry(parentDN)) {
                parentDN = parentDN.parent();
                delayWrappers++;
            } else {
                for (ConfigAddListener configListener : configRepository.getAddListeners(parentDN)) {
                    if (configListener instanceof DelayedConfigAddListener) {
                        DelayedConfigAddListener delayListener = (DelayedConfigAddListener) configListener;
                        ConfigAddListener wrappedListener;

                        int i = delayWrappers;
                        for (; i > 0; i--) {
                            wrappedListener = delayListener.getDelayedAddListener();
                            if (wrappedListener instanceof DelayedConfigAddListener) {
                                delayListener = (DelayedConfigAddListener) configListener;
                            } else {
                                break;
                            }
                        }

                        if (i > 0) {
                            // There are not enough level of wrapping
                            // so this can't be the listener we are looking for.
                            continue;
                        }

                        ConfigAddListener delayedListener = delayListener.getDelayedAddListener();

                        if (delayedListener instanceof ConfigAddListenerAdaptor) {
                            ConfigAddListenerAdaptor<?> adaptor = (ConfigAddListenerAdaptor<?>) delayedListener;
                            if (adaptor.getServerManagedObjectAddListener() == listener) {
                                configRepository.deregisterAddListener(parentDN, configListener);
                            }
                        }
                    }
                }
                return;
            }
        }
    }

    /**
     * Deregister a delayed listener with the nearest existing parent
     * entry to the provided base DN.
     */
    private <M extends Configuration> void deregisterDelayedDeleteListener(DN baseDN,
            ServerManagedObjectDeleteListener<M> listener) throws ConfigException {
        DN parentDN = baseDN.parent();
        int delayWrappers = 0;
        while (parentDN != null) {
            if (!configRepository.hasEntry(parentDN)) {
                parentDN = parentDN.parent();
                delayWrappers++;
            } else {
                for (ConfigAddListener configListener : configRepository.getAddListeners(parentDN)) {
                    if (configListener instanceof DelayedConfigAddListener) {
                        DelayedConfigAddListener delayListener = (DelayedConfigAddListener) configListener;
                        ConfigAddListener wrappedListener;

                        int i = delayWrappers;
                        for (; i > 0; i--) {
                            wrappedListener = delayListener.getDelayedAddListener();
                            if (wrappedListener instanceof DelayedConfigAddListener) {
                                delayListener = (DelayedConfigAddListener) configListener;
                            } else {
                                break;
                            }
                        }

                        if (i > 0) {
                            // There are not enough level of wrapping
                            // so this can't be the listener we are looking for.
                            continue;
                        }

                        ConfigDeleteListener delayedListener = delayListener.getDelayedDeleteListener();

                        if (delayedListener instanceof ConfigDeleteListenerAdaptor) {
                            ConfigDeleteListenerAdaptor<?> adaptor = (ConfigDeleteListenerAdaptor<?>) delayedListener;
                            if (adaptor.getServerManagedObjectDeleteListener() == listener) {
                                configRepository.deregisterAddListener(parentDN, configListener);
                            }
                        }
                    }
                }
                return;
            }
        }
    }

    /** Register an instantiable or optional relation delete listener. */
    private void registerDeleteListener(DN baseDN, ConfigDeleteListener adaptor) throws ConfigException {
        if (configRepository.hasEntry(baseDN)) {
            configRepository.registerDeleteListener(baseDN, adaptor);
        } else {
            // The relation entry does not exist yet
            // so register a delayed add listener.
            ConfigAddListener delayedListener = new DelayedConfigAddListener(baseDN, adaptor, configRepository);
            registerDelayedListener(baseDN, delayedListener);
        }
    }

    /** Validate that a relation definition belongs to this managed object. */
    private void validateRelationDefinition(RelationDefinition<?, ?> rd) {
        RelationDefinition<?, ?> tmp = definition.getRelationDefinition(rd.getName());
        if (tmp != rd) {
            throw new IllegalArgumentException("The relation " + rd.getName() + " is not associated with a "
                    + definition.getName());
        }
    }
}
