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

neil_a_wilson
21.09.2007 c179022074f9136fc38367f31b9843a74c529334
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
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' )
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;
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;
      }
    }
opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroup.java
New file
@@ -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("})");
  }
}
opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroupMemberList.java
New file
@@ -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();
  }
}
opendj-sdk/opends/src/server/org/opends/server/extensions/DynamicGroupSearchThread.java
New file
@@ -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.
  }
}
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.");
  }
}
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);
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;
}
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)
    {
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);
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.
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,
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";
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));
  }
}