From 05b440995cb1a2b3aeaf8831d4e563bc463ce8f4 Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Wed, 11 Apr 2007 22:39:23 +0000
Subject: [PATCH] Implement support for virtual static groups, which are entries which appear to be static groups but get their membership information from another group and present it through a virtual attribute.  This can make it possible to use a dynamic group to actually define the set of membership, but still support applications which can only interact with static groups.

---
 opendj-sdk/opends/src/server/org/opends/server/extensions/VirtualStaticGroup.java                                 |  484 +++++++++++++++
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/VirtualStaticGroupTestCase.java |  859 +++++++++++++++++++++++++++
 opendj-sdk/opends/resource/config/config.ldif                                                                     |   27 
 opendj-sdk/opends/src/server/org/opends/server/extensions/StaticGroup.java                                        |   13 
 opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java                                   |   96 +++
 opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java                                          |   17 
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/core/GroupManagerTestCase.java             |    2 
 opendj-sdk/opends/src/server/org/opends/server/extensions/MemberVirtualAttributeProvider.java                     |  349 +++++++++++
 opendj-sdk/opends/resource/schema/02-config.ldif                                                                  |    6 
 9 files changed, 1,849 insertions(+), 4 deletions(-)

diff --git a/opendj-sdk/opends/resource/config/config.ldif b/opendj-sdk/opends/resource/config/config.ldif
index b219bd5..9752405 100644
--- a/opendj-sdk/opends/resource/config/config.ldif
+++ b/opendj-sdk/opends/resource/config/config.ldif
@@ -441,6 +441,13 @@
 ds-cfg-group-implementation-class: org.opends.server.extensions.StaticGroup
 ds-cfg-group-implementation-enabled: true
 
+dn: cn=Virtual Static,cn=Group Implementations,cn=config
+objectClass: top
+objectClass: ds-cfg-group-implementation
+cn: Virtual Static
+ds-cfg-group-implementation-class: org.opends.server.extensions.VirtualStaticGroup
+ds-cfg-group-implementation-enabled: true
+
 dn: cn=Identity Mappers,cn=config
 objectClass: top
 objectClass: ds-cfg-branch
@@ -1731,6 +1738,26 @@
 ds-cfg-virtual-attribute-type: subschemaSubentry
 ds-cfg-virtual-attribute-conflict-behavior: virtual-overrides-real
 
+dn: cn=Virtual Static member,cn=Virtual Attributes,cn=config
+objectClass: top
+objectClass: ds-cfg-virtual-attribute
+cn: Virtual Static member
+ds-cfg-virtual-attribute-class: org.opends.server.extensions.MemberVirtualAttributeProvider
+ds-cfg-virtual-attribute-enabled: true
+ds-cfg-virtual-attribute-type: member
+ds-cfg-virtual-attribute-conflict-behavior: virtual-overrides-real
+ds-cfg-virtual-attribute-filter: (&(objectClass=groupOfNames)(objectClass=ds-virtual-static-group))
+
+dn: cn=Virtual Static uniqueMember,cn=Virtual Attributes,cn=config
+objectClass: top
+objectClass: ds-cfg-virtual-attribute
+cn: Virtual Static uniqueMember
+ds-cfg-virtual-attribute-class: org.opends.server.extensions.MemberVirtualAttributeProvider
+ds-cfg-virtual-attribute-enabled: true
+ds-cfg-virtual-attribute-type: uniqueMember
+ds-cfg-virtual-attribute-conflict-behavior: virtual-overrides-real
+ds-cfg-virtual-attribute-filter: (&(objectClass=groupOfUniqueNames)(objectClass=ds-virtual-static-group))
+
 dn: cn=Work Queue,cn=config
 objectClass: top
 objectClass: ds-cfg-work-queue
diff --git a/opendj-sdk/opends/resource/schema/02-config.ldif b/opendj-sdk/opends/resource/schema/02-config.ldif
index bc9bf86..1bced74 100644
--- a/opendj-sdk/opends/resource/schema/02-config.ldif
+++ b/opendj-sdk/opends/resource/schema/02-config.ldif
@@ -1150,6 +1150,9 @@
   NAME 'ds-task-rebuild-max-threads'
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
   X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.343 NAME 'ds-target-group-dn'
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE
+  X-ORIGIN 'OpenDS Directory Server' )
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
   NAME 'ds-cfg-access-control-handler' SUP top STRUCTURAL
   MUST ( cn $ ds-cfg-acl-handler-class $ ds-cfg-acl-handler-enabled )
@@ -1602,5 +1605,8 @@
   MUST ( ds-task-rebuild-base-dn $ ds-task-rebuild-index )
   MAY ( ds-task-rebuild-max-threads )
   X-ORIGIN 'OpenDS Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.26027.1.2.99
+  NAME 'ds-virtual-static-group' SUP top AUXILIARY MUST ds-target-group-dn
+  X-ORIGIN 'OpenDS Directory Server' )
 
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/MemberVirtualAttributeProvider.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/MemberVirtualAttributeProvider.java
new file mode 100644
index 0000000..42db6c0
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/MemberVirtualAttributeProvider.java
@@ -0,0 +1,349 @@
+/*
+ * 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.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+import org.opends.server.admin.std.server.VirtualAttributeCfg;
+import org.opends.server.api.Group;
+import org.opends.server.api.VirtualAttributeProvider;
+import org.opends.server.config.ConfigException;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.SearchOperation;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.ByteString;
+import org.opends.server.types.ConditionResult;
+import org.opends.server.types.DebugLogLevel;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.MemberList;
+import org.opends.server.types.MembershipException;
+import org.opends.server.types.ResultCode;
+import org.opends.server.types.VirtualAttributeRule;
+
+import static org.opends.server.loggers.debug.DebugLogger.*;
+import static org.opends.server.util.ServerConstants.*;
+
+
+
+/**
+ * This class implements a virtual attribute provider that works in conjunction
+ * with virtual static groups to generate the values for the member or
+ * uniqueMember attribute.
+ */
+public class MemberVirtualAttributeProvider
+       extends VirtualAttributeProvider<VirtualAttributeCfg>
+{
+  // The attribute type used to indicate which target group should be used to
+  // obtain the member list.
+  private AttributeType targetGroupType;
+
+
+
+  /**
+   * Creates a new instance of this member virtual attribute provider.
+   */
+  public MemberVirtualAttributeProvider()
+  {
+    super();
+
+    // All initialization should be performed in the
+    // initializeVirtualAttributeProvider method.
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void initializeVirtualAttributeProvider(
+                            VirtualAttributeCfg configuration)
+         throws ConfigException, InitializationException
+  {
+    targetGroupType =
+         DirectoryServer.getAttributeType(ATTR_TARGET_GROUP_DN, true);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean isMultiValued()
+  {
+    return true;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public LinkedHashSet<AttributeValue> getValues(Entry entry,
+                                                 VirtualAttributeRule rule)
+  {
+    Group g = DirectoryServer.getGroupManager().getGroupInstance(entry.getDN());
+    if (g == null)
+    {
+      return new LinkedHashSet<AttributeValue>(0);
+    }
+
+    LinkedHashSet<AttributeValue> values = new LinkedHashSet<AttributeValue>();
+    try
+    {
+      MemberList memberList = g.getMembers();
+      while (memberList.hasMoreMembers())
+      {
+        try
+        {
+          DN memberDN = memberList.nextMemberDN();
+          if (memberDN != null)
+          {
+            values.add(new AttributeValue(rule.getAttributeType(),
+                                          memberDN.toString()));
+          }
+        }
+        catch (MembershipException me)
+        {
+          if (! me.continueIterating())
+          {
+            break;
+          }
+        }
+      }
+    }
+    catch (Exception e)
+    {
+      if (debugEnabled())
+      {
+        debugCaught(DebugLogLevel.ERROR, e);
+      }
+    }
+
+    return values;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean hasValue(Entry entry, VirtualAttributeRule rule)
+  {
+    Group g = DirectoryServer.getGroupManager().getGroupInstance(entry.getDN());
+    if (g == null)
+    {
+      return false;
+    }
+
+    try
+    {
+      MemberList memberList = g.getMembers();
+      while (memberList.hasMoreMembers())
+      {
+        try
+        {
+          DN memberDN = memberList.nextMemberDN();
+          if (memberDN != null)
+          {
+            memberList.close();
+            return true;
+          }
+        }
+        catch (MembershipException me)
+        {
+          if (! me.continueIterating())
+          {
+            break;
+          }
+        }
+      }
+    }
+    catch (Exception e)
+    {
+      if (debugEnabled())
+      {
+        debugCaught(DebugLogLevel.ERROR, e);
+      }
+    }
+
+    return false;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean hasValue(Entry entry, VirtualAttributeRule rule,
+                          AttributeValue value)
+  {
+    Group g = DirectoryServer.getGroupManager().getGroupInstance(entry.getDN());
+    if (g == null)
+    {
+      return false;
+    }
+
+    try
+    {
+      return g.isMember(DN.decode(value.getValue()));
+    }
+    catch (Exception e)
+    {
+      if (debugEnabled())
+      {
+        debugCaught(DebugLogLevel.ERROR, e);
+      }
+    }
+
+    return false;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean hasAnyValue(Entry entry, VirtualAttributeRule rule,
+                             Collection<AttributeValue> values)
+  {
+    for (AttributeValue v : values)
+    {
+      if (hasValue(entry, rule, v))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public ConditionResult matchesSubstring(Entry entry,
+                                          VirtualAttributeRule rule,
+                                          ByteString subInitial,
+                                          List<ByteString> subAny,
+                                          ByteString subFinal)
+  {
+    // DNs cannot be used in substring matching.
+    return ConditionResult.UNDEFINED;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public ConditionResult greaterThanOrEqualTo(Entry entry,
+                              VirtualAttributeRule rule,
+                              AttributeValue value)
+  {
+    // DNs cannot be used in ordering matching.
+    return ConditionResult.UNDEFINED;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public ConditionResult lessThanOrEqualTo(Entry entry,
+                              VirtualAttributeRule rule,
+                              AttributeValue value)
+  {
+    // DNs cannot be used in ordering matching.
+    return ConditionResult.UNDEFINED;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public ConditionResult approximatelyEqualTo(Entry entry,
+                              VirtualAttributeRule rule,
+                              AttributeValue value)
+  {
+    // DNs cannot be used in approximate matching.
+    return ConditionResult.UNDEFINED;
+  }
+
+
+
+  /**
+   * {@inheritDoc}.  This virtual attribute will support search operations only
+   * if one of the following is true about the search filter:
+   * <UL>
+   *   <LI>It is an equality filter targeting the associated attribute
+   *       type.</LI>
+   *   <LI>It is an AND filter in which at least one of the components is an
+   *       equality filter targeting the associated attribute type.</LI>
+   *   <LI>It is an OR filter in which all of the components are equality
+   *       filters targeting the associated attribute type.</LI>
+   * </UL>
+   */
+  @Override()
+  public boolean isSearchable(VirtualAttributeRule rule,
+                              SearchOperation searchOperation)
+  {
+    return false;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void processSearch(VirtualAttributeRule rule,
+                            SearchOperation searchOperation)
+  {
+    searchOperation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+    return;
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/StaticGroup.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/StaticGroup.java
index 64871ad..ee7fda9 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/extensions/StaticGroup.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/StaticGroup.java
@@ -79,9 +79,6 @@
 public class StaticGroup
        extends Group
 {
-
-
-
   // The attribute type used to hold the membership list for this group.
   private AttributeType memberAttributeType;
 
@@ -241,7 +238,8 @@
     // FIXME -- This needs to exclude enhanced groups once we have support for
     // them.
     String filterString =
-         "(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames))";
+         "(&(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames))" +
+            "(!(objectClass=ds-virtual-static-group))";
     return SearchFilter.createFilterFromString(filterString);
   }
 
@@ -257,6 +255,13 @@
 
     // FIXME -- This needs to exclude enhanced groups once we have support for
     //them.
+    ObjectClass virtualStaticGroupClass =
+         DirectoryConfig.getObjectClass(OC_VIRTUAL_STATIC_GROUP, true);
+    if (entry.hasObjectClass(virtualStaticGroupClass))
+    {
+      return false;
+    }
+
     ObjectClass groupOfNamesClass =
          DirectoryConfig.getObjectClass(OC_GROUP_OF_NAMES_LC, true);
     ObjectClass groupOfUniqueNamesClass =
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/VirtualStaticGroup.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/VirtualStaticGroup.java
new file mode 100644
index 0000000..2ff7c80
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/VirtualStaticGroup.java
@@ -0,0 +1,484 @@
+/*
+ * 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.List;
+
+import org.opends.server.api.Group;
+import org.opends.server.core.DirectoryServer;
+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.DirectoryException;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.MemberList;
+import org.opends.server.types.ObjectClass;
+import org.opends.server.types.ResultCode;
+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.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 virtual static group implementation, in which
+ * membership is based on membership of another group.
+ */
+public class VirtualStaticGroup
+       extends Group
+{
+  // The DN of the entry that holds the definition for this group.
+  private DN groupEntryDN;
+
+  // The DN of the target group that will provide membership information.
+  private DN targetGroupDN;
+
+
+
+  /**
+   * Creates a new, uninitialized virtual static group instance.  This is
+   * intended for internal use only.
+   */
+  public VirtualStaticGroup()
+  {
+    super();
+
+    // No initialization is required here.
+  }
+
+
+
+  /**
+   * Creates a new virtual static 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  targetGroupDN  The DN of the target group that will provide
+   *                        membership information.  It must not be
+   *                        {@code null}.
+   */
+  public VirtualStaticGroup(DN groupEntryDN, DN targetGroupDN)
+  {
+    super();
+
+    ensureNotNull(groupEntryDN, targetGroupDN);
+
+    this.groupEntryDN  = groupEntryDN;
+    this.targetGroupDN = targetGroupDN;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void initializeGroupImplementation(ConfigEntry configEntry)
+         throws ConfigException, InitializationException
+  {
+    // No additional initialization is required.
+  }
+
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public VirtualStaticGroup newInstance(Entry groupEntry)
+         throws DirectoryException
+  {
+    ensureNotNull(groupEntry);
+
+
+    // Get the target group DN attribute from the entry, if there is one.
+    DN targetDN = null;
+    AttributeType targetType =
+         DirectoryServer.getAttributeType(ATTR_TARGET_GROUP_DN, true);
+    List<Attribute> attrList = groupEntry.getAttribute(targetType);
+    if (attrList != null)
+    {
+      for (Attribute a : attrList)
+      {
+        for (AttributeValue v : a.getValues())
+        {
+          if (targetDN != null)
+          {
+            int    msgID   = MSGID_VIRTUAL_STATIC_GROUP_MULTIPLE_TARGETS;
+            String message = getMessage(msgID,
+                                        String.valueOf(groupEntry.getDN()));
+            throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION,
+                                         message, msgID);
+          }
+
+          try
+          {
+            targetDN = DN.decode(v.getValue());
+          }
+          catch (DirectoryException de)
+          {
+            if (debugEnabled())
+            {
+              debugCaught(DebugLogLevel.ERROR, de);
+            }
+
+            int    msgID   = MSGID_VIRTUAL_STATIC_GROUP_CANNOT_DECODE_TARGET;
+            String message = getMessage(msgID, v.getStringValue(),
+                                        String.valueOf(groupEntry.getDN()),
+                                        de.getErrorMessage());
+            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
+                                         message, msgID, de);
+          }
+        }
+      }
+    }
+
+    if (targetDN == null)
+    {
+      int    msgID   = MSGID_VIRTUAL_STATIC_GROUP_NO_TARGET;
+      String message = getMessage(msgID, String.valueOf(groupEntry.getDN()));
+      throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message,
+                                   msgID);
+    }
+
+    return new VirtualStaticGroup(groupEntry.getDN(), targetDN);
+  }
+
+
+
+  /**
+   * {@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_VIRTUAL_STATIC_GROUP + ")");
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean isGroupDefinition(Entry entry)
+  {
+    ensureNotNull(entry);
+
+    // FIXME -- This needs to exclude enhanced groups once we have support for
+    //them.
+    ObjectClass virtualStaticGroupClass =
+         DirectoryServer.getObjectClass(OC_VIRTUAL_STATIC_GROUP, true);
+    return entry.hasObjectClass(virtualStaticGroupClass);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public DN getGroupDN()
+  {
+    return groupEntryDN;
+  }
+
+
+
+  /**
+   * Retrieves the DN of the target group for this virtual static group.
+   *
+   * @return  The DN of the target group for this virtual static group.
+   */
+  public DN getTargetGroupDN()
+  {
+    return targetGroupDN;
+  }
+
+
+
+  /**
+   * {@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_VIRTUAL_STATIC_GROUP_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_VIRTUAL_STATIC_GROUP_NESTING_NOT_SUPPORTED;
+    String message = getMessage(msgID);
+    throw new UnsupportedOperationException(message);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean isMember(DN userDN)
+         throws DirectoryException
+  {
+    Group targetGroup =
+         DirectoryServer.getGroupManager().getGroupInstance(targetGroupDN);
+    if (targetGroup == null)
+    {
+      int    msgID   = MSGID_VIRTUAL_STATIC_GROUP_NO_TARGET_GROUP;
+      String message = getMessage(msgID, String.valueOf(targetGroupDN),
+                                  String.valueOf(groupEntryDN));
+      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
+                                   message, msgID);
+    }
+    else if (targetGroup instanceof VirtualStaticGroup)
+    {
+      int    msgID   = MSGID_VIRTUAL_STATIC_GROUP_TARGET_CANNOT_BE_VIRTUAL;
+      String message = getMessage(msgID, String.valueOf(groupEntryDN),
+                                  String.valueOf(targetGroupDN));
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message,
+                                   msgID);
+    }
+    else
+    {
+      return targetGroup.isMember(userDN);
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean isMember(Entry userEntry)
+         throws DirectoryException
+  {
+    Group targetGroup =
+         DirectoryServer.getGroupManager().getGroupInstance(targetGroupDN);
+    if (targetGroup == null)
+    {
+      int    msgID   = MSGID_VIRTUAL_STATIC_GROUP_NO_TARGET_GROUP;
+      String message = getMessage(msgID, String.valueOf(targetGroupDN),
+                                  String.valueOf(groupEntryDN));
+      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
+                                   message, msgID);
+    }
+    else if (targetGroup instanceof VirtualStaticGroup)
+    {
+      int    msgID   = MSGID_VIRTUAL_STATIC_GROUP_TARGET_CANNOT_BE_VIRTUAL;
+      String message = getMessage(msgID, String.valueOf(groupEntryDN),
+                                  String.valueOf(targetGroupDN));
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message,
+                                   msgID);
+    }
+    else
+    {
+      return targetGroup.isMember(userEntry);
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public MemberList getMembers()
+         throws DirectoryException
+  {
+    Group targetGroup =
+         DirectoryServer.getGroupManager().getGroupInstance(targetGroupDN);
+    if (targetGroup == null)
+    {
+      int    msgID   = MSGID_VIRTUAL_STATIC_GROUP_NO_TARGET_GROUP;
+      String message = getMessage(msgID, String.valueOf(targetGroupDN),
+                                  String.valueOf(groupEntryDN));
+      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
+                                   message, msgID);
+    }
+    else if (targetGroup instanceof VirtualStaticGroup)
+    {
+      int    msgID   = MSGID_VIRTUAL_STATIC_GROUP_TARGET_CANNOT_BE_VIRTUAL;
+      String message = getMessage(msgID, String.valueOf(groupEntryDN),
+                                  String.valueOf(targetGroupDN));
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message,
+                                   msgID);
+    }
+    else
+    {
+      return targetGroup.getMembers();
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public MemberList getMembers(DN baseDN, SearchScope scope,
+                               SearchFilter filter)
+         throws DirectoryException
+  {
+    Group targetGroup =
+         DirectoryServer.getGroupManager().getGroupInstance(targetGroupDN);
+    if (targetGroup == null)
+    {
+      int    msgID   = MSGID_VIRTUAL_STATIC_GROUP_NO_TARGET_GROUP;
+      String message = getMessage(msgID, String.valueOf(targetGroupDN),
+                                  String.valueOf(groupEntryDN));
+      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
+                                   message, msgID);
+    }
+    else if (targetGroup instanceof VirtualStaticGroup)
+    {
+      int    msgID   = MSGID_VIRTUAL_STATIC_GROUP_TARGET_CANNOT_BE_VIRTUAL;
+      String message = getMessage(msgID, String.valueOf(groupEntryDN),
+                                  String.valueOf(targetGroupDN));
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message,
+                                   msgID);
+    }
+    else
+    {
+      return targetGroup.getMembers(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_VIRTUAL_STATIC_GROUP_ALTERING_MEMBERS_NOT_SUPPORTED;
+    String message = getMessage(msgID, String.valueOf(groupEntryDN));
+    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_VIRTUAL_STATIC_GROUP_ALTERING_MEMBERS_NOT_SUPPORTED;
+    String message = getMessage(msgID, String.valueOf(groupEntryDN));
+    throw new UnsupportedOperationException(message);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void toString(StringBuilder buffer)
+  {
+    buffer.append("VirtualStaticGroup(dn=");
+    buffer.append(groupEntryDN);
+    buffer.append(",targetGroupDN=");
+    buffer.append(targetGroupDN);
+    buffer.append(")");
+  }
+}
+
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 96dd595..bd082cc 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
@@ -4941,6 +4941,78 @@
 
 
   /**
+   * The message ID for the message that will be used if the virtual static
+   * group has multiple targets.  This takes a single argument, which is the DN
+   * of the group.
+   */
+  public static final int MSGID_VIRTUAL_STATIC_GROUP_MULTIPLE_TARGETS =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 470;
+
+
+
+  /**
+   * The message ID for the message that will be used if the virtual static
+   * group has a target that can't be decoded as a DN.  This takes three
+   * arguments, which are the target group value, the group DN, and a message
+   * explaining the problem that occurred.
+   */
+  public static final int MSGID_VIRTUAL_STATIC_GROUP_CANNOT_DECODE_TARGET =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 471;
+
+
+
+  /**
+   * The message ID for the message that will be used if the virtual static
+   * group does not have a target group DN.  This takes a single argument, which
+   * is the DN of the group.
+   */
+  public static final int MSGID_VIRTUAL_STATIC_GROUP_NO_TARGET =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 472;
+
+
+
+  /**
+   * The message ID for the message that will be used if an attempt is made to
+   * nest a virtual static group.  This takes a single argument, which is the
+   * DN of the group.
+   */
+  public static final int MSGID_VIRTUAL_STATIC_GROUP_NESTING_NOT_SUPPORTED =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 473;
+
+
+
+  /**
+   * The message ID for the message that will be used if the target group does
+   * not exist.  This takes two arguments, which is the target group DN and the
+   * virtual static group DN.
+   */
+  public static final int MSGID_VIRTUAL_STATIC_GROUP_NO_TARGET_GROUP =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 474;
+
+
+
+  /**
+   * The message ID for the message that will be used if an attempt is made to
+   * alter the membership for a virtual static group.  This takes a single
+   * argument, which is the DN of the group.
+   */
+  public static final int
+       MSGID_VIRTUAL_STATIC_GROUP_ALTERING_MEMBERS_NOT_SUPPORTED =
+            CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 475;
+
+
+
+  /**
+   * The message ID for the message that will be used if a virtual static group
+   * target is also a virtual static group.  This takes two arguments, which are
+   * the object group DN and the target group DN.
+   */
+  public static final int MSGID_VIRTUAL_STATIC_GROUP_TARGET_CANNOT_BE_VIRTUAL=
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 476;
+
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined in this
    * class.
    */
@@ -7109,6 +7181,30 @@
                     "The provided character set definition '%s' is invalid " +
                     "because it contains character '%s' which has already " +
                     "been used.");
+
+
+    registerMessage(MSGID_VIRTUAL_STATIC_GROUP_MULTIPLE_TARGETS,
+                    "The virtual static group defined in entry %s contains " +
+                    "multiple target group DNs, but only one is allowed.");
+    registerMessage(MSGID_VIRTUAL_STATIC_GROUP_CANNOT_DECODE_TARGET,
+                    "Unable to decode \"%s\" as the target DN for group %s:  " +
+                    "%s.");
+    registerMessage(MSGID_VIRTUAL_STATIC_GROUP_NO_TARGET,
+                    "The virtual static group defined in entry %s does not " +
+                    "contain a target group definition.");
+    registerMessage(MSGID_VIRTUAL_STATIC_GROUP_NESTING_NOT_SUPPORTED,
+                    "Virtual static groups do not support nesting.");
+    registerMessage(MSGID_VIRTUAL_STATIC_GROUP_NO_TARGET_GROUP,
+                    "Target group %s referenced by virtual static group %s " +
+                    "does not exist.");
+    registerMessage(MSGID_VIRTUAL_STATIC_GROUP_ALTERING_MEMBERS_NOT_SUPPORTED,
+                    "Altering membership for virtual static group %s is not " +
+                    "allowed.");
+    registerMessage(MSGID_VIRTUAL_STATIC_GROUP_TARGET_CANNOT_BE_VIRTUAL,
+                    "Virtual static group %s references target group %s " +
+                    "which is itself a virtual static group.  One " +
+                    "virtual static group is not allowed to reference " +
+                    "another as its target group.");
   }
 }
 
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 57ebefc..8484a2f 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
@@ -490,6 +490,14 @@
 
 
   /**
+   * The name of the attribute that is used to specify the DN of the target
+   * group for a virtual static group.
+   */
+  public static final String ATTR_TARGET_GROUP_DN = "ds-target-group-dn";
+
+
+
+  /**
    * The name of the attribute that is used to specify the total number of
    * connections established since startup, formatted in camel case.
    */
@@ -824,6 +832,15 @@
 
 
   /**
+   * The name of the ds-virtual-static-group objectclass in all lowercase
+   * characters.
+   */
+  public static final String OC_VIRTUAL_STATIC_GROUP =
+       "ds-virtual-static-group";
+
+
+
+  /**
    * The English name for the basic disabled log severity used for all
    * log severities.
    */
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 cc4a76d..dc81c2b 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
@@ -43,6 +43,7 @@
 import org.opends.server.core.ModifyDNOperation;
 import org.opends.server.extensions.DynamicGroup;
 import org.opends.server.extensions.StaticGroup;
+import org.opends.server.extensions.VirtualStaticGroup;
 import org.opends.server.protocols.internal.InternalClientConnection;
 import org.opends.server.protocols.internal.InternalSearchOperation;
 import org.opends.server.types.Attribute;
@@ -100,6 +101,7 @@
     LinkedHashSet<Class> groupClasses = new LinkedHashSet<Class>();
     groupClasses.add(StaticGroup.class);
     groupClasses.add(DynamicGroup.class);
+    groupClasses.add(VirtualStaticGroup.class);
 
     for (Group g : groupManager.getGroupImplementations())
     {
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/VirtualStaticGroupTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/VirtualStaticGroupTestCase.java
new file mode 100644
index 0000000..fc43be7
--- /dev/null
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/VirtualStaticGroupTestCase.java
@@ -0,0 +1,859 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.extensions;
+
+
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import org.opends.server.TestCaseUtils;
+import org.opends.server.admin.std.meta.VirtualAttributeCfgDefn;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.GroupManager;
+import org.opends.server.core.ModifyOperation;
+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.AttributeType;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.ConditionResult;
+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;
+import org.opends.server.types.Modification;
+import org.opends.server.types.ModificationType;
+import org.opends.server.types.ResultCode;
+import org.opends.server.types.SearchScope;
+import org.opends.server.types.SearchFilter;
+import org.opends.server.types.VirtualAttributeRule;
+
+import static org.testng.Assert.*;
+
+
+
+/**
+ * A set of test cases for the virtual static group implementation and the
+ * member virtual attribute provider.
+ */
+public class VirtualStaticGroupTestCase
+       extends ExtensionsTestCase
+{
+  /**
+   * The lines comprising the LDIF test data.
+   */
+  private static final String[] LDIF_LINES =
+  {
+    "dn: ou=People,o=test",
+    "objectClass: top",
+    "objectClass: organizationalUnit",
+    "ou: People",
+    "",
+    "dn: uid=test.1,ou=People,o=test",
+    "objectClass: top",
+    "objectClass: person",
+    "objectClass: organizationalPerson",
+    "objectClass: inetOrgPerson",
+    "uid: test.1",
+    "givenName: Test",
+    "sn: 1",
+    "cn: Test 1",
+    "userPassword: password",
+    "",
+    "dn: uid=test.2,ou=People,o=test",
+    "objectClass: top",
+    "objectClass: person",
+    "objectClass: organizationalPerson",
+    "objectClass: inetOrgPerson",
+    "uid: test.2",
+    "givenName: Test",
+    "sn: 2",
+    "cn: Test 2",
+    "userPassword: password",
+    "",
+    "dn: uid=test.3,ou=People,o=test",
+    "objectClass: top",
+    "objectClass: person",
+    "objectClass: organizationalPerson",
+    "objectClass: inetOrgPerson",
+    "uid: test.3",
+    "givenName: Test",
+    "sn: 3",
+    "cn: Test 3",
+    "userPassword: password",
+    "",
+    "dn: uid=test.4,ou=People,o=test",
+    "objectClass: top",
+    "objectClass: person",
+    "objectClass: organizationalPerson",
+    "objectClass: inetOrgPerson",
+    "uid: test.4",
+    "givenName: Test",
+    "sn: 4",
+    "cn: Test 4",
+    "userPassword: password",
+    "",
+    "dn: ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: organizationalUnit",
+    "ou: Groups",
+    "",
+    "dn: cn=Dynamic All Users,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfURLs",
+    "cn: Dynamic All Users",
+    "memberURL: ldap:///ou=People,o=test??sub?(objectClass=person)",
+    "",
+    "dn: cn=Dynamic One User,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfURLs",
+    "cn: Dynamic One User",
+    "memberURL: ldap:///ou=People,o=test??sub?(&(objectClass=person)(sn=4))",
+    "",
+    "dn: cn=Static member List,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfNames",
+    "cn: Static member List",
+    "member: uid=test.1,ou=People,o=test",
+    "member: uid=test.3,ou=People,o=test",
+    "",
+    "dn: cn=Static uniqueMember List,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfUniqueNames",
+    "cn: Static uniqueMember List",
+    "uniqueMember: uid=test.2,ou=People,o=test",
+    "uniqueMember: uid=test.3,ou=People,o=test",
+    "uniqueMember: uid=no-such-user,ou=People,o=test",
+    "",
+    "dn: cn=Virtual member All Users,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfNames",
+    "objectClass: ds-virtual-static-group",
+    "cn: Virtual member All Users",
+    "ds-target-group-dn: cn=Dynamic All Users,ou=Groups,o=test",
+    "",
+    "dn: cn=Virtual uniqueMember All Users,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfUniqueNames",
+    "objectClass: ds-virtual-static-group",
+    "cn: Virtual uniqueMember All Users",
+    "ds-target-group-dn: cn=Dynamic All Users,ou=Groups,o=test",
+    "",
+    "dn: cn=Virtual member One User,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfNames",
+    "objectClass: ds-virtual-static-group",
+    "cn: Virtual member One User",
+    "ds-target-group-dn: cn=Dynamic One User,ou=Groups,o=test",
+    "",
+    "dn: cn=Virtual uniqueMember One User,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfUniqueNames",
+    "objectClass: ds-virtual-static-group",
+    "cn: Virtual uniqueMember One User",
+    "ds-target-group-dn: cn=Dynamic One User,ou=Groups,o=test",
+    "",
+    "dn: cn=Virtual Static member List,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfNames",
+    "objectClass: ds-virtual-static-group",
+    "cn: Virtual Static member List",
+    "ds-target-group-dn: cn=Static member List,ou=Groups,o=test",
+    "",
+    "dn: cn=Virtual Static uniqueMember List,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfUniqueNames",
+    "objectClass: ds-virtual-static-group",
+    "cn: Virtual Static uniqueMember List",
+    "ds-target-group-dn: cn=Static uniqueMember List,ou=Groups,o=test",
+    "",
+    "dn: cn=Crossover member Static Group,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfUniqueNames",
+    "objectClass: ds-virtual-static-group",
+    "cn: Crossover member Static Group",
+    "ds-target-group-dn: cn=Static member List,ou=Groups,o=test",
+    "",
+    "dn: cn=Crossover uniqueMember Static Group,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfNames",
+    "objectClass: ds-virtual-static-group",
+    "cn: Crossover uniqueMember Static Group",
+    "ds-target-group-dn: cn=Static uniqueMember List,ou=Groups,o=test",
+    "",
+    "dn: cn=Virtual Nonexistent,ou=Groups,o=test",
+    "objectClass: top",
+    "objectClass: groupOfNames",
+    "objectClass: ds-virtual-static-group",
+    "cn: Virtual Nonexistent",
+    "ds-target-group-dn: cn=Nonexistent,ou=Groups,o=test"
+  };
+
+
+
+  // The attribute type for the member attribute.
+  private AttributeType memberType;
+
+  // The attribute type for the uniqueMember attribute.
+  private AttributeType uniqueMemberType;
+
+  // The server group manager.
+  private GroupManager groupManager;
+
+  // The DNs of the various entries in the data set.
+  private DN u1;
+  private DN u2;
+  private DN u3;
+  private DN u4;
+  private DN da;
+  private DN d1;
+  private DN sm;
+  private DN su;
+  private DN vmda;
+  private DN vuda;
+  private DN vmd1;
+  private DN vud1;
+  private DN vsm;
+  private DN vsu;
+  private DN vcm;
+  private DN vcu;
+  private DN vn;
+  private DN ne;
+
+
+
+  /**
+   * Ensures that the Directory Server is running.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @BeforeClass()
+  public void startServer()
+         throws Exception
+  {
+    TestCaseUtils.startServer();
+
+    memberType = DirectoryServer.getAttributeType("member", false);
+    assertNotNull(memberType);
+
+    uniqueMemberType = DirectoryServer.getAttributeType("uniquemember", false);
+    assertNotNull(uniqueMemberType);
+
+    groupManager = DirectoryServer.getGroupManager();
+
+    u1 = DN.decode("uid=test.1,ou=People,o=test");
+    u2 = DN.decode("uid=test.2,ou=People,o=test");
+    u3 = DN.decode("uid=test.3,ou=People,o=test");
+    u4 = DN.decode("uid=test.4,ou=People,o=test");
+    da = DN.decode("cn=Dynamic All Users,ou=Groups,o=test");
+    d1 = DN.decode("cn=Dynamic One User,ou=Groups,o=test");
+    sm = DN.decode("cn=Static member List,ou=Groups,o=test");
+    su = DN.decode("cn=Static uniqueMember List,ou=Groups,o=test");
+    vmda = DN.decode("cn=Virtual member All Users,ou=Groups,o=test");
+    vuda = DN.decode("cn=Virtual uniqueMember All Users,ou=Groups,o=test");
+    vmd1 = DN.decode("cn=Virtual member One User,ou=Groups,o=test");
+    vud1 = DN.decode("cn=Virtual uniqueMember One User,ou=Groups,o=test");
+    vsm = DN.decode("cn=Virtual Static member List,ou=Groups,o=test");
+    vsu = DN.decode("cn=Virtual Static uniqueMember List,ou=Groups,o=test");
+    vcm = DN.decode("cn=Crossover member Static Group,ou=Groups,o=test");
+    vcu = DN.decode("cn=Crossover uniqueMember Static Group,ou=Groups,o=test");
+    vn = DN.decode("cn=Virtual Nonexistent,ou=Groups,o=test");
+    ne = DN.decode("cn=Nonexistent,ou=Groups,o=test");
+  }
+
+
+
+  /**
+   * Tests creating a new instance of a virtual static group from a valid entry.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testCreateValidGroup()
+         throws Exception
+  {
+    Entry entry = TestCaseUtils.makeEntry(
+      "dn: cn=Valid Virtual Static Group,ou=Groups,o=test",
+      "objectClass: top",
+      "objectClass: groupOfNames",
+      "objectClass: ds-virtual-static-group",
+      "cn: Valid Virtual Static Group",
+      "ds-target-group-dn: cn=Static member List,ou=Groups,o=test");
+
+    VirtualStaticGroup groupImplementation = new VirtualStaticGroup();
+    VirtualStaticGroup groupInstance = groupImplementation.newInstance(entry);
+    assertNotNull(groupInstance);
+    groupImplementation.finalizeGroupImplementation();
+  }
+
+
+
+  /**
+   * Retrieves a set of invalid vittual static group definition entries.
+   *
+   * @return  A set of invalid virtul static group definition entries.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @DataProvider(name = "invalidGroups")
+  public Object[][] getInvalidGroupDefinitions()
+         throws Exception
+  {
+    List<Entry> groupEntries = TestCaseUtils.makeEntries(
+      "dn: cn=Not a Virtual Static Group,ou=Groups,o=test",
+      "objectClass: top",
+      "objectClass: groupOfNames",
+      "cn: Not a Virtual Static Group",
+      "member: uid=test.1,ou=People,o=test",
+      "",
+      "dn: cn=No Target,ou=Groups,o=test",
+      "objectClass: top",
+      "objectClass: groupOfNames",
+      "objectClass: ds-virtual-static-group",
+      "cn: No Target",
+      "",
+      "dn: cn=Invalid Target,ou=Groups,o=test",
+      "objectClass: top",
+      "objectClass: groupOfNames",
+      "objectClass: ds-virtual-static-group",
+      "cn: Invalid Target",
+      "ds-target-group-dn: invalid");
+
+    Object[][] entryArray = new Object[groupEntries.size()][1];
+    for (int i=0; i < entryArray.length; i++)
+    {
+      entryArray[i][0] = groupEntries.get(i);
+    }
+
+    return entryArray;
+  }
+
+
+
+  /**
+   * Tests creating a new instance of a virtual static group from an invalid
+   * entry.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(dataProvider = "invalidGroups",
+        expectedExceptions = { DirectoryException.class })
+  public void testCreateInvalidGroup(Entry entry)
+         throws Exception
+  {
+    VirtualStaticGroup groupImplementation = new VirtualStaticGroup();
+    try
+    {
+      VirtualStaticGroup groupInstance = groupImplementation.newInstance(entry);
+    }
+    finally
+    {
+      groupImplementation.finalizeGroupImplementation();
+    }
+  }
+
+
+
+  /**
+   * Performs general tests of the group API for virtual static groups with a
+   * group that has a real target group.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testGroupAPI()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.addEntries(LDIF_LINES);
+
+    VirtualStaticGroup g =
+         (VirtualStaticGroup) groupManager.getGroupInstance(vmda);
+    assertNotNull(g);
+    assertTrue(g.isMember(u1));
+
+    assertNotNull(g.getGroupDefinitionFilter());
+    assertEquals(g.getGroupDN(), vmda);
+    assertEquals(g.getTargetGroupDN(), da);
+    assertFalse(g.supportsNestedGroups());
+    assertTrue(g.getNestedGroupDNs().isEmpty());
+    assertFalse(g.mayAlterMemberList());
+
+    Entry entry = DirectoryServer.getEntry(u1);
+    assertTrue(g.isMember(entry));
+
+    MemberList memberList = g.getMembers();
+    assertTrue(memberList.hasMoreMembers());
+    assertNotNull(memberList.nextMemberDN());
+    assertNotNull(memberList.nextMemberEntry());
+    assertNotNull(memberList.nextMemberDN());
+    assertNotNull(memberList.nextMemberDN());
+    assertFalse(memberList.hasMoreMembers());
+
+    SearchFilter filter = SearchFilter.createFilterFromString("(sn=1)");
+    memberList = g.getMembers(DN.decode("o=test"), SearchScope.WHOLE_SUBTREE,
+                              filter);
+    assertTrue(memberList.hasMoreMembers());
+    assertNotNull(memberList.nextMemberDN());
+    assertFalse(memberList.hasMoreMembers());
+
+    try
+    {
+      g.addNestedGroup(d1);
+      fail("Expected an exception from addNestedGroupDN");
+    } catch (Exception e) {}
+
+    try
+    {
+      g.removeNestedGroup(d1);
+      fail("Expected an exception from removeNestedGroupDN");
+    } catch (Exception e) {}
+
+    try
+    {
+      g.addMember(entry);
+      fail("Expected an exception from addMember");
+    } catch (Exception e) {}
+
+    try
+    {
+      g.removeMember(u1);
+      fail("Expected an exception from removeMember");
+    } catch (Exception e) {}
+
+    assertNotNull(g.toString());
+
+    cleanUp();
+  }
+
+
+
+  /**
+   * Performs general tests of the group API for virtual static groups with a
+   * group that has a nonexistent target group.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testGroupAPINonexistent()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.addEntries(LDIF_LINES);
+
+    VirtualStaticGroup g =
+         (VirtualStaticGroup) groupManager.getGroupInstance(vn);
+    assertNotNull(g);
+
+    assertNotNull(g.getGroupDefinitionFilter());
+    assertEquals(g.getGroupDN(), vn);
+    assertEquals(g.getTargetGroupDN(), ne);
+    assertFalse(g.supportsNestedGroups());
+    assertTrue(g.getNestedGroupDNs().isEmpty());
+    assertFalse(g.mayAlterMemberList());
+
+    Entry entry = DirectoryServer.getEntry(u1);
+
+    try
+    {
+      g.isMember(u1);
+      fail("Expected an exception from isMember(DN)");
+    } catch (Exception e) {}
+
+    try
+    {
+      g.isMember(entry);
+      fail("Expected an exception from isMember(Entry)");
+    } catch (Exception e) {}
+
+    try
+    {
+      g.getMembers();
+      fail("Expected an exception from getMembers()");
+    } catch (Exception e) {}
+
+    try
+    {
+      SearchFilter filter = SearchFilter.createFilterFromString("(sn=1)");
+      g.getMembers(DN.decode("o=test"), SearchScope.WHOLE_SUBTREE, filter);
+      fail("Expected an exception from getMembers(base, scope, filter)");
+    } catch (Exception e) {}
+
+    try
+    {
+      g.addNestedGroup(d1);
+      fail("Expected an exception from addNestedGroupDN");
+    } catch (Exception e) {}
+
+    try
+    {
+      g.removeNestedGroup(d1);
+      fail("Expected an exception from removeNestedGroupDN");
+    } catch (Exception e) {}
+
+    try
+    {
+      g.addMember(entry);
+      fail("Expected an exception from addMember");
+    } catch (Exception e) {}
+
+    try
+    {
+      g.removeMember(u1);
+      fail("Expected an exception from removeMember");
+    } catch (Exception e) {}
+
+    assertNotNull(g.toString());
+
+    cleanUp();
+  }
+
+
+
+  /**
+   * Tests the behavior of the virtual static group with a dynamic group.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testVirtualGroupDynamicGroupWithMember()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.addEntries(LDIF_LINES);
+
+    VirtualStaticGroup g =
+         (VirtualStaticGroup) groupManager.getGroupInstance(vmda);
+    assertNotNull(g);
+    assertTrue(g.isMember(u1));
+    assertTrue(g.isMember(u2));
+    assertTrue(g.isMember(u3));
+    assertTrue(g.isMember(u4));
+
+    cleanUp();
+  }
+
+
+
+  /**
+   * Tests the behavior of the virtual static group with a static group based on
+   * the member attribute.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testVirtualGroupStaticGroupWithMember()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.addEntries(LDIF_LINES);
+
+    VirtualStaticGroup g =
+         (VirtualStaticGroup) groupManager.getGroupInstance(vsm);
+    assertNotNull(g);
+    assertTrue(g.isMember(u1));
+    assertFalse(g.isMember(u2));
+    assertTrue(g.isMember(u3));
+    assertFalse(g.isMember(u4));
+
+    cleanUp();
+  }
+
+
+
+  /**
+   * Tests the behavior of the virtual static group with a static group based on
+   * the uniqueMember attribute.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testVirtualGroupStaticGroupWithUniqueMember()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.addEntries(LDIF_LINES);
+
+    VirtualStaticGroup g =
+         (VirtualStaticGroup) groupManager.getGroupInstance(vsu);
+    assertNotNull(g);
+    assertFalse(g.isMember(u1));
+    assertTrue(g.isMember(u2));
+    assertTrue(g.isMember(u3));
+    assertFalse(g.isMember(u4));
+
+    cleanUp();
+  }
+
+
+
+  /**
+   * Performs general tests of the virtual attribute provider API for the member
+   * virtual attribute with a target group that exists.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testVirtualAttributeAPI()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.addEntries(LDIF_LINES);
+
+    VirtualAttributeRule rule = null;
+    for (VirtualAttributeRule r : DirectoryServer.getVirtualAttributes())
+    {
+      if (r.getAttributeType().equals(memberType))
+      {
+        rule = r;
+        break;
+      }
+    }
+    assertNotNull(rule);
+
+    MemberVirtualAttributeProvider provider =
+         (MemberVirtualAttributeProvider) rule.getProvider();
+    assertNotNull(provider);
+
+    Entry entry = DirectoryServer.getEntry(vsm);
+    assertNotNull(entry);
+
+    assertTrue(provider.isMultiValued());
+
+    LinkedHashSet<AttributeValue> values = provider.getValues(entry, rule);
+    assertNotNull(values);
+    assertFalse(values.isEmpty());
+    assertTrue(provider.hasValue(entry, rule));
+    assertTrue(provider.hasValue(entry, rule,
+                    new AttributeValue(memberType, u1.toString())));
+    assertFalse(provider.hasValue(entry, rule,
+                    new AttributeValue(memberType, ne.toString())));
+    assertTrue(provider.hasAnyValue(entry, rule, values));
+    assertFalse(provider.hasAnyValue(entry, rule,
+                                     Collections.<AttributeValue>emptySet()));
+    assertEquals(provider.matchesSubstring(entry, rule, null, null, null),
+                 ConditionResult.UNDEFINED);
+    assertEquals(provider.greaterThanOrEqualTo(entry, rule, null),
+                 ConditionResult.UNDEFINED);
+    assertEquals(provider.lessThanOrEqualTo(entry, rule, null),
+                 ConditionResult.UNDEFINED);
+    assertEquals(provider.approximatelyEqualTo(entry, rule, null),
+                 ConditionResult.UNDEFINED);
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    InternalSearchOperation searchOperation =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), null, DN.decode("o=test"),
+                  SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString(
+                       "(member=" + u1.toString() + ")"),
+                  null, null);
+    assertFalse(provider.isSearchable(rule, searchOperation));
+
+    provider.processSearch(rule, searchOperation);
+    assertFalse(searchOperation.getResultCode() == ResultCode.SUCCESS);
+
+    cleanUp();
+  }
+
+
+
+  /**
+   * Performs general tests of the virtual attribute provider API for the member
+   * virtual attribute with a target group that does not exist.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testVirtualAttributeAPINonexistent()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.addEntries(LDIF_LINES);
+
+    VirtualAttributeRule rule = null;
+    for (VirtualAttributeRule r : DirectoryServer.getVirtualAttributes())
+    {
+      if (r.getAttributeType().equals(memberType))
+      {
+        rule = r;
+        break;
+      }
+    }
+    assertNotNull(rule);
+
+    MemberVirtualAttributeProvider provider =
+         (MemberVirtualAttributeProvider) rule.getProvider();
+    assertNotNull(provider);
+
+    Entry entry = DirectoryServer.getEntry(vn);
+    assertNotNull(entry);
+
+    assertTrue(provider.isMultiValued());
+
+    LinkedHashSet<AttributeValue> values = provider.getValues(entry, rule);
+    assertNotNull(values);
+    assertTrue(values.isEmpty());
+    assertFalse(provider.hasValue(entry, rule));
+    assertFalse(provider.hasValue(entry, rule,
+                    new AttributeValue(memberType, u1.toString())));
+    assertFalse(provider.hasValue(entry, rule,
+                    new AttributeValue(memberType, ne.toString())));
+    assertFalse(provider.hasAnyValue(entry, rule, values));
+    assertFalse(provider.hasAnyValue(entry, rule,
+                                     Collections.<AttributeValue>emptySet()));
+    assertEquals(provider.matchesSubstring(entry, rule, null, null, null),
+                 ConditionResult.UNDEFINED);
+    assertEquals(provider.greaterThanOrEqualTo(entry, rule, null),
+                 ConditionResult.UNDEFINED);
+    assertEquals(provider.lessThanOrEqualTo(entry, rule, null),
+                 ConditionResult.UNDEFINED);
+    assertEquals(provider.approximatelyEqualTo(entry, rule, null),
+                 ConditionResult.UNDEFINED);
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    InternalSearchOperation searchOperation =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), null, DN.decode("o=test"),
+                  SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString(
+                       "(member=" + u1.toString() + ")"),
+                  null, null);
+    assertFalse(provider.isSearchable(rule, searchOperation));
+
+    provider.processSearch(rule, searchOperation);
+    assertFalse(searchOperation.getResultCode() == ResultCode.SUCCESS);
+
+    cleanUp();
+  }
+
+
+
+  /**
+   * Tests the behavior of the member virtual attribute with a dynamic group.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testVirtualAttrDynamicGroupWithMember()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.addEntries(LDIF_LINES);
+
+    Entry e = DirectoryServer.getEntry(vmda);
+    assertNotNull(e);
+    assertTrue(e.hasAttribute(memberType));
+
+    Attribute a = e.getAttribute(memberType).get(0);
+    assertEquals(a.getValues().size(), 4);
+
+    AttributeValue v = new AttributeValue(memberType, u1.toString());
+    assertTrue(a.hasValue(v));
+
+    cleanUp();
+  }
+
+
+
+  /**
+   * Tests the behavior of the member virtual attribute with a dynamic group.
+   * The target dynamic group will initially have only one memberURL which
+   * matches only one user, but will then be updated on the fly to contain a
+   * second URL that matches all users.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testVirtualAttrDynamicGroupWithUpdatedMemberURLs()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.addEntries(LDIF_LINES);
+
+    Entry e = DirectoryServer.getEntry(vmd1);
+    assertNotNull(e);
+    assertTrue(e.hasAttribute(memberType));
+
+    Attribute a = e.getAttribute(memberType).get(0);
+    assertEquals(a.getValues().size(), 1);
+
+    AttributeValue v = new AttributeValue(memberType, u4.toString());
+    assertTrue(a.hasValue(v));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    LinkedList<Modification> mods = new LinkedList<Modification>();
+    mods.add(new Modification(ModificationType.ADD,
+         new Attribute("memberurl",
+                       "ldap:///o=test??sub?(objectClass=person)")));
+    ModifyOperation modifyOperation = conn.processModify(d1, mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+
+    a = e.getAttribute(memberType).get(0);
+    assertEquals(a.getValues().size(), 4);
+    assertTrue(a.hasValue(v));
+
+    cleanUp();
+  }
+
+
+
+  /**
+   * Removes all of the groups that have been added to the server.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void cleanUp()
+          throws Exception
+  {
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    InternalSearchOperation searchOperation =
+         conn.processSearch(DN.decode("ou=Groups,dc=example,dc=com"),
+              SearchScope.SINGLE_LEVEL,
+              SearchFilter.createFilterFromString("(objectClass=*)"));
+    for (Entry e : searchOperation.getSearchEntries())
+    {
+      conn.processDelete(e.getDN());
+    }
+  }
+}
+

--
Gitblit v1.10.0