001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.extensions;
018
019import static com.forgerock.opendj.util.StaticUtils.getBytes;
020
021import java.io.UnsupportedEncodingException;
022import java.util.Arrays;
023import java.util.Collections;
024import java.util.HashSet;
025import java.util.LinkedHashSet;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Set;
029import java.util.concurrent.atomic.AtomicReference;
030import java.util.concurrent.locks.ReadWriteLock;
031import java.util.concurrent.locks.ReentrantReadWriteLock;
032
033import org.forgerock.i18n.LocalizableMessage;
034import org.forgerock.i18n.LocalizedIllegalArgumentException;
035import org.forgerock.i18n.slf4j.LocalizedLogger;
036import org.forgerock.opendj.config.server.ConfigException;
037import org.forgerock.opendj.ldap.ByteString;
038import org.forgerock.opendj.ldap.DN;
039import org.forgerock.opendj.ldap.ModificationType;
040import org.forgerock.opendj.ldap.ResultCode;
041import org.forgerock.opendj.ldap.SearchScope;
042import org.forgerock.opendj.ldap.schema.AttributeType;
043import org.forgerock.opendj.server.config.server.GroupImplementationCfg;
044import org.forgerock.opendj.server.config.server.StaticGroupImplementationCfg;
045import org.forgerock.util.Reject;
046import org.forgerock.util.annotations.VisibleForTesting;
047import org.opends.server.api.Group;
048import org.opends.server.core.DirectoryServer;
049import org.opends.server.core.ModifyOperation;
050import org.opends.server.core.ModifyOperationBasis;
051import org.opends.server.core.ServerContext;
052import org.opends.server.protocols.ldap.LDAPControl;
053import org.opends.server.types.AcceptRejectWarn;
054import org.opends.server.types.Attribute;
055import org.opends.server.types.Attributes;
056import org.opends.server.types.Control;
057import org.opends.server.types.DirectoryException;
058import org.opends.server.types.Entry;
059import org.opends.server.types.InitializationException;
060import org.opends.server.types.MemberList;
061import org.opends.server.types.MembershipException;
062import org.opends.server.types.Modification;
063import org.opends.server.types.SearchFilter;
064
065import static org.forgerock.opendj.ldap.schema.CoreSchema.*;
066import static org.opends.messages.ExtensionMessages.*;
067import static org.opends.server.core.DirectoryServer.*;
068import static org.opends.server.protocols.internal.InternalClientConnection.*;
069import static org.opends.server.util.CollectionUtils.*;
070import static org.opends.server.util.ServerConstants.*;
071
072/**
073 * A static group implementation, in which the DNs of all members are explicitly
074 * listed.
075 * <p>
076 * There are three variants of static groups:
077 * <ul>
078 *   <li>one based on the {@code groupOfNames} object class: which stores the
079 * member list in the {@code member} attribute</li>
080 *   <li>one based on the {@code groupOfEntries} object class, which also stores
081 * the member list in the {@code member} attribute</li>
082 *   <li>one based on the {@code groupOfUniqueNames} object class, which stores
083 * the member list in the {@code uniqueMember} attribute.</li>
084 * </ul>
085 */
086public class StaticGroup extends Group<StaticGroupImplementationCfg>
087{
088  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
089
090  /** The attribute type used to hold the membership list for this group. */
091  private AttributeType memberAttributeType;
092
093  /** The DN of the entry that holds the definition for this group. */
094  private DN groupEntryDN;
095
096  /** The set of the DNs of the members for this group. */
097  private HashSet<CompactDn> memberDNs;
098
099  /** The list of nested group DNs for this group. */
100  private LinkedList<DN> nestedGroups = new LinkedList<>();
101
102  /** Passed to the group manager to see if the nested group list needs to be refreshed. */
103  private long nestedGroupRefreshToken = DirectoryServer.getGroupManager().refreshToken();
104
105  /** Read/write lock protecting memberDNs and nestedGroups. */
106  private ReadWriteLock lock = new ReentrantReadWriteLock();
107
108  private ServerContext serverContext;
109
110  /**
111   * Creates an uninitialized static group. This is intended for internal use
112   * only, to allow {@code GroupManager} to dynamically create a group.
113   */
114  public StaticGroup()
115  {
116    super();
117  }
118
119  /**
120   * Creates a new static group instance with the provided information.
121   *
122   * @param  groupEntryDN         The DN of the entry that holds the definition
123   *                              for this group.
124   * @param  memberAttributeType  The attribute type used to hold the membership
125   *                              list for this group.
126   * @param  memberDNs            The set of the DNs of the members for this
127   *                              group.
128   */
129  private StaticGroup(ServerContext serverContext, DN groupEntryDN, AttributeType memberAttributeType,
130      LinkedHashSet<CompactDn> memberDNs)
131  {
132    super();
133    Reject.ifNull(groupEntryDN, memberAttributeType, memberDNs);
134
135    this.serverContext       = serverContext;
136    this.groupEntryDN        = groupEntryDN;
137    this.memberAttributeType = memberAttributeType;
138    this.memberDNs           = memberDNs;
139  }
140
141  @Override
142  public void initializeGroupImplementation(StaticGroupImplementationCfg configuration)
143         throws ConfigException, InitializationException
144  {
145    // No additional initialization is required.
146  }
147
148  @Override
149  public StaticGroup newInstance(ServerContext serverContext, Entry groupEntry) throws DirectoryException
150  {
151    Reject.ifNull(groupEntry);
152
153    // Determine whether it is a groupOfNames, groupOfEntries or
154    // groupOfUniqueNames entry.  If not, then that's a problem.
155    AttributeType someMemberAttributeType;
156    boolean hasGroupOfEntriesClass = hasObjectClass(groupEntry, OC_GROUP_OF_ENTRIES_LC);
157    boolean hasGroupOfNamesClass = hasObjectClass(groupEntry, OC_GROUP_OF_NAMES_LC);
158    boolean hasGroupOfUniqueNamesClass = hasObjectClass(groupEntry, OC_GROUP_OF_UNIQUE_NAMES_LC);
159    if (hasGroupOfEntriesClass)
160    {
161      if (hasGroupOfNamesClass)
162      {
163        LocalizableMessage message = ERR_STATICGROUP_INVALID_OC_COMBINATION.get(
164            groupEntry.getName(), OC_GROUP_OF_ENTRIES, OC_GROUP_OF_NAMES);
165        throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message);
166      }
167      else if (hasGroupOfUniqueNamesClass)
168      {
169        LocalizableMessage message = ERR_STATICGROUP_INVALID_OC_COMBINATION.get(
170            groupEntry.getName(), OC_GROUP_OF_ENTRIES, OC_GROUP_OF_UNIQUE_NAMES);
171        throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message);
172      }
173
174      someMemberAttributeType = getMemberAttributeType();
175    }
176    else if (hasGroupOfNamesClass)
177    {
178      if (hasGroupOfUniqueNamesClass)
179      {
180        LocalizableMessage message = ERR_STATICGROUP_INVALID_OC_COMBINATION.get(
181            groupEntry.getName(), OC_GROUP_OF_NAMES, OC_GROUP_OF_UNIQUE_NAMES);
182        throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message);
183      }
184
185      someMemberAttributeType = getMemberAttributeType();
186    }
187    else if (hasGroupOfUniqueNamesClass)
188    {
189      someMemberAttributeType = getUniqueMemberAttributeType();
190    }
191    else
192    {
193      LocalizableMessage message =
194          ERR_STATICGROUP_NO_VALID_OC.get(groupEntry.getName(), OC_GROUP_OF_NAMES, OC_GROUP_OF_UNIQUE_NAMES);
195      throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message);
196    }
197
198    List<Attribute> memberAttrList = groupEntry.getAttribute(someMemberAttributeType);
199    int membersCount = 0;
200    for (Attribute a : memberAttrList)
201    {
202      membersCount += a.size();
203    }
204    LinkedHashSet<CompactDn> someMemberDNs = new LinkedHashSet<>(membersCount);
205    for (Attribute a : memberAttrList)
206    {
207      for (ByteString v : a)
208      {
209        try
210        {
211          someMemberDNs.add(new CompactDn(DN.valueOf(v.toString())));
212        }
213        catch (LocalizedIllegalArgumentException e)
214        {
215          logger.traceException(e);
216          if (DirectoryServer.getSyntaxEnforcementPolicy() == AcceptRejectWarn.REJECT)
217          {
218            logger.error(ERR_STATICGROUP_CANNOT_DECODE_MEMBER_VALUE_AS_DN,
219              v, someMemberAttributeType.getNameOrOID(), groupEntry.getName(), e.getMessageObject());
220          }
221          // else just ignore this value (issue OPENDJ-2833)
222        }
223      }
224    }
225    return new StaticGroup(serverContext, groupEntry.getName(), someMemberAttributeType, someMemberDNs);
226  }
227
228  @Override
229  public SearchFilter getGroupDefinitionFilter()
230         throws DirectoryException
231  {
232    // FIXME -- This needs to exclude enhanced groups once we have support for them.
233    String filterString =
234         "(&(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames)" +
235            "(objectClass=groupOfEntries))" +
236            "(!(objectClass=ds-virtual-static-group)))";
237    return SearchFilter.createFilterFromString(filterString);
238  }
239
240  @Override
241  public boolean isGroupDefinition(Entry entry)
242  {
243    Reject.ifNull(entry);
244
245    // FIXME -- This needs to exclude enhanced groups once we have support for them.
246    if (hasObjectClass(entry, OC_VIRTUAL_STATIC_GROUP))
247    {
248      return false;
249    }
250
251    boolean hasGroupOfEntriesClass = hasObjectClass(entry, OC_GROUP_OF_ENTRIES_LC);
252    boolean hasGroupOfNamesClass = hasObjectClass(entry, OC_GROUP_OF_NAMES_LC);
253    boolean hasGroupOfUniqueNamesClass = hasObjectClass(entry, OC_GROUP_OF_UNIQUE_NAMES_LC);
254    if (hasGroupOfEntriesClass)
255    {
256      return !hasGroupOfNamesClass
257          && !hasGroupOfUniqueNamesClass;
258    }
259    else if (hasGroupOfNamesClass)
260    {
261      return !hasGroupOfUniqueNamesClass;
262    }
263    else
264    {
265      return hasGroupOfUniqueNamesClass;
266    }
267  }
268
269  private boolean hasObjectClass(Entry entry, String ocName)
270  {
271    return entry.hasObjectClass(DirectoryServer.getSchema().getObjectClass(ocName));
272  }
273
274  @Override
275  public DN getGroupDN()
276  {
277    return groupEntryDN;
278  }
279
280  @Override
281  public void setGroupDN(DN groupDN)
282  {
283    groupEntryDN = groupDN;
284  }
285
286  @Override
287  public boolean supportsNestedGroups()
288  {
289    return true;
290  }
291
292  @Override
293  public List<DN> getNestedGroupDNs()
294  {
295    try
296    {
297       reloadIfNeeded();
298    }
299    catch (DirectoryException ex)
300    {
301      return Collections.<DN>emptyList();
302    }
303    lock.readLock().lock();
304    try
305    {
306      return nestedGroups;
307    }
308    finally
309    {
310      lock.readLock().unlock();
311    }
312  }
313
314  @Override
315  public void addNestedGroup(DN nestedGroupDN)
316         throws UnsupportedOperationException, DirectoryException
317  {
318    Reject.ifNull(nestedGroupDN);
319
320    lock.writeLock().lock();
321    try
322    {
323      if (nestedGroups.contains(nestedGroupDN))
324      {
325        LocalizableMessage msg = ERR_STATICGROUP_ADD_NESTED_GROUP_ALREADY_EXISTS.get(nestedGroupDN, groupEntryDN);
326        throw new DirectoryException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS, msg);
327      }
328
329      ModifyOperation modifyOperation = newModifyOperation(ModificationType.ADD, nestedGroupDN);
330      modifyOperation.run();
331      if (modifyOperation.getResultCode() != ResultCode.SUCCESS)
332      {
333        LocalizableMessage msg = ERR_STATICGROUP_ADD_MEMBER_UPDATE_FAILED.get(
334            nestedGroupDN, groupEntryDN, modifyOperation.getErrorMessage());
335        throw new DirectoryException(modifyOperation.getResultCode(), msg);
336      }
337
338      LinkedList<DN> newNestedGroups = new LinkedList<>(nestedGroups);
339      newNestedGroups.add(nestedGroupDN);
340      nestedGroups = newNestedGroups;
341      //Add it to the member DN list.
342      HashSet<CompactDn> newMemberDNs = new HashSet<>(memberDNs);
343      newMemberDNs.add(new CompactDn(nestedGroupDN));
344      memberDNs = newMemberDNs;
345    }
346    finally
347    {
348      lock.writeLock().unlock();
349    }
350  }
351
352  @Override
353  public void removeNestedGroup(DN nestedGroupDN)
354         throws UnsupportedOperationException, DirectoryException
355  {
356    Reject.ifNull(nestedGroupDN);
357
358    lock.writeLock().lock();
359    try
360    {
361      if (! nestedGroups.contains(nestedGroupDN))
362      {
363        throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE,
364                ERR_STATICGROUP_REMOVE_NESTED_GROUP_NO_SUCH_GROUP.get(nestedGroupDN, groupEntryDN));
365      }
366
367      ModifyOperation modifyOperation = newModifyOperation(ModificationType.DELETE, nestedGroupDN);
368      modifyOperation.run();
369      if (modifyOperation.getResultCode() != ResultCode.SUCCESS)
370      {
371        LocalizableMessage message = ERR_STATICGROUP_REMOVE_MEMBER_UPDATE_FAILED.get(
372            nestedGroupDN, groupEntryDN, modifyOperation.getErrorMessage());
373        throw new DirectoryException(modifyOperation.getResultCode(), message);
374      }
375
376      LinkedList<DN> newNestedGroups = new LinkedList<>(nestedGroups);
377      newNestedGroups.remove(nestedGroupDN);
378      nestedGroups = newNestedGroups;
379      //Remove it from the member DN list.
380      LinkedHashSet<CompactDn> newMemberDNs = new LinkedHashSet<>(memberDNs);
381      newMemberDNs.remove(new CompactDn(nestedGroupDN));
382      memberDNs = newMemberDNs;
383    }
384    finally
385    {
386      lock.writeLock().unlock();
387    }
388  }
389
390  @Override
391  public boolean isMember(DN userDN, AtomicReference<Set<DN>> examinedGroups) throws DirectoryException
392  {
393    reloadIfNeeded();
394    CompactDn compactUserDN = new CompactDn(userDN);
395    lock.readLock().lock();
396    try
397    {
398      if (memberDNs.contains(compactUserDN))
399      {
400        return true;
401      }
402      if (nestedGroups.isEmpty()) {
403        return false;
404      }
405
406      // there are nested groups
407      Set<DN> groups = getExaminedGroups(examinedGroups);
408      if (!groups.add(getGroupDN()))
409      {
410        return false;
411      }
412      for (DN nestedGroupDN : nestedGroups)
413      {
414        Group<? extends GroupImplementationCfg> group = getGroupManager().getGroupInstance(nestedGroupDN);
415        if (group != null && group.isMember(userDN, examinedGroups))
416        {
417          return true;
418        }
419      }
420    }
421    finally
422    {
423      lock.readLock().unlock();
424    }
425    return false;
426  }
427
428  private Set<DN> getExaminedGroups(AtomicReference<Set<DN>> examinedGroups)
429  {
430    Set<DN> groups = examinedGroups.get();
431    if (groups == null)
432    {
433      groups = new HashSet<DN>();
434      examinedGroups.set(groups);
435    }
436    return groups;
437  }
438
439  @Override
440  public boolean isMember(Entry userEntry, AtomicReference<Set<DN>> examinedGroups)
441         throws DirectoryException
442  {
443    return isMember(userEntry.getName(), examinedGroups);
444  }
445
446  /**
447   * Check if the group manager has registered a new group instance or removed a
448   * a group instance that might impact this group's membership list.
449   */
450  private void reloadIfNeeded() throws DirectoryException
451  {
452    //Check if group instances have changed by passing the group manager
453    //the current token.
454    if (DirectoryServer.getGroupManager().hasInstancesChanged(nestedGroupRefreshToken))
455    {
456      lock.writeLock().lock();
457      try
458      {
459        Group<?> thisGroup = DirectoryServer.getGroupManager().getGroupInstance(groupEntryDN);
460        // Check if the group itself has been removed
461        if (thisGroup == null)
462        {
463          throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE,
464              ERR_STATICGROUP_GROUP_INSTANCE_INVALID.get(groupEntryDN));
465        }
466        else if (thisGroup != this)
467        {
468          LinkedHashSet<CompactDn> newMemberDNs = new LinkedHashSet<>();
469          MemberList memberList = thisGroup.getMembers();
470          while (memberList.hasMoreMembers())
471          {
472            try
473            {
474              newMemberDNs.add(new CompactDn(memberList.nextMemberDN()));
475            }
476            catch (MembershipException ex)
477            {
478              // TODO: should we throw an exception there instead of silently fail ?
479            }
480          }
481          memberDNs = newMemberDNs;
482        }
483        nestedGroups.clear();
484        for (CompactDn compactDn : memberDNs)
485        {
486          DN dn = compactDn.toDn(serverContext);
487          Group<?> group = DirectoryServer.getGroupManager().getGroupInstance(dn);
488          if (group != null)
489          {
490            nestedGroups.add(group.getGroupDN());
491          }
492        }
493        nestedGroupRefreshToken = DirectoryServer.getGroupManager().refreshToken();
494      }
495      finally
496      {
497        lock.writeLock().unlock();
498      }
499    }
500  }
501
502  @Override
503  public MemberList getMembers() throws DirectoryException
504  {
505    reloadIfNeeded();
506    lock.readLock().lock();
507    try
508    {
509      return new SimpleStaticGroupMemberList(serverContext, groupEntryDN, memberDNs);
510    }
511    finally
512    {
513      lock.readLock().unlock();
514    }
515  }
516
517  @Override
518  public MemberList getMembers(DN baseDN, SearchScope scope, SearchFilter filter) throws DirectoryException
519  {
520    reloadIfNeeded();
521    lock.readLock().lock();
522    try
523    {
524      if (baseDN == null && filter == null)
525      {
526        return new SimpleStaticGroupMemberList(serverContext, groupEntryDN, memberDNs);
527      }
528      return new FilteredStaticGroupMemberList(serverContext, groupEntryDN, memberDNs, baseDN, scope, filter);
529    }
530    finally
531    {
532      lock.readLock().unlock();
533    }
534  }
535
536  @Override
537  public boolean mayAlterMemberList()
538  {
539    return true;
540  }
541
542  @Override
543  public void updateMembers(List<Modification> modifications)
544         throws UnsupportedOperationException, DirectoryException
545  {
546    Reject.ifNull(memberDNs);
547    Reject.ifNull(nestedGroups);
548
549    reloadIfNeeded();
550    lock.writeLock().lock();
551    try
552    {
553      for (Modification mod : modifications)
554      {
555        Attribute attribute = mod.getAttribute();
556        if (attribute.getAttributeDescription().getAttributeType().equals(memberAttributeType))
557        {
558          switch (mod.getModificationType().asEnum())
559          {
560            case ADD:
561              for (ByteString v : attribute)
562              {
563                DN member = DN.valueOf(v);
564                memberDNs.add(new CompactDn(member));
565                if (DirectoryServer.getGroupManager().getGroupInstance(member) != null)
566                {
567                  nestedGroups.add(member);
568                }
569              }
570              break;
571            case DELETE:
572              if (attribute.isEmpty())
573              {
574                memberDNs.clear();
575                nestedGroups.clear();
576              }
577              else
578              {
579                for (ByteString v : attribute)
580                {
581                  DN member = DN.valueOf(v);
582                  memberDNs.remove(new CompactDn(member));
583                  nestedGroups.remove(member);
584                }
585              }
586              break;
587            case REPLACE:
588              memberDNs.clear();
589              nestedGroups.clear();
590              for (ByteString v : attribute)
591              {
592                DN member = DN.valueOf(v);
593                memberDNs.add(new CompactDn(member));
594                if (DirectoryServer.getGroupManager().getGroupInstance(member) != null)
595                {
596                  nestedGroups.add(member);
597                }
598              }
599              break;
600          }
601        }
602      }
603    }
604    finally {
605      lock.writeLock().unlock();
606    }
607  }
608
609  @Override
610  public void addMember(Entry userEntry) throws UnsupportedOperationException, DirectoryException
611  {
612    Reject.ifNull(userEntry);
613
614    lock.writeLock().lock();
615    try
616    {
617      DN userDN = userEntry.getName();
618      CompactDn compactUserDN = new CompactDn(userDN);
619
620      if (memberDNs.contains(compactUserDN))
621      {
622        LocalizableMessage message = ERR_STATICGROUP_ADD_MEMBER_ALREADY_EXISTS.get(userDN, groupEntryDN);
623        throw new DirectoryException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS, message);
624      }
625
626      ModifyOperation modifyOperation = newModifyOperation(ModificationType.ADD, userDN);
627      modifyOperation.run();
628      if (modifyOperation.getResultCode() != ResultCode.SUCCESS)
629      {
630        throw new DirectoryException(modifyOperation.getResultCode(),
631            ERR_STATICGROUP_ADD_MEMBER_UPDATE_FAILED.get(userDN, groupEntryDN, modifyOperation.getErrorMessage()));
632      }
633
634      LinkedHashSet<CompactDn> newMemberDNs = new LinkedHashSet<CompactDn>(memberDNs);
635      newMemberDNs.add(compactUserDN);
636      memberDNs = newMemberDNs;
637    }
638    finally
639    {
640      lock.writeLock().unlock();
641    }
642  }
643
644  @Override
645  public void removeMember(DN userDN) throws UnsupportedOperationException, DirectoryException
646  {
647    Reject.ifNull(userDN);
648
649    CompactDn compactUserDN = new CompactDn(userDN);
650    lock.writeLock().lock();
651    try
652    {
653      if (! memberDNs.contains(compactUserDN))
654      {
655        LocalizableMessage message = ERR_STATICGROUP_REMOVE_MEMBER_NO_SUCH_MEMBER.get(userDN, groupEntryDN);
656        throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, message);
657      }
658
659      ModifyOperation modifyOperation = newModifyOperation(ModificationType.DELETE, userDN);
660      modifyOperation.run();
661      if (modifyOperation.getResultCode() != ResultCode.SUCCESS)
662      {
663        throw new DirectoryException(modifyOperation.getResultCode(),
664            ERR_STATICGROUP_REMOVE_MEMBER_UPDATE_FAILED.get(userDN, groupEntryDN, modifyOperation.getErrorMessage()));
665      }
666
667      LinkedHashSet<CompactDn> newMemberDNs = new LinkedHashSet<>(memberDNs);
668      newMemberDNs.remove(compactUserDN);
669      memberDNs = newMemberDNs;
670      //If it is in the nested group list remove it.
671      if (nestedGroups.contains(userDN))
672      {
673        LinkedList<DN> newNestedGroups = new LinkedList<>(nestedGroups);
674        newNestedGroups.remove(userDN);
675        nestedGroups = newNestedGroups;
676      }
677    }
678    finally
679    {
680      lock.writeLock().unlock();
681    }
682  }
683
684  private ModifyOperation newModifyOperation(ModificationType modType, DN userDN)
685  {
686    Attribute attr = Attributes.create(memberAttributeType, userDN.toString());
687    LinkedList<Modification> mods = newLinkedList(new Modification(modType, attr));
688    Control control = new LDAPControl(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE, false);
689
690    return new ModifyOperationBasis(getRootConnection(), nextOperationID(), nextMessageID(),
691        newLinkedList(control), groupEntryDN, mods);
692  }
693
694  @Override
695  public void toString(StringBuilder buffer)
696  {
697    buffer.append("StaticGroup(");
698    buffer.append(groupEntryDN);
699    buffer.append(")");
700  }
701
702  /**
703   * A compact representation of a DN, suitable for equality and comparisons, and providing a natural hierarchical
704   * ordering.
705   * <p>
706   * The memory consumption compared to a regular DN object is minimal.
707   */
708  static final class CompactDn implements Comparable<CompactDn>
709  {
710    /** Original string corresponding to the DN. */
711    private final byte[] originalValue;
712
713    /**
714     * Normalized byte string, suitable for equality and comparisons, and providing a natural
715     * hierarchical ordering, but not usable as a valid DN.
716     */
717    private final byte[] normalizedValue;
718
719    @VisibleForTesting
720    CompactDn(DN dn)
721    {
722      this.originalValue = getBytes(dn.toString());
723      this.normalizedValue = dn.toNormalizedByteString().toByteArray();
724    }
725
726    @Override
727    public int compareTo(final CompactDn other)
728    {
729      final int length1 = normalizedValue.length;
730      final int length2 = other.normalizedValue.length;
731      int count = Math.min(length1, length2);
732      int i = 0;
733      int j = 0;
734      while (count-- != 0)
735      {
736        final int firstByte = 0xFF & normalizedValue[i++];
737        final int secondByte = 0xFF & other.normalizedValue[j++];
738        if (firstByte != secondByte)
739        {
740          return firstByte - secondByte;
741        }
742      }
743      return length1 - length2;
744    }
745
746    /**
747     * Returns the DN corresponding to this compact representation.
748     *
749     * @param serverContext
750     *          The server context.
751     *
752     * @return the DN
753     */
754    public DN toDn(ServerContext serverContext)
755    {
756      return DN.valueOf(toString(), serverContext.getSchemaNG());
757    }
758
759    @Override
760    public int hashCode()
761    {
762      return Arrays.hashCode(normalizedValue);
763    }
764
765    @Override
766    public boolean equals(Object obj)
767    {
768      if (this == obj)
769      {
770        return true;
771      }
772      else if (obj instanceof CompactDn)
773      {
774        final CompactDn other = (CompactDn) obj;
775        return Arrays.equals(normalizedValue, other.normalizedValue);
776      }
777      else
778      {
779        return false;
780      }
781    }
782
783    @Override
784    public String toString()
785    {
786      final int length = originalValue.length;
787      if (length == 0) {
788          return "";
789      }
790      try {
791          return new String(originalValue, 0, length, "UTF-8");
792      } catch (final UnsupportedEncodingException e) {
793          // TODO: I18N
794          throw new RuntimeException("Unable to decode bytes as UTF-8 string", e);
795      }
796    }
797  }
798}