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

matthew_swift
12.37.2007 429235331815195cfdc50ff1b0f31be270ed5eba
Partial fix for issue 1449: improve server-side referential integrity support.

Previous to this change referential integrity was only enforced when a referencing (aggregating) component was added or modified. The support did not prevent a referenced component from being disabled or deleted. This change adds this remaining support:

* a component cannot be deleted if it is referenced by one or more components

* a component cannot be disabled if it is referenced by one or more components (it is possible to restrict this constraint so that it only applies when the referencing component(s) are enabled)

This implementation only enforces referential integrity for referencing components which have listeners associated with them. For example, if component A references component B, then referential integrity will only be enforced automatically if component A has a change listener registered against it, or if it is "added" using an add listener that was registered against its parent. In effect, referential integrity is only enforced for components which are in use or are about to be used.
5 files modified
1100 ■■■■ changed files
opends/src/messages/messages/admin.properties 16 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/admin/AggregationPropertyDefinition.java 307 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/admin/MockLDAPProfile.java 2 ●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/admin/TestCfg.java 14 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/admin/server/AggregationTest.java 761 ●●●● patch | view | raw | blame | history
opends/src/messages/messages/admin.properties
@@ -260,4 +260,18 @@
SEVERE_ERR_CONSTRAINT_VIOLATION_EXCEPTION_PLURAL_113=The following \
 constraint violations occurred: %s
SEVERE_ERR_SERVER_REFINT_DANGLING_REFERENCE_114=The value "%s" in \
 property "%s" in the %s in entry "%s" refers to a non-existent %s at "%s"
 property "%s" in the %s in entry "%s" refers to a non-existent %s \
 in entry "%s"
SEVERE_ERR_SERVER_REFINT_SOURCE_ENABLED_TARGET_DISABLED_115=The value \
 "%s" in property "%s" in the enabled %s in entry "%s" refers to a \
 disabled %s in entry "%s"
SEVERE_ERR_SERVER_REFINT_TARGET_DISABLED_116=The value "%s" in \
 property "%s" in the %s in entry "%s" refers to a disabled %s in \
 entry "%s"
SEVERE_ERR_SERVER_REFINT_CANNOT_DELETE_117=The %s in entry "%s" \
 cannot be deleted because it is referenced by the "%s" property \
 of the %s in entry "%s"
SEVERE_ERR_SERVER_REFINT_CANNOT_DISABLE_118=The %s in entry "%s" \
 cannot be disabled because it is referenced by the "%s" property \
 of the %s in entry "%s"
opends/src/server/org/opends/server/admin/AggregationPropertyDefinition.java
@@ -34,14 +34,21 @@
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import org.opends.messages.Message;
import org.opends.server.admin.client.ClientConstraintHandler;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.server.ConfigurationDeleteListener;
import org.opends.server.admin.server.ServerConstraintHandler;
import org.opends.server.admin.server.ServerManagedObject;
import org.opends.server.admin.server.ServerManagementContext;
import org.opends.server.config.ConfigException;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.DN;
@@ -285,11 +292,124 @@
  /**
   * A change listener which prevents the named component from being
   * disabled.
   */
  private class ReferentialIntegrityChangeListener implements
      ConfigurationChangeListener<S> {
    // The error message which should be returned if an attempt is
    // made to disable the referenced component.
    private final Message message;
    // The path of the referenced component.
    private final ManagedObjectPath<C, S> path;
    // Creates a new referential integrity delete listener.
    private ReferentialIntegrityChangeListener(ManagedObjectPath<C, S> path,
        Message message) {
      this.path = path;
      this.message = message;
    }
    /**
     * {@inheritDoc}
     */
    public ConfigChangeResult applyConfigurationChange(S configuration) {
      throw new IllegalStateException("Attempting to disable a referenced "
          + configuration.definition().getUserFriendlyName());
    }
    /**
     * {@inheritDoc}
     */
    public boolean isConfigurationChangeAcceptable(S configuration,
        List<Message> unacceptableReasons) {
      // Always prevent the referenced component from being
      // disabled.
      PropertyProvider provider = configuration.properties();
      Collection<Boolean> values = provider
          .getPropertyValues(getTargetEnabledPropertyDefinition());
      if (values.iterator().next() == false) {
        unacceptableReasons.add(message);
        return false;
      }
      return true;
    }
    // Gets the path associated with this listener.
    private ManagedObjectPath<C, S> getManagedObjectPath() {
      return path;
    }
  }
  /**
   * A delete listener which prevents the named component from being
   * deleted.
   */
  private class ReferentialIntegrityDeleteListener implements
      ConfigurationDeleteListener<S> {
    // The DN of the referenced configuration entry.
    private final DN dn;
    // The error message which should be returned if an attempt is
    // made to delete the referenced component.
    private final Message message;
    // Creates a new referential integrity delete listener.
    private ReferentialIntegrityDeleteListener(DN dn, Message message) {
      this.dn = dn;
      this.message = message;
    }
    /**
     * {@inheritDoc}
     */
    public ConfigChangeResult applyConfigurationDelete(S configuration) {
      throw new IllegalStateException("Attempting to delete a referenced "
          + configuration.definition().getUserFriendlyName());
    }
    /**
     * {@inheritDoc}
     */
    public boolean isConfigurationDeleteAcceptable(S configuration,
        List<Message> unacceptableReasons) {
      if (configuration.dn().equals(dn)) {
        // Always prevent deletion of the referenced component.
        unacceptableReasons.add(message);
        return false;
      }
      return true;
    }
  }
  /**
   * The server-side constraint handler implementation.
   */
  private static class ServerHandler
      <C extends ConfigurationClient, S extends Configuration>
      extends ServerConstraintHandler {
  private class ServerHandler extends ServerConstraintHandler {
    // The associated property definition.
    private final AggregationPropertyDefinition<C, S> pd;
@@ -311,23 +431,178 @@
        Collection<Message> unacceptableReasons) throws ConfigException {
      SortedSet<String> names = managedObject.getPropertyValues(pd);
      ServerManagementContext context = ServerManagementContext.getInstance();
      boolean isUsable = true;
      BooleanPropertyDefinition tpd = pd.getTargetEnabledPropertyDefinition();
      BooleanPropertyDefinition spd = pd.getSourceEnabledPropertyDefinition();
      Message thisUFN = managedObject.getManagedObjectDefinition()
          .getUserFriendlyName();
      String thisDN = managedObject.getDN().toString();
      Message thatUFN = pd.getRelationDefinition().getUserFriendlyName();
      boolean isUsable = true;
      for (String name : names) {
        ManagedObjectPath<C, S> path = pd.getChildPath(name);
        String thatDN = path.toDN().toString();
        if (!context.managedObjectExists(path)) {
          Message msg = ERR_SERVER_REFINT_DANGLING_REFERENCE.get(name, pd
              .getName(), managedObject.getManagedObjectDefinition()
              .getUserFriendlyName(), managedObject.getDN().toString(), pd
              .getRelationDefinition().getUserFriendlyName(), path.toDN()
              .toString());
              .getName(), thisUFN, thisDN, thatUFN, thatDN);
          unacceptableReasons.add(msg);
          isUsable = false;
        } else if (tpd != null) {
          // Check that the referenced component is enabled.
          ServerManagedObject<? extends S> ref = context.getManagedObject(path);
          if (spd != null) {
            // Target must be enabled but only if the source is
            // enabled.
            if (managedObject.getPropertyValue(spd)
                && !ref.getPropertyValue(tpd)) {
              Message msg = ERR_SERVER_REFINT_SOURCE_ENABLED_TARGET_DISABLED
                  .get(name, pd.getName(), thisUFN, thisDN, thatUFN, thatDN);
              unacceptableReasons.add(msg);
              isUsable = false;
            }
          } else {
            // Target must always be enabled.
            if (!ref.getPropertyValue(tpd)) {
              Message msg = ERR_SERVER_REFINT_TARGET_DISABLED.get(name, pd
                  .getName(), thisUFN, thisDN, thatUFN, thatDN);
              unacceptableReasons.add(msg);
              isUsable = false;
            }
          }
        }
      }
      return isUsable;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void performPostAdd(ServerManagedObject<?> managedObject)
        throws ConfigException {
      // First make sure existing listeners associated with this
      // managed object are removed. This is required in order to
      // prevent multiple change listener registrations from
      // occurring, for example if this call-back is invoked multiple
      // times after the same add event.
      performPostDelete(managedObject);
      // Add change and delete listeners against all referenced
      // components.
      BooleanPropertyDefinition tpd = pd.getTargetEnabledPropertyDefinition();
      BooleanPropertyDefinition spd = pd.getSourceEnabledPropertyDefinition();
      Message thisUFN = managedObject.getManagedObjectDefinition()
          .getUserFriendlyName();
      String thisDN = managedObject.getDN().toString();
      Message thatUFN = pd.getRelationDefinition().getUserFriendlyName();
      // Referenced managed objects will only need a change listener
      // if they have can be disabled.
      boolean needsChangeListeners;
      if (tpd != null) {
        if (spd == null) {
          needsChangeListeners = true;
        } else {
          needsChangeListeners = managedObject.getPropertyValue(spd);
        }
      } else {
        needsChangeListeners = false;
      }
      // Delete listeners need to be registered against the parent
      // entry of the referenced components.
      ServerManagementContext context = ServerManagementContext.getInstance();
      ManagedObjectPath<?, ?> parentPath = pd.getParentPath();
      ServerManagedObject<?> parent = context.getManagedObject(parentPath);
      // Create entries in the listener tables.
      List<ReferentialIntegrityDeleteListener> dlist =
        new LinkedList<ReferentialIntegrityDeleteListener>();
      deleteListeners.put(managedObject.getDN(), dlist);
      List<ReferentialIntegrityChangeListener> clist =
        new LinkedList<ReferentialIntegrityChangeListener>();
      changeListeners.put(managedObject.getDN(), clist);
      for (String name : managedObject.getPropertyValues(pd)) {
        ManagedObjectPath<C, S> path = pd.getChildPath(name);
        DN dn = path.toDN();
        String thatDN = dn.toString();
        // Register the delete listener.
        Message msg = ERR_SERVER_REFINT_CANNOT_DELETE.get(thatUFN, thatDN, pd
            .getName(), thisUFN, thisDN);
        ReferentialIntegrityDeleteListener dl =
          new ReferentialIntegrityDeleteListener(dn, msg);
        parent.registerDeleteListener(pd.getRelationDefinition(), dl);
        dlist.add(dl);
        // Register the change listener if required.
        if (needsChangeListeners) {
          ServerManagedObject<? extends S> ref = context.getManagedObject(path);
          msg = ERR_SERVER_REFINT_CANNOT_DISABLE.get(thatUFN, thatDN, pd
              .getName(), thisUFN, thisDN);
          ReferentialIntegrityChangeListener cl =
            new ReferentialIntegrityChangeListener(path, msg);
          ref.registerChangeListener(cl);
          clist.add(cl);
        }
      }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void performPostDelete(ServerManagedObject<?> managedObject)
        throws ConfigException {
      // Remove any registered delete and change listeners.
      ServerManagementContext context = ServerManagementContext.getInstance();
      DN dn = managedObject.getDN();
      // Delete listeners need to be deregistered against the parent
      // entry of the referenced components.
      ManagedObjectPath<?, ?> parentPath = pd.getParentPath();
      ServerManagedObject<?> parent = context.getManagedObject(parentPath);
      if (deleteListeners.containsKey(dn)) {
        for (ReferentialIntegrityDeleteListener dl : deleteListeners.get(dn)) {
          parent.deregisterDeleteListener(pd.getRelationDefinition(), dl);
        }
        deleteListeners.remove(dn);
      }
      // Change listeners need to be deregistered from their
      // associated referenced component.
      if (changeListeners.containsKey(dn)) {
        for (ReferentialIntegrityChangeListener cl : changeListeners.get(dn)) {
          ManagedObjectPath<C, S> path = cl.getManagedObjectPath();
          ServerManagedObject<? extends S> ref = context.getManagedObject(path);
          ref.deregisterChangeListener(cl);
        }
        changeListeners.remove(dn);
      }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void performPostModify(ServerManagedObject<?> managedObject)
        throws ConfigException {
      // Remove all the constraints associated with this managed
      // object and then re-register them.
      performPostDelete(managedObject);
      performPostAdd(managedObject);
    }
  }
@@ -349,11 +624,23 @@
   * @return Returns the new aggregation property definition builder.
   */
  public static <C extends ConfigurationClient, S extends Configuration>
  Builder<C, S> createBuilder(
      Builder<C, S> createBuilder(
      AbstractManagedObjectDefinition<?, ?> d, String propertyName) {
    return new Builder<C, S>(d, propertyName);
  }
  // The active server-side referential integrity change listeners
  // associated with this property.
  private final Map<DN, List<ReferentialIntegrityChangeListener>>
      changeListeners =
        new HashMap<DN, List<ReferentialIntegrityChangeListener>>();
  // The active server-side referential integrity delete listeners
  // associated with this property.
  private final Map<DN, List<ReferentialIntegrityDeleteListener>>
      deleteListeners =
        new HashMap<DN, List<ReferentialIntegrityDeleteListener>>();
  // The name of the managed object which is the parent of the
  // aggregated managed objects.
  private final ManagedObjectPath<?, ?> parentPath;
@@ -502,7 +789,7 @@
   * {@inheritDoc}
   */
  public Collection<ServerConstraintHandler> getServerConstraintHandlers() {
    ServerConstraintHandler handler = new ServerHandler<C, S>(this);
    ServerConstraintHandler handler = new ServerHandler(this);
    return Collections.singleton(handler);
  }
opends/tests/unit-tests-testng/src/server/org/opends/server/admin/MockLDAPProfile.java
@@ -81,7 +81,7 @@
        return "ds-cfg-virtual-attribute-base-dn";
      } else if (pd == (PropertyDefinition<?>)td.getOptionalMultiValuedDNProperty2PropertyDefinition()) {
        return "ds-cfg-virtual-attribute-group-dn";
      } else if (pd == (PropertyDefinition<?>)td.getAggregationPropertyPropertyDefinition()) {
      } else if (pd.getName().equals("aggregation-property")) {
        return "ds-cfg-backend-base-dn";
      } else {
        throw new RuntimeException("Unexpected test-child property"
opends/tests/unit-tests-testng/src/server/org/opends/server/admin/TestCfg.java
@@ -228,6 +228,20 @@
  /**
   * Adds a property definition temporarily with test child
   * definition, replacing any existing property definition with the
   * same name.
   *
   * @param pd
   *          The property definition.
   */
  public static void addPropertyDefinition(PropertyDefinition<?> pd) {
    TestChildCfgDefn.getInstance().registerPropertyDefinition(pd);
  }
  /**
   * Removes a constraint from the test child definition.
   *
   * @param constraint
opends/tests/unit-tests-testng/src/server/org/opends/server/admin/server/AggregationTest.java
@@ -28,7 +28,9 @@
import java.net.ServerSocket;
import java.util.Collection;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
@@ -37,16 +39,31 @@
import org.opends.messages.Message;
import org.opends.server.TestCaseUtils;
import org.opends.server.admin.AdminTestCase;
import org.opends.server.admin.AdministratorAction;
import org.opends.server.admin.AggregationPropertyDefinition;
import org.opends.server.admin.IllegalPropertyValueStringException;
import org.opends.server.admin.ManagedObjectNotFoundException;
import org.opends.server.admin.ManagedObjectPath;
import org.opends.server.admin.PropertyException;
import org.opends.server.admin.PropertyOption;
import org.opends.server.admin.TestCfg;
import org.opends.server.admin.TestChildCfg;
import org.opends.server.admin.TestChildCfgDefn;
import org.opends.server.admin.TestParentCfg;
import org.opends.server.admin.UndefinedDefaultBehaviorProvider;
import org.opends.server.admin.client.OperationRejectedException;
import org.opends.server.admin.client.ldap.JNDIDirContextAdaptor;
import org.opends.server.admin.std.client.ConnectionHandlerCfgClient;
import org.opends.server.admin.std.client.LDAPConnectionHandlerCfgClient;
import org.opends.server.admin.std.client.RootCfgClient;
import org.opends.server.admin.std.meta.ConnectionHandlerCfgDefn;
import org.opends.server.admin.std.meta.LDAPConnectionHandlerCfgDefn;
import org.opends.server.admin.std.server.ConnectionHandlerCfg;
import org.opends.server.admin.std.server.RootCfg;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.ResultCode;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
@@ -57,9 +74,70 @@
/**
 * Test cases for aggregations on the server-side.
 */
@Test(sequential=true)
@Test(sequential = true)
public final class AggregationTest extends AdminTestCase {
  /**
   * Dummy change listener for triggering change constraint
   * call-backs.
   */
  private static final class DummyChangeListener implements
      ConfigurationChangeListener<TestChildCfg> {
    /**
     * {@inheritDoc}
     */
    public ConfigChangeResult applyConfigurationChange(
        TestChildCfg configuration) {
      return new ConfigChangeResult(ResultCode.SUCCESS, false);
    }
    /**
     * {@inheritDoc}
     */
    public boolean isConfigurationChangeAcceptable(TestChildCfg configuration,
        List<Message> unacceptableReasons) {
      return true;
    }
  }
  /**
   * Dummy delete listener for triggering delete constraint
   * call-backs.
   */
  private static final class DummyDeleteListener implements
      ConfigurationDeleteListener<TestChildCfg> {
    /**
     * {@inheritDoc}
     */
    public ConfigChangeResult applyConfigurationDelete(
        TestChildCfg configuration) {
      return new ConfigChangeResult(ResultCode.SUCCESS, false);
    }
    /**
     * {@inheritDoc}
     */
    public boolean isConfigurationDeleteAcceptable(TestChildCfg configuration,
        List<Message> unacceptableReasons) {
      return true;
    }
  }
  private static final String TEST_CHILD_7_DN = "cn=test child 7,cn=test children,cn=test parent 1,cn=test parents,cn=config";
  private static final String TEST_CHILD_6_DN = "cn=test child 6,cn=test children,cn=test parent 1,cn=test parents,cn=config";
  // The name of the test connection handler.
  private static final String TEST_CONNECTION_HANDLER_NAME = "Test Connection Handler";
  // Test child 1 LDIF.
  private static final String[] TEST_CHILD_1 = new String[] {
      "dn: cn=test child 1,cn=test children,cn=test parent 1,cn=test parents,cn=config",
@@ -72,17 +150,6 @@
      "ds-cfg-virtual-attribute-conflict-behavior: virtual-overrides-real"
  };
  // Assert that the values of child 1 are correct.
  private void assertChild1(TestChildCfg child) {
    Assert.assertEquals(child.getMandatoryClassProperty(),
        "org.opends.server.extensions.UserDefinedVirtualAttributeProvider");
    Assert.assertEquals(child.getMandatoryReadOnlyAttributeTypeProperty(),
        DirectoryServer.getAttributeType("description"));
    assertSetEquals(child.getAggregationProperty(), new String[0]);
  }
  // Test child 2 LDIF.
  private static final String[] TEST_CHILD_2 = new String[] {
      "dn: cn=test child 2,cn=test children,cn=test parent 1,cn=test parents,cn=config",
@@ -96,23 +163,6 @@
      "ds-cfg-backend-base-dn: cn=LDAP Connection Handler, cn=connection handlers, cn=config"
  };
  // Assert that the values of child 2 are correct.
  private void assertChild2(TestChildCfg child) {
    Assert.assertEquals(child.getMandatoryClassProperty(),
        "org.opends.server.extensions.UserDefinedVirtualAttributeProvider");
    Assert.assertEquals(child.getMandatoryReadOnlyAttributeTypeProperty(),
        DirectoryServer.getAttributeType("description"));
    // Test normalization.
    assertSetEquals(child.getAggregationProperty(), "LDAP Connection Handler");
    assertSetEquals(child.getAggregationProperty(),
        "  LDAP   Connection  Handler ");
    assertSetEquals(child.getAggregationProperty(),
        "  ldap connection HANDLER ");
  }
  // Test child 3 LDIF (invalid reference).
  private static final String[] TEST_CHILD_3 = new String[] {
      "dn: cn=test child 3,cn=test children,cn=test parent 1,cn=test parents,cn=config",
@@ -140,20 +190,6 @@
      "ds-cfg-backend-base-dn: cn=LDAPS Connection Handler, cn=connection handlers, cn=config"
  };
  // Assert that the values of child 4 are correct.
  private void assertChild4(TestChildCfg child) {
    Assert.assertEquals(child.getMandatoryClassProperty(),
        "org.opends.server.extensions.UserDefinedVirtualAttributeProvider");
    Assert.assertEquals(child.getMandatoryReadOnlyAttributeTypeProperty(),
        DirectoryServer.getAttributeType("description"));
    assertSetEquals(child.getAggregationProperty(), "LDAPS Connection Handler",
        "LDAP Connection Handler");
  }
  // Test child 5 LDIF.
  private static final String[] TEST_CHILD_5 = new String[] {
      "dn: cn=test child 5,cn=test children,cn=test parent 1,cn=test parents,cn=config",
@@ -169,6 +205,34 @@
      "ds-cfg-backend-base-dn: cn=LDAP Connection Handler, cn=connection handlers, cn=config"
  };
  // Test child 6 LDIF.
  private static final String[] TEST_CHILD_6 = new String[] {
      "dn: cn=test child 6,cn=test children,cn=test parent 1,cn=test parents,cn=config",
      "objectclass: top",
      "objectclass: ds-cfg-test-child-dummy",
      "cn: test child 6",
      "ds-cfg-virtual-attribute-enabled: true",
      "ds-cfg-virtual-attribute-class: org.opends.server.extensions.UserDefinedVirtualAttributeProvider",
      "ds-cfg-virtual-attribute-type: description",
      "ds-cfg-virtual-attribute-conflict-behavior: virtual-overrides-real",
      "ds-cfg-backend-base-dn: cn=" + TEST_CONNECTION_HANDLER_NAME
          + ", cn=connection handlers, cn=config"
  };
  // Test child 7 LDIF.
  private static final String[] TEST_CHILD_7 = new String[] {
      "dn: cn=test child 7,cn=test children,cn=test parent 1,cn=test parents,cn=config",
      "objectclass: top",
      "objectclass: ds-cfg-test-child-dummy",
      "cn: test child 7",
      "ds-cfg-virtual-attribute-enabled: false",
      "ds-cfg-virtual-attribute-class: org.opends.server.extensions.UserDefinedVirtualAttributeProvider",
      "ds-cfg-virtual-attribute-type: description",
      "ds-cfg-virtual-attribute-conflict-behavior: virtual-overrides-real",
      "ds-cfg-backend-base-dn: cn=" + TEST_CONNECTION_HANDLER_NAME
          + ", cn=connection handlers, cn=config"
  };
  // Test LDIF.
  private static final String[] TEST_LDIF = new String[] {
      // Base entries.
@@ -198,6 +262,17 @@
  // JNDI LDAP context.
  private JNDIDirContextAdaptor adaptor = null;
  // The saved test child configuration "aggregation-property"
  // property definition.
  private AggregationPropertyDefinition<ConnectionHandlerCfgClient, ConnectionHandlerCfg> aggregationPropertyDefinitionDefault = null;
  // An aggregation where the target must be enabled if the source is
  // enabled.
  private AggregationPropertyDefinition<ConnectionHandlerCfgClient, ConnectionHandlerCfg> aggregationPropertyDefinitionTargetAndSourceMustBeEnabled = null;
  // An aggregation where the target must be enabled.
  private AggregationPropertyDefinition<ConnectionHandlerCfgClient, ConnectionHandlerCfg> aggregationPropertyDefinitionTargetMustBeEnabled = null;
  /**
@@ -215,6 +290,42 @@
    // Add test managed objects.
    TestCaseUtils.addEntries(TEST_LDIF);
    // Save the aggregation property definition so that it can be
    // replaced and restored later.
    aggregationPropertyDefinitionDefault = TestChildCfgDefn.getInstance()
        .getAggregationPropertyPropertyDefinition();
    // Create the two test aggregation properties.
    AggregationPropertyDefinition.Builder<ConnectionHandlerCfgClient, ConnectionHandlerCfg> builder;
    TestChildCfgDefn d = TestChildCfgDefn.getInstance();
    builder = AggregationPropertyDefinition.createBuilder(d,
        "aggregation-property");
    builder.setOption(PropertyOption.MULTI_VALUED);
    builder.setAdministratorAction(new AdministratorAction(
        AdministratorAction.Type.NONE, d, "aggregation-property"));
    builder
        .setDefaultBehaviorProvider(new UndefinedDefaultBehaviorProvider<String>());
    builder.setParentPath(ManagedObjectPath.valueOf("/"));
    builder.setRelationDefinition("connection-handler");
    builder.setManagedObjectDefinition(ConnectionHandlerCfgDefn.getInstance());
    builder.setTargetEnabledPropertyName("enabled");
    aggregationPropertyDefinitionTargetMustBeEnabled = builder.getInstance();
    builder = AggregationPropertyDefinition.createBuilder(d,
        "aggregation-property");
    builder.setOption(PropertyOption.MULTI_VALUED);
    builder.setAdministratorAction(new AdministratorAction(
        AdministratorAction.Type.NONE, d, "aggregation-property"));
    builder
        .setDefaultBehaviorProvider(new UndefinedDefaultBehaviorProvider<String>());
    builder.setParentPath(ManagedObjectPath.valueOf("/"));
    builder.setRelationDefinition("connection-handler");
    builder.setManagedObjectDefinition(ConnectionHandlerCfgDefn.getInstance());
    builder.setTargetEnabledPropertyName("enabled");
    builder.setSourceEnabledPropertyName("mandatory-boolean-property");
    aggregationPropertyDefinitionTargetAndSourceMustBeEnabled = builder
        .getInstance();
  }
@@ -229,6 +340,9 @@
  public void tearDown() throws Exception {
    TestCfg.cleanup();
    // Restore the test child aggregation definition.
    TestCfg.addPropertyDefinition(aggregationPropertyDefinitionDefault);
    // Remove test entries.
    deleteSubtree("cn=test parents,cn=config");
  }
@@ -236,50 +350,6 @@
  /**
   * Tests that aggregation contains no values when it
   * contains does not contain any DN attribute values.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAggregationEmpty() throws Exception {
    // Add the entry.
    TestCaseUtils.addEntry(TEST_CHILD_1);
    try {
      TestParentCfg parent = getParent("test parent 1");
      assertChild1(parent.getTestChild("test child 1"));
    } finally {
      deleteSubtree("cn=test child 1,cn=test children,cn=test parent 1,cn=test parents,cn=config");
    }
  }
  /**
   * Tests that aggregation contains single valid value when it
   * contains a single valid DN attribute values.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAggregationSingle() throws Exception {
    // Add the entry.
    TestCaseUtils.addEntry(TEST_CHILD_2);
    try {
      TestParentCfg parent = getParent("test parent 1");
      assertChild2(parent.getTestChild("test child 2"));
    } finally {
      deleteSubtree("cn=test child 2,cn=test children,cn=test parent 1,cn=test parents,cn=config");
    }
  }
  /**
   * Tests that aggregation is rejected when the LDAP DN contains a
   * valid RDN but an invalid parent DN.
   *
@@ -329,28 +399,6 @@
  /**
   * Tests that aggregation contains multiple valid values when it
   * contains a multiple valid DN attribute values.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAggregationMultipleValues() throws Exception {
    // Add the entry.
    TestCaseUtils.addEntry(TEST_CHILD_4);
    try {
      TestParentCfg parent = getParent("test parent 1");
      assertChild4(parent.getTestChild("test child 4"));
    } finally {
      deleteSubtree("cn=test child 4,cn=test children,cn=test parent 1,cn=test parents,cn=config");
    }
  }
  /**
   * Tests that aggregation is rejected by a constraint violation when
   * the DN values are dangling.
   *
@@ -385,6 +433,476 @@
  /**
   * Tests that aggregation is rejected by a constraint violation when
   * an enabled component references a disabled component and the
   * referenced component must always be enabled.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAggregationDisabledReference1() throws Exception {
    // Add the entry and the connection handler.
    TestCaseUtils.addEntry(TEST_CHILD_6);
    try {
      createConnectionHandler(false);
    } catch (Exception e) {
      deleteSubtree(TEST_CHILD_6_DN);
      throw e;
    }
    // Register the temporary aggregation definition.
    TestCfg.removeConstraint(aggregationPropertyDefinitionDefault);
    TestCfg
        .addPropertyDefinition(aggregationPropertyDefinitionTargetMustBeEnabled);
    TestCfg.addConstraint(aggregationPropertyDefinitionTargetMustBeEnabled);
    try {
      TestParentCfg parent = getParent("test parent 1");
      parent.getTestChild("test child 6");
      Assert
          .fail("Unexpectedly added test child 6 when it had a disabled reference");
    } catch (ConfigException e) {
      // Check that we have a constraint violation as the cause.
      Throwable cause = e.getCause();
      if (cause instanceof ConstraintViolationException) {
        ConstraintViolationException cve = (ConstraintViolationException) cause;
        Collection<Message> causes = cve.getMessages();
        Assert.assertEquals(causes.size(), 1);
      } else {
        // Got an unexpected cause.
        throw e;
      }
    } finally {
      // Put back the default aggregation definition.
      TestCfg
          .removeConstraint(aggregationPropertyDefinitionTargetMustBeEnabled);
      TestCfg.addPropertyDefinition(aggregationPropertyDefinitionDefault);
      TestCfg.addConstraint(aggregationPropertyDefinitionDefault);
      try {
        deleteSubtree(TEST_CHILD_6_DN);
      } finally {
        deleteConnectionHandler();
      }
    }
  }
  /**
   * Tests that aggregation is rejected by a constraint violation when
   * a disabled component references a disabled component and the
   * referenced component must always be enabled.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAggregationDisabledReference2() throws Exception {
    // Add the entry.
    TestCaseUtils.addEntry(TEST_CHILD_7);
    try {
      createConnectionHandler(false);
    } catch (Exception e) {
      deleteSubtree(TEST_CHILD_7_DN);
      throw e;
    }
    // Register the temporary aggregation definition.
    TestCfg.removeConstraint(aggregationPropertyDefinitionDefault);
    TestCfg
        .addPropertyDefinition(aggregationPropertyDefinitionTargetMustBeEnabled);
    TestCfg.addConstraint(aggregationPropertyDefinitionTargetMustBeEnabled);
    try {
      TestParentCfg parent = getParent("test parent 1");
      parent.getTestChild("test child 7");
      Assert
          .fail("Unexpectedly added test child 7 when it had a disabled reference");
    } catch (ConfigException e) {
      // Check that we have a constraint violation as the cause.
      Throwable cause = e.getCause();
      if (cause instanceof ConstraintViolationException) {
        ConstraintViolationException cve = (ConstraintViolationException) cause;
        Collection<Message> causes = cve.getMessages();
        Assert.assertEquals(causes.size(), 1);
      } else {
        // Got an unexpected cause.
        throw e;
      }
    } finally {
      // Put back the default aggregation definition.
      TestCfg
          .removeConstraint(aggregationPropertyDefinitionTargetMustBeEnabled);
      TestCfg.addPropertyDefinition(aggregationPropertyDefinitionDefault);
      TestCfg.addConstraint(aggregationPropertyDefinitionDefault);
      try {
        deleteSubtree(TEST_CHILD_7_DN);
      } finally {
        deleteConnectionHandler();
      }
    }
  }
  /**
   * Tests that aggregation is rejected by a constraint violation when
   * an enabled component references a disabled component and the
   * referenced component must always be enabled when the referencing
   * component is enabled.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAggregationDisabledReference3() throws Exception {
    // Add the entry.
    TestCaseUtils.addEntry(TEST_CHILD_6);
    try {
      createConnectionHandler(false);
    } catch (Exception e) {
      deleteSubtree(TEST_CHILD_6_DN);
      throw e;
    }
    // Register the temporary aggregation definition.
    TestCfg.removeConstraint(aggregationPropertyDefinitionDefault);
    TestCfg
        .addPropertyDefinition(aggregationPropertyDefinitionTargetAndSourceMustBeEnabled);
    TestCfg
        .addConstraint(aggregationPropertyDefinitionTargetAndSourceMustBeEnabled);
    try {
      TestParentCfg parent = getParent("test parent 1");
      parent.getTestChild("test child 6");
      Assert
          .fail("Unexpectedly added test child 6 when it had a disabled reference");
    } catch (ConfigException e) {
      // Check that we have a constraint violation as the cause.
      Throwable cause = e.getCause();
      if (cause instanceof ConstraintViolationException) {
        ConstraintViolationException cve = (ConstraintViolationException) cause;
        Collection<Message> causes = cve.getMessages();
        Assert.assertEquals(causes.size(), 1);
      } else {
        // Got an unexpected cause.
        throw e;
      }
    } finally {
      // Put back the default aggregation definition.
      TestCfg
          .removeConstraint(aggregationPropertyDefinitionTargetAndSourceMustBeEnabled);
      TestCfg.addPropertyDefinition(aggregationPropertyDefinitionDefault);
      TestCfg.addConstraint(aggregationPropertyDefinitionDefault);
      try {
        deleteSubtree(TEST_CHILD_6_DN);
      } finally {
        deleteConnectionHandler();
      }
    }
  }
  /**
   * Tests that aggregation is allowed when a disabled component
   * references a disabled component and the referenced component must
   * always be enabled when the referencing component is enabled.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAggregationDisabledReference4() throws Exception {
    // Add the entry.
    TestCaseUtils.addEntry(TEST_CHILD_7);
    try {
      createConnectionHandler(false);
    } catch (Exception e) {
      deleteSubtree(TEST_CHILD_7_DN);
      throw e;
    }
    // Register the temporary aggregation definition.
    TestCfg.removeConstraint(aggregationPropertyDefinitionDefault);
    TestCfg
        .addPropertyDefinition(aggregationPropertyDefinitionTargetAndSourceMustBeEnabled);
    TestCfg
        .addConstraint(aggregationPropertyDefinitionTargetAndSourceMustBeEnabled);
    try {
      TestParentCfg parent = getParent("test parent 1");
      parent.getTestChild("test child 7");
    } finally {
      // Put back the default aggregation definition.
      TestCfg
          .removeConstraint(aggregationPropertyDefinitionTargetAndSourceMustBeEnabled);
      TestCfg.addPropertyDefinition(aggregationPropertyDefinitionDefault);
      TestCfg.addConstraint(aggregationPropertyDefinitionDefault);
      try {
        deleteSubtree(TEST_CHILD_7_DN);
      } finally {
        deleteConnectionHandler();
      }
    }
  }
  /**
   * Tests that aggregation contains no values when it contains does
   * not contain any DN attribute values.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAggregationEmpty() throws Exception {
    // Add the entry.
    TestCaseUtils.addEntry(TEST_CHILD_1);
    try {
      TestParentCfg parent = getParent("test parent 1");
      assertChild1(parent.getTestChild("test child 1"));
    } finally {
      deleteSubtree("cn=test child 1,cn=test children,cn=test parent 1,cn=test parents,cn=config");
    }
  }
  /**
   * Tests that aggregation contains multiple valid values when it
   * contains a multiple valid DN attribute values.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAggregationMultipleValues() throws Exception {
    // Add the entry.
    TestCaseUtils.addEntry(TEST_CHILD_4);
    try {
      TestParentCfg parent = getParent("test parent 1");
      assertChild4(parent.getTestChild("test child 4"));
    } finally {
      deleteSubtree("cn=test child 4,cn=test children,cn=test parent 1,cn=test parents,cn=config");
    }
  }
  /**
   * Tests that aggregation contains single valid value when it
   * contains a single valid DN attribute values.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAggregationSingle() throws Exception {
    // Add the entry.
    TestCaseUtils.addEntry(TEST_CHILD_2);
    try {
      TestParentCfg parent = getParent("test parent 1");
      assertChild2(parent.getTestChild("test child 2"));
    } finally {
      deleteSubtree("cn=test child 2,cn=test children,cn=test parent 1,cn=test parents,cn=config");
    }
  }
  /**
   * Tests that it is impossible to delete a referenced component when
   * the referenced component must always exist regardless of whether
   * the referencing component is enabled or not.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testCannotDeleteReferencedComponent() throws Exception {
    // Add the entry.
    TestCaseUtils.addEntry(TEST_CHILD_7);
    try {
      createConnectionHandler(true);
    } catch (Exception e) {
      deleteSubtree(TEST_CHILD_7_DN);
      throw e;
    }
    // Register the temporary aggregation definition.
    TestCfg.removeConstraint(aggregationPropertyDefinitionDefault);
    TestCfg
        .addPropertyDefinition(aggregationPropertyDefinitionTargetMustBeEnabled);
    TestCfg.addConstraint(aggregationPropertyDefinitionTargetMustBeEnabled);
    ConfigurationDeleteListener<TestChildCfg> dl = new DummyDeleteListener();
    ConfigurationChangeListener<TestChildCfg> cl = new DummyChangeListener();
    try {
      // Retrieve the parent and child managed objects and register
      // delete and change listeners respectively in order to trigger
      // the constraint call-backs.
      TestParentCfg parent = getParent("test parent 1");
      parent.addTestChildDeleteListener(dl);
      TestChildCfg child = parent.getTestChild("test child 7");
      child.addChangeListener(cl);
      // Now attempt to delete the referenced connection handler.
      // This should fail.
      try {
        deleteConnectionHandler();
        Assert.fail("Successfully deleted a referenced component");
      } catch (OperationRejectedException e) {
        // This is the expected exception - do nothing.
      }
    } finally {
      try {
        deleteSubtree(TEST_CHILD_7_DN);
      } finally {
        try {
          deleteConnectionHandler();
        } catch (ManagedObjectNotFoundException e) {
          // Ignore as it may have been deleted already.
        } finally {
          // Remove the temporary delete listener.
          TestParentCfg parent = getParent("test parent 1");
          parent.removeTestChildDeleteListener(dl);
          // Put back the default aggregation definition.
          TestCfg
              .removeConstraint(aggregationPropertyDefinitionTargetAndSourceMustBeEnabled);
          TestCfg.addPropertyDefinition(aggregationPropertyDefinitionDefault);
          TestCfg.addConstraint(aggregationPropertyDefinitionDefault);
        }
      }
    }
  }
  /**
   * Tests that it is impossible to disable a referenced component
   * when the referenced component must always be enabled regardless
   * of whether the referencing component is enabled or not.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testCannotDisableReferencedComponent() throws Exception {
    // Add the entry.
    TestCaseUtils.addEntry(TEST_CHILD_7);
    try {
      createConnectionHandler(true);
    } catch (Exception e) {
      deleteSubtree(TEST_CHILD_7_DN);
      throw e;
    }
    // Register the temporary aggregation definition.
    TestCfg.removeConstraint(aggregationPropertyDefinitionDefault);
    TestCfg
        .addPropertyDefinition(aggregationPropertyDefinitionTargetMustBeEnabled);
    TestCfg.addConstraint(aggregationPropertyDefinitionTargetMustBeEnabled);
    ConfigurationDeleteListener<TestChildCfg> dl = new DummyDeleteListener();
    ConfigurationChangeListener<TestChildCfg> cl = new DummyChangeListener();
    try {
      // Retrieve the parent and child managed objects and register
      // delete and change listeners respectively in order to trigger
      // the constraint call-backs.
      TestParentCfg parent = getParent("test parent 1");
      parent.addTestChildDeleteListener(dl);
      TestChildCfg child = parent.getTestChild("test child 7");
      child.addChangeListener(cl);
      // Now attempt to disable the referenced connection handler.
      // This should fail.
      try {
        RootCfgClient root = TestCaseUtils.getRootConfiguration();
        ConnectionHandlerCfgClient client = root
            .getConnectionHandler(TEST_CONNECTION_HANDLER_NAME);
        client.setEnabled(false);
        client.commit();
        Assert.fail("Successfully disabled a referenced component");
      } catch (OperationRejectedException e) {
        // This is the expected exception - do nothing.
      }
    } finally {
      try {
        deleteSubtree(TEST_CHILD_7_DN);
      } finally {
        try {
          deleteConnectionHandler();
        } finally {
          // Remove the temporary delete listener.
          TestParentCfg parent = getParent("test parent 1");
          parent.removeTestChildDeleteListener(dl);
          // Put back the default aggregation definition.
          TestCfg
              .removeConstraint(aggregationPropertyDefinitionTargetAndSourceMustBeEnabled);
          TestCfg.addPropertyDefinition(aggregationPropertyDefinitionDefault);
          TestCfg.addConstraint(aggregationPropertyDefinitionDefault);
        }
      }
    }
  }
  // Assert that the values of child 1 are correct.
  private void assertChild1(TestChildCfg child) {
    Assert.assertEquals(child.getMandatoryClassProperty(),
        "org.opends.server.extensions.UserDefinedVirtualAttributeProvider");
    Assert.assertEquals(child.getMandatoryReadOnlyAttributeTypeProperty(),
        DirectoryServer.getAttributeType("description"));
    assertSetEquals(child.getAggregationProperty(), new String[0]);
  }
  // Assert that the values of child 2 are correct.
  private void assertChild2(TestChildCfg child) {
    Assert.assertEquals(child.getMandatoryClassProperty(),
        "org.opends.server.extensions.UserDefinedVirtualAttributeProvider");
    Assert.assertEquals(child.getMandatoryReadOnlyAttributeTypeProperty(),
        DirectoryServer.getAttributeType("description"));
    // Test normalization.
    assertSetEquals(child.getAggregationProperty(), "LDAP Connection Handler");
    assertSetEquals(child.getAggregationProperty(),
        "  LDAP   Connection  Handler ");
    assertSetEquals(child.getAggregationProperty(),
        "  ldap connection HANDLER ");
  }
  // Assert that the values of child 4 are correct.
  private void assertChild4(TestChildCfg child) {
    Assert.assertEquals(child.getMandatoryClassProperty(),
        "org.opends.server.extensions.UserDefinedVirtualAttributeProvider");
    Assert.assertEquals(child.getMandatoryReadOnlyAttributeTypeProperty(),
        DirectoryServer.getAttributeType("description"));
    assertSetEquals(child.getAggregationProperty(), "LDAPS Connection Handler",
        "LDAP Connection Handler");
  }
  // Asserts that the actual set of DNs contains the expected values.
  private void assertSetEquals(SortedSet<String> actual, String... expected) {
    SortedSet<String> values = new TreeSet<String>(TestChildCfgDefn
@@ -399,6 +917,31 @@
  // Creates a test connection handler for testing.
  private void createConnectionHandler(boolean enabled) throws Exception {
    ServerSocket freeSocket = TestCaseUtils.bindFreePort();
    int freePort = freeSocket.getLocalPort();
    freeSocket.close();
    RootCfgClient root = TestCaseUtils.getRootConfiguration();
    LDAPConnectionHandlerCfgClient client = root.createConnectionHandler(
        LDAPConnectionHandlerCfgDefn.getInstance(),
        TEST_CONNECTION_HANDLER_NAME, null);
    client.setEnabled(enabled);
    client.setListenPort(freePort);
    client.commit();
  }
  // Deletes the test connection handler after testing.
  private void deleteConnectionHandler() throws Exception {
    RootCfgClient root = TestCaseUtils.getRootConfiguration();
    root.removeConnectionHandler(TEST_CONNECTION_HANDLER_NAME);
  }
  // Deletes the named sub-tree.
  private void deleteSubtree(String dn) throws Exception {
    getAdaptor().deleteSubtree(new LdapName(dn));