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

neil_a_wilson
02.51.2007 2b1ccc8723b7cce6708c6f2ac8c10fc1c670b708
Add initial support for static groups.  At present, this does not allow for
nested static groups, but it does handle changes to the set of available groups
and to group membership while the server is online. It also includes a
backend initialization listener API, which makes it possible for components to
perform custom processing when a backend is brought online or offline, and this
is used to identify all groups at the time that the server is started.

OpenDS Issue Number: 422
6 files added
11 files modified
4463 ■■■■■ changed files
opends/resource/config/config.ldif 12 ●●●●● patch | view | raw | blame | history
opends/resource/schema/02-config.ldif 14 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/api/BackendInitializationListener.java 61 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/api/ClientConnection.java 49 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/api/Group.java 100 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/config/ConfigConstants.java 39 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/BackendConfigManager.java 39 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/DirectoryServer.java 125 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/GroupManager.java 1362 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/FilteredStaticGroupMemberList.java 338 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/SimpleStaticGroupMemberList.java 185 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/StaticGroup.java 592 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/messages/ConfigMessages.java 255 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/messages/ExtensionsMessages.java 139 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/internal/InternalClientConnection.java 3 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/ServerConstants.java 65 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/core/GroupManagerTestCase.java 1085 ●●●●● patch | view | raw | blame | history
opends/resource/config/config.ldif
@@ -341,6 +341,18 @@
ds-cfg-extended-operation-handler-class: org.opends.server.extensions.WhoAmIExtendedOperation
ds-cfg-extended-operation-handler-enabled: true
dn: cn=Group Implementations,cn=config
objectClass: top
objectClass: ds-cfg-branch
cn: Group Implementations
dn: cn=Static,cn=Group Implementations,cn=config
objectClass: top
objectClass: ds-cfg-group-implementation
cn: Static
ds-cfg-group-implementation-class: org.opends.server.extensions.StaticGroup
ds-cfg-group-implementation-enabled: true
dn: cn=Identity Mappers,cn=config
objectClass: top
objectClass: ds-cfg-branch
opends/resource/schema/02-config.ldif
@@ -1031,6 +1031,13 @@
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.302
  NAME 'ds-task-schema-file-name' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.303
  NAME 'ds-cfg-group-implementation-class' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.304
  NAME 'ds-cfg-group-implementation-enabled'
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
  NAME 'ds-cfg-access-control-handler' SUP top STRUCTURAL
  MUST ( cn $ ds-cfg-acl-handler-class $ ds-cfg-acl-handler-enabled )
@@ -1407,12 +1414,15 @@
  SUP ds-monitor-entry STRUCTURAL MAY ( ds-backend-id $ ds-backend-base-dn $
  ds-backend-entry-count $ ds-backend-writability-mode $ ds-backend-is-private )
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.79 NAME
  'ds-connectionhandler-monitor-entry' SUP ds-monitor-entry STRUCTURAL
objectClasses: ( 1.3.6.1.4.1.26027.1.2.79
  NAME 'ds-connectionhandler-monitor-entry' SUP ds-monitor-entry STRUCTURAL
  MAY ( ds-connectionhandler-connection $ ds-connectionhandler-listener $
  ds-connectionhandler-num-connections $ ds-connectionhandler-protocol )
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.80 NAME 'ds-task-add-schema-file'
  SUP ds-task MUST ds-task-schema-file-name
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.81 NAME 'ds-cfg-group-implementation'
  SUP top STRUCTURAL MUST ( cn $ ds-cfg-group-implementation-class $
  ds-cfg-group-implementation-enabled ) X-ORIGIN 'OpenDS Directory Server' )
opends/src/server/org/opends/server/api/BackendInitializationListener.java
New file
@@ -0,0 +1,61 @@
/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.api;
/**
 * This interface defines a set of methods that may be used by server
 * components to perform any processing that they might find necessary
 * whenever a backend is initialized and/or finalized.
 */
public interface BackendInitializationListener
{
  /**
   * Performs any processing that may be required whenever a backend
   * is initialized for use in the Directory Server.  This method will
   * be invoked after the backend has been initialized but before it
   * has been put into service.
   *
   * @param  backend  The backend that has been initialized and is
   *                  about to be put into service.
   */
  public void performBackendInitializationProcessing(Backend backend);
  /**
   * Performs any processing that may be required whenever a backend
   * is finalized.  This method will be invoked after the backend has
   * been taken out of service but before it has been finalized.
   *
   * @param  backend  The backend that has been taken out of service
   *                  and is about to be finalized.
   */
  public void performBackendFinalizationProcessing(Backend backend);
}
opends/src/server/org/opends/server/api/ClientConnection.java
@@ -22,7 +22,7 @@
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2006 Sun Microsystems, Inc.
 *      Portions Copyright 2006-2007 Sun Microsystems, Inc.
 */
package org.opends.server.api;
@@ -31,6 +31,7 @@
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -45,6 +46,8 @@
import org.opends.server.types.CancelResult;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DisconnectReason;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.IntermediateResponse;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
@@ -980,8 +983,48 @@
    assert debugEnter(CLASS_NAME, "getGroups",
                      String.valueOf(operation));
    // NYI -- Add a mechanism for making this determination.
    return java.util.Collections.<Group>emptySet();
    // FIXME -- This probably isn't the most efficient implementation.
    DN authzDN;
    if (operation == null)
    {
      if ((authenticationInfo == null) ||
          (! authenticationInfo.isAuthenticated()))
      {
        authzDN = null;
      }
      else
      {
        authzDN = authenticationInfo.getAuthorizationDN();
      }
    }
    else
    {
      authzDN = operation.getAuthorizationDN();
    }
    if ((authzDN == null) || authzDN.isNullDN())
    {
      return java.util.Collections.<Group>emptySet();
    }
    Entry userEntry = DirectoryServer.getEntry(authzDN);
    if (userEntry == null)
    {
      return java.util.Collections.<Group>emptySet();
    }
    HashSet<Group> groupSet = new HashSet<Group>();
    for (Group g :
         DirectoryServer.getGroupManager().getGroupInstances())
    {
      if (g.isMember(userEntry))
      {
        groupSet.add(g);
      }
    }
    return groupSet;
  }
opends/src/server/org/opends/server/api/Group.java
@@ -22,7 +22,7 @@
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2006 Sun Microsystems, Inc.
 *      Portions Copyright 2006-2007 Sun Microsystems, Inc.
 */
package org.opends.server.api;
@@ -30,9 +30,12 @@
import java.util.List;
import org.opends.server.config.ConfigEntry;
import org.opends.server.config.ConfigException;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.InitializationException;
import org.opends.server.types.MemberList;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchScope;
@@ -70,6 +73,101 @@
  /**
   * Initializes a "shell" instance of this group implementation that
   * may be used to identify and instantiate instances of this type of
   * group in the directory data.
   *
   * @param  configEntry  The configuration entry that may contain
   *                      information about the way that this group
   *                      implementation should operate.
   *
   * @throws  ConfigException  If there is a problem with the provided
   *                           configuration entry.
   *
   * @throws  InitializationException  If a problem occurs while
   *                                   attempting to initialize this
   *                                   group implementation that is
   *                                   not related to the server
   *                                   configuration.
   */
  public abstract void initializeGroupImplementation(
                            ConfigEntry configEntry)
         throws ConfigException, InitializationException;
  /**
   * Performs any necessary finalization that may be needed whenever
   * this group implementation is taken out of service within the
   * Directory Server (e.g., if it is disabled or the server is
   * shutting down).
   */
  public void finalizeGroupImplementation()
  {
    assert debugEnter(CLASS_NAME, "finalizeGroupImplementation");
    // No implementation is required by default.
  }
  /**
   * Creates a new group of this type based on the definition
   * contained in the provided entry.  This method must be designed so
   * that it may be invoked on the "shell" instance created using the
   * default constructor and initialized with the
   * {@code initializeGroupImplementation} method.
   *
   * @param  groupEntry  The entry containing the definition for the
   *                     group to be created.
   *
   * @return  The group instance created from the definition in the
   *          provided entry.
   *
   * @throws  DirectoryException  If a problem occurs while trying to
   *                              create the group instance.
   */
  public abstract Group newInstance(Entry groupEntry)
         throws DirectoryException;
  /**
   * Retrieves a search filter that may be used to identify entries
   * containing definitions for groups of this type in the Directory
   * Server.  This method must be designed so that it may be invoked
   * on the "shell" instance created using the default constructor and
   * initialized with the {@code initializeGroupImplementation}
   * method.
   *
   * @return  A search filter that may be used to identify entries
   *          containing definitions for groups of this type in the
   *          Directory Server.
   *
   * @throws  DirectoryException  If a problem occurs while trying to
   *                              locate all of the applicable group
   *                              definition entries.
   */
  public abstract SearchFilter getGroupDefinitionFilter()
         throws DirectoryException;
  /**
   * Indicates whether the provided entry contains a valid definition
   * for this type of group.
   *
   * @param  entry  The entry for which to make the determination.
   *
   * @return  {@code true} if the provided entry does contain a valid
   *          definition for this type of group, or {@code false} if
   *          it does not.
   */
  public abstract boolean isGroupDefinition(Entry entry);
  /**
   * Retrieves the DN of the entry that contains the definition for
   * this group.
   *
opends/src/server/org/opends/server/config/ConfigConstants.java
@@ -701,6 +701,24 @@
  /**
   * The name of the configuration attribute that specifies the fully-qualified
   * class name for a group implementation.
   */
  public static final String ATTR_GROUP_IMPLEMENTATION_CLASS =
       NAME_PREFIX_CFG + "group-implementation-class";
  /**
   * The name of the configuration attribute that indicates whether a group
   * implementation should be enabled for use in the server.
   */
  public static final String ATTR_GROUP_IMPLEMENTATION_ENABLED =
       NAME_PREFIX_CFG + "group-implementation-enabled";
  /**
   * The name of the configuration attribute that holds the address of the KDC
   * to use when processing SASL GSSAPI binds.
   */
@@ -2537,6 +2555,15 @@
  /**
   * The DN of the entry that will serve as the base for the configuration
   * for all Directory Server group implementations.
   */
  public static final String DN_GROUP_IMPLEMENTATION_CONFIG_BASE =
       "cn=Group Implementations," + DN_CONFIG_ROOT;
  /**
   * The DN of the entry that will serve as the base for the configuration
   * for all Directory Server identity mappers.
   */
  public static final String DN_IDMAPPER_CONFIG_BASE =
@@ -2903,6 +2930,15 @@
  /**
   * The name of the objectclass that will be used for a Directory Server group
   * implementation.
   */
  public static final String OC_GROUP_IMPLEMENTATION =
       NAME_PREFIX_CFG + "group-implementation";
  /**
   * The name of the objectclass that will be used for a Directory Server
   * identity mapper.
   */
@@ -3727,9 +3763,6 @@
   */
  public static final String ATTR_TASK_BACKUP_SIGN_HASH =
       NAME_PREFIX_TASK + "backup-sign-hash";
  /**
   * The name of the attribute in the add schema file task definition that
   * specifies the name of the schema file to be added.
opends/src/server/org/opends/server/core/BackendConfigManager.java
@@ -36,6 +36,7 @@
import java.util.concurrent.ConcurrentHashMap;
import org.opends.server.api.Backend;
import org.opends.server.api.BackendInitializationListener;
import org.opends.server.api.ConfigAddListener;
import org.opends.server.api.ConfigChangeListener;
import org.opends.server.api.ConfigDeleteListener;
@@ -541,6 +542,14 @@
      }
      // Notify any backend initialization listeners.
      for (BackendInitializationListener listener :
           DirectoryServer.getBackendInitializationListeners())
      {
        listener.performBackendInitializationProcessing(backend);
      }
      // Register the backend with the server.
      try
      {
@@ -954,6 +963,13 @@
          // Directory Server.
          registeredBackends.remove(backendDN);
          DirectoryServer.deregisterBackend(backend);
          for (BackendInitializationListener listener :
               DirectoryServer.getBackendInitializationListeners())
          {
            listener.performBackendFinalizationProcessing(backend);
          }
          backend.finalizeBackend();
          // Remove the shared lock for this backend.
@@ -1337,6 +1353,15 @@
                                      messages);
      }
      // Notify any backend initialization listeners.
      for (BackendInitializationListener listener :
           DirectoryServer.getBackendInitializationListeners())
      {
        listener.performBackendInitializationProcessing(backend);
      }
      // Register the backend with the server.
      try
      {
@@ -2075,6 +2100,14 @@
    }
    // Notify any backend initialization listeners.
    for (BackendInitializationListener listener :
         DirectoryServer.getBackendInitializationListeners())
    {
      listener.performBackendInitializationProcessing(backend);
    }
    // At this point, the backend should be online.  Add it as one of the
    // registered backends for this backend config manager.
    try
@@ -2203,6 +2236,12 @@
        assert debugException(CLASS_NAME, "applyConfigurationDelete", e);
      }
      for (BackendInitializationListener listener :
           DirectoryServer.getBackendInitializationListeners())
      {
        listener.performBackendFinalizationProcessing(backend);
      }
      DirectoryServer.deregisterBackend(backend);
      return new ConfigChangeResult(resultCode, adminActionRequired,
opends/src/server/org/opends/server/core/DirectoryServer.java
@@ -53,6 +53,7 @@
import org.opends.server.api.ApproximateMatchingRule;
import org.opends.server.api.AttributeSyntax;
import org.opends.server.api.Backend;
import org.opends.server.api.BackendInitializationListener;
import org.opends.server.api.CertificateMapper;
import org.opends.server.api.ChangeNotificationListener;
import org.opends.server.api.ClientConnection;
@@ -355,6 +356,11 @@
  private CopyOnWriteArrayList<SynchronizationProvider>
               synchronizationProviders;
  // The set of backend initialization listeners registered with the Directory
  // Server.
  private CopyOnWriteArraySet<BackendInitializationListener>
               backendInitializationListeners;
  // The set of root DNs registered with the Directory Server.
  private CopyOnWriteArraySet<DN> rootDNs;
@@ -386,6 +392,9 @@
  // The configuration manager for extended operation handlers.
  private ExtendedOperationConfigManager extendedOperationConfigManager;
  // The group manager for the Directory Server.
  private GroupManager groupManager;
  // The configuration manager for identity mappers.
  private IdentityMapperConfigManager identityMapperConfigManager;
@@ -615,6 +624,8 @@
    directoryServer.monitorProviders =
         new ConcurrentHashMap<String,MonitorProvider>();
    directoryServer.backends = new TreeMap<String,Backend>();
    directoryServer.backendInitializationListeners =
         new CopyOnWriteArraySet<BackendInitializationListener>();
    directoryServer.baseDNs = new TreeMap<DN,Backend>();
    directoryServer.publicNamingContexts = new TreeMap<DN,Backend>();
    directoryServer.privateNamingContexts = new TreeMap<DN,Backend>();
@@ -724,6 +735,12 @@
    totalConnections       = 0;
    // Create the plugin config manager, but don't initialize it yet.  This will
    // make it possible to process internal operations before the plugins have
    // been loaded.
    pluginConfigManager = new PluginConfigManager();
    // If we have gotten here, then the configuration should be properly
    // bootstrapped.
    synchronized (directoryServer)
@@ -964,6 +981,10 @@
      new RootDNConfigManager().initializeRootDNs();
      // Initialize the group manager.
      initializeGroupManager();
      // Initialize all the backends and their associated suffixes.
      initializeBackends();
@@ -1899,6 +1920,60 @@
  /**
   * Retrieves the set of backend initialization listeners that have been
   * registered with the Directory Server.  The contents of the returned set
   * must not be altered.
   *
   * @return  The set of backend initialization listeners that have been
   *          registered with the Directory Server.
   */
  public static Set<BackendInitializationListener>
                     getBackendInitializationListeners()
  {
    assert debugEnter(CLASS_NAME, "getBackendInitializationListeners");
    return directoryServer.backendInitializationListeners;
  }
  /**
   * Registers the provided backend initialization listener with the Directory
   * Server.
   *
   * @param  listener  The backend initialization listener to register with the
   *                   Directory Server.
   */
  public static void registerBackendInitializationListener(
                          BackendInitializationListener listener)
  {
    assert debugEnter(CLASS_NAME, "registerBackendInitializationListener",
                      String.valueOf(listener));
    directoryServer.backendInitializationListeners.add(listener);
  }
  /**
   * Deegisters the provided backend initialization listener with the Directory
   * Server.
   *
   * @param  listener  The backend initialization listener to deregister with
   *                   the Directory Server.
   */
  public static void deregisterBackendInitializationListener(
                          BackendInitializationListener listener)
  {
    assert debugEnter(CLASS_NAME, "deregisterBackendInitializationListener",
                      String.valueOf(listener));
    directoryServer.backendInitializationListeners.remove(listener);
  }
  /**
   * Initializes the set of backends defined in the Directory Server.
   *
   * @throws  ConfigException  If there is a configuration problem with any of
@@ -1942,6 +2017,45 @@
  /**
   * Initializes the Directory Server group manager.
   *
   * @throws  ConfigException  If there is a configuration problem with any of
   *                           the group implementations.
   *
   * @throws  InitializationException  If a problem occurs while initializing
   *                                   the group manager that is not related to
   *                                   the server configuration.
   */
  public void initializeGroupManager()
         throws ConfigException, InitializationException
  {
    assert debugEnter(CLASS_NAME, "initializeGroupManager");
    groupManager = new GroupManager();
    groupManager.initializeGroupImplementations();
    // The configuration backend has already been registered by this point
    // so we need to handle it explicitly.
    groupManager.performBackendInitializationProcessing(configHandler);
  }
  /**
   * Retrieves the Directory Server group manager.
   *
   * @return  The Directory Server group manager.
   */
  public static GroupManager getGroupManager()
  {
    assert debugEnter(CLASS_NAME, "getGroupManager");
    return directoryServer.groupManager;
  }
  /**
   * Initializes the set of supported controls for the Directory Server.
   *
   * @throws  ConfigException  If there is a configuration problem with the
@@ -2203,7 +2317,6 @@
  {
    assert debugEnter(CLASS_NAME, "initializePlugins");
    pluginConfigManager = new PluginConfigManager();
    pluginConfigManager.initializePluginConfig(null);
  }
@@ -7121,6 +7234,10 @@
    }
    // Perform any necessary cleanup work for the group manager.
    directoryServer.groupManager.finalizeGroupManager();
    // Shut down all the other components that may need special handling.
    // NYI
@@ -7144,6 +7261,12 @@
    {
      try
      {
        for (BackendInitializationListener listener :
             directoryServer.backendInitializationListeners)
        {
          listener.performBackendFinalizationProcessing(backend);
        }
        backend.finalizeBackend();
        // Remove the shared lock for this backend.
opends/src/server/org/opends/server/core/GroupManager.java
New file
@@ -0,0 +1,1362 @@
/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.core;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.opends.server.api.Backend;
import org.opends.server.api.BackendInitializationListener;
import org.opends.server.api.ChangeNotificationListener;
import org.opends.server.api.ConfigAddListener;
import org.opends.server.api.ConfigChangeListener;
import org.opends.server.api.ConfigDeleteListener;
import org.opends.server.api.ConfigHandler;
import org.opends.server.api.ConfigurableComponent;
import org.opends.server.api.Group;
import org.opends.server.config.BooleanConfigAttribute;
import org.opends.server.config.ConfigEntry;
import org.opends.server.config.ConfigException;
import org.opends.server.config.StringConfigAttribute;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.Control;
import org.opends.server.types.DereferencePolicy;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.ErrorLogCategory;
import org.opends.server.types.ErrorLogSeverity;
import org.opends.server.types.InitializationException;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchScope;
import org.opends.server.types.SearchFilter;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.loggers.Debug.*;
import static org.opends.server.loggers.Error.*;
import static org.opends.server.messages.ConfigMessages.*;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;
/**
 * This class provides a mechanism for interacting with all groups defined in
 * the Directory Server.  It will handle all necessary processing at server
 * startup to identify and load all group implementations, as well as to find
 * all group instances within the server.
 * <BR><BR>
 * FIXME:  At the present time, it assumes that all of the necessary
 * information about all of the groups defined in the server can be held in
 * memory.  If it is determined that this approach is not workable in all cases,
 * then we will need an alternate strategy.
 */
public class GroupManager
       implements ConfigChangeListener, ConfigAddListener, ConfigDeleteListener,
                  BackendInitializationListener, ChangeNotificationListener
{
  /**
   * The fully-qualified name of this class for debugging purposes.
   */
  private static final String CLASS_NAME =
       "org.opends.server.core.GroupManager";
  // A mapping between the DNs of the config entries and the associated
  // group implementations.
  private ConcurrentHashMap<DN,Group> groupImplementations;
  // A mapping between the DNs of all group entries and the corresponding
  // group instances.
  private ConcurrentHashMap<DN,Group> groupInstances;
  // The configuration handler for the Directory Server.
  private ConfigHandler configHandler;
  /**
   * Creates a new instance of this group manager.
   */
  public GroupManager()
  {
    assert debugConstructor(CLASS_NAME);
    configHandler        = DirectoryServer.getConfigHandler();
    groupImplementations = new ConcurrentHashMap<DN,Group>();
    groupInstances       = new ConcurrentHashMap<DN,Group>();
    DirectoryServer.registerBackendInitializationListener(this);
    DirectoryServer.registerChangeNotificationListener(this);
  }
  /**
   * Initializes all group implementations currently defined in the Directory
   * Server configuration.  This should only be called at Directory Server
   * startup.
   *
   * @throws  ConfigException  If a configuration problem causes the group
   *                           implementation initialization process to fail.
   *
   * @throws  InitializationException  If a problem occurs while initializing
   *                                   the group implementations that is not
   *                                   related to the server configuration.
   */
  public void initializeGroupImplementations()
         throws ConfigException, InitializationException
  {
    assert debugEnter(CLASS_NAME, "initializeGroupImplementations");
    // First, get the configuration base entry.
    ConfigEntry baseEntry;
    try
    {
      DN groupImplementationBaseDN =
              DN.decode(DN_GROUP_IMPLEMENTATION_CONFIG_BASE);
      baseEntry = configHandler.getConfigEntry(groupImplementationBaseDN);
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "initializeGroupImplementations", e);
      int    msgID   = MSGID_CONFIG_GROUP_CANNOT_GET_BASE;
      String message = getMessage(msgID, String.valueOf(e));
      throw new ConfigException(msgID, message, e);
    }
    if (baseEntry == null)
    {
      // The group implementation base entry does not exist.  This is not
      // acceptable, so throw an exception.
      int    msgID   = MSGID_CONFIG_GROUP_BASE_DOES_NOT_EXIST;
      String message = getMessage(msgID);
      throw new ConfigException(msgID, message);
    }
    // Register add and delete listeners with the group implementation base
    // entry.  We don't care about modifications to it.
    baseEntry.registerAddListener(this);
    baseEntry.registerDeleteListener(this);
    // See if the base entry has any children.  If not, then we don't need to do
    // anything else.
    if (! baseEntry.hasChildren())
    {
      return;
    }
    // Iterate through the child entries and process them as group
    // implementation configuration entries.
    for (ConfigEntry childEntry : baseEntry.getChildren().values())
    {
      childEntry.registerChangeListener(this);
      StringBuilder unacceptableReason = new StringBuilder();
      if (! configAddIsAcceptable(childEntry, unacceptableReason))
      {
        logError(ErrorLogCategory.CONFIGURATION, ErrorLogSeverity.SEVERE_ERROR,
                 MSGID_CONFIG_GROUP_ENTRY_UNACCEPTABLE,
                 childEntry.getDN().toString(), unacceptableReason.toString());
        continue;
      }
      try
      {
        ConfigChangeResult result = applyConfigurationAdd(childEntry);
        if (result.getResultCode() != ResultCode.SUCCESS)
        {
          StringBuilder buffer = new StringBuilder();
          List<String> resultMessages = result.getMessages();
          if ((resultMessages == null) || (resultMessages.isEmpty()))
          {
            buffer.append(getMessage(MSGID_CONFIG_UNKNOWN_UNACCEPTABLE_REASON));
          }
          else
          {
            Iterator<String> iterator = resultMessages.iterator();
            buffer.append(iterator.next());
            while (iterator.hasNext())
            {
              buffer.append(EOL);
              buffer.append(iterator.next());
            }
          }
          logError(ErrorLogCategory.CONFIGURATION,
                   ErrorLogSeverity.SEVERE_ERROR,
                   MSGID_CONFIG_GROUP_CANNOT_CREATE_IMPLEMENTATION,
                   childEntry.getDN().toString(), buffer.toString());
        }
      }
      catch (Exception e)
      {
        logError(ErrorLogCategory.CONFIGURATION, ErrorLogSeverity.SEVERE_ERROR,
                 MSGID_CONFIG_GROUP_CANNOT_CREATE_IMPLEMENTATION,
                 childEntry.getDN().toString(), String.valueOf(e));
      }
    }
  }
  /**
   * Performs any cleanup work that may be needed when the server is shutting
   * down.
   */
  public void finalizeGroupManager()
  {
    assert debugEnter(CLASS_NAME, "finalizeGroupManager");
    deregisterAllGroups();
    for (Group groupImplementation : groupImplementations.values())
    {
      groupImplementation.finalizeGroupImplementation();
    }
    groupImplementations.clear();
  }
  /**
   * Retrieves an {@code Iterable} object that may be used to cursor across the
   * group implementations defined in the server.
   *
   * @return  An {@code Iterable} object that may be used to cursor across the
   *          group implementations defined in the server.
   */
  public Iterable<Group> getGroupImplementations()
  {
    assert debugEnter(CLASS_NAME, "getGroupImplementations");
    return groupImplementations.values();
  }
  /**
   * Retrieves an {@code Iterable} object that may be used to cursor across the
   * group instances defined in the server.
   *
   * @return  An {@code Iterable} object that may be used to cursor across the
   *          group instances defined in the server.
   */
  public Iterable<Group> getGroupInstances()
  {
    assert debugEnter(CLASS_NAME, "getGroupInstances");
    return groupInstances.values();
  }
  /**
   * Retrieves the group instance defined in the entry with the specified DN.
   *
   * @param  entryDN  The DN of the entry containing the definition of the group
   *                  instance to retrieve.
   *
   * @return  The group instance defined in the entry with the specified DN, or
   *          {@code null} if no such group is currently defined.
   */
  public Group getGroupInstance(DN entryDN)
  {
    assert debugEnter(CLASS_NAME, "getGroupInstance", String.valueOf(entryDN));
    Group group = groupInstances.get(entryDN);
    if (group == null)
    {
      // FIXME -- Should we try to retrieve the corresponding entry and see if
      // it is a group?
    }
    return group;
  }
  /**
   * Indicates whether the configuration entry that will result from a proposed
   * modification is acceptable to this change listener.
   *
   * @param  configEntry         The configuration entry that will result from
   *                             the requested update.
   * @param  unacceptableReason  A buffer to which this method can append a
   *                             human-readable message explaining why the
   *                             proposed change is not acceptable.
   *
   * @return  <CODE>true</CODE> if the proposed entry contains an acceptable
   *          configuration, or <CODE>false</CODE> if it does not.
   */
  public boolean configChangeIsAcceptable(ConfigEntry configEntry,
                                          StringBuilder unacceptableReason)
  {
    assert debugEnter(CLASS_NAME, "configChangeIsAcceptable",
                      String.valueOf(configEntry), "java.lang.StringBuilder");
    // Make sure that the entry has an appropriate objectclass for an extended
    // operation handler.
    if (! configEntry.hasObjectClass(OC_GROUP_IMPLEMENTATION))
    {
      int    msgID   = MSGID_CONFIG_GROUP_INVALID_OBJECTCLASS;
      String message = getMessage(msgID, configEntry.getDN().toString());
      unacceptableReason.append(message);
      return false;
    }
    // Make sure that the entry specifies the group implementation class name.
    StringConfigAttribute classNameAttr;
    try
    {
      StringConfigAttribute classStub =
           new StringConfigAttribute(ATTR_GROUP_IMPLEMENTATION_CLASS,
                    getMessage(MSGID_CONFIG_GROUP_DESCRIPTION_CLASS_NAME),
                    true, false, true);
      classNameAttr = (StringConfigAttribute)
                      configEntry.getConfigAttribute(classStub);
      if (classNameAttr == null)
      {
        int    msgID   = MSGID_CONFIG_GROUP_NO_CLASS_NAME;
        String message = getMessage(msgID, configEntry.getDN().toString());
        unacceptableReason.append(message);
        return false;
      }
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "configChangeIsAcceptable", e);
      int    msgID   = MSGID_CONFIG_GROUP_INVALID_CLASS_NAME;
      String message = getMessage(msgID, configEntry.getDN().toString(),
                                  String.valueOf(e));
      unacceptableReason.append(message);
      return false;
    }
    Class groupClass;
    try
    {
      // FIXME -- Should this be done with a custom class loader?
      groupClass = Class.forName(classNameAttr.pendingValue());
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "configChangeIsAcceptable", e);
      int    msgID   = MSGID_CONFIG_GROUP_INVALID_CLASS_NAME;
      String message = getMessage(msgID, configEntry.getDN().toString(),
                                  String.valueOf(e));
      unacceptableReason.append(message);
      return false;
    }
    try
    {
      Group group = (Group) groupClass.newInstance();
    }
    catch(Exception e)
    {
      assert debugException(CLASS_NAME, "configChangeIsAcceptable", e);
      int    msgID   = MSGID_CONFIG_GROUP_INVALID_CLASS;
      String message = getMessage(msgID, groupClass.getName(),
                                  String.valueOf(configEntry.getDN()),
                                  String.valueOf(e));
      unacceptableReason.append(message);
      return false;
    }
    // See if this extended operation handler should be enabled.
    BooleanConfigAttribute enabledAttr;
    try
    {
      BooleanConfigAttribute enabledStub =
           new BooleanConfigAttribute(ATTR_GROUP_IMPLEMENTATION_ENABLED,
                    getMessage(MSGID_CONFIG_GROUP_DESCRIPTION_ENABLED),
                               false);
      enabledAttr = (BooleanConfigAttribute)
                    configEntry.getConfigAttribute(enabledStub);
      if (enabledAttr == null)
      {
        int    msgID   = MSGID_CONFIG_GROUP_NO_ENABLED_ATTR;
        String message = getMessage(msgID, configEntry.getDN().toString());
        unacceptableReason.append(message);
        return false;
      }
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "configChangeIsAcceptable", e);
      int    msgID   = MSGID_CONFIG_GROUP_INVALID_ENABLED_VALUE;
      String message = getMessage(msgID, configEntry.getDN().toString(),
                                  String.valueOf(e));
      unacceptableReason.append(message);
      return false;
    }
    // If we've gotten here then the group implementation entry appears to be
    // acceptable.
    return true;
  }
  /**
   * Attempts to apply a new configuration to this Directory Server component
   * based on the provided changed entry.
   *
   * @param  configEntry  The configuration entry that containing the updated
   *                      configuration for this component.
   *
   * @return  Information about the result of processing the configuration
   *          change.
   */
  public ConfigChangeResult applyConfigurationChange(ConfigEntry configEntry)
  {
    assert debugEnter(CLASS_NAME, "applyConfigurationChange",
                      String.valueOf(configEntry));
    DN                configEntryDN       = configEntry.getDN();
    ResultCode        resultCode          = ResultCode.SUCCESS;
    boolean           adminActionRequired = false;
    ArrayList<String> messages            = new ArrayList<String>();
    // Make sure that the entry has an appropriate objectclass for a group
    // implementation.
    if (! configEntry.hasObjectClass(OC_GROUP_IMPLEMENTATION))
    {
      int msgID = MSGID_CONFIG_GROUP_INVALID_OBJECTCLASS;
      messages.add(getMessage(msgID, String.valueOf(configEntryDN)));
      resultCode = ResultCode.UNWILLING_TO_PERFORM;
      return new ConfigChangeResult(resultCode, adminActionRequired, messages);
    }
    // Get the corresponding group implementation instance if it is active.
    Group groupImplementation = groupImplementations.get(configEntryDN);
    // See if this handler should be enabled or disabled.
    boolean needsEnabled = false;
    BooleanConfigAttribute enabledAttr;
    try
    {
      BooleanConfigAttribute enabledStub =
           new BooleanConfigAttribute(ATTR_GROUP_IMPLEMENTATION_ENABLED,
                    getMessage(MSGID_CONFIG_GROUP_DESCRIPTION_ENABLED), false);
      enabledAttr = (BooleanConfigAttribute)
                    configEntry.getConfigAttribute(enabledStub);
      if (enabledAttr == null)
      {
        int msgID = MSGID_CONFIG_GROUP_NO_ENABLED_ATTR;
        messages.add(getMessage(msgID, String.valueOf(configEntryDN)));
        resultCode = ResultCode.UNWILLING_TO_PERFORM;
        return new ConfigChangeResult(resultCode, adminActionRequired,
                                      messages);
      }
      if (enabledAttr.activeValue())
      {
        if (groupImplementation == null)
        {
          needsEnabled = true;
        }
        else
        {
          // The group implementation is already active, so no action is
          // required.
        }
      }
      else
      {
        if (groupImplementation == null)
        {
          // The group implementation is already disabled, so no action is
          // required and we can short-circuit out of this processing.
          return new ConfigChangeResult(resultCode, adminActionRequired,
                                        messages);
        }
        else
        {
          // The group implementation is active, so it needs to be disabled.  Do
          // this and return that we were successful.
          groupImplementations.remove(configEntryDN);
          Iterator<Group> iterator = groupInstances.values().iterator();
          while (iterator.hasNext())
          {
            Group g = iterator.next();
            if (g.getClass().getName().equals(
                     groupImplementation.getClass().getName()))
            {
              iterator.remove();
            }
          }
          groupImplementation.finalizeGroupImplementation();
          return new ConfigChangeResult(resultCode, adminActionRequired,
                                        messages);
        }
      }
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "applyConfigurationChange", e);
      int msgID = MSGID_CONFIG_GROUP_INVALID_ENABLED_VALUE;
      messages.add(getMessage(msgID, String.valueOf(configEntryDN),
                              String.valueOf(e)));
      resultCode = DirectoryServer.getServerErrorResultCode();
      return new ConfigChangeResult(resultCode, adminActionRequired, messages);
    }
    // Make sure that the entry specifies the group implementation class name.
    // If it has changed, then we will not try to dynamically apply it.
    String className;
    try
    {
      StringConfigAttribute classStub =
           new StringConfigAttribute(ATTR_GROUP_IMPLEMENTATION_CLASS,
                    getMessage(MSGID_CONFIG_GROUP_DESCRIPTION_CLASS_NAME),
                    true, false, true);
      StringConfigAttribute classNameAttr =
           (StringConfigAttribute) configEntry.getConfigAttribute(classStub);
      if (classNameAttr == null)
      {
        int msgID = MSGID_CONFIG_GROUP_NO_CLASS_NAME;
        messages.add(getMessage(msgID, String.valueOf(configEntryDN)));
        resultCode = ResultCode.OBJECTCLASS_VIOLATION;
        return new ConfigChangeResult(resultCode, adminActionRequired,
                                      messages);
      }
      className = classNameAttr.pendingValue();
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "applyConfigurationChange", e);
      int msgID = MSGID_CONFIG_GROUP_INVALID_CLASS_NAME;
      messages.add(getMessage(msgID, String.valueOf(configEntryDN),
                              String.valueOf(e)));
      resultCode = DirectoryServer.getServerErrorResultCode();
      return new ConfigChangeResult(resultCode, adminActionRequired, messages);
    }
    boolean classChanged = false;
    String  oldClassName = null;
    if (groupImplementation != null)
    {
      oldClassName = groupImplementation.getClass().getName();
      classChanged = (! className.equals(oldClassName));
    }
    if (classChanged)
    {
      // This will not be applied dynamically.  Add a message to the response
      // and indicate that admin action is required.
      adminActionRequired = true;
      messages.add(getMessage(MSGID_CONFIG_GROUP_CLASS_ACTION_REQUIRED,
                              String.valueOf(oldClassName),
                              String.valueOf(className),
                              String.valueOf(configEntryDN)));
      return new ConfigChangeResult(resultCode, adminActionRequired, messages);
    }
    if (needsEnabled)
    {
      try
      {
        // FIXME -- Should this be done with a dynamic class loader?
        Class groupClass = Class.forName(className);
        groupImplementation = (Group) groupClass.newInstance();
      }
      catch (Exception e)
      {
        assert debugException(CLASS_NAME, "applyConfigurationChange", e);
        int msgID = MSGID_CONFIG_GROUP_INVALID_CLASS;
        messages.add(getMessage(msgID, className, String.valueOf(configEntryDN),
                                String.valueOf(e)));
        resultCode = DirectoryServer.getServerErrorResultCode();
        return new ConfigChangeResult(resultCode, adminActionRequired,
                                      messages);
      }
      try
      {
        groupImplementation.initializeGroupImplementation(configEntry);
      }
      catch (Exception e)
      {
        assert debugException(CLASS_NAME, "applyConfigurationChange", e);
        int msgID = MSGID_CONFIG_GROUP_INITIALIZATION_FAILED;
        messages.add(getMessage(msgID, className,
                                String.valueOf(configEntryDN),
                                String.valueOf(e)));
        resultCode = DirectoryServer.getServerErrorResultCode();
        return new ConfigChangeResult(resultCode, adminActionRequired,
                                      messages);
      }
      groupImplementations.put(configEntryDN, groupImplementation);
      // FIXME -- We need to make sure to find all groups of this type in the
      // server before returning.
      return new ConfigChangeResult(resultCode, adminActionRequired, messages);
    }
    // If we've gotten here, then there haven't been any changes to anything
    // that we care about.
    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
  }
  /**
   * Indicates whether the configuration entry that will result from a proposed
   * add is acceptable to this add listener.
   *
   * @param  configEntry         The configuration entry that will result from
   *                             the requested add.
   * @param  unacceptableReason  A buffer to which this method can append a
   *                             human-readable message explaining why the
   *                             proposed entry is not acceptable.
   *
   * @return  <CODE>true</CODE> if the proposed entry contains an acceptable
   *          configuration, or <CODE>false</CODE> if it does not.
   */
  public boolean configAddIsAcceptable(ConfigEntry configEntry,
                                       StringBuilder unacceptableReason)
  {
    assert debugEnter(CLASS_NAME, "configAddIsAcceptable",
                      String.valueOf(configEntry), "java.lang.StringBuilder");
    // Make sure that no entry already exists with the specified DN.
    DN configEntryDN = configEntry.getDN();
    if (groupImplementations.containsKey(configEntryDN))
    {
      int    msgID   = MSGID_CONFIG_GROUP_EXISTS;
      String message = getMessage(msgID, String.valueOf(configEntryDN));
      unacceptableReason.append(message);
      return false;
    }
    // Make sure that the entry has an appropriate objectclass for a group
    // implementation.
    if (! configEntry.hasObjectClass(OC_GROUP_IMPLEMENTATION))
    {
      int    msgID   = MSGID_CONFIG_GROUP_INVALID_OBJECTCLASS;
      String message = getMessage(msgID, configEntry.getDN().toString());
      unacceptableReason.append(message);
      return false;
    }
    // Make sure that the entry specifies the group implementation class.
    StringConfigAttribute classNameAttr;
    try
    {
      StringConfigAttribute classStub =
           new StringConfigAttribute(ATTR_GROUP_IMPLEMENTATION_CLASS,
                    getMessage(MSGID_CONFIG_GROUP_DESCRIPTION_CLASS_NAME),
                    true, false, true);
      classNameAttr = (StringConfigAttribute)
                      configEntry.getConfigAttribute(classStub);
      if (classNameAttr == null)
      {
        int    msgID   = MSGID_CONFIG_GROUP_NO_CLASS_NAME;
        String message = getMessage(msgID, configEntry.getDN().toString());
        unacceptableReason.append(message);
        return false;
      }
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "configAddIsAcceptable", e);
      int    msgID   = MSGID_CONFIG_GROUP_INVALID_CLASS_NAME;
      String message = getMessage(msgID, configEntry.getDN().toString(),
                                  String.valueOf(e));
      unacceptableReason.append(message);
      return false;
    }
    Class groupClass;
    try
    {
      // FIXME -- Should this be done with a custom class loader?
      groupClass = Class.forName(classNameAttr.pendingValue());
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "configAddIsAcceptable", e);
      int    msgID   = MSGID_CONFIG_GROUP_INVALID_CLASS_NAME;
      String message = getMessage(msgID, configEntry.getDN().toString(),
                                  String.valueOf(e));
      unacceptableReason.append(message);
      return false;
    }
    Group groupImplementation;
    try
    {
      groupImplementation = (Group) groupClass.newInstance();
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "configAddIsAcceptable", e);
      int    msgID   = MSGID_CONFIG_GROUP_INVALID_CLASS;
      String message = getMessage(msgID, groupClass.getName(),
                                  String.valueOf(configEntryDN),
                                  String.valueOf(e));
      unacceptableReason.append(message);
      return false;
    }
    // If the handler is a configurable component, then make sure that its
    // configuration is valid.
    if (groupImplementation instanceof ConfigurableComponent)
    {
      ConfigurableComponent cc = (ConfigurableComponent) groupImplementation;
      LinkedList<String> errorMessages = new LinkedList<String>();
      if (! cc.hasAcceptableConfiguration(configEntry, errorMessages))
      {
        if (errorMessages.isEmpty())
        {
          int msgID = MSGID_CONFIG_GROUP_UNACCEPTABLE_CONFIG;
          unacceptableReason.append(getMessage(msgID,
                                               String.valueOf(configEntryDN)));
        }
        else
        {
          Iterator<String> iterator = errorMessages.iterator();
          unacceptableReason.append(iterator.next());
          while (iterator.hasNext())
          {
            unacceptableReason.append("  ");
            unacceptableReason.append(iterator.next());
          }
        }
        return false;
      }
    }
    // See if this handler should be enabled.
    BooleanConfigAttribute enabledAttr;
    try
    {
      BooleanConfigAttribute enabledStub =
           new BooleanConfigAttribute(ATTR_GROUP_IMPLEMENTATION_ENABLED,
                    getMessage(MSGID_CONFIG_GROUP_DESCRIPTION_ENABLED),
                               false);
      enabledAttr = (BooleanConfigAttribute)
                    configEntry.getConfigAttribute(enabledStub);
      if (enabledAttr == null)
      {
        int    msgID   = MSGID_CONFIG_GROUP_NO_ENABLED_ATTR;
        String message = getMessage(msgID, configEntry.getDN().toString());
        unacceptableReason.append(message);
        return false;
      }
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "configAddIsAcceptable", e);
      int    msgID   = MSGID_CONFIG_GROUP_INVALID_ENABLED_VALUE;
      String message = getMessage(msgID, configEntry.getDN().toString(),
                                  String.valueOf(e));
      unacceptableReason.append(message);
      return false;
    }
    // If we've gotten here then the handler entry appears to be acceptable.
    return true;
  }
  /**
   * Attempts to apply a new configuration based on the provided added entry.
   *
   * @param  configEntry  The new configuration entry that contains the
   *                      configuration to apply.
   *
   * @return  Information about the result of processing the configuration
   *          change.
   */
  public ConfigChangeResult applyConfigurationAdd(ConfigEntry configEntry)
  {
    assert debugEnter(CLASS_NAME, "applyConfigurationAdd",
                      String.valueOf(configEntry));
    DN                configEntryDN       = configEntry.getDN();
    ResultCode        resultCode          = ResultCode.SUCCESS;
    boolean           adminActionRequired = false;
    ArrayList<String> messages            = new ArrayList<String>();
    // Make sure that the entry has an appropriate objectclass for a group
    // implementation.
    if (! configEntry.hasObjectClass(OC_GROUP_IMPLEMENTATION))
    {
      int    msgID   = MSGID_CONFIG_GROUP_INVALID_OBJECTCLASS;
      messages.add(getMessage(msgID, String.valueOf(configEntryDN)));
      resultCode = ResultCode.UNWILLING_TO_PERFORM;
      return new ConfigChangeResult(resultCode, adminActionRequired, messages);
    }
    // See if this group implementation should be enabled or disabled.
    BooleanConfigAttribute enabledAttr;
    try
    {
      BooleanConfigAttribute enabledStub =
           new BooleanConfigAttribute(ATTR_GROUP_IMPLEMENTATION_ENABLED,
                    getMessage(MSGID_CONFIG_GROUP_DESCRIPTION_ENABLED),
                               false);
      enabledAttr = (BooleanConfigAttribute)
                    configEntry.getConfigAttribute(enabledStub);
      if (enabledAttr == null)
      {
        // The attribute doesn't exist, so it will be disabled by default.
        int msgID = MSGID_CONFIG_GROUP_NO_ENABLED_ATTR;
        messages.add(getMessage(msgID, String.valueOf(configEntryDN)));
        resultCode = ResultCode.SUCCESS;
        return new ConfigChangeResult(resultCode, adminActionRequired,
                                      messages);
      }
      else if (! enabledAttr.activeValue())
      {
        // It is explicitly configured as disabled, so we don't need to do
        // anything.
        return new ConfigChangeResult(resultCode, adminActionRequired,
                                      messages);
      }
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "applyConfigurationAdd", e);
      int msgID = MSGID_CONFIG_GROUP_INVALID_ENABLED_VALUE;
      messages.add(getMessage(msgID, String.valueOf(configEntryDN),
                              String.valueOf(e)));
      resultCode = DirectoryServer.getServerErrorResultCode();
      return new ConfigChangeResult(resultCode, adminActionRequired, messages);
    }
    // Make sure that the entry specifies the group implementation class name.
    String className;
    try
    {
      StringConfigAttribute classStub =
           new StringConfigAttribute(ATTR_GROUP_IMPLEMENTATION_CLASS,
                    getMessage(MSGID_CONFIG_GROUP_DESCRIPTION_CLASS_NAME),
                    true, false, true);
      StringConfigAttribute classNameAttr =
           (StringConfigAttribute) configEntry.getConfigAttribute(classStub);
      if (classNameAttr == null)
      {
        int msgID = MSGID_CONFIG_GROUP_NO_CLASS_NAME;
        messages.add(getMessage(msgID, String.valueOf(configEntryDN)));
        resultCode = ResultCode.OBJECTCLASS_VIOLATION;
        return new ConfigChangeResult(resultCode, adminActionRequired,
                                      messages);
      }
      className = classNameAttr.pendingValue();
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "applyConfigurationAdd", e);
      int msgID = MSGID_CONFIG_GROUP_INVALID_CLASS_NAME;
      messages.add(getMessage(msgID, String.valueOf(configEntryDN),
                              String.valueOf(e)));
      resultCode = DirectoryServer.getServerErrorResultCode();
      return new ConfigChangeResult(resultCode, adminActionRequired, messages);
    }
    // Load and initialize the group implementation class, and register it with
    // the Directory Server.
    Group groupImplementation;
    try
    {
      // FIXME -- Should this be done with a dynamic class loader?
      Class groupClass = Class.forName(className);
      groupImplementation = (Group) groupClass.newInstance();
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "applyConfigurationAdd", e);
      int msgID = MSGID_CONFIG_GROUP_INVALID_CLASS;
      messages.add(getMessage(msgID, className, String.valueOf(configEntryDN),
                              String.valueOf(e)));
      resultCode = DirectoryServer.getServerErrorResultCode();
      return new ConfigChangeResult(resultCode, adminActionRequired, messages);
    }
    try
    {
      groupImplementation.initializeGroupImplementation(configEntry);
    }
    catch (Exception e)
    {
      assert debugException(CLASS_NAME, "applyConfigurationAdd", e);
      int msgID = MSGID_CONFIG_GROUP_INITIALIZATION_FAILED;
      messages.add(getMessage(msgID, className, String.valueOf(configEntryDN),
                              String.valueOf(e)));
      resultCode = DirectoryServer.getServerErrorResultCode();
      return new ConfigChangeResult(resultCode, adminActionRequired, messages);
    }
    groupImplementations.put(configEntryDN, groupImplementation);
    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
  }
  /**
   * Indicates whether it is acceptable to remove the provided configuration
   * entry.
   *
   * @param  configEntry         The configuration entry that will be removed
   *                             from the configuration.
   * @param  unacceptableReason  A buffer to which this method can append a
   *                             human-readable message explaining why the
   *                             proposed delete is not acceptable.
   *
   * @return  <CODE>true</CODE> if the proposed entry may be removed from the
   *          configuration, or <CODE>false</CODE> if not.
   */
  public boolean configDeleteIsAcceptable(ConfigEntry configEntry,
                                          StringBuilder unacceptableReason)
  {
    assert debugEnter(CLASS_NAME, "configDeleteIsAcceptable",
                      String.valueOf(configEntry), "java.lang.StringBuilder");
    // A delete should always be acceptable, so just return true.
    return true;
  }
  /**
   * Attempts to apply a new configuration based on the provided deleted entry.
   *
   * @param  configEntry  The new configuration entry that has been deleted.
   *
   * @return  Information about the result of processing the configuration
   *          change.
   */
  public ConfigChangeResult applyConfigurationDelete(ConfigEntry configEntry)
  {
    assert debugEnter(CLASS_NAME, "applyConfigurationDelete",
                      String.valueOf(configEntry));
    DN         configEntryDN       = configEntry.getDN();
    ResultCode resultCode          = ResultCode.SUCCESS;
    boolean    adminActionRequired = false;
    // See if the entry is registered as a group implementation.  If so, then
    // deregister it and remove any groups of that type.
    Group groupImplementation = groupImplementations.remove(configEntryDN);
    if (groupImplementation != null)
    {
      Iterator<Group> iterator = groupInstances.values().iterator();
      while (iterator.hasNext())
      {
        Group g = iterator.next();
        if (g.getClass().getName().equals(
                 groupImplementation.getClass().getName()))
        {
          iterator.remove();
        }
      }
      groupImplementation.finalizeGroupImplementation();
    }
    return new ConfigChangeResult(resultCode, adminActionRequired);
  }
  /**
   * {@inheritDoc}  In this case, the server will search the backend to find
   * all group instances that it may contain and register them with this group
   * manager.
   */
  public void performBackendInitializationProcessing(Backend backend)
  {
    assert debugEnter(CLASS_NAME, "performBackendInitializationProcessing",
                      String.valueOf(backend));
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    for (Group groupImplementation : groupImplementations.values())
    {
      SearchFilter filter;
      try
      {
        filter = groupImplementation.getGroupDefinitionFilter();
      }
      catch (Exception e)
      {
        assert debugException(CLASS_NAME,
                              "performBackendInitializationProcessing", e);
        // FIXME -- Is there anything that we need to do here?
        continue;
      }
      for (DN baseDN : backend.getBaseDNs())
      {
        try
        {
          if (! backend.entryExists(baseDN))
          {
            continue;
          }
        }
        catch (Exception e)
        {
          assert debugException(CLASS_NAME,
                                "performBackendInitializationProcessing", e);
          // FIXME -- Is there anything that we need to do here?
          continue;
        }
        InternalSearchOperation internalSearch =
             new InternalSearchOperation(conn, conn.nextOperationID(),
                                         conn.nextMessageID(), null, baseDN,
                                         SearchScope.WHOLE_SUBTREE,
                                         DereferencePolicy.NEVER_DEREF_ALIASES,
                                         0, 0, false, filter, null, null);
        try
        {
          backend.search(internalSearch);
        }
        catch (Exception e)
        {
          assert debugException(CLASS_NAME,
                                "performBackendInitializationProcessing", e);
          // FIXME -- Is there anything that we need to do here?
          continue;
        }
        for (SearchResultEntry entry : internalSearch.getSearchEntries())
        {
          try
          {
            Group groupInstance = groupImplementation.newInstance(entry);
            groupInstances.put(entry.getDN(), groupInstance);
          }
          catch (Exception e)
          {
            assert debugException(CLASS_NAME,
                                  "performBackendInitializationProcessing", e);
            // FIXME -- Handle this.
            continue;
          }
        }
      }
    }
  }
  /**
   * {@inheritDoc}  In this case, the server will de-register all group
   * instances associated with entries in the provided backend.
   */
  public void performBackendFinalizationProcessing(Backend backend)
  {
    assert debugEnter(CLASS_NAME, "performBackendFinalizationProcessing",
                      String.valueOf(backend));
    Iterator<Map.Entry<DN,Group>> iterator =
         groupInstances.entrySet().iterator();
    while (iterator.hasNext())
    {
      Map.Entry<DN,Group> mapEntry = iterator.next();
      DN groupEntryDN = mapEntry.getKey();
      if (backend.handlesEntry(groupEntryDN))
      {
        iterator.remove();
      }
    }
  }
  /**
   * {@inheritDoc}  In this case, each entry is checked to see if it contains
   * a group definition, and if so it will be instantiated and registered with
   * this group manager.
   */
  public void handleAddOperation(AddOperation addOperation,
                                 Entry entry)
  {
    assert debugEnter(CLASS_NAME, "handleAddOperation",
                      String.valueOf(addOperation), String.valueOf(entry));
    List<Control> requestControls = addOperation.getRequestControls();
    if (requestControls != null)
    {
      for (Control c : requestControls)
      {
        if (c.getOID().equals(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE))
        {
          return;
        }
      }
    }
    createAndRegisterGroup(entry);
  }
  /**
   * {@inheritDoc}  In this case, if the entry is associated with a registered
   * group instance, then that group instance will be deregistered.
   */
  public void handleDeleteOperation(DeleteOperation deleteOperation,
                                    Entry entry)
  {
    assert debugEnter(CLASS_NAME, "handleDeleteOperation",
                      String.valueOf(deleteOperation), String.valueOf(entry));
    List<Control> requestControls = deleteOperation.getRequestControls();
    if (requestControls != null)
    {
      for (Control c : requestControls)
      {
        if (c.getOID().equals(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE))
        {
          return;
        }
      }
    }
    groupInstances.remove(entry.getDN());
  }
  /**
   * {@inheritDoc}  In this case, if the entry is associated with a registered
   * group instance, then that instance will be recreated from the contents of
   * the provided entry and re-registered with the group manager.
   */
  public void handleModifyOperation(ModifyOperation modifyOperation,
                                    Entry oldEntry, Entry newEntry)
  {
    assert debugEnter(CLASS_NAME, "handleModifyOperation",
                      String.valueOf(modifyOperation), String.valueOf(oldEntry),
                      String.valueOf(newEntry));
    List<Control> requestControls = modifyOperation.getRequestControls();
    if (requestControls != null)
    {
      for (Control c : requestControls)
      {
        if (c.getOID().equals(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE))
        {
          return;
        }
      }
    }
    if (groupInstances.containsKey(oldEntry.getDN()))
    {
      synchronized (groupInstances)
      {
        if (! oldEntry.getDN().equals(newEntry.getDN()))
        {
          // This should never happen, but check for it anyway.
          groupInstances.remove(oldEntry.getDN());
        }
        createAndRegisterGroup(newEntry);
      }
    }
  }
  /**
   * {@inheritDoc}  In this case, if the entry is associated with a registered
   * group instance, then that instance will be recreated from the contents of
   * the provided entry and re-registered with the group manager under the new
   * DN, and the old instance will be deregistered.
   */
  public void handleModifyDNOperation(
                   ModifyDNOperation modifyDNOperation,
                   Entry oldEntry, Entry newEntry)
  {
    assert debugEnter(CLASS_NAME, "handleModifyDNOperation",
                      String.valueOf(modifyDNOperation),
                      String.valueOf(oldEntry), String.valueOf(newEntry));
    List<Control> requestControls = modifyDNOperation.getRequestControls();
    if (requestControls != null)
    {
      for (Control c : requestControls)
      {
        if (c.getOID().equals(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE))
        {
          return;
        }
      }
    }
    if (groupInstances.containsKey(oldEntry.getDN()))
    {
      synchronized (groupInstances)
      {
        createAndRegisterGroup(newEntry);
        groupInstances.remove(oldEntry.getDN());
      }
    }
  }
  /**
   * Attempts to create a group instance from the provided entry, and if that is
   * successful then register it with the server, overwriting any existing
   * group instance that may be registered with the same DN.
   *
   * @param  entry  The entry containing the potential group definition.
   */
  private void createAndRegisterGroup(Entry entry)
  {
    assert debugEnter(CLASS_NAME, "createAndRegisterGroup",
                      String.valueOf(entry));
    for (Group groupImplementation : groupImplementations.values())
    {
      try
      {
        if (groupImplementation.isGroupDefinition(entry))
        {
          Group groupInstance = groupImplementation.newInstance(entry);
          groupInstances.put(entry.getDN(), groupInstance);
        }
      }
      catch (Exception e)
      {
        assert debugException(CLASS_NAME, "createAndRegisterGroup", e);
        // FIXME -- Do we need to do anything else?
      }
    }
  }
  /**
   * Removes all group instances that might happen to be registered with the
   * group manager.  This method is only intended for testing purposes and
   * should not be called by any other code.
   */
  void deregisterAllGroups()
  {
    groupInstances.clear();
  }
}
opends/src/server/org/opends/server/extensions/FilteredStaticGroupMemberList.java
New file
@@ -0,0 +1,338 @@
/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.extensions;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import org.opends.server.types.DirectoryConfig;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.MemberList;
import org.opends.server.types.MembershipException;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchScope;
import static org.opends.server.loggers.Debug.*;
import static org.opends.server.messages.ExtensionsMessages.*;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.util.Validator.*;
/**
 * This class provides an implementation of the {@code MemberList} class that
 * may be used in conjunction when static groups when additional criteria is to
 * be used to select a subset of the group members.
 */
public class FilteredStaticGroupMemberList
       extends MemberList
{
  /**
   * The fully-qualified name of this class for debugging purposes.
   */
  private static final String CLASS_NAME =
       "org.opends.server.extensions.FilteredStaticGroupMemberList";
  // The base DN below which all returned members should exist.
  private DN baseDN;
  // The DN of the static group with which this member list is associated.
  private DN groupDN;
  // The entry of the next entry that matches the member list criteria.
  private Entry nextMatchingEntry;
  // The iterator used to traverse the set of member DNs.
  private Iterator<DN> memberDNIterator;
  // The set of DNs for the users that are members of the associated static
  // group.
  private LinkedList<DN> memberDNs;
  // The membership exception that should be thrown the next time a member is
  // requested.
  private MembershipException nextMembershipException;
  // The search filter that all returned members should match.
  private SearchFilter filter;
  // The search scope to apply against the base DN for the member subset.
  private SearchScope scope;
  /**
   * Creates a new filtered static group member list with the provided
   * information.
   *
   * @param  groupDN    The DN of the static group with which this member list
   *                    is associated.
   * @param  memberDNs  The set of DNs for the users that are members of the
   *                    associated static group.
   * @param  baseDN     The base DN below which all returned members should
   *                    exist.  If this is {@code null}, then all members will
   *                    be considered to match the base and scope criteria.
   * @param  scope      The search scope to apply against the base DN when
   *                    selecting eligible members.
   * @param  filter     The search filter which all returned members should
   *                    match.  If this is {@code null}, then all members will
   *                    be considered eligible.
   */
  public FilteredStaticGroupMemberList(DN groupDN, Set<DN> memberDNs, DN baseDN,
                                       SearchScope scope, SearchFilter filter)
  {
    assert debugConstructor(CLASS_NAME, String.valueOf(memberDNs));
    ensureNotNull(groupDN, memberDNs);
    this.groupDN   = groupDN;
    this.memberDNs = new LinkedList<DN>(memberDNs);
    memberDNIterator = memberDNs.iterator();
    this.baseDN = baseDN;
    this.filter = filter;
    if (scope == null)
    {
      this.scope = SearchScope.WHOLE_SUBTREE;
    }
    else
    {
      this.scope = scope;
    }
    nextMatchingEntry       = null;
    nextMembershipException = null;
    nextMemberInternal();
  }
  /**
   * Attempts to find the next member that matches the associated criteria.
   * When this method returns, if {@code nextMembershipException} is
   * non-{@code null}, then that exception should be thrown on the next attempt
   * to retrieve a member.  If {@code nextMatchingEntry} is non-{@code null},
   * then that entry should be returned on the next attempt to retrieve a
   * member.  If both are {@code null}, then there are no more members to
   * return.
   */
  private void nextMemberInternal()
  {
    while (memberDNIterator.hasNext())
    {
      DN nextDN = memberDNIterator.next();
      // Check to see if we can eliminate the entry as a possible match purely
      // based on base DN and scope.
      if (baseDN != null)
      {
        switch (scope)
        {
          case BASE_OBJECT:
            if (! baseDN.equals(nextDN))
            {
              continue;
            }
            break;
          case SINGLE_LEVEL:
            if (! baseDN.equals(nextDN.getParent()))
            {
              continue;
            }
            break;
          case SUBORDINATE_SUBTREE:
            if (baseDN.equals(nextDN) || (! baseDN.isAncestorOf(nextDN)))
            {
              continue;
            }
            break;
          default:
            if (! baseDN.isAncestorOf(nextDN))
            {
              continue;
            }
            break;
        }
      }
      // Get the entry for the potential member.  If we can't, then populate
      // the next membership exception.
      try
      {
        Entry memberEntry = DirectoryConfig.getEntry(nextDN);
        if (memberEntry == null)
        {
          int    msgID   = MSGID_STATICMEMBERS_NO_SUCH_ENTRY;
          String message = getMessage(msgID, String.valueOf(nextDN),
                                      String.valueOf(groupDN));
          nextMembershipException =
               new MembershipException(msgID, message, true);
          return;
        }
        if (filter == null)
        {
          nextMatchingEntry = memberEntry;
          return;
        }
        else
        {
          if (filter.matchesEntry(memberEntry))
          {
            nextMatchingEntry = memberEntry;
            return;
          }
          else
          {
            continue;
          }
        }
      }
      catch (DirectoryException de)
      {
        assert debugException(CLASS_NAME, "nextMemberEntry", de);
        int    msgID   = MSGID_STATICMEMBERS_CANNOT_GET_ENTRY;
        String message = getMessage(msgID, String.valueOf(nextDN),
                                    String.valueOf(groupDN),
                                    String.valueOf(de.getErrorMessage()));
        nextMembershipException =
             new MembershipException(msgID, message, true, de);
        return;
      }
    }
    // If we've gotten here, then there are no more members.
    nextMatchingEntry       = null;
    nextMembershipException = null;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean hasMoreMembers()
  {
    assert debugEnter(CLASS_NAME, "hasMoreMembers");
    if (! memberDNIterator.hasNext())
    {
      return false;
    }
    return ((nextMatchingEntry != null) || (nextMembershipException != null));
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public DN nextMemberDN()
         throws MembershipException
  {
    assert debugEnter(CLASS_NAME, "nextMemberDN");
    if (! memberDNIterator.hasNext())
    {
      return null;
    }
    Entry e = nextMemberEntry();
    if (e == null)
    {
      return null;
    }
    else
    {
      return e.getDN();
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public Entry nextMemberEntry()
         throws MembershipException
  {
    assert debugEnter(CLASS_NAME, "nextMemberEntry");
    if (! memberDNIterator.hasNext())
    {
      return null;
    }
    if (nextMembershipException == null)
    {
      Entry e = nextMatchingEntry;
      nextMatchingEntry = null;
      nextMemberInternal();
      return e;
    }
    else
    {
      MembershipException me = nextMembershipException;
      nextMembershipException = null;
      nextMemberInternal();
      throw me;
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void close()
  {
    assert debugEnter(CLASS_NAME, "close");
    // No implementation is required.
  }
}
opends/src/server/org/opends/server/extensions/SimpleStaticGroupMemberList.java
New file
@@ -0,0 +1,185 @@
/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.extensions;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import org.opends.server.types.DirectoryConfig;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.MemberList;
import org.opends.server.types.MembershipException;
import static org.opends.server.loggers.Debug.*;
import static org.opends.server.messages.ExtensionsMessages.*;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.util.Validator.*;
/**
 * This class provides an implementation of the {@code MemberList} class that
 * may be used in conjunction when static groups when no additional criteria is
 * to be used to select a subset of the group members.
 */
public class SimpleStaticGroupMemberList
       extends MemberList
{
  /**
   * The fully-qualified name of this class for debugging purposes.
   */
  private static final String CLASS_NAME =
       "org.opends.server.extensions.SimpleStaticGroupMemberList";
  // The DN of the static group with which this member list is associated.
  private DN groupDN;
  // The iterator used to traverse the set of member DNs.
  private Iterator<DN> memberDNIterator;
  // The set of DNs for the users that are members of the associated static
  // group.
  private LinkedList<DN> memberDNs;
  /**
   * Creates a new simple static group member list with the provided set of
   * member DNs.
   *
   * @param  groupDN    The DN of the static group with which this member list
   *                    is associated.
   * @param  memberDNs  The set of DNs for the users that are members of the
   *                    associated static group.
   */
  public SimpleStaticGroupMemberList(DN groupDN, Set<DN> memberDNs)
  {
    assert debugConstructor(CLASS_NAME, String.valueOf(memberDNs));
    ensureNotNull(groupDN, memberDNs);
    this.groupDN   = groupDN;
    this.memberDNs = new LinkedList<DN>(memberDNs);
    memberDNIterator = memberDNs.iterator();
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean hasMoreMembers()
  {
    assert debugEnter(CLASS_NAME, "hasMoreMembers");
    return memberDNIterator.hasNext();
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public DN nextMemberDN()
         throws MembershipException
  {
    assert debugEnter(CLASS_NAME, "nextMemberDN");
    if (memberDNIterator.hasNext())
    {
      return memberDNIterator.next();
    }
    return null;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public Entry nextMemberEntry()
         throws MembershipException
  {
    assert debugEnter(CLASS_NAME, "nextMemberEntry");
    if (memberDNIterator.hasNext())
    {
      DN memberDN = memberDNIterator.next();
      try
      {
        Entry memberEntry = DirectoryConfig.getEntry(memberDN);
        if (memberEntry == null)
        {
          int    msgID   = MSGID_STATICMEMBERS_NO_SUCH_ENTRY;
          String message = getMessage(msgID, String.valueOf(memberDN),
                                      String.valueOf(groupDN));
          throw new MembershipException(msgID, message, true);
        }
        return memberEntry;
      }
      catch (DirectoryException de)
      {
        assert debugException(CLASS_NAME, "nextMemberEntry", de);
        int    msgID   = MSGID_STATICMEMBERS_CANNOT_GET_ENTRY;
        String message = getMessage(msgID, String.valueOf(memberDN),
                                    String.valueOf(groupDN),
                                    String.valueOf(de.getErrorMessage()));
        throw new MembershipException(msgID, message, true, de);
      }
    }
    return null;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void close()
  {
    assert debugEnter(CLASS_NAME, "close");
    // No implementation is required.
  }
}
opends/src/server/org/opends/server/extensions/StaticGroup.java
New file
@@ -0,0 +1,592 @@
/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.extensions;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import org.opends.server.api.Group;
import org.opends.server.core.ModifyOperation;
import org.opends.server.config.ConfigEntry;
import org.opends.server.config.ConfigException;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.Control;
import org.opends.server.types.DirectoryConfig;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.ErrorLogCategory;
import org.opends.server.types.ErrorLogSeverity;
import org.opends.server.types.InitializationException;
import org.opends.server.types.MemberList;
import org.opends.server.types.Modification;
import org.opends.server.types.ModificationType;
import org.opends.server.types.ObjectClass;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchScope;
import static org.opends.server.loggers.Debug.*;
import static org.opends.server.loggers.Error.*;
import static org.opends.server.messages.ExtensionsMessages.*;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.Validator.*;
/**
 * This class provides a static group implementation, in which the DNs
 * of all members are explicitly listed.  There are two variants of
 * static groups:  one based on the {@code groupOfNames} object class,
 * which stores the member list in the {@code member} attribute, and
 * one based on the {@code groupOfUniqueNames} object class, which
 * stores the member list in the {@code uniqueMember} attribute.
 */
public class StaticGroup
       extends Group
{
  /**
   * The fully-qualified name of this class for debugging purposes.
   */
  private static final String CLASS_NAME =
       "org.opends.server.extensions.StaticGroup";
  // The attribute type used to hold the membership list for this group.
  private AttributeType memberAttributeType;
  // The DN of the entry that holds the definition for this group.
  private DN groupEntryDN;
  // The set of the DNs of the members for this group.
  private LinkedHashSet<DN> memberDNs;
  /**
   * Creates a new, uninitialized static group instance.  This is intended for
   * internal use only.
   */
  public StaticGroup()
  {
    super();
    assert debugConstructor(CLASS_NAME);
    // No initialization is required here.
  }
  /**
   * Creates a new static group instance with the provided information.
   *
   * @param  groupEntryDN         The DN of the entry that holds the definition
   *                              for this group.
   * @param  memberAttributeType  The attribute type used to hold the membership
   *                              list for this group.
   * @param  memberDNs            The set of the DNs of the members for this
   *                              group.
   */
  public StaticGroup(DN groupEntryDN, AttributeType memberAttributeType,
                     LinkedHashSet<DN> memberDNs)
  {
    super();
    assert debugConstructor(CLASS_NAME, String.valueOf(groupEntryDN),
                            String.valueOf(memberAttributeType),
                            String.valueOf(memberDNs));
    ensureNotNull(groupEntryDN, memberAttributeType, memberDNs);
    this.groupEntryDN        = groupEntryDN;
    this.memberAttributeType = memberAttributeType;
    this.memberDNs           = memberDNs;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void initializeGroupImplementation(ConfigEntry configEntry)
         throws ConfigException, InitializationException
  {
    assert debugEnter(CLASS_NAME, "initializeGroupImplementation",
                      String.valueOf(configEntry));
    // No additional initialization is required.
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public StaticGroup newInstance(Entry groupEntry)
         throws DirectoryException
  {
    assert debugEnter(CLASS_NAME, "newInstance", String.valueOf(groupEntry));
    ensureNotNull(groupEntry);
    // Determine whether it is a groupOfNames or groupOfUniqueNames entry.  If
    // neither, then that's a problem.
    AttributeType memberAttributeType;
    ObjectClass groupOfNamesClass =
         DirectoryConfig.getObjectClass(OC_GROUP_OF_NAMES_LC, true);
    ObjectClass groupOfUniqueNamesClass =
         DirectoryConfig.getObjectClass(OC_GROUP_OF_UNIQUE_NAMES_LC, true);
    if (groupEntry.hasObjectClass(groupOfNamesClass))
    {
      if (groupEntry.hasObjectClass(groupOfUniqueNamesClass))
      {
        int    msgID   = MSGID_STATICGROUP_INVALID_OC_COMBINATION;
        String message = getMessage(msgID, String.valueOf(groupEntry.getDN()),
                                    OC_GROUP_OF_NAMES,
                                    OC_GROUP_OF_UNIQUE_NAMES);
        throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message,
                                     msgID);
      }
      memberAttributeType = DirectoryConfig.getAttributeType(ATTR_MEMBER, true);
    }
    else if (groupEntry.hasObjectClass(groupOfUniqueNamesClass))
    {
      memberAttributeType =
           DirectoryConfig.getAttributeType(ATTR_UNIQUE_MEMBER_LC, true);
    }
    else
    {
      int    msgID   = MSGID_STATICGROUP_NO_VALID_OC;
      String message = getMessage(msgID, String.valueOf(groupEntry.getDN()),
                                  OC_GROUP_OF_NAMES, OC_GROUP_OF_UNIQUE_NAMES);
      throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message,
                                   msgID);
    }
    LinkedHashSet<DN> memberDNs = new LinkedHashSet<DN>();
    List<Attribute> memberAttrList =
         groupEntry.getAttribute(memberAttributeType);
    if (memberAttrList != null)
    {
      for (Attribute a : memberAttrList)
      {
        for (AttributeValue v : a.getValues())
        {
          try
          {
            DN memberDN = DN.decode(v.getValue());
            memberDNs.add(memberDN);
          }
          catch (DirectoryException de)
          {
            assert debugException(CLASS_NAME, "newInstance", de);
            int    msgID   = MSGID_STATICGROUP_CANNOT_DECODE_MEMBER_VALUE_AS_DN;
            String message =
                 getMessage(msgID, v.getStringValue(),
                            memberAttributeType.getNameOrOID(),
                            String.valueOf(groupEntry.getDN()),
                            de.getErrorMessage());
            logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.MILD_ERROR,
                     message, msgID);
          }
        }
      }
    }
    return new StaticGroup(groupEntry.getDN(), memberAttributeType, memberDNs);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public SearchFilter getGroupDefinitionFilter()
         throws DirectoryException
  {
    assert debugEnter(CLASS_NAME, "getGroupDefinitionFilter");
    // FIXME -- This needs to exclude enhanced groups once we have support for
    // them.
    String filterString =
         "(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames))";
    return SearchFilter.createFilterFromString(filterString);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isGroupDefinition(Entry entry)
  {
    assert debugEnter(CLASS_NAME, "isGroupDefinition", String.valueOf(entry));
    ensureNotNull(entry);
    // FIXME -- This needs to exclude enhanced groups once we have support for
    //them.
    ObjectClass groupOfNamesClass =
         DirectoryConfig.getObjectClass(OC_GROUP_OF_NAMES_LC, true);
    ObjectClass groupOfUniqueNamesClass =
         DirectoryConfig.getObjectClass(OC_GROUP_OF_UNIQUE_NAMES_LC, true);
    if (entry.hasObjectClass(groupOfNamesClass))
    {
      if (entry.hasObjectClass(groupOfUniqueNamesClass))
      {
        return false;
      }
      return true;
    }
    else if (entry.hasObjectClass(groupOfUniqueNamesClass))
    {
      return true;
    }
    else
    {
      return false;
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public DN getGroupDN()
  {
    assert debugEnter(CLASS_NAME, "getGroupDN");
    return groupEntryDN;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean supportsNestedGroups()
  {
    assert debugEnter(CLASS_NAME, "supportsNestedGroups");
    // FIXME -- We should add support for nested groups.
    return false;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public List<DN> getNestedGroupDNs()
  {
    assert debugEnter(CLASS_NAME, "getNestedGroupDNs");
    // FIXME -- We should add support for nested groups.
    return Collections.<DN>emptyList();
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void addNestedGroup(DN nestedGroupDN)
         throws UnsupportedOperationException, DirectoryException
  {
    assert debugEnter(CLASS_NAME, "addNestedGroup",
                      String.valueOf(nestedGroupDN));
    // FIXME -- We should add support for nested groups.
    throw new UnsupportedOperationException();
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void removeNestedGroup(DN nestedGroupDN)
         throws UnsupportedOperationException, DirectoryException
  {
    assert debugEnter(CLASS_NAME, "removeNestedGroup",
                      String.valueOf(nestedGroupDN));
    // FIXME -- We should add support for nested groups.
    throw new UnsupportedOperationException();
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isMember(DN userDN)
         throws DirectoryException
  {
    assert debugEnter(CLASS_NAME, "isMember", String.valueOf(userDN));
    return memberDNs.contains(userDN);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isMember(Entry userEntry)
         throws DirectoryException
  {
    assert debugEnter(CLASS_NAME, "isMember", String.valueOf(userEntry));
    return memberDNs.contains(userEntry.getDN());
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public MemberList getMembers()
         throws DirectoryException
  {
    assert debugEnter(CLASS_NAME, "getMembers");
    return new SimpleStaticGroupMemberList(groupEntryDN, memberDNs);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public MemberList getMembers(DN baseDN, SearchScope scope,
                               SearchFilter filter)
         throws DirectoryException
  {
    assert debugEnter(CLASS_NAME, "getMembers", String.valueOf(baseDN),
                      String.valueOf(scope), String.valueOf(filter));
    if ((baseDN == null) && (filter == null))
    {
      return new SimpleStaticGroupMemberList(groupEntryDN, memberDNs);
    }
    else
    {
      return new FilteredStaticGroupMemberList(groupEntryDN, memberDNs, baseDN,
                                               scope, filter);
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean mayAlterMemberList()
  {
    assert debugEnter(CLASS_NAME, "mayAlterMemberList");
    return true;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void addMember(Entry userEntry)
         throws UnsupportedOperationException, DirectoryException
  {
    assert debugEnter(CLASS_NAME, "addMember", String.valueOf(userEntry));
    ensureNotNull(userEntry);
    synchronized (this)
    {
      DN userDN = userEntry.getDN();
      if (memberDNs.contains(userDN))
      {
        int    msgID   = MSGID_STATICGROUP_ADD_MEMBER_ALREADY_EXISTS;
        String message = getMessage(msgID, String.valueOf(userDN),
                                    String.valueOf(groupEntryDN));
        throw new DirectoryException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS,
                                     message, msgID);
      }
      LinkedHashSet<AttributeValue> values =
           new LinkedHashSet<AttributeValue>(1);
      values.add(new AttributeValue(memberAttributeType, userDN.toString()));
      Attribute attr = new Attribute(memberAttributeType,
                                     memberAttributeType.getNameOrOID(),
                                     values);
      LinkedList<Modification> mods = new LinkedList<Modification>();
      mods.add(new Modification(ModificationType.ADD, attr));
      LinkedList<Control> requestControls = new LinkedList<Control>();
      requestControls.add(new Control(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE,
                                      false));
      InternalClientConnection conn =
           InternalClientConnection.getRootConnection();
      ModifyOperation modifyOperation =
           new ModifyOperation(conn, conn.nextOperationID(),
                               conn.nextMessageID(), requestControls,
                               groupEntryDN, mods);
      modifyOperation.run();
      if (modifyOperation.getResultCode() != ResultCode.SUCCESS)
      {
        int    msgID   = MSGID_STATICGROUP_ADD_MEMBER_UPDATE_FAILED;
        String message = getMessage(msgID, String.valueOf(userDN),
                              String.valueOf(groupEntryDN),
                              modifyOperation.getErrorMessage().toString());
        throw new DirectoryException(modifyOperation.getResultCode(), message,
                                     msgID);
      }
      LinkedHashSet<DN> newMemberDNs =
           new LinkedHashSet<DN>(memberDNs.size()+1);
      newMemberDNs.addAll(memberDNs);
      newMemberDNs.add(userDN);
      memberDNs = newMemberDNs;
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void removeMember(DN userDN)
         throws UnsupportedOperationException, DirectoryException
  {
    assert debugEnter(CLASS_NAME, "removeMember", String.valueOf(userDN));
    ensureNotNull(userDN);
    synchronized (this)
    {
      if (! memberDNs.contains(userDN))
      {
        int    msgID   = MSGID_STATICGROUP_REMOVE_MEMBER_NO_SUCH_MEMBER;
        String message = getMessage(msgID, String.valueOf(userDN),
                                    String.valueOf(groupEntryDN));
        throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, message,
                                     msgID);
      }
      LinkedHashSet<AttributeValue> values =
           new LinkedHashSet<AttributeValue>(1);
      values.add(new AttributeValue(memberAttributeType, userDN.toString()));
      Attribute attr = new Attribute(memberAttributeType,
                                     memberAttributeType.getNameOrOID(),
                                     values);
      LinkedList<Modification> mods = new LinkedList<Modification>();
      mods.add(new Modification(ModificationType.DELETE, attr));
      LinkedList<Control> requestControls = new LinkedList<Control>();
      requestControls.add(new Control(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE,
                                      false));
      InternalClientConnection conn =
           InternalClientConnection.getRootConnection();
      ModifyOperation modifyOperation =
           new ModifyOperation(conn, conn.nextOperationID(),
                               conn.nextMessageID(), requestControls,
                               groupEntryDN, mods);
      modifyOperation.run();
      if (modifyOperation.getResultCode() != ResultCode.SUCCESS)
      {
        int    msgID   = MSGID_STATICGROUP_REMOVE_MEMBER_UPDATE_FAILED;
        String message = getMessage(msgID, String.valueOf(userDN),
                              String.valueOf(groupEntryDN),
                              modifyOperation.getErrorMessage().toString());
        throw new DirectoryException(modifyOperation.getResultCode(), message,
                                     msgID);
      }
      LinkedHashSet<DN> newMemberDNs = new LinkedHashSet<DN>(memberDNs);
      newMemberDNs.remove(userDN);
      memberDNs = newMemberDNs;
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void toString(StringBuilder buffer)
  {
    assert debugEnter(CLASS_NAME, "toString", "java.lang.StringBuilder");
    buffer.append("StaticGroup(");
    buffer.append(groupEntryDN);
    buffer.append(")");
  }
}
opends/src/server/org/opends/server/messages/ConfigMessages.java
@@ -2792,8 +2792,8 @@
  /**
   * The message ID for the message that will be used if an entry below the
   * extended operation base does not contain a value for the logger class name.
   * This takes a single argument, which is the DN of the configuration entry.
   * extended operation base does not contain a value for the class name.  This
   * takes a single argument, which is the DN of the configuration entry.
   */
  public static final int MSGID_CONFIG_EXTOP_NO_CLASS_NAME =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_ERROR | 254;
@@ -6205,6 +6205,7 @@
            CATEGORY_MASK_CONFIG | SEVERITY_MASK_INFORMATIONAL | 576;
  /**
   * The message ID for the message that will be used if an error occurs while
   * trying to process the server configuration for rejecting the
@@ -6217,6 +6218,177 @@
           CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_ERROR | 577;
  /**
   * The message ID for the message that will be used if an error occurs while
   * attempting to retrieve the group implementation base entry from the
   * configuration.  This takes a single argument, which is a string
   * representation of the exception that was caught.
   */
  public static final int MSGID_CONFIG_GROUP_CANNOT_GET_BASE =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_FATAL_ERROR | 578;
  /**
   * The message ID for the message that will be used if the group
   * implementation base entry does not exist in the Directory Server
   * configuration.  This does not take any arguments.
   */
  public static final int MSGID_CONFIG_GROUP_BASE_DOES_NOT_EXIST =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_WARNING | 579;
  /**
   * The message ID for the message that will be used if a group implementation
   * configuration entry does not contain an acceptable group implementation
   * configuration.  This takes two arguments, which are the DN of the
   * configuration entry and the reason that it is not acceptable.
   */
  public static final int MSGID_CONFIG_GROUP_ENTRY_UNACCEPTABLE =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_ERROR | 580;
  /**
   * The message ID for the message that will be used if an error occurs while
   * trying to create a group implementation from a configuration entry.  This
   * takes two arguments, which are the DN of the configuration entry and a
   * message that explains the problem that occurred.
   */
  public static final int MSGID_CONFIG_GROUP_CANNOT_CREATE_IMPLEMENTATION =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_ERROR | 581;
  /**
   * The message ID for the message that will be used if an entry below the
   * group implementation base does not contain a valid objectclass.  This takes
   * a single argument, which is the DN of the configuration entry.
   */
  public static final int MSGID_CONFIG_GROUP_INVALID_OBJECTCLASS =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_ERROR | 582;
  /**
   * The message ID for the description of the group implementation class name
   * configuration attribute.  This does not take any arguments.
   */
  public static final int MSGID_CONFIG_GROUP_DESCRIPTION_CLASS_NAME =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_INFORMATIONAL | 583;
  /**
   * The message ID for the message that will be used if an entry below the
   * group implementation base does not contain a value for the class name.
   * This takes a single argument, which is the DN of the configuration entry.
   */
  public static final int MSGID_CONFIG_GROUP_NO_CLASS_NAME =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_ERROR | 584;
  /**
   * The message ID for the message that will be used if an entry below the
   * group implementation base contains an invalid value for the class name.
   * This takes two arguments, which are the DN of the configuration entry and a
   * string representation of the exception that was caught.
   */
  public static final int MSGID_CONFIG_GROUP_INVALID_CLASS_NAME =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_ERROR | 585;
  /**
   * The message ID for the message that will be used if a configuration entry
   * defines a Directory Server group implementation but the associated class
   * cannot be instantiated as a group implementation.  This takes three
   * arguments, which are the handler class name, the DN of the configuration
   * entry, and a string representation of the exception that was caught.
   */
  public static final int MSGID_CONFIG_GROUP_INVALID_CLASS =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_ERROR | 586;
  /**
   * The message ID for the description of the group implementation enabled
   * configuration attribute.  This does not take any arguments.
   */
  public static final int MSGID_CONFIG_GROUP_DESCRIPTION_ENABLED =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_INFORMATIONAL | 587;
  /**
   * The message ID for the message that will be used if an entry below the
   * group implementation base does not contain a value for the enabled
   * attribute.  This takes a single argument, which is the DN of the
   * configuration entry.
   */
  public static final int MSGID_CONFIG_GROUP_NO_ENABLED_ATTR =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_ERROR | 588;
  /**
   * The message ID for the message that will be used if an entry below the
   * group implementation base has an invalid value for the enabled attribute.
   * This takes two arguments, which are the DN of the configuration entry and a
   * string representation of the exception that was caught.
   */
  public static final int MSGID_CONFIG_GROUP_INVALID_ENABLED_VALUE =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_ERROR | 589;
  /**
   * The message ID for the message that will be used if the group
   * implementation class has changed and will require administrative action to
   * take effect.  This takes three arguments, which are the old class name, the
   * new class name, and the DN of the associated configuration entry.
   */
  public static final int MSGID_CONFIG_GROUP_CLASS_ACTION_REQUIRED =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_INFORMATIONAL | 590;
  /**
   * The message ID for the message that will be used if an error occurs while
   * initializing a Directory Server group implementation.  This takes three
   * arguments, which are the class name for the implementation class, the DN of
   * the configuration entry, and a string representation of the exception that
   * was caught.
   */
  public static final int MSGID_CONFIG_GROUP_INITIALIZATION_FAILED =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_MILD_ERROR | 591;
  /**
   * The message ID for the message that will be used if an attempt is made to
   * add a new group implementation entry with a DN that matches the DN of a
   * group implementation that already exists.  This takes a single argument,
   * which is the DN of the handler configuration entry.
   */
  public static final int MSGID_CONFIG_GROUP_EXISTS =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_MILD_ERROR | 592;
  /**
   * The message ID for the message that will be used if a group implementation
   * entry contains an unacceptable configuration but does not provide any
   * specific details about the nature of the problem.  This takes a single
   * argument, which is the DN of the configuration entry.
   */
  public static final int MSGID_CONFIG_GROUP_UNACCEPTABLE_CONFIG =
       CATEGORY_MASK_CONFIG | SEVERITY_MASK_SEVERE_ERROR | 593;
  /**
   * Associates a set of generic messages with the message IDs defined in this
   * class.
@@ -9001,6 +9173,84 @@
                   "Unable to set the requested file permissions to the " +
                   "backend database directory. The requested permissions " +
                   "will result in an inaccessable database.");
    registerMessage(MSGID_CONFIG_GROUP_CANNOT_GET_BASE,
                    "An error occurred while attempting to retrieve the " +
                    "group implementation base entry " +
                    DN_EXTENDED_OP_CONFIG_BASE +
                    " from the Directory Server configuration:  %s.");
    registerMessage(MSGID_CONFIG_GROUP_BASE_DOES_NOT_EXIST,
                    "The group implementation configuration base " +
                    DN_GROUP_IMPLEMENTATION_CONFIG_BASE + " does not exist " +
                    "in the Directory Server configuration.  This entry must " +
                    "be present for the server to function properly.");
    registerMessage(MSGID_CONFIG_GROUP_ENTRY_UNACCEPTABLE,
                    "Configuration entry %s does not contain a valid " +
                    "group implementation configuration:  %s.  It will be " +
                    "ignored.");
    registerMessage(MSGID_CONFIG_GROUP_CANNOT_CREATE_IMPLEMENTATION,
                    "An error occurred while attempting to create a " +
                    "Directory Server group implementation from the " +
                    "information in configuration entry %s:  %s.");
    registerMessage(MSGID_CONFIG_GROUP_INVALID_OBJECTCLASS,
                    "Configuration entry %s does not contain the " +
                    OC_GROUP_IMPLEMENTATION + " objectclass, which is " +
                    "required for group implementation definitions.");
    registerMessage(MSGID_CONFIG_GROUP_DESCRIPTION_CLASS_NAME,
                    "The fully-qualified name of the Java class that defines " +
                    "the Directory Server group implementation.  If this is " +
                    "while the associated implementation is enabled, then " +
                    "that group implementation must be disabled and " +
                    "re-enabled for the change to take effect.");
    registerMessage(MSGID_CONFIG_GROUP_NO_CLASS_NAME,
                    "Configuration entry %s does not contain a valid value " +
                    "for configuration attribute " +
                    ATTR_GROUP_IMPLEMENTATION_CLASS +
                    " which specifies the fully-qualified class name for " +
                    "the associated group implementation.");
    registerMessage(MSGID_CONFIG_GROUP_INVALID_CLASS_NAME,
                    "Configuration entry %s has an invalid value for " +
                    "attribute " + ATTR_GROUP_IMPLEMENTATION_CLASS + ":  %s.");
    registerMessage(MSGID_CONFIG_GROUP_INVALID_CLASS,
                    "Class %s specified in configuration entry %s does not " +
                    "contain a valid group implementation:  %s.");
    registerMessage(MSGID_CONFIG_GROUP_DESCRIPTION_ENABLED,
                    "Indicates whether this Directory Server group " +
                    "implementation should be enabled.  Changes to this " +
                    "attribute will take effect immediately.");
    registerMessage(MSGID_CONFIG_GROUP_NO_ENABLED_ATTR,
                    "Configuration entry %s does not contain a valid value " +
                    "for configuration attribute " +
                    ATTR_GROUP_IMPLEMENTATION_ENABLED +
                    " which indicates whether the group implementation " +
                    "should be enabled for use in the Directory Server.");
    registerMessage(MSGID_CONFIG_GROUP_INVALID_ENABLED_VALUE,
                    "Configuration entry %s has an invalid value for " +
                    "attribute " + ATTR_GROUP_IMPLEMENTATION_ENABLED +
                    ":  %s.");
    registerMessage(MSGID_CONFIG_GROUP_CLASS_ACTION_REQUIRED,
                    "The requested change in the group implementation class " +
                    "name from %s to %s in configuration entry %s cannot be " +
                    "dynamically applied.  This change will not take effect " +
                    "until the group implementation is disabled and " +
                    "re-enabled or the Directory Server is restarted.");
    registerMessage(MSGID_CONFIG_GROUP_INITIALIZATION_FAILED,
                    "An error occurred while trying to initialize an " +
                    "instance of class %s as a group implementation as " +
                    "in configuration entry %s:  %s.");
    registerMessage(MSGID_CONFIG_GROUP_EXISTS,
                    "Unable to add a new group implementation entry with DN " +
                    "%s because there is already a group implementation " +
                    "registered with that DN.");
    registerMessage(MSGID_CONFIG_GROUP_UNACCEPTABLE_CONFIG,
                    "The configuration for the group implementation defined " +
                    "in configuration entry %s was not acceptable according " +
                    "to its internal validation.  However, no specific " +
                    "information is available regarding the problem(s) with " +
                    "the entry.");
    registerMessage(
        MSGID_CONFIG_CORE_DESCRIPTION_REJECT_UNAUTHENTICATED_REQUESTS,
                    "Indicates whether the Directory Server should reject  " +
@@ -9014,7 +9264,6 @@
                    "configuration attribute " +
                    ATTR_REJECT_UNAUTHENTICATED_REQ + "(the value should " +
                    "be either true or false)");
  }
}
opends/src/server/org/opends/server/messages/ExtensionsMessages.java
@@ -22,7 +22,7 @@
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2006 Sun Microsystems, Inc.
 *      Portions Copyright 2006-2007 Sun Microsystems, Inc.
 */
package org.opends.server.messages;
@@ -4042,6 +4042,104 @@
  /**
   * The message ID for the message that will be used if a user entry refered
   * in a static group does not exist.  This takes two arguments, which are the
   * DN of the target entry and the DN of the static group entry.
   */
  public static final int MSGID_STATICMEMBERS_NO_SUCH_ENTRY =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 383;
  /**
   * The message ID for the message that will be used if an error occurs while
   * attempting to retrieve an entry as a potential member of the static group.
   * This takes three arguments, which are the DN of the target entry, the DN of
   * the static group entry, and a message explaining the problem that occured.
   */
  public static final int MSGID_STATICMEMBERS_CANNOT_GET_ENTRY =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 384;
  /**
   * The message ID for the message that will be used if a potential static
   * group entry contains both the groupOfNames and groupOfUniqueNames object
   * classes.  This takes three arguments, which are the DN of the entry, the
   * name of the groupOfNames object class, and the name of the
   * groupOfUniqueNames object class.
   */
  public static final int MSGID_STATICGROUP_INVALID_OC_COMBINATION =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 385;
  /**
   * The message ID for the message that will be used if a potential static
   * group entry does not contain either the groupOfNames or groupOfUniqueNames
   * object class.  This takes three arguments, which are the DN of the entry,
   * the name of the groupOfNames object class, and the name of the
   * groupOfUniqueNames object class.
   */
  public static final int MSGID_STATICGROUP_NO_VALID_OC =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 386;
  /**
   * The message ID for the message that will be used if a value for the member
   * attribute for a static group cannot be parsed as a DN.  This takes four
   * arguments, which are the provided value, the name of the member attribute,
   * the DN of the entry, and the reason the value was not a valid DN.
   */
  public static final int MSGID_STATICGROUP_CANNOT_DECODE_MEMBER_VALUE_AS_DN =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 387;
  /**
   * The message ID for the message that will be used if an attempt is made to
   * add a user to a static group that already includes that user.  This takes
   * two arguments, which is the DN of the user and the DN of the group.
   */
  public static final int MSGID_STATICGROUP_ADD_MEMBER_ALREADY_EXISTS =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 388;
  /**
   * The message ID for the message that will be used if an attempt is made to
   * remove a user from a static group that does not include that user.  This
   * takes two arguments, which is the DN of the user and the DN of the group.
   */
  public static final int MSGID_STATICGROUP_REMOVE_MEMBER_NO_SUCH_MEMBER =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 389;
  /**
   * The message ID for the message that will be used if an error occurs while
   * attempting to update a group entry to add a member.  This takes three
   * arguments, which are the member DN, the group DN, and a message explaining
   * the problem that occurred.
   */
  public static final int MSGID_STATICGROUP_ADD_MEMBER_UPDATE_FAILED =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 390;
  /**
   * The message ID for the message that will be used if an error occurs while
   * attempting to update a group entry to remove a member.  This takes three
   * arguments, which are the member DN, the group DN, and a message explaining
   * the problem that occurred.
   */
  public static final int MSGID_STATICGROUP_REMOVE_MEMBER_UPDATE_FAILED =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 391;
  /**
   * Associates a set of generic messages with the message IDs defined in this
   * class.
   */
@@ -5884,6 +5982,45 @@
    registerMessage(MSGID_ERRORLOG_ACCTNOTHANDLER_NOTIFICATION,
                    "Account-Status-Notification type='%s' userdn='%s' " +
                    "id=%d msg='%s'");
    registerMessage(MSGID_STATICMEMBERS_NO_SUCH_ENTRY,
                    "Unable to examine entry %s as a potential member of " +
                    "static group %s because that entry does not exist in " +
                    "the Directory Server.");
    registerMessage(MSGID_STATICMEMBERS_CANNOT_GET_ENTRY,
                    "An error occurred while attempting to retrieve entry %s " +
                    "as a potential member of static group %s:  %s.");
    registerMessage(MSGID_STATICGROUP_INVALID_OC_COMBINATION,
                    "Entry %s cannot be parsed as a valid static group " +
                    "because static groups are not allowed to have both the " +
                    "%s and %s object classes.");
    registerMessage(MSGID_STATICGROUP_NO_VALID_OC,
                    "Entry %s cannot be parsed as a valid static group " +
                    "because it does not contain exactly one of the %s or " +
                    "the %s object classes.");
    registerMessage(MSGID_STATICGROUP_CANNOT_DECODE_MEMBER_VALUE_AS_DN,
                    "Value %s for attribute %s in entry %s cannot be parsed " +
                    "as a valid DN:  %s.  It will be excluded from the set " +
                    "of group members.");
    registerMessage(MSGID_STATICGROUP_ADD_MEMBER_ALREADY_EXISTS,
                    "Cannot add user %s as a new member of static group %s " +
                    "because that user is already in the member list for the " +
                    "group.");
    registerMessage(MSGID_STATICGROUP_ADD_MEMBER_UPDATE_FAILED,
                    "Cannot add user %s as a new member of static group %s " +
                    "because an error occurred while attempting to perform " +
                    "an internal modification to update the group:  %s.");
    registerMessage(MSGID_STATICGROUP_REMOVE_MEMBER_NO_SUCH_MEMBER,
                    "Cannot remove user %s as a member of static group %s " +
                    "because that user is not included in the member list " +
                    "for the group.");
    registerMessage(MSGID_STATICGROUP_REMOVE_MEMBER_UPDATE_FAILED,
                    "Cannot remove user %s as a member of static group %s " +
                    "because an error occurred while attempting to perform " +
                    "an internal modification to update the group:  %s.");
  }
}
opends/src/server/org/opends/server/protocols/internal/InternalClientConnection.java
@@ -22,7 +22,7 @@
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2006 Sun Microsystems, Inc.
 *      Portions Copyright 2006-2007 Sun Microsystems, Inc.
 */
package org.opends.server.protocols.internal;
@@ -200,6 +200,7 @@
    assert debugConstructor(CLASS_NAME, String.valueOf(authInfo));
    this.authenticationInfo = authInfo;
    super.setAuthenticationInfo(authInfo);
    connectionID  = nextConnectionID.getAndDecrement();
    operationList = new LinkedList<Operation>();
opends/src/server/org/opends/server/util/ServerConstants.java
@@ -185,6 +185,14 @@
  /**
   * The name of the standard "member" attribute type, formatted in all '
   * lowercase characters.
   */
  public static final String ATTR_MEMBER = "member";
  /**
   * The name of the monitor attribute that is used to hold a backend ID.
   */
  public static final String ATTR_MONITOR_BACKEND_ID = "ds-backend-id";
@@ -464,6 +472,22 @@
  /**
   * The name of the standard "uniqueMember" attribute type, formatted in
   * camelCase.
   */
  public static final String ATTR_UNIQUE_MEMBER = "uniqueMember";
  /**
   * The name of the standard "uniqueMember" attribute type, formatted in all
   * lowercase characters.
   */
  public static final String ATTR_UNIQUE_MEMBER_LC = "uniquemember";
  /**
   * The name of the attribute that is used to specify the length of time that
   * the server has been online, formatted in camel case.
   */
@@ -558,6 +582,38 @@
  /**
   * The name of the standard "groupOfNames" object class, formatted in
   * camelCase.
   */
  public static final String OC_GROUP_OF_NAMES = "groupOfNames";
  /**
   * The name of the standard "groupOfNames" object class, formatted in all
   * lowercase characters.
   */
  public static final String OC_GROUP_OF_NAMES_LC = "groupofnames";
  /**
   * The name of the standard "groupOfUniqueNames" object class, formatted in
   * camelCase.
   */
  public static final String OC_GROUP_OF_UNIQUE_NAMES = "groupOfUniqueNames";
  /**
   * The name of the standard "groupOfUniqueNames" object class, formatted in
   * all lowercase characters.
   */
  public static final String OC_GROUP_OF_UNIQUE_NAMES_LC = "groupofuniquenames";
  /**
   * The request OID for the cancel extended operation.
   */
  public static final String OID_CANCEL_REQUEST = "1.3.6.1.1.8";
@@ -1601,6 +1657,15 @@
  /**
   * The OID for the control that will be included in modifications used to
   * alter group membership.
   */
  public static final String OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE =
       "1.3.6.1.4.1.26027.1.5.1";
  /**
   * The OID to include in the supportedFeatures list of the Directory Server
   * to indicate that it supports requesting attributes by objectclass.
   */
opends/tests/unit-tests-testng/src/server/org/opends/server/core/GroupManagerTestCase.java
New file
@@ -0,0 +1,1085 @@
/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.core;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import org.opends.server.TestCaseUtils;
import org.opends.server.api.Group;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.ModifyOperation;
import org.opends.server.core.ModifyDNOperation;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.types.Attribute;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.DereferencePolicy;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.MemberList;
import org.opends.server.types.MembershipException;
import org.opends.server.types.Modification;
import org.opends.server.types.ModificationType;
import org.opends.server.types.RDN;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchScope;
import static org.testng.Assert.*;
/**
 * A set of test cases that involve the use of groups and the Directory Server
 * Group Manager.
 */
public class GroupManagerTestCase
       extends CoreTestCase
{
  /**
   * Ensures that the Directory Server is running.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @BeforeClass()
  public void startServer()
         throws Exception
  {
    TestCaseUtils.startServer();
  }
  /**
   * Tests the {@code GroupManager.getGroupImplementations} method to ensure
   * that it contains the appropriate set of values.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testGetGroupImplementations()
         throws Exception
  {
    GroupManager groupManager = DirectoryServer.getGroupManager();
    assertTrue(groupManager.getGroupImplementations().iterator().hasNext());
  }
  /**
   * Invokes general group API methods on a static group.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testGenericStaticGroupAPI()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password",
      "",
      "dn: uid=user.2,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.2",
      "givenName: User",
      "sn: 2",
      "cn: User 2",
      "userPassword: password",
      "",
      "dn: uid=user.3,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.3",
      "givenName: User",
      "sn: 3",
      "cn: User 3",
      "userPassword: password",
      "",
      "dn: cn=Test Group of Names,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfNames",
      "cn: Test Group of Unique Names",
      "member: uid=user.1,ou=People,o=test",
      "member: uid=user.2,ou=People,o=test");
    DN groupDN = DN.decode("cn=Test Group of Names,ou=Groups,o=test");
    DN user1DN = DN.decode("uid=user.1,ou=People,o=test");
    DN user2DN = DN.decode("uid=user.2,ou=People,o=test");
    DN user3DN = DN.decode("uid=user.3,ou=People,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    assertTrue(groupInstance.isMember(user1DN));
    assertTrue(groupInstance.isMember(user2DN));
    assertFalse(groupInstance.isMember(user3DN));
    assertFalse(groupInstance.supportsNestedGroups());
    assertTrue(groupInstance.getNestedGroupDNs().isEmpty());
    try
    {
      groupInstance.addNestedGroup(DN.decode("uid=test,ou=People,o=test"));
      throw new AssertionError("Expected addNestedGroup to fail but it " +
                               "didn't");
    } catch (UnsupportedOperationException uoe) {}
    try
    {
      groupInstance.removeNestedGroup(
           DN.decode("uid=test,ou=People,o=test"));
      throw new AssertionError("Expected removeNestedGroup to fail but " +
                               "it didn't");
    } catch (UnsupportedOperationException uoe) {}
    assertTrue(groupInstance.mayAlterMemberList());
    Entry user3Entry = DirectoryServer.getEntry(user3DN);
    groupInstance.addMember(user3Entry);
    assertTrue(groupInstance.isMember(user3DN));
    groupInstance.removeMember(user2DN);
    assertFalse(groupInstance.isMember(user2DN));
    groupInstance.toString(new StringBuilder());
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
  /**
   * Tests that the server properly handles adding, deleting, and modifying a
   * static group based on the groupOfNames object class where that group
   * contains valid members.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testValidPopulatedGroupOfNames()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password",
      "",
      "dn: uid=user.2,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.2",
      "givenName: User",
      "sn: 2",
      "cn: User 2",
      "userPassword: password",
      "",
      "dn: uid=user.3,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.3",
      "givenName: User",
      "sn: 3",
      "cn: User 3",
      "userPassword: password");
    // Make sure that there aren't any groups registered with the server.
    assertFalse(groupManager.getGroupInstances().iterator().hasNext());
    // Add a new static group to the server and make sure it gets registered
    // with the group manager.
    TestCaseUtils.addEntry(
      "dn: cn=Test Group of Names,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfNames",
      "cn: Test Group of Names",
      "member: uid=user.1,ou=People,o=test",
      "member: uid=user.2,ou=People,o=test");
    // Perform a basic set of validation on the group itself.
    DN groupDN = DN.decode("cn=Test Group of Names,ou=Groups,o=test");
    DN user1DN = DN.decode("uid=user.1,ou=People,o=test");
    DN user2DN = DN.decode("uid=user.2,ou=People,o=test");
    DN user3DN = DN.decode("uid=user.3,ou=People,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    assertEquals(groupInstance.getGroupDN(), groupDN);
    assertTrue(groupInstance.isMember(user1DN));
    assertTrue(groupInstance.isMember(user2DN));
    assertFalse(groupInstance.isMember(user3DN));
    MemberList memberList = groupInstance.getMembers();
    while (memberList.hasMoreMembers())
    {
      DN memberDN = memberList.nextMemberDN();
      assertTrue(memberDN.equals(user1DN) || memberDN.equals(user2DN));
    }
    SearchFilter filter = SearchFilter.createFilterFromString("(uid=user.1)");
    memberList = groupInstance.getMembers(DN.decode("o=test"),
                                          SearchScope.WHOLE_SUBTREE, filter);
    assertTrue(memberList.hasMoreMembers());
    DN memberDN = memberList.nextMemberDN();
    assertTrue(memberDN.equals(user1DN));
    assertFalse(memberList.hasMoreMembers());
    filter = SearchFilter.createFilterFromString("(uid=user.3)");
    memberList = groupInstance.getMembers(DN.decode("o=test"),
                                          SearchScope.WHOLE_SUBTREE, filter);
    assertFalse(memberList.hasMoreMembers());
    // Modify the group and make sure the group manager gets updated
    // accordingly.
    LinkedList<Modification> mods = new LinkedList<Modification>();
    Attribute a2 = new Attribute("member", "uid=user.2,ou=People,o=test");
    Attribute a3 = new Attribute("member", "uid=user.3,ou=People,o=test");
    mods.add(new Modification(ModificationType.DELETE, a2));
    mods.add(new Modification(ModificationType.ADD, a3));
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    ModifyOperation modifyOperation = conn.processModify(groupDN, mods);
    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
    groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    assertEquals(groupInstance.getGroupDN(), groupDN);
    assertTrue(groupInstance.isMember(user1DN));
    assertFalse(groupInstance.isMember(user2DN));
    assertTrue(groupInstance.isMember(user3DN));
    // Delete the group and make sure the group manager gets updated
    // accordingly.
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
  /**
   * Tests that the server properly handles a groupOfNames object that doesn't
   * contain any members.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testValidEmptyGroupOfNames()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntry(
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups");
    // Make sure that there aren't any groups registered with the server.
    assertFalse(groupManager.getGroupInstances().iterator().hasNext());
    // Add a new static group to the server and make sure it gets registered
    // with the group manager.
    TestCaseUtils.addEntry(
      "dn: cn=Test Group of Names,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfNames",
      "cn: Test Group of Names");
    // Make sure that the group exists but doesn't have any members.
    DN groupDN = DN.decode("cn=Test Group of Names,ou=Groups,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    assertEquals(groupInstance.getGroupDN(), groupDN);
    assertFalse(groupInstance.getMembers().hasMoreMembers());
    // Delete the group and make sure the group manager gets updated
    // accordingly.
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
  /**
   * Tests that the server properly handles adding, deleting, and modifying a
   * static group based on the groupOfUniqueNames object class where that group
   * contains valid members.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testValidPopulatedGroupOfUniqueNames()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password",
      "",
      "dn: uid=user.2,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.2",
      "givenName: User",
      "sn: 2",
      "cn: User 2",
      "userPassword: password",
      "",
      "dn: uid=user.3,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.3",
      "givenName: User",
      "sn: 3",
      "cn: User 3",
      "userPassword: password");
    // Make sure that there aren't any groups registered with the server.
    assertFalse(groupManager.getGroupInstances().iterator().hasNext());
    // Add a new static group to the server and make sure it gets registered
    // with the group manager.
    TestCaseUtils.addEntry(
      "dn: cn=Test Group of Unique Names,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfUniqueNames",
      "cn: Test Group of Unique Names",
      "uniqueMember: uid=user.1,ou=People,o=test",
      "uniqueMember: uid=user.2,ou=People,o=test");
    // Perform a basic set of validation on the group itself.
    DN groupDN = DN.decode("cn=Test Group of Unique Names,ou=Groups,o=test");
    DN user1DN = DN.decode("uid=user.1,ou=People,o=test");
    DN user2DN = DN.decode("uid=user.2,ou=People,o=test");
    DN user3DN = DN.decode("uid=user.3,ou=People,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    assertEquals(groupInstance.getGroupDN(), groupDN);
    assertTrue(groupInstance.isMember(user1DN));
    assertTrue(groupInstance.isMember(user2DN));
    assertFalse(groupInstance.isMember(user3DN));
    MemberList memberList = groupInstance.getMembers();
    while (memberList.hasMoreMembers())
    {
      DN memberDN = memberList.nextMemberDN();
      assertTrue(memberDN.equals(user1DN) || memberDN.equals(user2DN));
    }
    SearchFilter filter = SearchFilter.createFilterFromString("(uid=user.1)");
    memberList = groupInstance.getMembers(DN.decode("o=test"),
                                          SearchScope.WHOLE_SUBTREE, filter);
    assertTrue(memberList.hasMoreMembers());
    DN memberDN = memberList.nextMemberDN();
    assertTrue(memberDN.equals(user1DN));
    assertFalse(memberList.hasMoreMembers());
    filter = SearchFilter.createFilterFromString("(uid=user.3)");
    memberList = groupInstance.getMembers(DN.decode("o=test"),
                                          SearchScope.WHOLE_SUBTREE, filter);
    assertFalse(memberList.hasMoreMembers());
    // Modify the group and make sure the group manager gets updated
    // accordingly.
    LinkedList<Modification> mods = new LinkedList<Modification>();
    Attribute a2 = new Attribute("uniquemember", "uid=user.2,ou=People,o=test");
    Attribute a3 = new Attribute("uniquemember", "uid=user.3,ou=People,o=test");
    mods.add(new Modification(ModificationType.DELETE, a2));
    mods.add(new Modification(ModificationType.ADD, a3));
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    ModifyOperation modifyOperation = conn.processModify(groupDN, mods);
    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
    groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    assertEquals(groupInstance.getGroupDN(), groupDN);
    assertTrue(groupInstance.isMember(user1DN));
    assertFalse(groupInstance.isMember(user2DN));
    assertTrue(groupInstance.isMember(user3DN));
    // Delete the group and make sure the group manager gets updated
    // accordingly.
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
  /**
   * Tests that the server properly handles a groupOfUniqueNames object that
   * doesn't contain any members.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testValidEmptyGroupOfUniqueNames()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntry(
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups");
    // Make sure that there aren't any groups registered with the server.
    assertFalse(groupManager.getGroupInstances().iterator().hasNext());
    // Add a new static group to the server and make sure it gets registered
    // with the group manager.
    TestCaseUtils.addEntry(
      "dn: cn=Test Group of Unique Names,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfUniqueNames",
      "cn: Test Group of Names");
    // Make sure that the group exists but doesn't have any members.
    DN groupDN = DN.decode("cn=Test Group of Unique Names,ou=Groups,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    assertEquals(groupInstance.getGroupDN(), groupDN);
    assertFalse(groupInstance.getMembers().hasMoreMembers());
    // Delete the group and make sure the group manager gets updated
    // accordingly.
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
  /**
   * Verifies that the group manager properly handles modify DN operations on
   * static group entries.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testRenameStaticGroup()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password");
    // Make sure that there aren't any groups registered with the server.
    assertFalse(groupManager.getGroupInstances().iterator().hasNext());
    // Add a new static group to the server and make sure it gets registered
    // with the group manager.
    TestCaseUtils.addEntry(
      "dn: cn=Test Group of Unique Names,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfUniqueNames",
      "cn: Test Group of Unique Names",
      "uniqueMember: uid=user.1,ou=People,o=test");
    // Perform a basic set of validation on the group itself.
    DN groupDN = DN.decode("cn=Test Group of Unique Names,ou=Groups,o=test");
    DN user1DN = DN.decode("uid=user.1,ou=People,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    assertEquals(groupInstance.getGroupDN(), groupDN);
    assertTrue(groupInstance.isMember(user1DN));
    // Rename the group and make sure the old one no longer exists but the new
    // one does.
    RDN newRDN = RDN.decode("cn=Renamed Group");
    DN  newDN  = DN.decode("cn=Renamed Group,ou=Groups,o=test");
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    ModifyDNOperation modifyDNOperation =
         conn.processModifyDN(groupDN, newRDN, true);
    assertEquals(modifyDNOperation.getResultCode(), ResultCode.SUCCESS);
    groupInstance = groupManager.getGroupInstance(groupDN);
    assertNull(groupInstance);
    groupInstance = groupManager.getGroupInstance(newDN);
    assertNotNull(groupInstance);
    assertEquals(groupInstance.getGroupDN(), newDN);
    assertTrue(groupInstance.isMember(user1DN));
    // Delete the group and make sure the group manager gets updated
    // accordingly.
    DeleteOperation deleteOperation = conn.processDelete(newDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(newDN));
  }
  /**
   * Tests the methods related to static group membership in the
   * {@code ClientConnection} class.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testStaticClientConnectionMembership()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password",
      "",
      "dn: uid=user.2,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.2",
      "givenName: User",
      "sn: 2",
      "cn: User 2",
      "userPassword: password",
      "",
      "dn: uid=user.3,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.3",
      "givenName: User",
      "sn: 3",
      "cn: User 3",
      "userPassword: password",
      "",
      "dn: cn=Group 1,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfNames",
      "cn: Group 1",
      "member: uid=user.1,ou=People,o=test",
      "member: uid=user.2,ou=People,o=test",
      "",
      "dn: cn=Group 2,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfUniqueNames",
      "cn: Group 3",
      "uniqueMember: uid=user.2,ou=People,o=test",
      "uniqueMember: uid=user.3,ou=People,o=test",
      "",
      "dn: cn=Group 3,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfUniqueNames",
      "cn: Group 3",
      "uniqueMember: uid=user.1,ou=People,o=test",
      "uniqueMember: uid=user.3,ou=People,o=test");
    // Perform basic validation on the groups.
    DN group1DN = DN.decode("cn=Group 1,ou=Groups,o=test");
    DN group2DN = DN.decode("cn=Group 2,ou=Groups,o=test");
    DN group3DN = DN.decode("cn=Group 3,ou=Groups,o=test");
    DN user1DN  = DN.decode("uid=user.1,ou=People,o=test");
    DN user2DN  = DN.decode("uid=user.2,ou=People,o=test");
    DN user3DN  = DN.decode("uid=user.3,ou=People,o=test");
    Group group1 = groupManager.getGroupInstance(group1DN);
    Group group2 = groupManager.getGroupInstance(group2DN);
    Group group3 = groupManager.getGroupInstance(group3DN);
    assertNotNull(group1);
    assertTrue(group1.isMember(user1DN));
    assertTrue(group1.isMember(user2DN));
    assertFalse(group1.isMember(user3DN));
    assertNotNull(group2);
    assertFalse(group2.isMember(user1DN));
    assertTrue(group2.isMember(user2DN));
    assertTrue(group2.isMember(user3DN));
    assertNotNull(group3);
    assertTrue(group3.isMember(user1DN));
    assertFalse(group3.isMember(user2DN));
    assertTrue(group3.isMember(user3DN));
    // Get a client connection authenticated as user1 and make sure it handles
    // group operations correctly.
    AuthenticationInfo authInfo = new AuthenticationInfo();
    InternalClientConnection conn0 = new InternalClientConnection(authInfo);
    InternalSearchOperation searchOperation =
         new InternalSearchOperation(conn0, conn0.nextOperationID(),
                  conn0.nextMessageID(), null, DN.nullDN(),
                  SearchScope.BASE_OBJECT,
                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
                  SearchFilter.createFilterFromString("(objectClass=*)"), null,
                  null);
    assertFalse(conn0.isMemberOf(group1, null));
    assertFalse(conn0.isMemberOf(group2, null));
    assertFalse(conn0.isMemberOf(group3, null));
    assertFalse(conn0.isMemberOf(group1, searchOperation));
    assertFalse(conn0.isMemberOf(group2, searchOperation));
    assertFalse(conn0.isMemberOf(group3, searchOperation));
    Set<Group> groupSet = conn0.getGroups(null);
    assertTrue(groupSet.isEmpty());
    groupSet = conn0.getGroups(searchOperation);
    assertTrue(groupSet.isEmpty());
    // Get a client connection authenticated as user1 and make sure it handles
    // group operations correctly.
    authInfo = new AuthenticationInfo(user1DN, false);
    InternalClientConnection conn1 = new InternalClientConnection(authInfo);
    searchOperation =
         new InternalSearchOperation(conn1, conn1.nextOperationID(),
                  conn1.nextMessageID(), null, DN.nullDN(),
                  SearchScope.BASE_OBJECT,
                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0,  false,
                  SearchFilter.createFilterFromString("(objectClass=*)"), null,
                  null);
    assertTrue(conn1.isMemberOf(group1, null));
    assertFalse(conn1.isMemberOf(group2, null));
    assertTrue(conn1.isMemberOf(group3, null));
    assertTrue(conn1.isMemberOf(group1, searchOperation));
    assertFalse(conn1.isMemberOf(group2, searchOperation));
    assertTrue(conn1.isMemberOf(group3, searchOperation));
    groupSet = conn1.getGroups(null);
    assertTrue(groupSet.contains(group1));
    assertFalse(groupSet.contains(group2));
    assertTrue(groupSet.contains(group3));
    groupSet = conn1.getGroups(searchOperation);
    assertTrue(groupSet.contains(group1));
    assertFalse(groupSet.contains(group2));
    assertTrue(groupSet.contains(group3));
    // Get a client connection authenticated as user2 and make sure it handles
    // group operations correctly.
    authInfo = new AuthenticationInfo(user2DN, false);
    InternalClientConnection conn2 = new InternalClientConnection(authInfo);
    searchOperation =
         new InternalSearchOperation(conn2, conn2.nextOperationID(),
                  conn2.nextMessageID(), null, DN.nullDN(),
                  SearchScope.BASE_OBJECT,
                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0,  false,
                  SearchFilter.createFilterFromString("(objectClass=*)"), null,
                  null);
    assertTrue(conn2.isMemberOf(group1, null));
    assertTrue(conn2.isMemberOf(group2, null));
    assertFalse(conn2.isMemberOf(group3, null));
    assertTrue(conn2.isMemberOf(group1, searchOperation));
    assertTrue(conn2.isMemberOf(group2, searchOperation));
    assertFalse(conn2.isMemberOf(group3, searchOperation));
    groupSet = conn2.getGroups(null);
    assertTrue(groupSet.contains(group1));
    assertTrue(groupSet.contains(group2));
    assertFalse(groupSet.contains(group3));
    groupSet = conn2.getGroups(searchOperation);
    assertTrue(groupSet.contains(group1));
    assertTrue(groupSet.contains(group2));
    assertFalse(groupSet.contains(group3));
    // Get a client connection authenticated as user3 and make sure it handles
    // group operations correctly.
    authInfo = new AuthenticationInfo(user3DN, false);
    InternalClientConnection conn3 = new InternalClientConnection(authInfo);
    searchOperation =
         new InternalSearchOperation(conn3, conn3.nextOperationID(),
                  conn3.nextMessageID(), null, DN.nullDN(),
                  SearchScope.BASE_OBJECT,
                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0,  false,
                  SearchFilter.createFilterFromString("(objectClass=*)"), null,
                  null);
    assertFalse(conn3.isMemberOf(group1, null));
    assertTrue(conn3.isMemberOf(group2, null));
    assertTrue(conn3.isMemberOf(group3, null));
    assertFalse(conn3.isMemberOf(group1, searchOperation));
    assertTrue(conn3.isMemberOf(group2, searchOperation));
    assertTrue(conn3.isMemberOf(group3, searchOperation));
    groupSet = conn3.getGroups(null);
    assertFalse(groupSet.contains(group1));
    assertTrue(groupSet.contains(group2));
    assertTrue(groupSet.contains(group3));
    groupSet = conn3.getGroups(searchOperation);
    assertFalse(groupSet.contains(group1));
    assertTrue(groupSet.contains(group2));
    assertTrue(groupSet.contains(group3));
    // Delete all of the groups and make sure the group manager gets updated
    // accordingly.
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    DeleteOperation deleteOperation = conn.processDelete(group1DN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(group1DN));
    deleteOperation = conn.processDelete(group2DN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(group2DN));
    deleteOperation = conn.processDelete(group3DN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(group3DN));
  }
  /**
   * Tests operations involving static group member lists.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testStaticMemberList()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password",
      "",
      "dn: uid=user.2,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.2",
      "givenName: User",
      "sn: 2",
      "cn: User 2",
      "userPassword: password",
      "",
      "dn: uid=user.3,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.3",
      "givenName: User",
      "sn: 3",
      "cn: User 3",
      "userPassword: password",
      "",
      "dn: cn=Test Group of Unique Names,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfUniqueNames",
      "cn: Test Group of Unique Names",
      "uniqueMember: uid=user.1,ou=People,o=test",
      "uniqueMember: uid=user.2,ou=People,o=test",
      "uniqueMember: uid=user.3,ou=People,o=test",
      "uniqueMember: uid=nonexistentUser,ou=People,o=test");
    // Get the group instance.
    DN groupDN = DN.decode("cn=Test Group of Unique Names,ou=Groups,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    assertEquals(groupInstance.getGroupDN(), groupDN);
    // Use a member list to iterate across the member DNs with no filter.
    MemberList memberList = groupInstance.getMembers();
    while (memberList.hasMoreMembers())
    {
      try
      {
        assertNotNull(memberList.nextMemberDN());
      } catch (MembershipException me) {}
    }
    assertNull(memberList.nextMemberDN());
    memberList.close();
    // Perform a filtered iteration across the member DNs.
    SearchFilter filter =
         SearchFilter.createFilterFromString("(objectClass=*)");
    memberList = groupInstance.getMembers(DN.decode("o=test"),
                                          SearchScope.WHOLE_SUBTREE, filter);
    while (memberList.hasMoreMembers())
    {
      try
      {
        assertNotNull(memberList.nextMemberDN());
      } catch (MembershipException me) {}
    }
    assertNull(memberList.nextMemberDN());
    memberList.close();
    // Use a member list to iterate across the member entries with no filter.
    memberList = groupInstance.getMembers();
    while (memberList.hasMoreMembers())
    {
      try
      {
        assertNotNull(memberList.nextMemberEntry());
      } catch (MembershipException me) {}
    }
    assertNull(memberList.nextMemberEntry());
    memberList.close();
    // Perform a filtered iteration across the member entries.
    filter = SearchFilter.createFilterFromString("(objectClass=*)");
    memberList = groupInstance.getMembers(DN.decode("o=test"),
                                          SearchScope.WHOLE_SUBTREE, filter);
    while (memberList.hasMoreMembers())
    {
      try
      {
        assertNotNull(memberList.nextMemberEntry());
      } catch (MembershipException me) {}
    }
    assertNull(memberList.nextMemberEntry());
    memberList.close();
    // Delete the group and make sure the group manager gets updated
    // accordingly.
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
}