From c179022074f9136fc38367f31b9843a74c529334 Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Tue, 20 Mar 2007 23:09:54 +0000
Subject: [PATCH] Add support for dynamic groups, which use the groupOfURLs object class and the memberURL attribute type to specify one or more LDAP URLs containing criteria for membership.

---
 opendj-sdk/opends/src/server/org/opends/server/protocols/jmx/JmxClientConnection.java                 |   11 
 opendj-sdk/opends/src/server/org/opends/server/api/ClientConnection.java                              |   14 
 opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroupSearchThread.java               |  208 ++++++
 opendj-sdk/opends/resource/schema/00-core.ldif                                                        |    2 
 opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalSearchOperation.java        |   15 
 opendj-sdk/opends/src/server/org/opends/server/types/LDAPURL.java                                     |   35 
 opendj-sdk/opends/resource/config/config.ldif                                                         |    7 
 opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java                       |   86 ++
 opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalSearchListener.java         |   19 
 opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroup.java                           |  433 ++++++++++++
 opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalClientConnection.java       |   10 
 opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java                              |   34 
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/core/GroupManagerTestCase.java |  610 +++++++++++++++++
 opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroupMemberList.java                 |  480 ++++++++++++++
 opendj-sdk/opends/src/server/org/opends/server/types/SearchFilter.java                                |    7 
 opendj-sdk/opends/src/server/org/opends/server/core/SearchOperation.java                              |   53 +
 16 files changed, 1,988 insertions(+), 36 deletions(-)

diff --git a/opendj-sdk/opends/resource/config/config.ldif b/opendj-sdk/opends/resource/config/config.ldif
index 1eedfcf..4788127 100644
--- a/opendj-sdk/opends/resource/config/config.ldif
+++ b/opendj-sdk/opends/resource/config/config.ldif
@@ -425,6 +425,13 @@
 objectClass: ds-cfg-branch
 cn: Group Implementations
 
+dn: cn=Dynamic,cn=Group Implementations,cn=config
+objectClass: top
+objectClass: ds-cfg-group-implementation
+cn: Dynamic
+ds-cfg-group-implementation-class: org.opends.server.extensions.DynamicGroup
+ds-cfg-group-implementation-enabled: true
+
 dn: cn=Static,cn=Group Implementations,cn=config
 objectClass: top
 objectClass: ds-cfg-group-implementation
diff --git a/opendj-sdk/opends/resource/schema/00-core.ldif b/opendj-sdk/opends/resource/schema/00-core.ldif
index cd51e55..d70f875 100644
--- a/opendj-sdk/opends/resource/schema/00-core.ldif
+++ b/opendj-sdk/opends/resource/schema/00-core.ldif
@@ -524,7 +524,7 @@
   STRUCTURAL  MUST ( inheritable )  MAY  ( blockInheritance )
   X-ORIGIN 'draft-ietf-ldup-subentry' )
 objectClasses: ( 2.16.840.1.113730.3.2.33 NAME 'groupOfURLs'
-  DESC 'Sun-defined objectclass' SUP top MUST ( cn )
+  DESC 'Sun-defined objectclass' SUP top STRUCTURAL MUST ( cn )
   MAY ( memberURL $ businessCategory $ description $ o $ ou $ owner $ seeAlso )
   X-ORIGIN 'Sun Java System Directory Server' )
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/api/ClientConnection.java b/opendj-sdk/opends/src/server/org/opends/server/api/ClientConnection.java
index d1ca3a3..b5dd809 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/api/ClientConnection.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/api/ClientConnection.java
@@ -412,10 +412,15 @@
    *                          entry is associated.
    * @param  searchEntry      The search result entry to be sent to
    *                          the client.
+   *
+   * @throws  DirectoryException  If a problem occurs while attempting
+   *                              to send the entry to the client and
+   *                              the search should be terminated.
    */
   public abstract void sendSearchEntry(
                             SearchOperation searchOperation,
-                            SearchResultEntry searchEntry);
+                            SearchResultEntry searchEntry)
+         throws DirectoryException;
 
 
 
@@ -431,10 +436,15 @@
    *          referrals, or <CODE>false</CODE> if the client cannot
    *          handle referrals and no more attempts should be made to
    *          send them for the associated search operation.
+   *
+   * @throws  DirectoryException  If a problem occurs while attempting
+   *                              to send the reference to the client
+   *                              and the search should be terminated.
    */
   public abstract boolean sendSearchReference(
                                SearchOperation searchOperation,
-                               SearchResultReference searchReference);
+                               SearchResultReference searchReference)
+         throws DirectoryException;
 
 
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/core/SearchOperation.java b/opendj-sdk/opends/src/server/org/opends/server/core/SearchOperation.java
index bd01dcf..25e88cb 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/core/SearchOperation.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/core/SearchOperation.java
@@ -1115,15 +1115,27 @@
     // Send the entry to the client.
     if (pluginResult.sendEntry())
     {
-      clientConnection.sendSearchEntry(this, searchEntry);
+      try
+      {
+        clientConnection.sendSearchEntry(this, searchEntry);
 
-      // Log the entry sent to the client.
-      logSearchResultEntry(this, searchEntry);
+        // Log the entry sent to the client.
+        logSearchResultEntry(this, searchEntry);
 
-      entriesSent++;
+        entriesSent++;
+      }
+      catch (DirectoryException de)
+      {
+        if (debugEnabled())
+        {
+          debugCaught(DebugLogLevel.ERROR, de);
+        }
+
+        setResponseData(de);
+        return false;
+      }
     }
 
-
     return pluginResult.continueSearch();
   }
 
@@ -1201,19 +1213,32 @@
     // to send any more.
     if (pluginResult.sendReference())
     {
-      if (clientConnection.sendSearchReference(this, reference))
+      try
       {
-        // Log the entry sent to the client.
-        logSearchResultReference(this, reference);
-        referencesSent++;
+        if (clientConnection.sendSearchReference(this, reference))
+        {
+          // Log the entry sent to the client.
+          logSearchResultReference(this, reference);
+          referencesSent++;
 
-        // FIXME -- Should the size limit apply here?
+          // FIXME -- Should the size limit apply here?
+        }
+        else
+        {
+          // We know that the client can't handle referrals, so we won't try to
+          // send it any more.
+          clientAcceptsReferrals = false;
+        }
       }
-      else
+      catch (DirectoryException de)
       {
-        // We know that the client can't handle referrals, so we won't try to
-        // send it any more.
-        clientAcceptsReferrals = false;
+        if (debugEnabled())
+        {
+          debugCaught(DebugLogLevel.ERROR, de);
+        }
+
+        setResponseData(de);
+        return false;
       }
     }
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroup.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroup.java
new file mode 100644
index 0000000..b348072
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroup.java
@@ -0,0 +1,433 @@
+/*
+ * 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.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.opends.server.api.Group;
+import org.opends.server.config.ConfigEntry;
+import org.opends.server.config.ConfigException;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.DebugLogLevel;
+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.LDAPURL;
+import org.opends.server.types.MemberList;
+import org.opends.server.types.ObjectClass;
+import org.opends.server.types.SearchFilter;
+import org.opends.server.types.SearchScope;
+
+import static org.opends.server.config.ConfigConstants.*;
+import static org.opends.server.loggers.Error.*;
+import static org.opends.server.loggers.debug.DebugLogger.*;
+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 dynamic group implementation, in which
+ * membership is determined dynamically based on criteria provided
+ * in the form of one or more LDAP URLs.  All dynamic groups should
+ * contain the groupOfURLs object class, with the memberURL attribute
+ * specifying the membership criteria.
+ */
+public class DynamicGroup
+       extends Group
+{
+  // The DN of the entry that holds the definition for this group.
+  private DN groupEntryDN;
+
+  // The set of the LDAP URLs that define the membership criteria.
+  private LinkedHashSet<LDAPURL> memberURLs;
+
+
+
+  /**
+   * Creates a new, uninitialized dynamic group instance.  This is intended for
+   * internal use only.
+   */
+  public DynamicGroup()
+  {
+    super();
+
+    // No initialization is required here.
+  }
+
+
+
+  /**
+   * Creates a new dynamic group instance with the provided information.
+   *
+   * @param  groupEntryDN  The DN of the entry that holds the definition for
+   *                       this group.  It must not be {@code null}.
+   * @param  memberURLs    The set of LDAP URLs that define the membership
+   *                       criteria for this group.  It must not be
+   *                       {@code null}.
+   */
+  public DynamicGroup(DN groupEntryDN, LinkedHashSet<LDAPURL> memberURLs)
+  {
+    super();
+
+    ensureNotNull(groupEntryDN, memberURLs);
+
+    this.groupEntryDN = groupEntryDN;
+    this.memberURLs   = memberURLs;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void initializeGroupImplementation(ConfigEntry configEntry)
+         throws ConfigException, InitializationException
+  {
+    // No additional initialization is required.
+  }
+
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public DynamicGroup newInstance(Entry groupEntry)
+         throws DirectoryException
+  {
+    ensureNotNull(groupEntry);
+
+
+    // Get the memberURL attribute from the entry, if there is one, and parse
+    // out the LDAP URLs that it contains.
+    LinkedHashSet<LDAPURL> memberURLs = new LinkedHashSet<LDAPURL>();
+    AttributeType memberURLType =
+         DirectoryConfig.getAttributeType(ATTR_MEMBER_URL_LC, true);
+    List<Attribute> attrList = groupEntry.getAttribute(memberURLType);
+    if (attrList != null)
+    {
+      for (Attribute a : attrList)
+      {
+        for (AttributeValue v : a.getValues())
+        {
+          try
+          {
+            memberURLs.add(LDAPURL.decode(v.getStringValue(), true));
+          }
+          catch (DirectoryException de)
+          {
+            if (debugEnabled())
+            {
+              debugCaught(DebugLogLevel.ERROR, de);
+            }
+
+            int    msgID   = MSGID_DYNAMICGROUP_CANNOT_DECODE_MEMBERURL;
+            String message = getMessage(msgID, v.getStringValue(),
+                                        String.valueOf(groupEntry.getDN()),
+                                        de.getErrorMessage());
+            logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.MILD_ERROR,
+                     message, msgID);
+          }
+        }
+      }
+    }
+
+    return new DynamicGroup(groupEntry.getDN(), memberURLs);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public SearchFilter getGroupDefinitionFilter()
+         throws DirectoryException
+  {
+    // FIXME -- This needs to exclude enhanced groups once we have support for
+    // them.
+    return SearchFilter.createFilterFromString("(" + ATTR_OBJECTCLASS + "=" +
+                                               OC_GROUP_OF_URLS + ")");
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean isGroupDefinition(Entry entry)
+  {
+    ensureNotNull(entry);
+
+    // FIXME -- This needs to exclude enhanced groups once we have support for
+    //them.
+    ObjectClass groupOfURLsClass =
+         DirectoryConfig.getObjectClass(OC_GROUP_OF_URLS_LC, true);
+    return entry.hasObjectClass(groupOfURLsClass);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public DN getGroupDN()
+  {
+    return groupEntryDN;
+  }
+
+
+
+  /**
+   * Retrieves the set of member URLs for this dynamic group.  The returned set
+   * must not be altered by the caller.
+   *
+   * @return  The set of member URLs for this dynamic group.
+   */
+  public Set<LDAPURL> getMemberURLs()
+  {
+    return memberURLs;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean supportsNestedGroups()
+  {
+    // Dynamic groups don't support nesting.
+    return false;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public List<DN> getNestedGroupDNs()
+  {
+    // Dynamic groups don't support nesting.
+    return Collections.<DN>emptyList();
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void addNestedGroup(DN nestedGroupDN)
+         throws UnsupportedOperationException, DirectoryException
+  {
+    // Dynamic groups don't support nesting.
+    int    msgID   = MSGID_DYNAMICGROUP_NESTING_NOT_SUPPORTED;
+    String message = getMessage(msgID);
+    throw new UnsupportedOperationException(message);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void removeNestedGroup(DN nestedGroupDN)
+         throws UnsupportedOperationException, DirectoryException
+  {
+    // Dynamic groups don't support nesting.
+    int    msgID   = MSGID_DYNAMICGROUP_NESTING_NOT_SUPPORTED;
+    String message = getMessage(msgID);
+    throw new UnsupportedOperationException(message);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean isMember(DN userDN)
+         throws DirectoryException
+  {
+    Entry entry = DirectoryConfig.getEntry(userDN);
+    if (entry == null)
+    {
+      return false;
+    }
+    else
+    {
+      return isMember(entry);
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean isMember(Entry userEntry)
+         throws DirectoryException
+  {
+    for (LDAPURL memberURL : memberURLs)
+    {
+      if (memberURL.matchesEntry(userEntry))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public MemberList getMembers()
+         throws DirectoryException
+  {
+    return new DynamicGroupMemberList(groupEntryDN, memberURLs);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public MemberList getMembers(DN baseDN, SearchScope scope,
+                               SearchFilter filter)
+         throws DirectoryException
+  {
+    if ((baseDN == null) && (filter == null))
+    {
+      return new DynamicGroupMemberList(groupEntryDN, memberURLs);
+    }
+    else
+    {
+      return new DynamicGroupMemberList(groupEntryDN, memberURLs, baseDN, scope,
+                                        filter);
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean mayAlterMemberList()
+  {
+    return false;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void addMember(Entry userEntry)
+         throws UnsupportedOperationException, DirectoryException
+  {
+    // Dynamic groups don't support altering the member list.
+    int    msgID   = MSGID_DYNAMICGROUP_ALTERING_MEMBERS_NOT_SUPPORTED;
+    String message = getMessage(msgID);
+    throw new UnsupportedOperationException(message);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void removeMember(DN userDN)
+         throws UnsupportedOperationException, DirectoryException
+  {
+    // Dynamic groups don't support altering the member list.
+    int    msgID   = MSGID_DYNAMICGROUP_ALTERING_MEMBERS_NOT_SUPPORTED;
+    String message = getMessage(msgID);
+    throw new UnsupportedOperationException(message);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void toString(StringBuilder buffer)
+  {
+    buffer.append("DynamicGroup(dn=");
+    buffer.append(groupEntryDN);
+    buffer.append(",urls={");
+
+    if (! memberURLs.isEmpty())
+    {
+      Iterator<LDAPURL> iterator = memberURLs.iterator();
+      buffer.append("\"");
+      iterator.next().toString(buffer, false);
+
+      while (iterator.hasNext())
+      {
+        buffer.append("\", ");
+        iterator.next().toString(buffer, false);
+      }
+
+      buffer.append("\"");
+    }
+
+    buffer.append("})");
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroupMemberList.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroupMemberList.java
new file mode 100644
index 0000000..92f5012
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroupMemberList.java
@@ -0,0 +1,480 @@
+/*
+ * 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.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.LDAPURL;
+import org.opends.server.types.MemberList;
+import org.opends.server.types.MembershipException;
+import org.opends.server.types.SearchFilter;
+import org.opends.server.types.SearchScope;
+
+
+
+/**
+ * This class defines a mechanism that may be used to iterate over the
+ * members of a dynamic group, optionally using an additional set of
+ * criteria to further filter the results.
+ */
+public class DynamicGroupMemberList
+       extends MemberList
+{
+  // Indicates whether the search thread has completed its processing.
+  private boolean searchesCompleted;
+
+  // The base DN to use when filtering the set of group members.
+  private final DN baseDN;
+
+  // The DN of the entry containing the group definition.
+  private final DN groupDN;
+
+  // The queue into which results will be placed while they are waiting to be
+  // returned.  The types of objects that may be placed in this queue are Entry
+  // objects to return or MembershipException objects to throw.
+  private final LinkedBlockingQueue<Object> resultQueue;
+
+  // The search filter to use when filtering the set of group members.
+  private final SearchFilter filter;
+
+  // The search scope to use when filtering the set of group members.
+  private final SearchScope scope;
+
+  // The set of LDAP URLs that define the membership criteria.
+  private final Set<LDAPURL> memberURLs;
+
+
+
+  /**
+   * Creates a new dynamic group member list with the provided information.
+   *
+   * @param  groupDN     The DN of the entry containing the group definition.
+   * @param  memberURLs  The set of LDAP URLs that define the membership
+   *                     criteria for the associated group.
+   *
+   * @throws  DirectoryException  If a problem occurs while creating the member
+   *                              list.
+   */
+  public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs)
+         throws DirectoryException
+  {
+    this(groupDN, memberURLs, null, null, null);
+  }
+
+
+
+  /**
+   * Creates a new dynamic group member list with the provided information.
+   *
+   * @param  groupDN     The DN of the entry containing the group definition.
+   * @param  memberURLs  The set of LDAP URLs that define the membership
+   *                     criteria for the associated group.
+   * @param  baseDN      The base DN that should be enforced for all entries to
+   *                     return.
+   * @param  scope       The scope that should be enforced for all entries to
+   *                     return.
+   * @param  filter      The filter that should be enforced for all entries to
+   *                     return.
+   *
+   * @throws  DirectoryException  If a problem occurs while creating the member
+   *                              list.
+   */
+  public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs,
+                                DN baseDN, SearchScope scope,
+                                SearchFilter filter)
+         throws DirectoryException
+  {
+    this.groupDN    = groupDN;
+    this.memberURLs = memberURLs;
+    this.baseDN     = baseDN;
+    this.filter     = filter;
+
+    if (scope == null)
+    {
+      this.scope = SearchScope.WHOLE_SUBTREE;
+    }
+    else
+    {
+      this.scope = scope;
+    }
+
+    searchesCompleted = false;
+    resultQueue = new LinkedBlockingQueue<Object>(10);
+
+
+    // We're going to have to perform one or more internal searches in order to
+    // get the results.  We need to be careful about the way that we construct
+    // them in order to avoid the possibility of getting duplicate results, so
+    // searches with overlapping bases will need to be combined.
+    LinkedHashMap<DN,LinkedList<LDAPURL>> baseDNs =
+         new LinkedHashMap<DN,LinkedList<LDAPURL>>();
+    for (LDAPURL memberURL : memberURLs)
+    {
+      // First, determine the base DN for the search.  It needs to be evaluated
+      // as relative to both the overall base DN specified in the set of
+      // criteria, as well as any other existing base DNs in the same hierarchy.
+      DN urlBaseDN = memberURL.getBaseDN();
+      if (baseDN != null)
+      {
+        if (baseDN.isDescendantOf(urlBaseDN))
+        {
+          // The base DN requested by the user is below the base DN for this
+          // URL, so we'll use the base DN requested by the user.
+          urlBaseDN = baseDN;
+        }
+        else if (! urlBaseDN.isDescendantOf(baseDN))
+        {
+          // The base DN from the URL is outside the base requested by the user,
+          // so we can skip this URL altogether.
+          continue;
+        }
+      }
+
+      // If this is the first URL, then we can just add it with the base DN.
+      // Otherwise, we need to see if it needs to be merged with other URLs in
+      // the same hierarchy.
+      if (baseDNs.isEmpty())
+      {
+        LinkedList<LDAPURL> urlList = new LinkedList<LDAPURL>();
+        urlList.add(memberURL);
+        baseDNs.put(urlBaseDN, urlList);
+      }
+      else
+      {
+        // See if the specified base DN is already in the map.  If so, then
+        // just add the new URL to the existing list.
+        LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
+        if (urlList == null)
+        {
+          // There's no existing list for the same base DN, but there might be
+          // DNs in an overlapping hierarchy.  If so, then use the base DN that
+          // is closest to the naming context.  If not, then add a new list with
+          // the current base DN.
+          boolean found = false;
+          Iterator<DN> iterator = baseDNs.keySet().iterator();
+          while (iterator.hasNext())
+          {
+            DN existingBaseDN = iterator.next();
+            if (urlBaseDN.isDescendantOf(existingBaseDN))
+            {
+              // The base DN for the current URL is below an existing base DN,
+              // so we can just add this URL to the existing list and be done.
+              urlList = baseDNs.get(existingBaseDN);
+              urlList.add(memberURL);
+              found = true;
+              break;
+            }
+            else if (existingBaseDN.isDescendantOf(urlBaseDN))
+            {
+              // The base DN for the current URL is above the existing base DN,
+              // so we should use the base DN for the current URL instead of the
+              // existing one.
+              urlList = baseDNs.get(existingBaseDN);
+              urlList.add(memberURL);
+              iterator.remove();
+              baseDNs.put(urlBaseDN, urlList);
+              found = true;
+              break;
+            }
+          }
+
+          if (! found)
+          {
+            urlList = new LinkedList<LDAPURL>();
+            urlList.add(memberURL);
+            baseDNs.put(urlBaseDN, urlList);
+          }
+        }
+        else
+        {
+          // There was already a list with the same base DN, so just add the
+          // URL.
+          urlList.add(memberURL);
+        }
+      }
+    }
+
+
+    // At this point, we should know what base DN(s) we need to use, so we can
+    // create the filter to use with that base DN.  There are some special-case
+    // optimizations that we can do here, but in general the filter will look
+    // like "(&(filter)(|(urlFilters)))".
+    LinkedHashMap<DN,SearchFilter> searchMap =
+         new LinkedHashMap<DN,SearchFilter>();
+    for (DN urlBaseDN : baseDNs.keySet())
+    {
+      LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
+      LinkedHashSet<SearchFilter> urlFilters =
+           new LinkedHashSet<SearchFilter>();
+      for (LDAPURL url : urlList)
+      {
+        urlFilters.add(url.getFilter());
+      }
+
+      SearchFilter combinedFilter;
+      if (filter == null)
+      {
+        if (urlFilters.size() == 1)
+        {
+          combinedFilter = urlFilters.iterator().next();
+        }
+        else
+        {
+          combinedFilter = SearchFilter.createORFilter(urlFilters);
+        }
+      }
+      else
+      {
+        if (urlFilters.size() == 1)
+        {
+          SearchFilter urlFilter = urlFilters.iterator().next();
+          if (urlFilter.equals(filter))
+          {
+            combinedFilter = filter;
+          }
+          else
+          {
+            LinkedHashSet<SearchFilter> filterSet =
+                 new LinkedHashSet<SearchFilter>();
+            filterSet.add(filter);
+            filterSet.add(urlFilter);
+            combinedFilter = SearchFilter.createANDFilter(filterSet);
+          }
+        }
+        else
+        {
+          if (urlFilters.contains(filter))
+          {
+            combinedFilter = filter;
+          }
+          else
+          {
+            LinkedHashSet<SearchFilter> filterSet =
+                 new LinkedHashSet<SearchFilter>();
+            filterSet.add(filter);
+            filterSet.add(SearchFilter.createORFilter(urlFilters));
+            combinedFilter = SearchFilter.createANDFilter(filterSet);
+          }
+        }
+      }
+
+      searchMap.put(urlBaseDN, combinedFilter);
+    }
+
+
+    // At this point, we should have all the information we need to perform the
+    // searches.  Create arrays of the elements for each.
+    DN[]           baseDNArray = new DN[baseDNs.size()];
+    SearchFilter[] filterArray = new SearchFilter[baseDNArray.length];
+    LDAPURL[][]    urlArray    = new LDAPURL[baseDNArray.length][];
+    Iterator<DN> iterator = baseDNs.keySet().iterator();
+    for (int i=0; i < baseDNArray.length; i++)
+    {
+      baseDNArray[i] = iterator.next();
+      filterArray[i] = searchMap.get(baseDNArray[i]);
+
+      LinkedList<LDAPURL> urlList = baseDNs.get(baseDNArray[i]);
+      urlArray[i] = new LDAPURL[urlList.size()];
+      int j=0;
+      for (LDAPURL url : urlList)
+      {
+        urlArray[i][j++] = url;
+      }
+    }
+
+
+    DynamicGroupSearchThread searchThread =
+         new DynamicGroupSearchThread(this, baseDNArray, filterArray, urlArray);
+    searchThread.start();
+  }
+
+
+
+  /**
+   * Retrieves the DN of the dynamic group with which this dynamic group member
+   * list is associated.
+   *
+   * @return  The DN of the dynamic group with which this dynamic group member
+   *          list is associated.
+   */
+  public final DN getDynamicGroupDN()
+  {
+    return groupDN;
+  }
+
+
+
+  /**
+   * Indicates that all of the searches needed to iterate across the member list
+   * have completed and there will not be any more results provided.
+   */
+  final void setSearchesCompleted()
+  {
+    searchesCompleted = true;
+  }
+
+
+
+  /**
+   * Adds the provided entry to the set of results that should be returned for
+   * this member list.
+   *
+   * @param  entry  The entry to add to the set of results that should be
+   *                returned for this member list.
+   *
+   * @return  {@code true} if the entry was added to the result set, or
+   *          {@code false} if it was not (either because a timeout expired or
+   *          the attempt was interrupted).  If this method returns
+   *          {@code false}, then the search thread should terminate
+   *          immediately.
+   */
+  final boolean addResult(Entry entry)
+  {
+    try
+    {
+      return resultQueue.offer(entry, 10, TimeUnit.SECONDS);
+    }
+    catch (InterruptedException ie)
+    {
+      return false;
+    }
+  }
+
+
+
+  /**
+   * Adds the provided membership exception so that it will be thrown along with
+   * the set of results for this member list.
+   *
+   * @param  membershipException  The membership exception to be thrown.
+   *
+   * @return  {@code true} if the exception was added to the result set, or
+   *          {@code false} if it was not (either because a timeout expired or
+   *          the attempt was interrupted).  If this method returns
+   *          {@code false}, then the search thread should terminate
+   *          immediately.
+   */
+  final boolean addResult(MembershipException membershipException)
+  {
+    try
+    {
+      return resultQueue.offer(membershipException, 10, TimeUnit.SECONDS);
+    }
+    catch (InterruptedException ie)
+    {
+      return false;
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean hasMoreMembers()
+  {
+    while (! searchesCompleted)
+    {
+      if (resultQueue.peek() != null)
+      {
+        return true;
+      }
+
+      try
+      {
+        Thread.sleep(0, 1000);
+      } catch (Exception e) {}
+    }
+
+    return (resultQueue.peek() != null);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public Entry nextMemberEntry()
+         throws MembershipException
+  {
+    if (! hasMoreMembers())
+    {
+      return null;
+    }
+
+    Object result = resultQueue.poll();
+    if (result == null)
+    {
+      close();
+      return null;
+    }
+    else if (result instanceof Entry)
+    {
+      return (Entry) result;
+    }
+    else if (result instanceof MembershipException)
+    {
+      MembershipException me = (MembershipException) result;
+      if (! me.continueIterating())
+      {
+        close();
+      }
+
+      throw me;
+    }
+
+    // We should never get here.
+    close();
+    return null;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void close()
+  {
+    searchesCompleted = true;
+    resultQueue.clear();
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroupSearchThread.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroupSearchThread.java
new file mode 100644
index 0000000..c780fd6
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroupSearchThread.java
@@ -0,0 +1,208 @@
+/*
+ * 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.LinkedHashSet;
+
+import org.opends.server.api.DirectoryThread;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.protocols.internal.InternalSearchListener;
+import org.opends.server.protocols.internal.InternalSearchOperation;
+import org.opends.server.types.DereferencePolicy;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.DN;
+import org.opends.server.types.ErrorLogCategory;
+import org.opends.server.types.ErrorLogSeverity;
+import org.opends.server.types.LDAPURL;
+import org.opends.server.types.MembershipException;
+import org.opends.server.types.ResultCode;
+import org.opends.server.types.SearchFilter;
+import org.opends.server.types.SearchResultEntry;
+import org.opends.server.types.SearchResultReference;
+import org.opends.server.types.SearchScope;
+
+import static org.opends.server.loggers.Error.*;
+import static org.opends.server.messages.ExtensionsMessages.*;
+import static org.opends.server.messages.MessageHandler.*;
+
+
+
+/**
+ * This class implements a Directory Server thread that will be used to perform
+ * a background search to retrieve all of the members of a dynamic group.
+ * <BR><BR>
+ */
+public class DynamicGroupSearchThread
+// FIXME -- Would it be better to implement this class using an Executor
+//          rather than always creating a custom thread?
+       extends DirectoryThread
+       implements InternalSearchListener
+{
+  // The set of base DNs for the search requests.
+  private final DN[] baseDNs;
+
+  // The member list with which this search thread is associated.
+  private final DynamicGroupMemberList memberList;
+
+  // A counter used to keep track of which search is currently in progress.
+  private int searchCounter;
+
+  // The set of member URLs for determining whether entries match the criteria.
+  private final LDAPURL[][] memberURLs;
+
+  // The set of search filters for the search requests.
+  private final SearchFilter[] searchFilters;
+
+
+
+  /**
+   * Creates a new dynamic group search thread that is associated with the
+   * provided member list and that will perform the search using the provided
+   * information.
+   *
+   * @param  memberList  The dynamic group member list with which this thread is
+   *                     associated.
+   * @param  baseDNs     The set of base DNs to use for the search requests.
+   * @param  filters     The set of search filters to use for the search
+   *                     requests.
+   * @param  memberURLs  The set of member URLs to use when determining if
+   *                     entries match the necessary group criteria.
+   */
+  public DynamicGroupSearchThread(DynamicGroupMemberList memberList,
+                                  DN[] baseDNs, SearchFilter[] filters,
+                                  LDAPURL[][] memberURLs)
+  {
+    super("Dynamic Group Search Thread " + memberList.getDynamicGroupDN());
+
+    this.memberList    = memberList;
+    this.baseDNs       = baseDNs;
+    this.searchFilters = filters;
+    this.memberURLs    = memberURLs;
+
+    searchCounter = 0;
+  }
+
+
+
+  /**
+   * Performs the set of searches and provides the results to the associated
+   * member list.
+   */
+  public void run()
+  {
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    LinkedHashSet<String> attributes = new LinkedHashSet<String>(0);
+
+    for (searchCounter = 0; searchCounter < baseDNs.length; searchCounter++)
+    {
+      InternalSearchOperation searchOperation =
+           conn.processSearch(baseDNs[searchCounter], SearchScope.WHOLE_SUBTREE,
+                              DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0,
+                              false, searchFilters[searchCounter], attributes,
+                              this);
+
+      ResultCode resultCode = searchOperation.getResultCode();
+      if (resultCode != ResultCode.SUCCESS)
+      {
+        if (resultCode == ResultCode.NO_SUCH_OBJECT)
+        {
+          int    msgID   = MSGID_DYNAMICGROUP_NONEXISTENT_BASE_DN;
+          String message =
+               getMessage(msgID, String.valueOf(baseDNs[searchCounter]),
+                          String.valueOf(memberList.getDynamicGroupDN()));
+          logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.MILD_WARNING,
+                   message, msgID);
+          continue;
+        }
+        else
+        {
+          int    msgID   = MSGID_DYNAMICGROUP_INTERNAL_SEARCH_FAILED;
+          String message =
+               getMessage(msgID, String.valueOf(baseDNs[searchCounter]),
+                          String.valueOf(searchFilters[searchCounter]),
+                          String.valueOf(memberList.getDynamicGroupDN()),
+                          String.valueOf(resultCode),
+                          String.valueOf(searchOperation.getErrorMessage()));
+          if (! memberList.addResult(
+                     new MembershipException(msgID, message, true)))
+          {
+            memberList.setSearchesCompleted();
+            return;
+          }
+        }
+      }
+    }
+
+    memberList.setSearchesCompleted();
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void handleInternalSearchEntry(InternalSearchOperation searchOperation,
+                                        SearchResultEntry searchEntry)
+         throws DirectoryException
+  {
+    for (LDAPURL url : memberURLs[searchCounter])
+    {
+      if (url.matchesEntry(searchEntry))
+      {
+        if (! memberList.addResult(searchEntry))
+        {
+          int msgID = MSGID_DYNAMICGROUP_CANNOT_RETURN_ENTRY;
+          String message = getMessage(msgID,
+                                String.valueOf(searchEntry.getDN()),
+                                String.valueOf(memberList.getDynamicGroupDN()));
+          throw new DirectoryException(
+                         DirectoryServer.getServerErrorResultCode(), message,
+                         msgID);
+        }
+
+        return;
+      }
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void handleInternalSearchReference(
+                   InternalSearchOperation searchOperation,
+                   SearchResultReference searchReference)
+  {
+    // No implementation required.
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java b/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
index dbb9802..a89e972 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
@@ -4704,6 +4704,71 @@
 
 
   /**
+   * The message ID for the message that will be used if an error occurs while
+   * attempting to decode a memberURL value as an LDAP URL.  This takes three
+   * arguments, which are the value that could not be decoded, the DN of the
+   * entry containing the value, and a message explaining the problem that
+   * occurred.
+   */
+  public static final int MSGID_DYNAMICGROUP_CANNOT_DECODE_MEMBERURL =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 447;
+
+
+
+  /**
+   * The message ID for the message that will be used if an attempt is made to
+   * use nesting in conjunction with a dynamic group.  This does not take any
+   * arguments.
+   */
+  public static final int MSGID_DYNAMICGROUP_NESTING_NOT_SUPPORTED =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 448;
+
+
+
+  /**
+   * The message ID for the message that will be used if an attempt is made to
+   * alter the set of members in a dynamic group.  This does not take any
+   * arguments.
+   */
+  public static final int MSGID_DYNAMICGROUP_ALTERING_MEMBERS_NOT_SUPPORTED =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 449;
+
+
+
+  /**
+   * The message ID for the message that will be used if a dynamic group
+   * includes a member URL with a base DN that doesn't exist.  This takes two
+   * arguments, which are the base DN and the DN of the dynamic group entry.
+   */
+  public static final int MSGID_DYNAMICGROUP_NONEXISTENT_BASE_DN =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_WARNING | 450;
+
+
+
+  /**
+   * The message ID for the message that will be used if an error occurs while
+   * processing an internal search to determine dynamic group membership.  This
+   * takes five arguments, which are the search base DN, the search filter,
+   * the DN of the dynamic group entry, the result code for the search, and the
+   * error message for the search.
+   */
+  public static final int MSGID_DYNAMICGROUP_INTERNAL_SEARCH_FAILED =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 451;
+
+
+
+  /**
+   * The message ID for the message that will be used if an error occurs while
+   * trying to return an entry for a user that matches a set of dynamic group
+   * criteria.  This takes two arguments, which are the DN of the entry that
+   * could not be returned and the DN of the dynamic group entry.
+   */
+  public static final int MSGID_DYNAMICGROUP_CANNOT_RETURN_ENTRY =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 452;
+
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined in this
    * class.
    */
@@ -6791,6 +6856,27 @@
     registerMessage(MSGID_FCM_MULTIPLE_MATCHING_ENTRIES,
                     "The certificate with fingerprint %s could not be mapped " +
                     "to exactly one user.  It maps to both %s and %s.");
+
+
+    registerMessage(MSGID_DYNAMICGROUP_CANNOT_DECODE_MEMBERURL,
+                    "Unable to decode value \"%s\" in entry \"%s\" as an " +
+                    "LDAP URL:  %s.");
+    registerMessage(MSGID_DYNAMICGROUP_NESTING_NOT_SUPPORTED,
+                    "Dynamic groups do not support nested groups.");
+    registerMessage(MSGID_DYNAMICGROUP_ALTERING_MEMBERS_NOT_SUPPORTED,
+                    "Dynamic groups do not support explicitly altering their " +
+                    "membership.");
+    registerMessage(MSGID_DYNAMICGROUP_NONEXISTENT_BASE_DN,
+                    "Base DN %s specified in dynamic group %s does not exist " +
+                    "in the server.");
+    registerMessage(MSGID_DYNAMICGROUP_INTERNAL_SEARCH_FAILED,
+                    "An error occurred while attempting perform an internal " +
+                    "search with base DN %s and filter %s to resolve the " +
+                    "member list for dynamic group %s:  result code %s, " +
+                    "error message %s.");
+    registerMessage(MSGID_DYNAMICGROUP_CANNOT_RETURN_ENTRY,
+                    "The server encountered a timeout while attempting to " +
+                    "add user %s to the member list for dynamic group %s.");
   }
 }
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalClientConnection.java b/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalClientConnection.java
index 9c64150..cfdf825 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalClientConnection.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalClientConnection.java
@@ -1324,9 +1324,14 @@
    *                          entry is associated.
    * @param  searchEntry      The search result entry to be sent to
    *                          the client.
+   *
+   * @throws  DirectoryException  If a problem occurs while processing
+   *                              the entry and the search should be
+   *                              terminated.
    */
   public void sendSearchEntry(SearchOperation searchOperation,
                               SearchResultEntry searchEntry)
+         throws DirectoryException
   {
     ((InternalSearchOperation) searchOperation).
          addSearchEntry(searchEntry);
@@ -1346,9 +1351,14 @@
    *          referrals, or <CODE>false</CODE> if the client cannot
    *          handle referrals and no more attempts should be made to
    *          send them for the associated search operation.
+   *
+   * @throws  DirectoryException  If a problem occurs while processing
+   *                              the entry and the search should be
+   *                              terminated.
    */
   public boolean sendSearchReference(SearchOperation searchOperation,
                       SearchResultReference searchReference)
+         throws DirectoryException
   {
     ((InternalSearchOperation)
      searchOperation).addSearchReference(searchReference);
diff --git a/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalSearchListener.java b/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalSearchListener.java
index e0d2326..172a4e2 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalSearchListener.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalSearchListener.java
@@ -28,6 +28,7 @@
 
 
 
+import org.opends.server.types.DirectoryException;
 import org.opends.server.types.SearchResultEntry;
 import org.opends.server.types.SearchResultReference;
 
@@ -49,10 +50,17 @@
    *                          processed.
    * @param  searchEntry      The matching search result entry to be
    *                          processed.
+   *
+   * @throws  DirectoryException  If a problem occurred while handling
+   *                              the provided entry.  Search
+   *                              processing will be terminated, and
+   *                              the search operation will result
+   *                              will be set based on this exception.
    */
   public void handleInternalSearchEntry(
                    InternalSearchOperation searchOperation,
-                   SearchResultEntry searchEntry);
+                   SearchResultEntry searchEntry)
+         throws DirectoryException;
 
 
 
@@ -64,9 +72,16 @@
    *                          processed.
    * @param  searchReference  The search result reference to be
    *                          processed.
+   *
+   * @throws  DirectoryException  If a problem occurred while handling
+   *                              the provided entry.  Search
+   *                              processing will be terminated, and
+   *                              the search operation will result
+   *                              will be set based on this exception.
    */
   public void handleInternalSearchReference(
                    InternalSearchOperation searchOperation,
-                   SearchResultReference searchReference);
+                   SearchResultReference searchReference)
+         throws DirectoryException;
 }
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalSearchOperation.java b/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalSearchOperation.java
index 31dd8d6..d2fd9fa 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalSearchOperation.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/protocols/internal/InternalSearchOperation.java
@@ -39,6 +39,7 @@
 import org.opends.server.types.Control;
 import org.opends.server.types.DN;
 import org.opends.server.types.DereferencePolicy;
+import org.opends.server.types.DirectoryException;
 import org.opends.server.types.SearchFilter;
 import org.opends.server.types.SearchResultEntry;
 import org.opends.server.types.SearchResultReference;
@@ -46,7 +47,6 @@
 
 
 
-
 /**
  * This class defines a subclass of the core search operation that is
  * to be used for internal searches.  The primary difference between
@@ -57,9 +57,6 @@
 public class InternalSearchOperation
        extends SearchOperation
 {
-
-
-
   // The internal search listener for this search, if one was
   // provided.
   private InternalSearchListener searchListener;
@@ -226,8 +223,13 @@
    *
    * @param  searchEntry  The search result entry returned for this
    *                      search.
+   *
+   * @throws  DirectoryException  If a problem occurs while processing
+   *                              the provided entry and the search
+   *                              should be terminated.
    */
   public void addSearchEntry(SearchResultEntry searchEntry)
+         throws DirectoryException
   {
     if (searchListener == null)
     {
@@ -263,9 +265,14 @@
    *
    * @param  searchReference  The search result reference returned for
    *                          this search.
+   *
+   * @throws  DirectoryException  If a problem occurs while processing
+   *                              the provided reference and the
+   *                              search should be terminated.
    */
   public void addSearchReference(
                    SearchResultReference searchReference)
+         throws DirectoryException
   {
     if (searchListener == null)
     {
diff --git a/opendj-sdk/opends/src/server/org/opends/server/protocols/jmx/JmxClientConnection.java b/opendj-sdk/opends/src/server/org/opends/server/protocols/jmx/JmxClientConnection.java
index 96c4391..1784efb 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/protocols/jmx/JmxClientConnection.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/protocols/jmx/JmxClientConnection.java
@@ -52,6 +52,7 @@
 import org.opends.server.types.Control;
 import org.opends.server.types.DN;
 import org.opends.server.types.DereferencePolicy;
+import org.opends.server.types.DirectoryException;
 import org.opends.server.types.DisconnectReason;
 import org.opends.server.types.IntermediateResponse;
 import org.opends.server.types.SearchResultEntry;
@@ -720,9 +721,14 @@
    * @param  searchOperation  The search operation with which the entry is
    *                          associated.
    * @param  searchEntry      The search result entry to be sent to the client.
+   *
+   * @throws  DirectoryException  If a problem occurs while attempting to send
+   *                              the entry to the client and the search should
+   *                              be terminated.
    */
   public void sendSearchEntry(SearchOperation searchOperation,
                               SearchResultEntry searchEntry)
+         throws DirectoryException
   {
     ((InternalSearchOperation) searchOperation).addSearchEntry(searchEntry);
   }
@@ -741,9 +747,14 @@
    *          <CODE>false</CODE> if the client cannot handle referrals and no
    *          more attempts should be made to send them for the associated
    *          search operation.
+   *
+   * @throws  DirectoryException  If a problem occurs while attempting to send
+   *                              the reference to the client and the search
+   *                              should be terminated.
    */
   public boolean sendSearchReference(SearchOperation searchOperation,
                                      SearchResultReference searchReference)
+         throws DirectoryException
   {
     ((InternalSearchOperation)
      searchOperation).addSearchReference(searchReference);
diff --git a/opendj-sdk/opends/src/server/org/opends/server/types/LDAPURL.java b/opendj-sdk/opends/src/server/org/opends/server/types/LDAPURL.java
index 4ea0ff9..21a80ab 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/types/LDAPURL.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/types/LDAPURL.java
@@ -35,9 +35,7 @@
 
 import org.opends.server.core.DirectoryServer;
 
-import static org.opends.server.loggers.debug.DebugLogger.debugCaught;
-import static
-    org.opends.server.loggers.debug.DebugLogger.debugEnabled;
+import static org.opends.server.loggers.debug.DebugLogger.*;
 import static org.opends.server.messages.MessageHandler.*;
 import static org.opends.server.messages.UtilityMessages.*;
 import static org.opends.server.util.StaticUtils.*;
@@ -53,9 +51,6 @@
  */
 public class LDAPURL
 {
-
-
-
   /**
    * The default scheme that will be used if none is provided.
    */
@@ -1307,6 +1302,34 @@
 
 
   /**
+   * Indicates whether the provided entry matches the criteria defined
+   * in this LDAP URL.
+   *
+   * @param  entry  The entry for which to make the determination.
+   *
+   * @return  {@code true} if the provided entry does match the
+   *          criteria specified in this LDAP URL, or {@code false} if
+   *          it does not.
+   *
+   * @throws  DirectoryException  If a problem occurs while attempting
+   *                              to make the determination.
+   */
+  public boolean matchesEntry(Entry entry)
+         throws DirectoryException
+  {
+    SearchScope scope = getScope();
+    if (scope == null)
+    {
+      scope = SearchScope.BASE_OBJECT;
+    }
+
+    return (entry.matchesBaseAndScope(getBaseDN(), scope) &&
+            getFilter().matchesEntry(entry));
+  }
+
+
+
+  /**
    * Indicates whether the provided object is equal to this LDAP URL.
    *
    * @param  o  The object for which to make the determination.
diff --git a/opendj-sdk/opends/src/server/org/opends/server/types/SearchFilter.java b/opendj-sdk/opends/src/server/org/opends/server/types/SearchFilter.java
index 0059332..b7048ba 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/types/SearchFilter.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/types/SearchFilter.java
@@ -63,9 +63,6 @@
  */
 public class SearchFilter
 {
-
-
-
   // The attribute type for this filter.
   private final AttributeType attributeType;
 
@@ -174,7 +171,7 @@
    *
    * @return  The constructed search filter.
    */
-  public static SearchFilter createANDFilter(List<SearchFilter>
+  public static SearchFilter createANDFilter(Collection<SearchFilter>
                                                   filterComponents)
   {
     return new SearchFilter(FilterType.AND, filterComponents, null,
@@ -192,7 +189,7 @@
    *
    * @return  The constructed search filter.
    */
-  public static SearchFilter createORFilter(List<SearchFilter>
+  public static SearchFilter createORFilter(Collection<SearchFilter>
                                                  filterComponents)
   {
     return new SearchFilter(FilterType.OR, filterComponents, null,
diff --git a/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java b/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java
index 4ccf23d..e5c3468 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java
@@ -195,7 +195,7 @@
 
 
   /**
-   * The name of the standard "member" attribute type, formatted in all '
+   * The name of the standard "member" attribute type, formatted in all
    * lowercase characters.
    */
   public static final String ATTR_MEMBER = "member";
@@ -203,6 +203,22 @@
 
 
   /**
+   * The name of the standard "memberURL" attribute type, formatted in camel
+   * case.
+   */
+  public static final String ATTR_MEMBER_URL = "memberURL";
+
+
+
+  /**
+   * The name of the standard "memberURL" attribute type, formatted in all
+   * lowercase characters.
+   */
+  public static final String ATTR_MEMBER_URL_LC = "memberurl";
+
+
+
+  /**
    * 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";
@@ -632,6 +648,22 @@
 
 
   /**
+   * The name of the standard "groupOfURLs" object class, formatted in camel
+   * case.
+   */
+  public static final String OC_GROUP_OF_URLS = "groupOfURLs";
+
+
+
+  /**
+   * The name of the standard "groupOfURLs" object class, formatted in all
+   * lowercase characters.
+   */
+  public static final String OC_GROUP_OF_URLS_LC = "groupofurls";
+
+
+
+  /**
    * The request OID for the cancel extended operation.
    */
   public static final String OID_CANCEL_REQUEST = "1.3.6.1.1.8";
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/core/GroupManagerTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/core/GroupManagerTestCase.java
index 5df00df..cc4a76d 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/core/GroupManagerTestCase.java
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/core/GroupManagerTestCase.java
@@ -28,6 +28,7 @@
 
 
 
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
@@ -40,11 +41,14 @@
 import org.opends.server.core.DeleteOperation;
 import org.opends.server.core.ModifyOperation;
 import org.opends.server.core.ModifyDNOperation;
+import org.opends.server.extensions.DynamicGroup;
+import org.opends.server.extensions.StaticGroup;
 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.DirectoryException;
 import org.opends.server.types.DN;
 import org.opends.server.types.Entry;
 import org.opends.server.types.MemberList;
@@ -93,7 +97,18 @@
   {
     GroupManager groupManager = DirectoryServer.getGroupManager();
 
-    assertTrue(groupManager.getGroupImplementations().iterator().hasNext());
+    LinkedHashSet<Class> groupClasses = new LinkedHashSet<Class>();
+    groupClasses.add(StaticGroup.class);
+    groupClasses.add(DynamicGroup.class);
+
+    for (Group g : groupManager.getGroupImplementations())
+    {
+      assertTrue(groupClasses.remove(g.getClass()),
+                 "Group class " + g.getClass() + " isn't registered");
+    }
+
+    assertTrue(groupClasses.isEmpty(),
+               "Unexpected group class(es) registered:  " + groupClasses);
   }
 
 
@@ -170,6 +185,7 @@
 
     Group groupInstance = groupManager.getGroupInstance(groupDN);
     assertNotNull(groupInstance);
+    assertEquals(groupInstance.getGroupDN(), groupDN);
     assertTrue(groupInstance.isMember(user1DN));
     assertTrue(groupInstance.isMember(user2DN));
     assertFalse(groupInstance.isMember(user3DN));
@@ -1081,5 +1097,597 @@
     assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
     assertNull(groupManager.getGroupInstance(groupDN));
   }
+
+
+
+  /**
+   * Invokes general group API methods on a dynamic group.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testGenericDynamicGroupAPI()
+         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 URLs,ou=Groups,o=test",
+      "objectClass: top",
+      "objectClass: groupOfURLs",
+      "cn: Test Group of URLs",
+      "memberURL: ldap:///o=test??sub?(sn<=2)");
+
+    DN groupDN = DN.decode("cn=Test Group of URLs,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");
+    DN bogusDN = DN.decode("uid=bogus,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));
+    assertFalse(groupInstance.isMember(bogusDN));
+
+    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) {}
+
+
+    assertFalse(groupInstance.mayAlterMemberList());
+
+    try
+    {
+      Entry user3Entry = DirectoryServer.getEntry(user3DN);
+      groupInstance.addMember(user3Entry);
+      throw new AssertionError("Expected addMember to fail but it didn't");
+    } catch (UnsupportedOperationException uoe) {}
+
+    try
+    {
+      groupInstance.removeMember(user2DN);
+      throw new AssertionError("Expected removeMember to fail but it didn't");
+    } catch (UnsupportedOperationException uoe) {}
+
+    groupInstance.toString(new StringBuilder());
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    DeleteOperation deleteOperation = conn.processDelete(groupDN);
+    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
+    assertNull(groupManager.getGroupInstance(groupDN));
+  }
+
+
+
+  /**
+   * Tests to ensure that an attempt to add a dynamic group with a malformed URL
+   * will cause it to be decoded as a group but any operations attempted with it
+   * will fail with an exception.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testDynamicGroupMalformedURL()
+         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: cn=Test Malformed URL,ou=Groups,o=test",
+      "objectClass: top",
+      "objectClass: groupOfURLs",
+      "cn: Test Malformed URL",
+      "memberURL: ldap:///o=test??sub?(malformed)");
+
+    DN groupDN = DN.decode("cn=Test Malformed URL,ou=Groups,o=test");
+
+    Group groupInstance = groupManager.getGroupInstance(groupDN);
+    assertNotNull(groupInstance);
+
+    DynamicGroup dynamicGroup = (DynamicGroup) groupInstance;
+    assertTrue(dynamicGroup.getMemberURLs().isEmpty());
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    DeleteOperation deleteOperation = conn.processDelete(groupDN);
+    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
+    assertNull(groupManager.getGroupInstance(groupDN));
+  }
+
+
+
+  /**
+   * Tests the {@code getMembers()} method for a dynamic group, using the
+   * variant that doesn't take any arguments.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testGetMembersSimple()
+         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 URLs,ou=Groups,o=test",
+      "objectClass: top",
+      "objectClass: groupOfURLs",
+      "cn: Test Group of URLs",
+      "memberURL: ldap:///o=test??sub?(sn<=2)");
+
+    DN groupDN = DN.decode("cn=Test Group of URLs,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");
+
+    Group groupInstance = groupManager.getGroupInstance(groupDN);
+    assertNotNull(groupInstance);
+
+
+    LinkedHashSet<DN> memberSet = new LinkedHashSet<DN>();
+    memberSet.add(user1DN);
+    memberSet.add(user2DN);
+
+    MemberList memberList = groupInstance.getMembers();
+    assertNotNull(memberList);
+    while (memberList.hasMoreMembers())
+    {
+      DN memberDN = memberList.nextMemberDN();
+      assertTrue(memberSet.remove(memberDN),
+                 "Returned unexpected member " + memberDN.toString());
+    }
+    memberList.close();
+    assertTrue(memberSet.isEmpty(),
+               "Expected member set to be empty but it was not:  " + memberSet);
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    DeleteOperation deleteOperation = conn.processDelete(groupDN);
+    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
+    assertNull(groupManager.getGroupInstance(groupDN));
+  }
+
+
+
+  /**
+   * Tests the {@code getMembers()} method for a dynamic group, using the
+   * variant that takes base, scope, and filter arguments.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testGetMembersComplex()
+         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 URLs,ou=Groups,o=test",
+      "objectClass: top",
+      "objectClass: groupOfURLs",
+      "cn: Test Group of URLs",
+      "memberURL: ldap:///o=test??sub?(sn<=2)");
+
+    DN groupDN = DN.decode("cn=Test Group of URLs,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");
+
+    Group groupInstance = groupManager.getGroupInstance(groupDN);
+    assertNotNull(groupInstance);
+
+
+    LinkedHashSet<DN> memberSet = new LinkedHashSet<DN>();
+    memberSet.add(user1DN);
+
+    MemberList memberList = groupInstance.getMembers(
+                                 DN.decode("ou=people,o=test"),
+                                 SearchScope.SINGLE_LEVEL,
+                                 SearchFilter.createFilterFromString("(sn=1)"));
+    assertNotNull(memberList);
+    while (memberList.hasMoreMembers())
+    {
+      DN memberDN = memberList.nextMemberDN();
+      assertTrue(memberSet.remove(memberDN),
+                 "Returned unexpected member " + memberDN.toString());
+    }
+    memberList.close();
+    assertTrue(memberSet.isEmpty(),
+               "Expected member set to be empty but it was not:  " + memberSet);
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    DeleteOperation deleteOperation = conn.processDelete(groupDN);
+    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
+    assertNull(groupManager.getGroupInstance(groupDN));
+  }
+
+
+
+  /**
+   * Tests the {@code getMembers()} method for a dynamic group that contains
+   * multiple member URLs containing non-overlapping criteria.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testGetMembersMultipleDistinctURLs()
+         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 URLs,ou=Groups,o=test",
+      "objectClass: top",
+      "objectClass: groupOfURLs",
+      "cn: Test Group of URLs",
+      "memberURL: ldap:///o=test??sub?(sn=1)",
+      "memberURL: ldap:///o=test??sub?(sn=2)");
+
+    DN groupDN = DN.decode("cn=Test Group of URLs,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");
+
+    Group groupInstance = groupManager.getGroupInstance(groupDN);
+    assertNotNull(groupInstance);
+    groupInstance.toString();
+
+
+    LinkedHashSet<DN> memberSet = new LinkedHashSet<DN>();
+    memberSet.add(user1DN);
+    memberSet.add(user2DN);
+
+    MemberList memberList = groupInstance.getMembers();
+    assertNotNull(memberList);
+    while (memberList.hasMoreMembers())
+    {
+      DN memberDN = memberList.nextMemberDN();
+      assertTrue(memberSet.remove(memberDN),
+                 "Returned unexpected member " + memberDN.toString());
+    }
+    memberList.close();
+    assertTrue(memberSet.isEmpty(),
+               "Expected member set to be empty but it was not:  " + memberSet);
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    DeleteOperation deleteOperation = conn.processDelete(groupDN);
+    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
+    assertNull(groupManager.getGroupInstance(groupDN));
+  }
+
+
+
+  /**
+   * Tests the {@code getMembers()} method for a dynamic group that contains
+   * multiple member URLs containing overlapping criteria.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testGetMembersMultipleOverlappingURLs()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.clearJEBackend(false, "userRoot", "dc=example,dc=com");
+
+    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 URLs,ou=Groups,o=test",
+      "objectClass: top",
+      "objectClass: groupOfURLs",
+      "cn: Test Group of URLs",
+      "memberURL: ldap:///dc=example,dc=com??sub?(cn=nonexistent)",
+      "memberURL: ldap:///uid=user.2,ou=People,o=test??sub?(sn=2)",
+      "memberURL: ldap:///o=test??sub?(sn=1)",
+      "memberURL: ldap:///ou=People,o=test??subordinate?(!(sn=3))");
+
+    DN groupDN = DN.decode("cn=Test Group of URLs,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");
+
+    Group groupInstance = groupManager.getGroupInstance(groupDN);
+    assertNotNull(groupInstance);
+    groupInstance.toString();
+
+
+    LinkedHashSet<DN> memberSet = new LinkedHashSet<DN>();
+    memberSet.add(user1DN);
+    memberSet.add(user2DN);
+
+    MemberList memberList =
+         groupInstance.getMembers(DN.nullDN(), SearchScope.WHOLE_SUBTREE,
+              SearchFilter.createFilterFromString("(objectClass=*)"));
+    assertNotNull(memberList);
+    while (memberList.hasMoreMembers())
+    {
+      DN memberDN = memberList.nextMemberDN();
+      assertTrue(memberSet.remove(memberDN),
+                 "Returned unexpected member " + memberDN.toString());
+    }
+    memberList.close();
+    assertTrue(memberSet.isEmpty(),
+               "Expected member set to be empty but it was not:  " + memberSet);
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    DeleteOperation deleteOperation = conn.processDelete(groupDN);
+    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
+    assertNull(groupManager.getGroupInstance(groupDN));
+  }
 }
 

--
Gitblit v1.10.0