/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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
 *
 *
 *      Copyright 2006-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.server.types;

import java.io.BufferedWriter;
import java.io.IOException;
import java.util.*;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteSequenceReader;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.schema.MatchingRule;
import org.forgerock.opendj.ldap.schema.ObjectClassType;
import org.opends.server.api.CompressedSchema;
import org.opends.server.api.ProtocolElement;
import org.opends.server.api.plugin.PluginResult;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PluginConfigManager;
import org.opends.server.core.SubentryManager;
import org.opends.server.types.SubEntry.CollectiveConflictBehavior;
import org.opends.server.util.LDIFException;
import org.opends.server.util.LDIFWriter;

import static org.forgerock.opendj.ldap.ResultCode.*;
import static org.opends.messages.CoreMessages.*;
import static org.opends.messages.UtilityMessages.*;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.util.CollectionUtils.*;
import static org.opends.server.util.LDIFWriter.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;

/**
 * This class defines a data structure for a Directory Server entry.
 * It includes a DN and a set of attributes.
 * <BR><BR>
 * The entry also contains a volatile attachment object, which should
 * be used to associate the entry with a special type of object that
 * is based on its contents.  For example, if the entry holds access
 * control information, then the attachment might be an object that
 * contains a representation of that access control definition in a
 * more useful form.  This is only useful if the entry is to be
 * cached, since the attachment may be accessed if the entry is
 * retrieved from the cache, but if the entry is retrieved from the
 * backend repository it cannot be guaranteed to contain any
 * attachment (and in most cases will not).  This attachment is
 * volatile in that it is not always guaranteed to be present, it may
 * be removed or overwritten at any time, and it will be invalidated
 * and removed if the entry is altered in any way.
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
     mayInstantiate=true,
     mayExtend=false,
     mayInvoke=true)
public class Entry
       implements ProtocolElement
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** The set of operational attributes for this entry. */
  private Map<AttributeType,List<Attribute>> operationalAttributes;

  /** The set of user attributes for this entry. */
  private Map<AttributeType,List<Attribute>> userAttributes;

  /**
   * The set of suppressed real attributes for this entry. It contains real
   * attributes that have been overridden by virtual attributes.
   */
  private final Map<AttributeType, List<Attribute>> suppressedAttributes = new LinkedHashMap<>();

  /** The set of objectclasses for this entry. */
  private Map<ObjectClass,String> objectClasses;

  private Attribute objectClassAttribute;

  /** The DN for this entry. */
  private DN dn;

  /**
   * A generic attachment that may be used to associate this entry with some
   * other object.
   */
  private transient Object attachment;

  /** The schema used to govern this entry. */
  private final Schema schema;



  /**
   * Creates a new entry with the provided information.
   *
   * @param  dn                     The distinguished name for this
   *                                entry.
   * @param  objectClasses          The set of objectclasses for this
   *                                entry as a mapping between the
   *                                objectclass and the name to use to
   *                                reference it.
   * @param  userAttributes         The set of user attributes for
   *                                this entry as a mapping between
   *                                the attribute type and the list of
   *                                attributes with that type.
   * @param  operationalAttributes  The set of operational attributes
   *                                for this entry as a mapping
   *                                between the attribute type and the
   *                                list of attributes with that type.
   */
  public Entry(DN dn, Map<ObjectClass,String> objectClasses,
               Map<AttributeType,List<Attribute>> userAttributes,
               Map<AttributeType,List<Attribute>> operationalAttributes)
  {
    schema = DirectoryServer.getSchema();

    setDN(dn);

    this.objectClasses = newMapIfNull(objectClasses);
    this.userAttributes = newMapIfNull(userAttributes);
    this.operationalAttributes = newMapIfNull(operationalAttributes);
  }

  /**
   * Returns a new Map if the passed in Map is null.
   *
   * @param <K>
   *          the type of the key
   * @param <V>
   *          the type of the value
   * @param map
   *          the map to test
   * @return a new Map if the passed in Map is null.
   */
  private <K, V> Map<K, V> newMapIfNull(Map<K, V> map)
  {
    if (map != null)
    {
      return map;
    }
    return new HashMap<>();
  }



  /**
   * Retrieves the distinguished name for this entry.
   *
   * @return  The distinguished name for this entry.
   */
  public DN getName()
  {
    return dn;
  }



  /**
   * Specifies the distinguished name for this entry.
   *
   * @param  dn  The distinguished name for this entry.
   */
  public void setDN(DN dn)
  {
    if (dn == null)
    {
      this.dn = DN.rootDN();
    }
    else
    {
      this.dn = dn;
    }

    attachment = null;
  }



  /**
   * Retrieves the set of objectclasses defined for this entry.  The
   * caller should be allowed to modify the contents of this list, but
   * if it does then it should also invalidate the attachment.
   *
   * @return  The set of objectclasses defined for this entry.
   */
  public Map<ObjectClass,String> getObjectClasses()
  {
    return objectClasses;
  }



  /**
   * Indicates whether this entry has the specified objectclass.
   *
   * @param  objectClass  The objectclass for which to make the
   *                      determination.
   *
   * @return  <CODE>true</CODE> if this entry has the specified
   *          objectclass, or <CODE>false</CODE> if not.
   */
  public boolean hasObjectClass(ObjectClass objectClass)
  {
    return objectClasses.containsKey(objectClass);
  }



  /**
   * Retrieves the structural objectclass for this entry.
   *
   * @return  The structural objectclass for this entry, or
   *          <CODE>null</CODE> if there is none for some reason.  If
   *          there are multiple structural classes in the entry, then
   *          the first will be returned.
   */
  public ObjectClass getStructuralObjectClass()
  {
    ObjectClass structuralClass = null;

    for (ObjectClass oc : objectClasses.keySet())
    {
      if (oc.getObjectClassType() == ObjectClassType.STRUCTURAL)
      {
        if (structuralClass == null)
        {
          structuralClass = oc;
        }
        else if (oc.isDescendantOf(structuralClass))
        {
          structuralClass = oc;
        }
      }
    }

    return structuralClass;
  }



  /**
   * Adds the provided objectClass to this entry.
   *
   * @param  oc The objectClass to add to this entry.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to add the objectclass to this
   *                              entry.
   */
  public void addObjectClass(ObjectClass oc)
         throws DirectoryException
  {
    attachment = null;

    if (objectClasses.containsKey(oc))
    {
      LocalizableMessage message = ERR_ENTRY_ADD_DUPLICATE_OC.get(oc.getNameOrOID(), dn);
      throw new DirectoryException(OBJECTCLASS_VIOLATION, message);
    }

    objectClasses.put(oc, oc.getNameOrOID());
  }



  /**
   * Retrieves the entire set of attributes for this entry.  This will
   * include both user and operational attributes.  The caller must
   * not modify the contents of this list.  Also note that this method
   * is less efficient than calling either (or both)
   * <CODE>getUserAttributes</CODE> or
   * <CODE>getOperationalAttributes</CODE>, so it should only be used
   * when calls to those methods are not appropriate.
   *
   * @return  The entire set of attributes for this entry.
   */
  public List<Attribute> getAttributes()
  {
    // Estimate the size.
    int size = userAttributes.size() + operationalAttributes.size();

    final List<Attribute> attributes = new ArrayList<>(size);
    for (List<Attribute> attrs : userAttributes.values())
    {
      attributes.addAll(attrs);
    }
    for (List<Attribute> attrs : operationalAttributes.values())
    {
      attributes.addAll(attrs);
    }
    return attributes;
  }

  /**
   * Retrieves the entire set of user (i.e., non-operational)
   * attributes for this entry.  The caller should be allowed to
   * modify the contents of this list, but if it does then it should
   * also invalidate the attachment.
   *
   * @return  The entire set of user attributes for this entry.
   */
  public Map<AttributeType,List<Attribute>> getUserAttributes()
  {
    return userAttributes;
  }



  /**
   * Retrieves the entire set of operational attributes for this
   * entry.  The caller should be allowed to modify the contents of
   * this list, but if it does then it should also invalidate the
   * attachment.
   *
   * @return  The entire set of operational attributes for this entry.
   */
  public Map<AttributeType,List<Attribute>> getOperationalAttributes()
  {
    return operationalAttributes;
  }



  /**
   * Retrieves an attribute holding the objectclass information for
   * this entry.  The returned attribute must not be altered.
   *
   * @return  An attribute holding the objectclass information for
   *          this entry, or <CODE>null</CODE> if it does not have any
   *          objectclass information.
   */
  public Attribute getObjectClassAttribute()
  {
    if (objectClasses == null || objectClasses.isEmpty())
    {
      return null;
    }

    if(objectClassAttribute == null)
    {
      AttributeType ocType = DirectoryServer.getObjectClassAttributeType();
      AttributeBuilder builder = new AttributeBuilder(ocType, ATTR_OBJECTCLASS);
      builder.addAllStrings(objectClasses.values());
      objectClassAttribute = builder.toAttribute();
    }

    return objectClassAttribute;
  }



  /**
   * Indicates whether this entry contains the specified attribute.
   * Any subordinate attribute of the specified attribute will also be
   * used in the determination.
   *
   * @param attributeType
   *          The attribute type for which to make the determination.
   * @return <CODE>true</CODE> if this entry contains the specified
   *         attribute, or <CODE>false</CODE> if not.
   */
  public boolean hasAttribute(AttributeType attributeType)
  {
    return hasAttribute(attributeType, null, true);
  }


  /**
   * Indicates whether this entry contains the specified attribute.
   *
   * @param  attributeType       The attribute type for which to
   *                             make the determination.
   * @param  includeSubordinates Whether to include any subordinate
   *                             attributes of the attribute type
   *                             being retrieved.
   *
   * @return  <CODE>true</CODE> if this entry contains the specified
   *          attribute, or <CODE>false</CODE> if not.
   */
  public boolean hasAttribute(AttributeType attributeType,
                              boolean includeSubordinates)
  {
    return hasAttribute(attributeType, null, includeSubordinates);
  }



  /**
   * Indicates whether this entry contains the specified attribute
   * with all of the options in the provided set. Any subordinate
   * attribute of the specified attribute will also be used in the
   * determination.
   *
   * @param attributeType
   *          The attribute type for which to make the determination.
   * @param options
   *          The set of options to use in the determination.
   * @return <CODE>true</CODE> if this entry contains the specified
   *         attribute, or <CODE>false</CODE> if not.
   */
  public boolean hasAttribute(AttributeType attributeType, Set<String> options)
  {
    return hasAttribute(attributeType, options, true);
  }



  /**
   * Indicates whether this entry contains the specified attribute
   * with all of the options in the provided set.
   *
   * @param attributeType
   *          The attribute type for which to make the determination.
   * @param options
   *          The set of options to use in the determination.
   * @param includeSubordinates
   *          Whether to include any subordinate attributes of the
   *          attribute type being retrieved.
   * @return <CODE>true</CODE> if this entry contains the specified
   *         attribute, or <CODE>false</CODE> if not.
   */
  public boolean hasAttribute(
      AttributeType attributeType,
      Set<String> options,
      boolean includeSubordinates)
  {
    // Handle object class.
    if (attributeType.isObjectClass())
    {
      return !objectClasses.isEmpty() && (options == null || options.isEmpty());
    }

    if (!includeSubordinates)
    {
      // It's possible that there could be an attribute without any
      // values, which we should treat as not having the requested
      // attribute.
      Attribute attribute = getExactAttribute(attributeType, options);
      return attribute != null && !attribute.isEmpty();
    }

    // Check all matching attributes.
    List<Attribute> attributes = getAttributes(attributeType);
    if (attributes != null)
    {
      for (Attribute attribute : attributes)
      {
        // It's possible that there could be an attribute without any
        // values, which we should treat as not having the requested
        // attribute.
        if (!attribute.isEmpty() && attribute.hasAllOptions(options))
        {
          return true;
        }
      }
    }

    // Check sub-types.
    if (attributeType.mayHaveSubordinateTypes())
    {
      for (AttributeType subType : schema.getSubTypes(attributeType))
      {
        attributes = getAttributes(subType);
        if (attributes != null)
        {
          for (Attribute attribute : attributes)
          {
            // It's possible that there could be an attribute without any values,
            // which we should treat as not having the requested attribute.
            if (!attribute.isEmpty() && attribute.hasAllOptions(options))
            {
              return true;
            }
          }
        }
      }
    }

    return false;
  }

  /**
   * Returns the attributes Map corresponding to the operational status of the
   * supplied attribute type.
   *
   * @param attrType
   *          the attribute type
   * @return the user of operational attributes Map
   */
  private Map<AttributeType, List<Attribute>> getUserOrOperationalAttributes(
      AttributeType attrType)
  {
    if (attrType.isOperational())
    {
      return operationalAttributes;
    }
    return userAttributes;
  }

  /**
   * Return the List of attributes for the passed in attribute type.
   *
   * @param attrType
   *          the attribute type
   * @return the List of user or operational attributes
   */
  private List<Attribute> getAttributes(AttributeType attrType)
  {
    return getUserOrOperationalAttributes(attrType).get(attrType);
  }

  /**
   * Puts the supplied List of attributes for the passed in attribute type into
   * the map of attributes.
   *
   * @param attrType
   *          the attribute type
   * @param attributes
   *          the List of user or operational attributes to put
   */
  private void putAttributes(AttributeType attrType, List<Attribute> attributes)
  {
    getUserOrOperationalAttributes(attrType).put(attrType, attributes);
  }

  /**
   * Removes the List of attributes for the passed in attribute type from the
   * map of attributes.
   *
   * @param attrType
   *          the attribute type
   */
  private void removeAttributes(AttributeType attrType)
  {
    getUserOrOperationalAttributes(attrType).remove(attrType);
  }

  /**
   * Retrieves the requested attribute element(s) for the specified
   * attribute type. The list returned may include multiple elements
   * if the same attribute exists in the entry multiple times with
   * different sets of options. It may also include any subordinate
   * attributes of the attribute being retrieved.
   *
   * @param attributeType
   *          The attribute type to retrieve.
   * @return The requested attribute element(s) for the specified
   *         attribute type, or <CODE>null</CODE> if the specified
   *         attribute type is not present in this entry.
   */
  public List<Attribute> getAttribute(AttributeType attributeType)
  {
    return getAttribute(attributeType, true);
  }


  /**
   * Retrieves the requested attribute element(s) for the specified
   * attribute type.  The list returned may include multiple elements
   * if the same attribute exists in the entry multiple times with
   * different sets of options.
   *
   * @param  attributeType       The attribute type to retrieve.
   * @param  includeSubordinates Whether to include any subordinate
   *                             attributes of the attribute type
   *                             being retrieved.
   *
   * @return  The requested attribute element(s) for the specified
   *          attribute type, or <CODE>null</CODE> if the specified
   *          attribute type is not present in this entry.
   */
  public List<Attribute> getAttribute(AttributeType attributeType,
                                      boolean includeSubordinates)
  {
    if (includeSubordinates && attributeType.mayHaveSubordinateTypes())
    {
      List<Attribute> attributes = new LinkedList<>();
      addAllIfNotNull(attributes, userAttributes.get(attributeType));
      addAllIfNotNull(attributes, operationalAttributes.get(attributeType));

      for (AttributeType at : schema.getSubTypes(attributeType))
      {
        addAllIfNotNull(attributes, userAttributes.get(at));
        addAllIfNotNull(attributes, operationalAttributes.get(at));
      }

      if (!attributes.isEmpty())
      {
        return attributes;
      }
      return null;
    }

    List<Attribute> attributes = userAttributes.get(attributeType);
    if (attributes != null)
    {
      return attributes;
    }
    attributes = operationalAttributes.get(attributeType);
    if (attributes != null)
    {
      return attributes;
    }
    if (attributeType.isObjectClass() && !objectClasses.isEmpty())
    {
      return newArrayList(getObjectClassAttribute());
    }
    return null;
  }

  /**
   * Add to the destination all the elements from a non null source .
   *
   * @param dest
   *          the destination where to add
   * @param source
   *          the source with the elements to be added
   */
  private void addAllIfNotNull(List<Attribute> dest, List<Attribute> source)
  {
    if (source != null)
    {
      dest.addAll(source);
    }
  }



  /**
   * Retrieves the requested attribute element(s) for the attribute
   * with the specified name or OID.  The list returned may include
   * multiple elements if the same attribute exists in the entry
   * multiple times with different sets of options. It may also
   * include any subordinate attributes of the attribute being
   * retrieved.
   * <BR><BR>
   * Note that this method should only be used in cases in which the
   * Directory Server schema has no reference of an attribute type
   * with the specified name.  It is not as accurate or efficient as
   * the version of this method that takes an
   * <CODE>AttributeType</CODE> argument.
   *
   * @param  lowerName  The name or OID of the attribute to return,
   *                    formatted in all lowercase characters.
   *
   * @return  The requested attribute element(s) for the specified
   *          attribute type, or <CODE>null</CODE> if the specified
   *          attribute type is not present in this entry.
   */
  public List<Attribute> getAttribute(String lowerName)
  {
    for (AttributeType attr : userAttributes.keySet())
    {
      if (attr.hasNameOrOID(lowerName))
      {
        return getAttribute(attr, true);
      }
    }

    for (AttributeType attr : operationalAttributes.keySet())
    {
      if (attr.hasNameOrOID(lowerName))
      {
        return getAttribute(attr, true);
      }
    }

    if (lowerName.equals(OBJECTCLASS_ATTRIBUTE_TYPE_NAME)
        && !objectClasses.isEmpty())
    {
      return newLinkedList(getObjectClassAttribute());
    }
    return null;
  }

  /**
   * Retrieves the requested attribute element(s) for the specified
   * attribute type.  The list returned may include multiple elements
   * if the same attribute exists in the entry multiple times with
   * different sets of options. It may also include any subordinate
   * attributes of the attribute being retrieved.
   *
   * @param  attributeType       The attribute type to retrieve.
   * @param  options             The set of attribute options to
   *                             include in matching elements.
   *
   * @return  The requested attribute element(s) for the specified
   *          attribute type, or <CODE>null</CODE> if the specified
   *          attribute type is not present in this entry with the
   *          provided set of options.
   */
  public List<Attribute> getAttribute(AttributeType attributeType,
                                      Set<String> options)
  {
    return getAttribute(attributeType, true, options);
  }

  /**
   * Retrieves the requested attribute element(s) for the specified
   * attribute type.  The list returned may include multiple elements
   * if the same attribute exists in the entry multiple times with
   * different sets of options.
   *
   * @param  attributeType       The attribute type to retrieve.
   * @param  includeSubordinates Whether to include any subordinate
   *                             attributes of the attribute type
   *                             being retrieved.
   * @param  options             The set of attribute options to
   *                             include in matching elements.
   *
   * @return  The requested attribute element(s) for the specified
   *          attribute type, or <CODE>null</CODE> if the specified
   *          attribute type is not present in this entry with the
   *          provided set of options.
   */
  public List<Attribute> getAttribute(AttributeType attributeType,
                                      boolean includeSubordinates,
                                      Set<String> options)
  {
    List<Attribute> attributes = new LinkedList<>();
    if (includeSubordinates && attributeType.mayHaveSubordinateTypes())
    {
      addAllIfNotNull(attributes, userAttributes.get(attributeType));
      addAllIfNotNull(attributes, operationalAttributes.get(attributeType));

      for (AttributeType at : schema.getSubTypes(attributeType))
      {
        addAllIfNotNull(attributes, userAttributes.get(at));
        addAllIfNotNull(attributes, operationalAttributes.get(at));
      }
    }
    else
    {
      List<Attribute> attrs = userAttributes.get(attributeType);
      if (attrs == null)
      {
        attrs = operationalAttributes.get(attributeType);
        if (attrs == null)
        {
          if (attributeType.isObjectClass()
              && !objectClasses.isEmpty()
              && (options == null || options.isEmpty()))
          {
            attributes.add(getObjectClassAttribute());
            return attributes;
          }
          return null;
        }
      }
      attributes.addAll(attrs);
    }

    onlyKeepAttributesWithAllOptions(attributes, options);

    if (!attributes.isEmpty())
    {
      return attributes;
    }
    return null;
  }



  /**
   * Retrieves the requested attribute element(s) for the attribute
   * with the specified name or OID and set of options.  The list
   * returned may include multiple elements if the same attribute
   * exists in the entry multiple times with different sets of
   * matching options.
   * <BR><BR>
   * Note that this method should only be used in cases in which the
   * Directory Server schema has no reference of an attribute type
   * with the specified name.  It is not as accurate or efficient as
   * the version of this method that takes an
   * <CODE>AttributeType</CODE> argument.
   *
   * @param  lowerName  The name or OID of the attribute to return,
   *                    formatted in all lowercase characters.
   * @param  options    The set of attribute options to include in
   *                    matching elements.
   *
   * @return  The requested attribute element(s) for the specified
   *          attribute type, or <CODE>null</CODE> if the specified
   *          attribute type is not present in this entry.
   */
  public List<Attribute> getAttribute(String lowerName,
                                      Set<String> options)
  {
    for (AttributeType attr : userAttributes.keySet())
    {
      if (attr.hasNameOrOID(lowerName))
      {
        return getAttribute(attr, options);
      }
    }

    for (AttributeType attr : operationalAttributes.keySet())
    {
      if (attr.hasNameOrOID(lowerName))
      {
        return getAttribute(attr, options);
      }
    }

    if (lowerName.equals(OBJECTCLASS_ATTRIBUTE_TYPE_NAME) &&
        (options == null || options.isEmpty()))
    {
      return newLinkedList(getObjectClassAttribute());
    }
    return null;
  }


  /**
   * Returns a parser for the named attribute contained in this entry.
   * <p>
   * The attribute description will be decoded using the schema associated
   * with this entry (usually the default schema).
   *
   * @param attributeDescription
   *            The name of the attribute to be parsed.
   * @return A parser for the named attribute.
   * @throws LocalizedIllegalArgumentException
   *             If {@code attributeDescription} could not be decoded using
   *             the schema associated with this entry.
   * @throws NullPointerException
   *             If {@code attributeDescription} was {@code null}.
   */
  public AttributeParser parseAttribute(String attributeDescription)
      throws LocalizedIllegalArgumentException, NullPointerException
  {
    final List<Attribute> attribute = getAttribute(attributeDescription);
    boolean notEmpty = attribute != null && !attribute.isEmpty();
    return AttributeParser.parseAttribute(notEmpty ? attribute.get(0) : null);
  }



  /**
   * Indicates whether this entry contains the specified user
   * attribute.
   *
   * @param attributeType
   *          The attribute type for which to make the determination.
   * @return <CODE>true</CODE> if this entry contains the specified
   *         user attribute, or <CODE>false</CODE> if not.
   */
  public boolean hasUserAttribute(AttributeType attributeType)
  {
    return hasAttribute(userAttributes, attributeType);
  }



  /**
   * Retrieves the requested user attribute element(s) for the
   * specified attribute type.  The list returned may include multiple
   * elements if the same attribute exists in the entry multiple times
   * with different sets of options.
   *
   * @param  attributeType  The attribute type to retrieve.
   *
   * @return  The requested attribute element(s) for the specified
   *          attribute type, or <CODE>null</CODE> if there is no such
   *          user attribute.
   */
  public List<Attribute> getUserAttribute(AttributeType attributeType)
  {
    return getAttribute(attributeType, userAttributes);
  }

  /**
   * Returns the List of attributes for a given attribute type.
   *
   * @param attributeType
   *          the attribute type to be looked for
   * @param attrs
   *          the attributes Map where to find the attributes
   * @return the List of attributes
   */
  private List<Attribute> getAttribute(AttributeType attributeType,
      Map<AttributeType, List<Attribute>> attrs)
  {
    if (attributeType.mayHaveSubordinateTypes())
    {
      List<Attribute> attributes = new LinkedList<>();
      addAllIfNotNull(attributes, attrs.get(attributeType));
      for (AttributeType at : schema.getSubTypes(attributeType))
      {
        addAllIfNotNull(attributes, attrs.get(at));
      }

      if (!attributes.isEmpty())
      {
        return attributes;
      }
      return null;
    }
    return attrs.get(attributeType);
  }



  /**
   * Retrieves the requested user attribute element(s) for the
   * specified attribute type.  The list returned may include multiple
   * elements if the same attribute exists in the entry multiple times
   * with different sets of options.
   *
   * @param  attributeType  The attribute type to retrieve.
   * @param  options        The set of attribute options to include in
   *                        matching elements.
   *
   * @return  The requested attribute element(s) for the specified
   *          attribute type, or <CODE>null</CODE> if there is no such
   *          user attribute with the specified set of options.
   */
  public List<Attribute> getUserAttribute(AttributeType attributeType,
                                          Set<String> options)
  {
    return getAttribute(attributeType, options, userAttributes);
  }

  /**
   * Returns the List of attributes for a given attribute type having all the
   * required options.
   *
   * @param attributeType
   *          the attribute type to be looked for
   * @param options
   *          the options that must all be present
   * @param attrs
   *          the attributes Map where to find the attributes
   * @return the filtered List of attributes
   */
  private List<Attribute> getAttribute(AttributeType attributeType,
      Set<String> options, Map<AttributeType, List<Attribute>> attrs)
  {
    List<Attribute> attributes = new LinkedList<>();
    addAllIfNotNull(attributes, attrs.get(attributeType));

    if (attributeType.mayHaveSubordinateTypes())
    {
      for (AttributeType at : schema.getSubTypes(attributeType))
      {
        addAllIfNotNull(attributes, attrs.get(at));
      }
    }

    onlyKeepAttributesWithAllOptions(attributes, options);

    if (!attributes.isEmpty())
    {
      return attributes;
    }
    return null;
  }

  /**
   * Removes all the attributes that do not have all the supplied options.
   *
   * @param attributes
   *          the attributes to filter.
   * @param options
   *          the options to look for
   */
  private void onlyKeepAttributesWithAllOptions(List<Attribute> attributes,
      Set<String> options)
  {
    Iterator<Attribute> iterator = attributes.iterator();
    while (iterator.hasNext())
    {
      Attribute a = iterator.next();
      if (!a.hasAllOptions(options))
      {
        iterator.remove();
      }
    }
  }

  /**
   * Indicates whether this entry contains the specified operational
   * attribute.
   *
   * @param  attributeType  The attribute type for which to make the
   *                        determination.
   *
   * @return  <CODE>true</CODE> if this entry contains the specified
   *          operational attribute, or <CODE>false</CODE> if not.
   */
  public boolean hasOperationalAttribute(AttributeType attributeType)
  {
    return hasAttribute(operationalAttributes, attributeType);
  }

  private boolean hasAttribute(Map<AttributeType, List<Attribute>> attributes, AttributeType attributeType)
  {
    if (attributes.containsKey(attributeType))
    {
      return true;
    }

    if (attributeType.mayHaveSubordinateTypes())
    {
      for (AttributeType at : schema.getSubTypes(attributeType))
      {
        if (attributes.containsKey(at))
        {
          return true;
        }
      }
    }

    return false;
  }



  /**
   * Retrieves the requested operational attribute element(s) for the
   * specified attribute type.  The list returned may include multiple
   * elements if the same attribute exists in the entry multiple times
   * with different sets of options.
   *
   * @param  attributeType  The attribute type to retrieve.
   *
   * @return  The requested attribute element(s) for the specified
   *          attribute type, or <CODE>null</CODE> if there is no such
   *          operational attribute.
   */
  public List<Attribute> getOperationalAttribute(AttributeType attributeType)
  {
    return getAttribute(attributeType, operationalAttributes);
  }




  /**
   * Retrieves the requested operational attribute element(s) for the
   * specified attribute type.  The list returned may include multiple
   * elements if the same attribute exists in the entry multiple times
   * with different sets of options.
   *
   * @param  attributeType  The attribute type to retrieve.
   * @param  options        The set of attribute options to include in
   *                        matching elements.
   *
   * @return  The requested attribute element(s) for the specified
   *          attribute type, or <CODE>null</CODE> if there is no such
   *          operational attribute with the specified set of options.
   */
  public List<Attribute> getOperationalAttribute(
                              AttributeType attributeType,
                              Set<String> options)
  {
    return getAttribute(attributeType, options, operationalAttributes);
  }



  /**
   * Puts the provided attribute in this entry.  If an attribute
   * already exists with the provided type, it will be overwritten.
   * Otherwise, a new attribute will be added.  Note that no
   * validation will be performed.
   *
   * @param  attributeType  The attribute type for the set of
   *                        attributes to add.
   * @param  attributeList  The set of attributes to add for the given
   *                        type.
   */
  public void putAttribute(AttributeType attributeType,
                           List<Attribute> attributeList)
  {
    attachment = null;


    // See if there is already a set of attributes with the specified
    // type.  If so, then overwrite it.
    List<Attribute> attrList = userAttributes.get(attributeType);
    if (attrList != null)
    {
      userAttributes.put(attributeType, attributeList);
      return;
    }

    attrList = operationalAttributes.get(attributeType);
    if (attrList != null)
    {
      operationalAttributes.put(attributeType, attributeList);
      return;
    }

    putAttributes(attributeType, attributeList);
  }



  /**
   * Ensures that this entry contains the provided attribute and its
   * values. If an attribute with the provided type already exists,
   * then its attribute values will be merged.
   * <p>
   * This method handles object class additions but will not perform
   * any object class validation. In particular, it will create
   * default object classes when an object class is unknown.
   * <p>
   * This method implements LDAP modification add semantics, with the
   * exception that it allows empty attributes to be added.
   *
   * @param attribute
   *          The attribute to add or merge with this entry.
   * @param duplicateValues
   *          A list to which any duplicate values will be added.
   */
  public void addAttribute(Attribute attribute, List<ByteString> duplicateValues)
  {
    setAttribute(attribute, duplicateValues, false /* merge */);
  }



  /**
   * Puts the provided attribute into this entry. If an attribute with
   * the provided type and options already exists, then it will be
   * replaced. If the provided attribute is empty then any existing
   * attribute will be completely removed.
   * <p>
   * This method handles object class replacements but will not
   * perform any object class validation. In particular, it will
   * create default object classes when an object class is unknown.
   * <p>
   * This method implements LDAP modification replace semantics.
   *
   * @param attribute
   *          The attribute to replace in this entry.
   */
  public void replaceAttribute(Attribute attribute)
  {
    // There can never be duplicate values for a replace.
    setAttribute(attribute, null, true /* replace */);
  }



  /**
   * Increments an attribute in this entry by the amount specified in
   * the provided attribute.
   *
   * @param attribute
   *          The attribute identifying the attribute to be increment
   *          and the amount it is to be incremented by. The attribute
   *          must contain a single value.
   * @throws DirectoryException
   *           If a problem occurs while attempting to increment the
   *           provided attribute. This may occur if the provided
   *           attribute was not single valued or if it could not be
   *           parsed as an integer of if the existing attribute
   *           values could not be parsed as integers.
   */
  public void incrementAttribute(
      Attribute attribute) throws DirectoryException
  {
    // Get the attribute that is to be incremented.
    AttributeType attributeType = attribute.getAttributeType();
    Attribute a = getExactAttribute(attributeType, attribute.getOptions());
    if (a == null)
    {
      LocalizableMessage message = ERR_ENTRY_INCREMENT_NO_SUCH_ATTRIBUTE.get(
            attribute.getName());
      throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, message);
    }

    // Decode the increment.
    Iterator<ByteString> i = attribute.iterator();
    if (!i.hasNext())
    {
      LocalizableMessage message = ERR_ENTRY_INCREMENT_INVALID_VALUE_COUNT.get(
          attribute.getName());
      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
    }

    String incrementValue = i.next().toString();
    long increment;
    try
    {
      increment = Long.parseLong(incrementValue);
    }
    catch (NumberFormatException e)
    {
      LocalizableMessage message = ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT.get(
          attribute.getName());
      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
    }

    if (i.hasNext())
    {
      LocalizableMessage message = ERR_ENTRY_INCREMENT_INVALID_VALUE_COUNT.get(
          attribute.getName());
      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
    }

    // Increment each attribute value by the specified amount.
    AttributeBuilder builder = new AttributeBuilder(a, true);

    for (ByteString v : a)
    {
      long currentValue;
      try
      {
        currentValue = Long.parseLong(v.toString());
      }
      catch (NumberFormatException e)
      {
        LocalizableMessage message = ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT.get(
            attribute.getName());
        throw new DirectoryException(
            ResultCode.CONSTRAINT_VIOLATION, message);
      }

      long newValue = currentValue + increment;
      builder.add(String.valueOf(newValue));
    }

    replaceAttribute(builder.toAttribute());
  }



  /**
   * Removes all instances of the specified attribute type from this
   * entry, including any instances with options. If the provided
   * attribute type is the objectclass type, then all objectclass
   * values will be removed (but must be replaced for the entry to be
   * valid). If the specified attribute type is not present in this
   * entry, then this method will have no effect.
   *
   * @param attributeType
   *          The attribute type for the attribute to remove from this
   *          entry.
   * @return <CODE>true</CODE> if the attribute was found and
   *         removed, or <CODE>false</CODE> if it was not present in
   *         the entry.
   */
  public boolean removeAttribute(AttributeType attributeType)
  {
    attachment = null;

    if (attributeType.isObjectClass())
    {
      objectClasses.clear();
      return true;
    }
    return userAttributes.remove(attributeType) != null
        || operationalAttributes.remove(attributeType) != null;
  }



  /**
   * Ensures that this entry does not contain the provided attribute
   * values. If the provided attribute is empty, then all values of
   * the associated attribute type will be removed. Otherwise, only
   * the specified values will be removed.
   * <p>
   * This method handles object class deletions.
   * <p>
   * This method implements LDAP modification delete semantics.
   *
   * @param attribute
   *          The attribute containing the information to use to
   *          perform the removal.
   * @param missingValues
   *          A list to which any values contained in the provided
   *          attribute but not present in the entry will be added.
   * @return <CODE>true</CODE> if the attribute type was present and
   *         the specified values that were present were removed, or
   *         <CODE>false</CODE> if the attribute type was not
   *         present in the entry. If the attribute type was present
   *         but only contained some of the values in the provided
   *         attribute, then this method will return <CODE>true</CODE>
   *         but will add those values to the provided list.
   */
  public boolean removeAttribute(Attribute attribute,
      List<ByteString> missingValues)
  {
    attachment = null;

    if (attribute.getAttributeType().isObjectClass())
    {
      if (attribute.isEmpty())
      {
        objectClasses.clear();
        return true;
      }

      boolean allSuccessful = true;

      MatchingRule rule =
          attribute.getAttributeType().getEqualityMatchingRule();
      for (ByteString v : attribute)
      {
        String ocName = toLowerName(rule, v);

        boolean matchFound = false;
        for (ObjectClass oc : objectClasses.keySet())
        {
          if (oc.hasNameOrOID(ocName))
          {
            matchFound = true;
            objectClasses.remove(oc);
            break;
          }
        }

        if (!matchFound)
        {
          allSuccessful = false;
          missingValues.add(v);
        }
      }

      return allSuccessful;
    }

    AttributeType attributeType = attribute.getAttributeType();
    List<Attribute> attributes = getAttributes(attributeType);
    if (attributes == null)
    {
      // There are no attributes with the same attribute type.
      for (ByteString v : attribute)
      {
        missingValues.add(v);
      }
      return false;
    }

    // There are already attributes with the same attribute type.
    Set<String> options = attribute.getOptions();
    for (int i = 0; i < attributes.size(); i++)
    {
      Attribute a = attributes.get(i);
      if (a.optionsEqual(options))
      {
        if (attribute.isEmpty())
        {
          // Remove the entire attribute.
          attributes.remove(i);
        }
        else
        {
          // Remove Specified values.
          AttributeBuilder builder = new AttributeBuilder(a);
          for (ByteString v : attribute)
          {
            if (!builder.remove(v))
            {
              missingValues.add(v);
            }
          }

          // Remove / replace the attribute as necessary.
          if (!builder.isEmpty())
          {
            attributes.set(i, builder.toAttribute());
          }
          else
          {
            attributes.remove(i);
          }
        }

        // If the attribute list is now empty remove it.
        if (attributes.isEmpty())
        {
          removeAttributes(attributeType);
        }

        return true;
      }
    }

    // No matching attribute found.
    return false;
  }

  private String toLowerName(MatchingRule rule, ByteString value)
  {
    try
    {
      return normalize(rule, value).toString();
    }
    catch (Exception e)
    {
      logger.traceException(e);
      return toLowerCase(value.toString());
    }
  }


  /**
   * Indicates whether this entry contains the specified attribute
   * value.
   *
   * @param  attributeType  The attribute type for the attribute.
   * @param  options        The set of options for the attribute.
   * @param  value          The value for the attribute.
   *
   * @return  <CODE>true</CODE> if this entry contains the specified
   *          attribute value, or <CODE>false</CODE> if it does not.
   */
  public boolean hasValue(AttributeType attributeType,
                          Set<String> options, ByteString value)
  {
    List<Attribute> attrList = getAttribute(attributeType, true);
    if (attrList == null || attrList.isEmpty())
    {
      return false;
    }

    for (Attribute a : attrList)
    {
      if (a.optionsEqual(options) && a.contains(value))
      {
        return true;
      }
    }
    return false;
  }



  /**
   * Applies the provided modification to this entry.  No schema
   * checking will be performed.
   *
   * @param  mod  The modification to apply to this entry.
   * @param  relaxConstraints indicates if the modification
   *                          constraints are relaxed to match
   *                          the ones of a set (add existing
   *                          value and delete absent value do not fail)
   *
   * @throws  DirectoryException  If a problem occurs while
   *                              attempting to apply the
   *                              modification. Note
   *                              that even if a problem occurs, then
   *                              the entry may have been altered in some way.
   */
  public void applyModification(Modification mod, boolean relaxConstraints)
         throws DirectoryException
  {
    Attribute     a = mod.getAttribute();
    AttributeType t = a.getAttributeType();

    if (t.isObjectClass())
    {
      applyModificationToObjectclass(mod, relaxConstraints);
    }
    else
    {
      applyModificationToNonObjectclass(mod, relaxConstraints);
    }
  }

  private void applyModificationToObjectclass(Modification mod, boolean relaxConstraints) throws DirectoryException
  {
    Attribute a = mod.getAttribute();

    Map<ObjectClass, String> ocs = new LinkedHashMap<>();
    for (ByteString v : a)
    {
      String ocName = v.toString();
      String lowerName = toLowerCase(ocName);
      ObjectClass oc = DirectoryServer.getObjectClass(lowerName, true);
      ocs.put(oc, ocName);
    }

    switch (mod.getModificationType().asEnum())
    {
    case ADD:
      for (ObjectClass oc : ocs.keySet())
      {
        if (objectClasses.containsKey(oc))
        {
          if (!relaxConstraints)
          {
            LocalizableMessage message = ERR_ENTRY_DUPLICATE_VALUES.get(a.getName());
            throw new DirectoryException(ATTRIBUTE_OR_VALUE_EXISTS, message);
          }
        }
        else
        {
          objectClasses.put(oc, ocs.get(oc));
        }
      }
      objectClassAttribute = null;
      break;

    case DELETE:
      for (ObjectClass oc : ocs.keySet())
      {
        if (objectClasses.remove(oc) == null && !relaxConstraints)
        {
          LocalizableMessage message = ERR_ENTRY_NO_SUCH_VALUE.get(a.getName());
          throw new DirectoryException(NO_SUCH_ATTRIBUTE, message);
        }
      }
      objectClassAttribute = null;
      break;

    case REPLACE:
      objectClasses = ocs;
      objectClassAttribute = null;
      break;

    case INCREMENT:
      LocalizableMessage message = ERR_ENTRY_OC_INCREMENT_NOT_SUPPORTED.get();
      throw new DirectoryException(CONSTRAINT_VIOLATION, message);

    default:
      message = ERR_ENTRY_UNKNOWN_MODIFICATION_TYPE.get(mod.getModificationType());
      throw new DirectoryException(UNWILLING_TO_PERFORM, message);
    }
  }

  private void applyModificationToNonObjectclass(Modification mod, boolean relaxConstraints) throws DirectoryException
  {
    Attribute a = mod.getAttribute();
    switch (mod.getModificationType().asEnum())
    {
    case ADD:
      List<ByteString> duplicateValues = new LinkedList<>();
      addAttribute(a, duplicateValues);
      if (!duplicateValues.isEmpty() && !relaxConstraints)
      {
        LocalizableMessage message = ERR_ENTRY_DUPLICATE_VALUES.get(a.getName());
        throw new DirectoryException(ATTRIBUTE_OR_VALUE_EXISTS, message);
      }
      break;

    case DELETE:
      List<ByteString> missingValues = new LinkedList<>();
      removeAttribute(a, missingValues);
      if (!missingValues.isEmpty() && !relaxConstraints)
      {
        LocalizableMessage message = ERR_ENTRY_NO_SUCH_VALUE.get(a.getName());
        throw new DirectoryException(NO_SUCH_ATTRIBUTE, message);
      }
      break;

    case REPLACE:
      replaceAttribute(a);
      break;

    case INCREMENT:
      incrementAttribute(a);
      break;

    default:
      LocalizableMessage message = ERR_ENTRY_UNKNOWN_MODIFICATION_TYPE.get(mod.getModificationType());
      throw new DirectoryException(UNWILLING_TO_PERFORM, message);
    }
  }

  /**
   * Applies the provided modification to this entry.  No schema
   * checking will be performed.
   *
   * @param  mod  The modification to apply to this entry.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to apply the modification.  Note
   *                              that even if a problem occurs, then
   *                              the entry may have been altered in some way.
   */
  public void applyModification(Modification mod) throws DirectoryException
  {
    applyModification(mod, false);
  }

  /**
   * Applies all of the provided modifications to this entry.
   *
   * @param  mods  The modifications to apply to this entry.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to apply the modifications.  Note
   *                              that even if a problem occurs, then
   *                              the entry may have been altered in some way.
   */
  public void applyModifications(List<Modification> mods)
         throws DirectoryException
  {
    for (Modification m : mods)
    {
      applyModification(m);
    }
  }



  /**
   * Indicates whether this entry conforms to the server's schema
   * requirements.  The checks performed by this method include:
   *
   * <UL>
   *   <LI>Make sure that all required attributes are present, either
   *       in the list of user or operational attributes.</LI>
   *   <LI>Make sure that all user attributes are allowed by at least
   *       one of the objectclasses.  The operational attributes will
   *       not be checked in this manner.</LI>
   *   <LI>Make sure that all single-valued attributes contained in
   *       the entry have only a single value.</LI>
   *   <LI>Make sure that the entry contains a single structural
   *       objectclass.</LI>
   *   <LI>Make sure that the entry complies with any defined name
   *       forms, DIT content rules, and DIT structure rules.</LI>
   * </UL>
   *
   * @param  parentEntry             The entry that is the immediate
   *                                 parent of this entry, which may
   *                                 be checked for DIT structure rule
   *                                 conformance.  This may be
   *                                 {@code null} if there is no
   *                                 parent or if it is unavailable
   *                                to the caller.
   * @param  parentProvided          Indicates whether the caller
   *                                 attempted to provide the parent.
   *                                 If not, then the parent entry
   *                                 will be loaded on demand if it is
   *                                 required.
   * @param  validateNameForms       Indicates whether to validate the
   *                                 entry against name form
   *                                 definitions.  This should only be
   *                                 {@code true} for add and modify
   *                                 DN operations, as well as for
   *                                 for imports.
   * @param  validateStructureRules  Indicates whether to validate the
   *                                 entry against DIT structure rule
   *                                 definitions.  This should only
   *                                 be {@code true} for add and
   *                                 modify DN operations.
   * @param  invalidReason           The buffer to which an
   *                                 explanation will be appended if
   *                                 this entry does not conform to
   *                                 the server's schema
   *                                 configuration.
   *
   * @return  {@code true} if this entry conforms to the server's
   *          schema requirements, or {@code false} if it does not.
   */
  public boolean conformsToSchema(Entry parentEntry,
                                  boolean parentProvided,
                                  boolean validateNameForms,
                                  boolean validateStructureRules,
                                  LocalizableMessageBuilder invalidReason)
  {
    // Get the structural objectclass for the entry.  If there isn't
    // one, or if there's more than one, then see if that's OK.
    AcceptRejectWarn structuralPolicy =
         DirectoryServer.getSingleStructuralObjectClassPolicy();
    ObjectClass structuralClass = null;
    boolean multipleOCErrorLogged = false;
    for (ObjectClass oc : objectClasses.keySet())
    {
      if (oc.getObjectClassType() == ObjectClassType.STRUCTURAL)
      {
        if (structuralClass == null || oc.isDescendantOf(structuralClass))
        {
          structuralClass = oc;
        }
        else if (! structuralClass.isDescendantOf(oc))
        {
          LocalizableMessage message =
                  ERR_ENTRY_SCHEMA_MULTIPLE_STRUCTURAL_CLASSES.get(
                    dn,
                    structuralClass.getNameOrOID(),
                    oc.getNameOrOID());

          if (structuralPolicy == AcceptRejectWarn.REJECT)
          {
            invalidReason.append(message);
            return false;
          }
          else if (structuralPolicy == AcceptRejectWarn.WARN
              && !multipleOCErrorLogged)
          {
            logger.error(message);
            multipleOCErrorLogged = true;
          }
        }
      }
    }

    NameForm         nameForm         = null;
    DITContentRule   ditContentRule   = null;
    DITStructureRule ditStructureRule = null;
    if (structuralClass == null)
    {
      LocalizableMessage message = ERR_ENTRY_SCHEMA_NO_STRUCTURAL_CLASS.get(dn);
      if (structuralPolicy == AcceptRejectWarn.REJECT)
      {
        invalidReason.append(message);
        return false;
      }
      else if (structuralPolicy == AcceptRejectWarn.WARN)
      {
        logger.error(message);
      }

      if (! checkAttributesAndObjectClasses(null,
              structuralPolicy, invalidReason))
      {
          return false;
      }

    }
    else
    {
      ditContentRule = DirectoryServer.getDITContentRule(structuralClass);
      if (ditContentRule != null && ditContentRule.isObsolete())
      {
        ditContentRule = null;
      }

      if (! checkAttributesAndObjectClasses(ditContentRule,
                 structuralPolicy, invalidReason))
      {
        return false;
      }

      if (validateNameForms)
      {
        /**
         * There may be multiple nameforms registered with this
         * structural objectclass.However, we need to select only one
         * of the nameforms and its corresponding DITstructure rule.
         * We will iterate over all the nameforms and see if atleast
         * one is acceptable before rejecting the entry.
         * DITStructureRules corresponding to other non-acceptable
         * nameforms are not applied.
         */
        List<NameForm> listForms = DirectoryServer.getNameForm(structuralClass);
        if(listForms != null)
        {
          boolean matchFound = false;
          boolean obsolete = true;
          for(int index=0; index <listForms.size(); index++)
          {
            NameForm nf = listForms.get(index);
            if(!nf.isObsolete())
            {
              obsolete = false;
              matchFound = checkNameForm(nf, structuralPolicy, invalidReason);

              if(matchFound)
              {
                nameForm = nf;
                break;
              }

              if(index != listForms.size()-1)
              {
                invalidReason.append(",");
              }
            }
          }
          if(! obsolete && !matchFound)
          {
            // We couldn't match this entry against any of the nameforms.
            return false;
          }
        }


        if (validateStructureRules && nameForm != null)
        {
          ditStructureRule = DirectoryServer.getDITStructureRule(nameForm);
          if (ditStructureRule != null && ditStructureRule.isObsolete())
          {
            ditStructureRule = null;
          }
        }
      }
    }


    // If there is a DIT content rule for this entry, then make sure
    // that the entry is in compliance with it.
    if (ditContentRule != null
       && !checkDITContentRule(ditContentRule, structuralPolicy, invalidReason))
    {
      return false;
    }

    return checkDITStructureRule(ditStructureRule, structuralClass,
        parentEntry, parentProvided, validateStructureRules, structuralPolicy,
        invalidReason);
  }



  /**
   * Checks the attributes and object classes contained in this entry
   * to determine whether they conform to the server schema
   * requirements.
   *
   * @param  ditContentRule    The DIT content rule for this entry, if
   *                           any.
   * @param  structuralPolicy  The policy that should be used for
   *                           structural object class compliance.
   * @param  invalidReason     A buffer into which an invalid reason
   *                           may be added.
   *
   * @return {@code true} if this entry passes all of the checks, or
   *         {@code false} if there are any failures.
   */
  private boolean checkAttributesAndObjectClasses(
                       DITContentRule ditContentRule,
                       AcceptRejectWarn structuralPolicy,
                       LocalizableMessageBuilder invalidReason)
  {
    // Make sure that we recognize all of the objectclasses, that all
    // auxiliary classes are allowed by the DIT content rule, and that
    // all attributes required by the object classes are present.
    for (ObjectClass o : objectClasses.keySet())
    {
      if (DirectoryServer.getObjectClass(o.getOID()) == null)
      {
        LocalizableMessage message = ERR_ENTRY_SCHEMA_UNKNOWN_OC.get(dn, o.getNameOrOID());
        invalidReason.append(message);
        return false;
      }

      if (o.getObjectClassType() == ObjectClassType.AUXILIARY
          && ditContentRule != null && !ditContentRule.getAuxiliaryClasses().contains(o))
      {
        LocalizableMessage message =
                ERR_ENTRY_SCHEMA_DISALLOWED_AUXILIARY_CLASS.get(
                  dn,
                  o.getNameOrOID(),
                  ditContentRule.getNameOrOID());
        if (structuralPolicy == AcceptRejectWarn.REJECT)
        {
          invalidReason.append(message);
          return false;
        }
        else if (structuralPolicy == AcceptRejectWarn.WARN)
        {
          logger.error(message);
        }
      }

      for (AttributeType t : o.getRequiredAttributes())
      {
        if (!userAttributes.containsKey(t)
            && !operationalAttributes.containsKey(t)
            && !t.isObjectClass())
        {
          LocalizableMessage message =
                  ERR_ENTRY_SCHEMA_MISSING_REQUIRED_ATTR_FOR_OC.get(
                    dn,
                    t.getNameOrOID(),
                    o.getNameOrOID());
          invalidReason.append(message);
          return false;
        }
      }
    }


    // Make sure all the user attributes are allowed, have at least
    // one value, and if they are single-valued that they have exactly
    // one value.
    for (AttributeType t : userAttributes.keySet())
    {
      boolean found = false;
      for (ObjectClass o : objectClasses.keySet())
      {
        if (o.isRequiredOrOptional(t))
        {
          found = true;
          break;
        }
      }

      if (!found && ditContentRule != null
          && ditContentRule.isRequiredOrOptional(t))
      {
        found = true;
      }

      if (! found)
      {
        LocalizableMessage message =
                ERR_ENTRY_SCHEMA_DISALLOWED_USER_ATTR_FOR_OC.get( dn, t.getNameOrOID());
        invalidReason.append(message);
        return false;
      }

      List<Attribute> attrList = userAttributes.get(t);
      if (attrList != null)
      {
        for (Attribute a : attrList)
        {
          if (a.isEmpty())
          {
            invalidReason.append(ERR_ENTRY_SCHEMA_ATTR_NO_VALUES.get(dn, t.getNameOrOID()));
            return false;
          }
          else if (t.isSingleValue() && a.size() != 1)
          {
            invalidReason.append(ERR_ENTRY_SCHEMA_ATTR_SINGLE_VALUED.get(dn, t.getNameOrOID()));
            return false;
          }
        }
      }
    }


    // Iterate through all of the operational attributes and make sure
    // that all of the single-valued attributes only have one value.
    for (AttributeType t : operationalAttributes.keySet())
    {
      if (t.isSingleValue())
      {
        List<Attribute> attrList = operationalAttributes.get(t);
        if (attrList != null)
        {
          for (Attribute a : attrList)
          {
            if (a.size() > 1)
            {
              invalidReason.append(ERR_ENTRY_SCHEMA_ATTR_SINGLE_VALUED.get(dn, t.getNameOrOID()));
              return false;
            }
          }
        }
      }
    }


    // If we've gotten here, then things are OK.
    return true;
  }



  /**
   * Performs any processing needed for name form validation.
   *
   * @param  nameForm          The name form to validate against this
   *                           entry.
   * @param  structuralPolicy  The policy that should be used for
   *                           structural object class compliance.
   * @param  invalidReason     A buffer into which an invalid reason
   *                           may be added.
   *
   * @return {@code true} if this entry passes all of the checks, or
   *         {@code false} if there are any failures.
   */
  private boolean checkNameForm(NameForm nameForm,
                       AcceptRejectWarn structuralPolicy,
                       LocalizableMessageBuilder invalidReason)
  {
    RDN rdn = dn.rdn();
    if (rdn != null)
    {
        // Make sure that all the required attributes are present.
        for (AttributeType t : nameForm.getRequiredAttributes())
        {
          if (! rdn.hasAttributeType(t))
          {
            LocalizableMessage message =
                    ERR_ENTRY_SCHEMA_RDN_MISSING_REQUIRED_ATTR.get(
                      dn,
                      t.getNameOrOID(),
                      nameForm.getNameOrOID());

            if (structuralPolicy == AcceptRejectWarn.REJECT)
            {
              invalidReason.append(message);
              return false;
            }
            else if (structuralPolicy == AcceptRejectWarn.WARN)
            {
              logger.error(message);
            }
          }
        }

          // Make sure that all attributes in the RDN are allowed.
          int numAVAs = rdn.getNumValues();
          for (int i = 0; i < numAVAs; i++)
          {
            AttributeType t = rdn.getAttributeType(i);
            if (! nameForm.isRequiredOrOptional(t))
            {
              LocalizableMessage message =
                      ERR_ENTRY_SCHEMA_RDN_DISALLOWED_ATTR.get(
                        dn,
                        t.getNameOrOID(),
                        nameForm.getNameOrOID());

              if (structuralPolicy == AcceptRejectWarn.REJECT)
              {
                invalidReason.append(message);
                return false;
              }
              else if (structuralPolicy == AcceptRejectWarn.WARN)
              {
                logger.error(message);
              }
            }
          }
    }

    // If we've gotten here, then things are OK.
    return true;
  }



  /**
   * Performs any processing needed for DIT content rule validation.
   *
   * @param  ditContentRule    The DIT content rule to validate
   *                           against this entry.
   * @param  structuralPolicy  The policy that should be used for
   *                           structural object class compliance.
   * @param  invalidReason     A buffer into which an invalid reason
   *                           may be added.
   *
   * @return {@code true} if this entry passes all of the checks, or
   *         {@code false} if there are any failures.
   */
  private boolean checkDITContentRule(DITContentRule ditContentRule,
                       AcceptRejectWarn structuralPolicy,
                       LocalizableMessageBuilder invalidReason)
  {
    // Make sure that all of the required attributes are present.
    for (AttributeType t : ditContentRule.getRequiredAttributes())
    {
      if (!userAttributes.containsKey(t)
          && !operationalAttributes.containsKey(t)
          && !t.isObjectClass())
      {
        LocalizableMessage message =
                ERR_ENTRY_SCHEMA_MISSING_REQUIRED_ATTR_FOR_DCR.get(
                  dn,
                  t.getNameOrOID(),
                  ditContentRule.getNameOrOID());

        if (structuralPolicy == AcceptRejectWarn.REJECT)
        {
          invalidReason.append(message);
          return false;
        }
        else if (structuralPolicy == AcceptRejectWarn.WARN)
        {
          logger.error(message);
        }
      }
    }

    // Make sure that none of the prohibited attributes are present.
    for (AttributeType t : ditContentRule.getProhibitedAttributes())
    {
      if (userAttributes.containsKey(t) ||
          operationalAttributes.containsKey(t))
      {
        LocalizableMessage message =
                ERR_ENTRY_SCHEMA_PROHIBITED_ATTR_FOR_DCR.get(
                  dn,
                  t.getNameOrOID(),
                  ditContentRule.getNameOrOID());

        if (structuralPolicy == AcceptRejectWarn.REJECT)
        {
          invalidReason.append(message);
          return false;
        }
        else if (structuralPolicy == AcceptRejectWarn.WARN)
        {
          logger.error(message);
        }
      }
    }

    // If we've gotten here, then things are OK.
    return true;
  }



  /**
   * Performs any processing needed for DIT structure rule validation.
   *
   * @param  ditStructureRule        The DIT structure rule for this
   *                                 entry.
   * @param  structuralClass         The structural object class for
   *                                 this entry.
   * @param  parentEntry             The parent entry, if available
   *                                 and applicable.
   * @param  parentProvided          Indicates whether the parent
   *                                 entry was provided.
   * @param  validateStructureRules  Indicates whether to check to see
   *                                 if this entry violates a DIT
   *                                 structure rule for its parent.
   * @param  structuralPolicy        The policy that should be used
   *                                 for structural object class
   *                                 compliance.
   * @param  invalidReason           A buffer into which an invalid
   *                                 reason may be added.
   *
   * @return {@code true} if this entry passes all of the checks, or
   *         {@code false} if there are any failures.
   */
  private boolean checkDITStructureRule(
                       DITStructureRule ditStructureRule,
                       ObjectClass structuralClass,
                       Entry parentEntry, boolean parentProvided,
                       boolean validateStructureRules,
                       AcceptRejectWarn structuralPolicy,
                       LocalizableMessageBuilder invalidReason)
  {
    // If there is a DIT structure rule for this entry, then make sure
    // that the entry is in compliance with it.
    if (ditStructureRule != null && ditStructureRule.hasSuperiorRules())
    {
      if (parentProvided)
      {
        if (parentEntry != null)
        {
          boolean dsrValid =
               validateDITStructureRule(ditStructureRule,
                                        structuralClass, parentEntry,
                                        structuralPolicy,
                                        invalidReason);
          if (! dsrValid)
          {
            return false;
          }
        }
      }
      else
      {
        // Get the DN of the parent entry if possible.
        DN parentDN = dn.getParentDNInSuffix();
        if (parentDN != null)
        {
          try
          {
            parentEntry = DirectoryServer.getEntry(parentDN);
            if (parentEntry == null)
            {
              LocalizableMessage message = ERR_ENTRY_SCHEMA_DSR_NO_PARENT_ENTRY.get(dn, parentDN);

              if (structuralPolicy == AcceptRejectWarn.REJECT)
              {
                invalidReason.append(message);
                return false;
              }
              else if (structuralPolicy == AcceptRejectWarn.WARN)
              {
                logger.error(message);
              }
            }
            else
            {
              boolean dsrValid =
                   validateDITStructureRule(ditStructureRule,
                                            structuralClass,
                                            parentEntry,
                                            structuralPolicy,
                                            invalidReason);
              if (! dsrValid)
              {
                return false;
              }
            }
          }
          catch (Exception e)
          {
            logger.traceException(e);

            LocalizableMessage message =
                 ERR_ENTRY_SCHEMA_COULD_NOT_CHECK_DSR.get(
                         dn,
                         ditStructureRule.getNameOrRuleID(),
                         getExceptionMessage(e));

            if (structuralPolicy == AcceptRejectWarn.REJECT)
            {
              invalidReason.append(message);
              return false;
            }
            else if (structuralPolicy == AcceptRejectWarn.WARN)
            {
              logger.error(message);
            }
          }
        }
      }
    }
    else if (validateStructureRules)
    {
      // There is no DIT structure rule for this entry, but there may
      // be one for the parent entry.  If there is such a rule for the
      // parent entry, then this entry will not be valid.
      boolean parentExists = false;
      ObjectClass parentStructuralClass = null;
      if (parentEntry != null)
      {
        parentExists = true;
        parentStructuralClass = parentEntry.getStructuralObjectClass();
      }
      else if (! parentProvided)
      {
        DN parentDN = getName().getParentDNInSuffix();
        if (parentDN != null)
        {
          try
          {
            parentEntry = DirectoryServer.getEntry(parentDN);
            if (parentEntry == null)
            {
              LocalizableMessage message =
                   ERR_ENTRY_SCHEMA_DSR_NO_PARENT_ENTRY.get(
                       dn, parentDN);

              if (structuralPolicy == AcceptRejectWarn.REJECT)
              {
                invalidReason.append(message);
                return false;
              }
              else if (structuralPolicy == AcceptRejectWarn.WARN)
              {
                logger.error(message);
              }
            }
            else
            {
              parentExists = true;
              parentStructuralClass = parentEntry.getStructuralObjectClass();
            }
          }
          catch (Exception e)
          {
            logger.traceException(e);

            LocalizableMessage message =
                 ERR_ENTRY_SCHEMA_COULD_NOT_CHECK_PARENT_DSR.get(
                     dn, getExceptionMessage(e));

            if (structuralPolicy == AcceptRejectWarn.REJECT)
            {
              invalidReason.append(message);
              return false;
            }
            else if (structuralPolicy == AcceptRejectWarn.WARN)
            {
              logger.error(message);
            }
          }
        }
      }

      if (parentExists)
      {
        if (parentStructuralClass == null)
        {
          LocalizableMessage message = ERR_ENTRY_SCHEMA_DSR_NO_PARENT_OC.get(
              dn, parentEntry.getName());

          if (structuralPolicy == AcceptRejectWarn.REJECT)
          {
            invalidReason.append(message);
            return false;
          }
          else if (structuralPolicy == AcceptRejectWarn.WARN)
          {
            logger.error(message);
          }
        }
        else
        {
          List<NameForm> allNFs =
               DirectoryServer.getNameForm(parentStructuralClass);
          if(allNFs != null)
          {
            for(NameForm parentNF : allNFs)
            {
              if (parentNF != null && !parentNF.isObsolete())
              {
                DITStructureRule parentDSR =
                     DirectoryServer.getDITStructureRule(parentNF);
                if (parentDSR != null && !parentDSR.isObsolete())
                {
                  LocalizableMessage message =
                       ERR_ENTRY_SCHEMA_VIOLATES_PARENT_DSR.get(dn, parentEntry.getName());

                  if (structuralPolicy == AcceptRejectWarn.REJECT)
                  {
                    invalidReason.append(message);
                    return false;
                  }
                  else if (structuralPolicy == AcceptRejectWarn.WARN)
                  {
                    logger.error(message);
                  }
                }
              }
            }
          }
        }
      }
    }

    // If we've gotten here, then things are OK.
    return true;
  }



  /**
   * Determines whether this entry is in conformance to the provided
   * DIT structure rule.
   *
   * @param  dsr               The DIT structure rule to use in the
   *                           determination.
   * @param  structuralClass   The structural objectclass for this
   *                           entry to use in the determination.
   * @param  parentEntry       The reference to the parent entry to
   *                           check.
   * @param  structuralPolicy  The policy that should be used around
   *                           enforcement of DIT structure rules.
   * @param  invalidReason     The buffer to which the invalid reason
   *                           should be appended if a problem is
   *                           found.
   *
   * @return  <CODE>true</CODE> if this entry conforms to the provided
   *          DIT structure rule, or <CODE>false</CODE> if not.
   */
  private boolean validateDITStructureRule(DITStructureRule dsr,
                       ObjectClass structuralClass, Entry parentEntry,
                       AcceptRejectWarn structuralPolicy,
                       LocalizableMessageBuilder invalidReason)
  {
    ObjectClass oc = parentEntry.getStructuralObjectClass();
    if (oc == null)
    {
      LocalizableMessage message = ERR_ENTRY_SCHEMA_DSR_NO_PARENT_OC.get(
          dn, parentEntry.getName());

      if (structuralPolicy == AcceptRejectWarn.REJECT)
      {
        invalidReason.append(message);
        return false;
      }
      else if (structuralPolicy == AcceptRejectWarn.WARN)
      {
        logger.error(message);
      }
    }

    boolean matchFound = false;
    for (DITStructureRule dsr2 : dsr.getSuperiorRules())
    {
      if (dsr2.getStructuralClass().equals(oc))
      {
        matchFound = true;
      }
    }

    if (! matchFound)
    {
      LocalizableMessage message =
              ERR_ENTRY_SCHEMA_DSR_DISALLOWED_SUPERIOR_OC.get(
                dn,
                dsr.getNameOrRuleID(),
                structuralClass.getNameOrOID(),
                oc.getNameOrOID());

      if (structuralPolicy == AcceptRejectWarn.REJECT)
      {
        invalidReason.append(message);
        return false;
      }
      else if (structuralPolicy == AcceptRejectWarn.WARN)
      {
        logger.error(message);
      }
    }

    return true;
  }



  /**
   * Retrieves the attachment for this entry.
   *
   * @return  The attachment for this entry, or <CODE>null</CODE> if
   *          there is none.
   */
  public Object getAttachment()
  {
    return attachment;
  }



  /**
   * Specifies the attachment for this entry.  This will replace any
   * existing attachment that might be defined.
   *
   * @param  attachment  The attachment for this entry, or
   *                     <CODE>null</CODE> if there should not be an
   *                     attachment.
   */
  public void setAttachment(Object attachment)
  {
    this.attachment = attachment;
  }



  /**
   * Creates a duplicate of this entry that may be altered without
   * impacting the information in this entry.
   *
   * @param  processVirtual  Indicates whether virtual attribute
   *                         processing should be performed for the
   *                         entry.
   *
   * @return  A duplicate of this entry that may be altered without
   *          impacting the information in this entry.
   */
  public Entry duplicate(boolean processVirtual)
  {
    Map<ObjectClass, String> objectClassesCopy = new HashMap<>(objectClasses);

    Map<AttributeType, List<Attribute>> userAttrsCopy = new HashMap<>(userAttributes.size());
    deepCopy(userAttributes, userAttrsCopy, false, false, false,
        true, false);

    Map<AttributeType, List<Attribute>> operationalAttrsCopy =
         new HashMap<>(operationalAttributes.size());
    deepCopy(operationalAttributes, operationalAttrsCopy, false,
        false, false, true, false);

    // Put back all the suppressed attributes where they belonged to.
    // Then hopefully processVirtualAttributes() will rebuild the suppressed
    // attribute list correctly.
    for (AttributeType t : suppressedAttributes.keySet())
    {
      List<Attribute> attrList = suppressedAttributes.get(t);
      if (t.isOperational())
      {
        operationalAttrsCopy.put(t, attrList);
      }
      else
      {
        userAttrsCopy.put(t, attrList);
      }
    }

    Entry e = new Entry(dn, objectClassesCopy, userAttrsCopy,
                        operationalAttrsCopy);
    if (processVirtual)
    {
      e.processVirtualAttributes();
    }
    return e;
  }



  /**
   * Performs a deep copy from the source map to the target map.
   * In this case, the attributes in the list will be duplicates
   * rather than re-using the same reference.
   *
   * @param source
   *          The source map from which to obtain the information.
   * @param target
   *          The target map into which to place the copied
   *          information.
   * @param omitValues
   *          Indicates whether to omit attribute values when
   *          processing.
   * @param omitEmpty
   *          Indicates whether to omit empty attributes when
   *          processing.
   * @param omitReal
   *          Indicates whether to exclude real attributes.
   * @param omitVirtual
   *          Indicates whether to exclude virtual attributes.
   * @param mergeDuplicates
   *          Indicates whether duplicate attributes should be merged.
   */
  private void deepCopy(Map<AttributeType,List<Attribute>> source,
                        Map<AttributeType,List<Attribute>> target,
                        boolean omitValues,
                        boolean omitEmpty,
                        boolean omitReal,
                        boolean omitVirtual,
                        boolean mergeDuplicates)
  {
    for (Map.Entry<AttributeType, List<Attribute>> mapEntry :
      source.entrySet())
    {
      AttributeType t = mapEntry.getKey();
      List<Attribute> sourceList = mapEntry.getValue();
      List<Attribute> targetList = new ArrayList<>(sourceList.size());

      for (Attribute a : sourceList)
      {
        if ((omitReal && a.isReal())
            || (omitVirtual && a.isVirtual())
            || (omitEmpty && a.isEmpty()))
        {
          continue;
        }

        if (omitValues)
        {
          a = Attributes.empty(a);
        }

        if (!targetList.isEmpty() && mergeDuplicates)
        {
          // Ensure that there is only one attribute with the same type and options.
          // This is not very efficient but will occur very rarely.
          boolean found = false;
          for (int i = 0; i < targetList.size(); i++)
          {
            Attribute otherAttribute = targetList.get(i);
            if (otherAttribute.optionsEqual(a.getOptions()))
            {
              targetList.set(i, Attributes.merge(a, otherAttribute));
              found = true;
            }
          }

          if (!found)
          {
            targetList.add(a);
          }
        }
        else
        {
          targetList.add(a);
        }
      }

      if (!targetList.isEmpty())
      {
        target.put(t, targetList);
      }
    }
  }



  /**
   * Indicates whether this entry meets the criteria to consider it a referral
   * (e.g., it contains the "referral" objectclass and a "ref" attribute).
   *
   * @return  <CODE>true</CODE> if this entry meets the criteria to
   *          consider it a referral, or <CODE>false</CODE> if not.
   */
  public boolean isReferral()
  {
    return hasObjectClassOrAttribute(OC_REFERRAL, ATTR_REFERRAL_URL);
  }

  /**
   * Returns whether the current entry has a specific object class or attribute.
   *
   * @param objectClassName
   *          the name of the object class to look for
   * @param attrTypeName
   *          the attribute type name of the object class to look for
   * @return true if the current entry has the object class or the attribute,
   *         false otherwise
   */
  private boolean hasObjectClassOrAttribute(String objectClassName,
      String attrTypeName)
  {
    ObjectClass oc = DirectoryServer.getObjectClass(objectClassName);
    if (oc == null)
    {
      // This should not happen
      // The server doesn't have this objectclass defined.
      if (logger.isTraceEnabled())
      {
        logger.trace(
            "No %s objectclass is defined in the server schema.",
            objectClassName);
      }
      return containsObjectClassByName(objectClassName);
    }
    if (!objectClasses.containsKey(oc))
    {
      return false;
    }


    AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(attrTypeName);
    if (attrType == null)
    {
      // This should not happen
      // The server doesn't have this attribute type defined.
      if (logger.isTraceEnabled())
      {
        logger.trace("No %s attribute type is defined in the server schema.", attrTypeName);
      }
      return false;
    }
    return userAttributes.containsKey(attrType)
        || operationalAttributes.containsKey(attrType);
  }

  /**
   * Whether the object class name exists in the objectClass of this entry.
   *
   * @param objectClassName
   *          the name of the object class to look for
   * @return true if the object class name exists in the objectClass of this
   *         entry, false otherwise
   */
  private boolean containsObjectClassByName(String objectClassName)
  {
    for (String ocName : objectClasses.values())
    {
      if (objectClassName.equalsIgnoreCase(ocName))
      {
        return true;
      }
    }
    return false;
  }

  /**
   * Retrieves the set of referral URLs that are included in this
   * referral entry.  This should only be called if
   * <CODE>isReferral()</CODE> returns <CODE>true</CODE>.
   *
   * @return  The set of referral URLs that are included in this entry
   *          if it is a referral, or <CODE>null</CODE> if it is not a
   *          referral.
   */
  public Set<String> getReferralURLs()
  {
    AttributeType referralType = DirectoryServer.getAttributeTypeOrNull(ATTR_REFERRAL_URL);
    if (referralType == null)
    {
      // This should not happen -- The server doesn't have a ref attribute type defined.
      logger.trace("No %s attribute type is defined in the server schema.", ATTR_REFERRAL_URL);
      return null;
    }

    List<Attribute> refAttrs = userAttributes.get(referralType);
    if (refAttrs == null)
    {
      refAttrs = operationalAttributes.get(referralType);
      if (refAttrs == null)
      {
        return null;
      }
    }

    Set<String> referralURLs = new LinkedHashSet<>();
    for (Attribute a : refAttrs)
    {
      for (ByteString v : a)
      {
        referralURLs.add(v.toString());
      }
    }

    return referralURLs;
  }



  /**
   * Indicates whether this entry meets the criteria to consider it an
   * alias (e.g., it contains the "aliasObject" objectclass and a
   * "alias" attribute).
   *
   * @return  <CODE>true</CODE> if this entry meets the criteria to
   *          consider it an alias, or <CODE>false</CODE> if not.
   */
  public boolean isAlias()
  {
    return hasObjectClassOrAttribute(OC_ALIAS, ATTR_ALIAS_DN);
  }



  /**
   * Retrieves the DN of the entry referenced by this alias entry.
   * This should only be called if <CODE>isAlias()</CODE> returns
   * <CODE>true</CODE>.
   *
   * @return  The DN of the entry referenced by this alias entry, or
   *          <CODE>null</CODE> if it is not an alias.
   *
   * @throws  DirectoryException  If there is an aliasedObjectName
   *                              attribute but its value cannot be
   *                              parsed as a DN.
   */
  public DN getAliasedDN() throws DirectoryException
  {
    AttributeType aliasType = DirectoryServer.getAttributeTypeOrNull(ATTR_REFERRAL_URL);
    if (aliasType == null)
    {
      // This should not happen -- The server doesn't have an aliasedObjectName attribute type defined.
      logger.trace("No %s attribute type is defined in the server schema.", ATTR_ALIAS_DN);
      return null;
    }

    List<Attribute> aliasAttrs = userAttributes.get(aliasType);
    if (aliasAttrs == null)
    {
      aliasAttrs = operationalAttributes.get(aliasType);
      if (aliasAttrs == null)
      {
        return null;
      }
    }

    if (!aliasAttrs.isEmpty())
    {
      // There should only be a single alias attribute in an entry,
      // and we'll skip the check for others for performance reasons.
      // We would just end up taking the first one anyway. The same
      // is true with the set of values, since it should be a
      // single-valued attribute.
      Attribute aliasAttr = aliasAttrs.get(0);
      if (!aliasAttr.isEmpty())
      {
        return DN.valueOf(aliasAttr.iterator().next().toString());
      }
    }
    return null;
  }



  /**
   * Indicates whether this entry meets the criteria to consider it an
   * LDAP subentry (i.e., it contains the "ldapSubentry" objectclass).
   *
   * @return  <CODE>true</CODE> if this entry meets the criteria to
   *          consider it an LDAP subentry, or <CODE>false</CODE> if
   *          not.
   */
  public boolean isLDAPSubentry()
  {
    return hasObjectClass(OC_LDAP_SUBENTRY_LC);
  }

  /**
   * Returns whether the current entry has a specific object class.
   *
   * @param objectClassLowerCase
   *          the lowercase name of the object class to look for
   * @return true if the current entry has the object class, false otherwise
   */
  private boolean hasObjectClass(String objectClassLowerCase)
  {
    ObjectClass oc = DirectoryServer.getObjectClass(objectClassLowerCase);
    if (oc == null)
    {
      // This should not happen
      // The server doesn't have this object class defined.
      if (logger.isTraceEnabled())
      {
        logger.trace(
            "No %s objectclass is defined in the server schema.",
            objectClassLowerCase);
      }
      return containsObjectClassByName(objectClassLowerCase);
    }

    // Make the determination based on whether this entry has this objectclass.
    return objectClasses.containsKey(oc);
  }



  /**
   * Indicates whether this entry meets the criteria to consider it
   * an RFC 3672 LDAP subentry (i.e., it contains the "subentry"
   * objectclass).
   *
   * @return  <CODE>true</CODE> if this entry meets the criteria to
   *          consider it an RFC 3672 LDAP subentry, or <CODE>false
   *          </CODE> if not.
   */
  public boolean isSubentry()
  {
    return hasObjectClass(OC_SUBENTRY);
  }



  /**
   * Indicates whether the entry meets the criteria to consider it an
   * RFC 3671 LDAP collective attributes subentry (i.e., it contains
   * the "collectiveAttributeSubentry" objectclass).
   *
   * @return  <CODE>true</CODE> if this entry meets the criteria to
   *          consider it an RFC 3671 LDAP collective attributes
   *          subentry, or <CODE>false</CODE> if not.
   */
  public boolean isCollectiveAttributeSubentry()
  {
    return hasObjectClass(OC_COLLECTIVE_ATTR_SUBENTRY_LC);
  }



  /**
   * Indicates whether the entry meets the criteria to consider it an
   * inherited collective attributes subentry (i.e., it contains
   * the "inheritedCollectiveAttributeSubentry" objectclass).
   *
   * @return  <CODE>true</CODE> if this entry meets the criteria to
   *          consider it an inherited collective attributes
   *          subentry, or <CODE>false</CODE> if not.
   */
  public boolean isInheritedCollectiveAttributeSubentry()
  {
    return hasObjectClass(OC_INHERITED_COLLECTIVE_ATTR_SUBENTRY_LC);
  }



  /**
   * Indicates whether the entry meets the criteria to consider it an inherited
   * from DN collective attributes subentry (i.e., it contains the
   * "inheritedFromDNCollectiveAttributeSubentry" objectclass).
   *
   * @return <CODE>true</CODE> if this entry meets the criteria to consider it
   *         an inherited from DN collective attributes subentry, or
   *         <CODE>false</CODE> if not.
   */
  public boolean isInheritedFromDNCollectiveAttributeSubentry()
  {
    return hasObjectClass(OC_INHERITED_FROM_DN_COLLECTIVE_ATTR_SUBENTRY_LC);
  }



  /**
   * Indicates whether the entry meets the criteria to consider it
   * an inherited from RDN collective attributes subentry (i.e.,
   * it contains the "inheritedFromRDNCollectiveAttributeSubentry"
   * objectclass).
   *
   * @return  <CODE>true</CODE> if this entry meets the criteria to
   *          consider it an inherited from RDN collective attributes
   *          subentry, or <CODE>false</CODE> if not.
   */
  public boolean isInheritedFromRDNCollectiveAttributeSubentry()
  {
    return hasObjectClass(OC_INHERITED_FROM_RDN_COLLECTIVE_ATTR_SUBENTRY_LC);
  }



  /**
   * Indicates whether the entry meets the criteria to consider it a
   * LDAP password policy subentry (i.e., it contains the "pwdPolicy"
   * objectclass of LDAP Password Policy Internet-Draft).
   *
   * @return  <CODE>true</CODE> if this entry meets the criteria to
   *          consider it a LDAP Password Policy Internet-Draft
   *          subentry, or <CODE>false</CODE> if not.
   */
  public boolean isPasswordPolicySubentry()
  {
    return hasObjectClass(OC_PWD_POLICY_SUBENTRY_LC);
  }



  /**
   * Indicates whether this entry falls within the range of the
   * provided search base DN and scope.
   *
   * @param  baseDN  The base DN for which to make the determination.
   * @param  scope   The search scope for which to make the
   *                 determination.
   *
   * @return  <CODE>true</CODE> if this entry is within the given
   *          base and scope, or <CODE>false</CODE> if it is not.
   */
  public boolean matchesBaseAndScope(DN baseDN, SearchScope scope)
  {
    return dn.matchesBaseAndScope(baseDN, scope);
  }



  /**
   * Performs any necessary collective attribute processing for this
   * entry.  This should only be called at the time the entry is
   * decoded or created within the backend.
   */
  private void processCollectiveAttributes()
  {
    if (isSubentry() || isLDAPSubentry())
    {
      return;
    }

    SubentryManager manager =
            DirectoryServer.getSubentryManager();
    if(manager == null)
    {
      //Subentry manager may not have been initialized by
      //a component that doesn't require it.
      return;
    }
    // Get applicable collective subentries.
    List<SubEntry> collectiveAttrSubentries =
            manager.getCollectiveSubentries(this);

    if (collectiveAttrSubentries == null || collectiveAttrSubentries.isEmpty())
    {
      // Nothing to see here, move along.
      return;
    }

    // Get collective attribute exclusions.
    AttributeType exclusionsType = DirectoryServer.getAttributeTypeOrNull(ATTR_COLLECTIVE_EXCLUSIONS_LC);
    List<Attribute> exclusionsAttrList = operationalAttributes.get(exclusionsType);
    Set<String> exclusionsNameSet = new HashSet<>();
    if (exclusionsAttrList != null && !exclusionsAttrList.isEmpty())
    {
      for (Attribute attr : exclusionsAttrList)
      {
        for (ByteString attrValue : attr)
        {
          String exclusionsName = attrValue.toString().toLowerCase();
          if (VALUE_COLLECTIVE_EXCLUSIONS_EXCLUDE_ALL_LC.equals(exclusionsName)
              || OID_COLLECTIVE_EXCLUSIONS_EXCLUDE_ALL.equals(exclusionsName))
          {
            return;
          }
          exclusionsNameSet.add(exclusionsName);
        }
      }
    }

    // Process collective attributes.
    for (SubEntry subEntry : collectiveAttrSubentries)
    {
      if (subEntry.isCollective() || subEntry.isInheritedCollective())
      {
        Entry inheritFromEntry = null;
        if (subEntry.isInheritedCollective())
        {
          if (subEntry.isInheritedFromDNCollective() &&
              hasAttribute(subEntry.getInheritFromDNType()))
          {
            try
            {
              DN inheritFromDN = null;
              for (Attribute attr : getAttribute(
                   subEntry.getInheritFromDNType()))
              {
                for (ByteString value : attr)
                {
                  inheritFromDN = DN.decode(value);
                  // Respect subentry root scope.
                  if (!inheritFromDN.isDescendantOf(
                       subEntry.getDN().parent()))
                  {
                    inheritFromDN = null;
                  }
                  break;
                }
              }
              if (inheritFromDN == null)
              {
                continue;
              }

              // TODO : ACI check; needs re-factoring to happen.
              inheritFromEntry = DirectoryServer.getEntry(inheritFromDN);
            }
            catch (DirectoryException de)
            {
              logger.traceException(de);
            }
          }
          else if (subEntry.isInheritedFromRDNCollective() &&
                   hasAttribute(subEntry.getInheritFromRDNAttrType()))
          {
            DN inheritFromDN = subEntry.getInheritFromBaseDN();
            if (inheritFromDN != null)
            {
              try
              {
                for (Attribute attr : getAttribute(
                   subEntry.getInheritFromRDNAttrType()))
                {
                  inheritFromDN = subEntry.getInheritFromBaseDN();
                  for (ByteString value : attr)
                  {
                    inheritFromDN = inheritFromDN.child(
                        RDN.create(subEntry.getInheritFromRDNType(),
                        value));
                    break;
                  }
                }

                // TODO : ACI check; needs re-factoring to happen.
                inheritFromEntry = DirectoryServer.getEntry(inheritFromDN);
              }
              catch (DirectoryException de)
              {
                logger.traceException(de);
              }
            }
            else
            {
              continue;
            }
          }
        }
        List<Attribute> collectiveAttrList = subEntry.getCollectiveAttributes();
        for (Attribute collectiveAttr : collectiveAttrList)
        {
          AttributeType attributeType = collectiveAttr.getAttributeType();
          if (exclusionsNameSet.contains(
                  attributeType.getNormalizedPrimaryNameOrOID()))
          {
            continue;
          }
          if (subEntry.isInheritedCollective())
          {
            if (inheritFromEntry != null)
            {
              collectiveAttr = inheritFromEntry.getExactAttribute(
                      collectiveAttr.getAttributeType(),
                      collectiveAttr.getOptions());
              if (collectiveAttr == null || collectiveAttr.isEmpty())
              {
                continue;
              }
              collectiveAttr = new CollectiveVirtualAttribute(collectiveAttr);
            }
            else
            {
              continue;
            }
          }
          List<Attribute> attrList = userAttributes.get(attributeType);
          if (attrList == null || attrList.isEmpty())
          {
            attrList = operationalAttributes.get(attributeType);
            if (attrList == null || attrList.isEmpty())
            {
              // There aren't any conflicts, so we can just add the attribute to the entry.
              putAttributes(attributeType, newLinkedList(collectiveAttr));
            }
            else
            {
              // There is a conflict with an existing operational attribute.
              resolveCollectiveConflict(subEntry.getConflictBehavior(),
                  collectiveAttr, attrList, operationalAttributes, attributeType);
            }
          }
          else
          {
            // There is a conflict with an existing user attribute.
            resolveCollectiveConflict(subEntry.getConflictBehavior(),
                collectiveAttr, attrList, userAttributes, attributeType);
          }
        }
      }
    }
  }

  private ByteString normalize(MatchingRule matchingRule, ByteString value)
      throws DirectoryException
  {
    try
    {
      return matchingRule.normalizeAttributeValue(value);
    }
    catch (DecodeException e)
    {
      throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
          e.getMessageObject(), e);
    }
  }

  /**
   * Resolves a conflict arising with a collective attribute.
   *
   * @param conflictBehavior
   *          the behavior of the conflict
   * @param collectiveAttr
   *          the attribute in conflict
   * @param attrList
   *          the List of attribute where to resolve the conflict
   * @param attributes
   *          the Map of attributes where to solve the conflict
   * @param attributeType
   *          the attribute type used with the Map
   */
  private void resolveCollectiveConflict(
      CollectiveConflictBehavior conflictBehavior, Attribute collectiveAttr,
      List<Attribute> attrList, Map<AttributeType, List<Attribute>> attributes,
      AttributeType attributeType)
  {
    if (attrList.get(0).isVirtual())
    {
      // The existing attribute is already virtual,
      // so we've got a different conflict, but we'll let the first win.
      // FIXME -- Should we handle this differently?
      return;
    }

    // The conflict is with a real attribute. See what the
    // conflict behavior is and figure out how to handle it.
    switch (conflictBehavior)
    {
    case REAL_OVERRIDES_VIRTUAL:
      // We don't need to update the entry because the real attribute will take
      // precedence.
      break;

    case VIRTUAL_OVERRIDES_REAL:
      // We need to move the real attribute to the suppressed list
      // and replace it with the virtual attribute.
      suppressedAttributes.put(attributeType, attrList);
      attributes.put(attributeType, newLinkedList(collectiveAttr));
      break;

    case MERGE_REAL_AND_VIRTUAL:
      // We need to add the virtual attribute to the
      // list and keep the existing real attribute(s).
      attrList.add(collectiveAttr);
      break;
    }
  }



  /**
   * Performs any necessary virtual attribute processing for this
   * entry.  This should only be called at the time the entry is
   * decoded or created within the backend.
   */
  public void processVirtualAttributes()
  {
    for (VirtualAttributeRule rule : DirectoryServer.getVirtualAttributes(this))
    {
      AttributeType attributeType = rule.getAttributeType();
      List<Attribute> attrList = userAttributes.get(attributeType);
      if (attrList == null || attrList.isEmpty())
      {
        attrList = operationalAttributes.get(attributeType);
        if (attrList == null || attrList.isEmpty())
        {
          // There aren't any conflicts, so we can just add the attribute to the entry.
          Attribute attr = new VirtualAttribute(attributeType, this, rule);
          putAttributes(attributeType, newLinkedList(attr));
        }
        else
        {
          // There is a conflict with an existing operational attribute.
          resolveVirtualConflict(rule, attrList, operationalAttributes, attributeType);
        }
      }
      else
      {
        // There is a conflict with an existing user attribute.
        resolveVirtualConflict(rule, attrList, userAttributes, attributeType);
      }
    }

    // Collective attributes.
    processCollectiveAttributes();
  }

  /**
   * Resolves a conflict arising with a virtual attribute.
   *
   * @param rule
   *          the VirtualAttributeRule in conflict
   * @param attrList
   *          the List of attribute where to resolve the conflict
   * @param attributes
   *          the Map of attribute where to resolve the conflict
   * @param attributeType
   *          the attribute type used with the Map
   */
  private void resolveVirtualConflict(VirtualAttributeRule rule,
      List<Attribute> attrList, Map<AttributeType, List<Attribute>> attributes,
      AttributeType attributeType)
  {
    if (attrList.get(0).isVirtual())
    {
      // The existing attribute is already virtual, so we've got
      // a different conflict, but we'll let the first win.
      // FIXME -- Should we handle this differently?
      return;
    }

    // The conflict is with a real attribute. See what the
    // conflict behavior is and figure out how to handle it.
    switch (rule.getConflictBehavior())
    {
    case REAL_OVERRIDES_VIRTUAL:
      // We don't need to update the entry because the real
      // attribute will take precedence.
      break;

    case VIRTUAL_OVERRIDES_REAL:
      // We need to move the real attribute to the suppressed
      // list and replace it with the virtual attribute.
      suppressedAttributes.put(attributeType, attrList);
      Attribute attr = new VirtualAttribute(attributeType, this, rule);
      attributes.put(attributeType, newLinkedList(attr));
      break;

    case MERGE_REAL_AND_VIRTUAL:
      // We need to add the virtual attribute to the list and
      // keep the existing real attribute(s).
      attrList.add(new VirtualAttribute(attributeType, this, rule));
      break;
    }
  }


  /**
   * Encodes this entry into a form that is suitable for long-term
   * persistent storage.  The encoding will have a version number so
   * that if the way we store entries changes in the future we will
   * still be able to read entries encoded in an older format.
   *
   * @param  buffer  The buffer to encode into.
   * @param  config  The configuration that may be used to control how
   *                 the entry is encoded.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to encode the entry.
   */
  public void encode(ByteStringBuilder buffer,
                     EntryEncodeConfig config)
         throws DirectoryException
  {
    encodeV3(buffer, config);
  }

  /**
   * Encodes this entry using the V3 encoding.
   *
   * @param  buffer  The buffer to encode into.
   * @param  config  The configuration that should be used to encode
   *                 the entry.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to encode the entry.
   */
  private void encodeV3(ByteStringBuilder buffer,
                        EntryEncodeConfig config)
         throws DirectoryException
  {
    // The version number will be one byte.
    buffer.appendByte(0x03);

    // Get the encoded representation of the config.
    config.encode(buffer);

    // If we should include the DN, then it will be encoded as a
    // one-to-five byte length followed by the UTF-8 byte
    // representation.
    if (! config.excludeDN())
    {
      // TODO: Can we encode the DN directly into buffer?
      byte[] dnBytes  = getBytes(dn.toString());
      buffer.appendBERLength(dnBytes.length);
      buffer.appendBytes(dnBytes);
    }


    // Encode the object classes in the appropriate manner.
    if (config.compressObjectClassSets())
    {
      config.getCompressedSchema().encodeObjectClasses(buffer, objectClasses);
    }
    else
    {
      // Encode number of OCs and 0 terminated names.
      buffer.appendBERLength(objectClasses.size());
      for (String ocName : objectClasses.values())
      {
        buffer.appendUtf8(ocName);
        buffer.appendByte(0x00);
      }
    }


    // Encode the user attributes in the appropriate manner.
    encodeAttributes(buffer, userAttributes, config);


    // The operational attributes will be encoded in the same way as
    // the user attributes.
    encodeAttributes(buffer, operationalAttributes, config);
  }

  /**
   * Encode the given attributes of an entry.
   *
   * @param  buffer  The buffer to encode into.
   * @param  attributes The attributes to encode.
   * @param  config  The configuration that may be used to control how
   *                 the entry is encoded.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to encode the entry.
   */
  private void encodeAttributes(ByteStringBuilder buffer,
                    Map<AttributeType,List<Attribute>> attributes,
                                EntryEncodeConfig config)
      throws DirectoryException
  {
    int numAttributes = 0;

    // First count how many attributes are there to encode.
    for (List<Attribute> attrList : attributes.values())
    {
      Attribute a;
      for (int i = 0; i < attrList.size(); i++)
      {
        a = attrList.get(i);
        if (a.isVirtual() || a.isEmpty())
        {
          continue;
        }

        numAttributes++;
      }
    }

    // Encoded one-to-five byte number of attributes
    buffer.appendBERLength(numAttributes);

    if (config.compressAttributeDescriptions())
    {
      for (List<Attribute> attrList : attributes.values())
      {
        for (Attribute a : attrList)
        {
          if (a.isVirtual() || a.isEmpty())
          {
            continue;
          }

          config.getCompressedSchema().encodeAttribute(buffer, a);
        }
      }
    }
    else
    {
      // The attributes will be encoded as a sequence of:
      // - A UTF-8 byte representation of the attribute name.
      // - A zero delimiter
      // - A one-to-five byte number of values for the attribute
      // - A sequence of:
      //   - A one-to-five byte length for the value
      //   - A UTF-8 byte representation for the value
      for (List<Attribute> attrList : attributes.values())
      {
        for (Attribute a : attrList)
        {
          byte[] nameBytes = getBytes(a.getNameWithOptions());
          buffer.appendBytes(nameBytes);
          buffer.appendByte(0x00);

          buffer.appendBERLength(a.size());
          for(ByteString v : a)
          {
            buffer.appendBERLength(v.length());
            buffer.appendBytes(v);
          }
        }
      }
    }
  }


  /**
   * Decodes the provided byte array as an entry.
   *
   * @param  entryBuffer  The byte array containing the data to be
   *                      decoded.
   *
   * @return  The decoded entry.
   *
   * @throws  DirectoryException  If the provided byte array cannot be
   *                              decoded as an entry.
   */
  public static Entry decode(ByteSequenceReader entryBuffer)
         throws DirectoryException
  {
    return decode(entryBuffer,
                  DirectoryServer.getDefaultCompressedSchema());
  }



    /**
   * Decodes the provided byte array as an entry using the V3
   * encoding.
   *
   * @param  entryBuffer       The byte buffer containing the data to
   *                           be decoded.
   * @param  compressedSchema  The compressed schema manager to use
   *                           when decoding tokenized schema
   *                           elements.
   *
   * @return  The decoded entry.
   *
   * @throws  DirectoryException  If the provided byte array cannot be
   *                              decoded as an entry.
   */
  public static Entry decode(ByteSequenceReader entryBuffer,
                             CompressedSchema compressedSchema)
         throws DirectoryException
  {
    try
    {
      // The first byte must be the entry version.  If it's not one
      // we recognize, then that's an error.
      Byte version = entryBuffer.readByte();
      if (version != 0x03 && version != 0x02 && version != 0x01)
      {
        LocalizableMessage message = ERR_ENTRY_DECODE_UNRECOGNIZED_VERSION.get(
            byteToHex(version));
        throw new DirectoryException(
                       DirectoryServer.getServerErrorResultCode(),
                       message);
      }

      EntryEncodeConfig config;
      if(version != 0x01)
      {
        // Next is the length of the encoded configuration.
        int configLength = entryBuffer.readBERLength();

        // Next is the encoded configuration itself.
        config =
            EntryEncodeConfig.decode(entryBuffer, configLength,
                compressedSchema);
      }
      else
      {
        config = EntryEncodeConfig.DEFAULT_CONFIG;
      }

      // If we should have included the DN in the entry, then it's
      // next.
      DN dn;
      if (config.excludeDN())
      {
        dn = DN.NULL_DN;
      }
      else
      {
        // Next is the length of the DN.  It may be a single byte or
        // multiple bytes.
        int dnLength = entryBuffer.readBERLength();


        // Next is the DN itself.
        ByteSequence dnBytes = entryBuffer.readByteSequence(dnLength);
        dn = DN.decode(dnBytes.toByteString());
      }


      // Next is the set of encoded object classes.  The encoding will
      // depend on the configuration.
      Map<ObjectClass,String> objectClasses =
          decodeObjectClasses(version, entryBuffer, config);


      // Now, we should iterate through the user and operational attributes and
      // decode each one.
      Map<AttributeType, List<Attribute>> userAttributes =
          decodeAttributes(version, entryBuffer, config);
      Map<AttributeType, List<Attribute>> operationalAttributes =
          decodeAttributes(version, entryBuffer, config);


      // We've got everything that we need, so create and return the entry.
      return new Entry(dn, objectClasses, userAttributes,
          operationalAttributes);
    }
    catch (DirectoryException de)
    {
      throw de;
    }
    catch (Exception e)
    {
      logger.traceException(e);

      LocalizableMessage message =
          ERR_ENTRY_DECODE_EXCEPTION.get(getExceptionMessage(e));
      throw new DirectoryException(
                     DirectoryServer.getServerErrorResultCode(),
                     message, e);
    }
  }


  /**
   * Decode the object classes of an encoded entry.
   *
   * @param  ver The version of the entry encoding.
   * @param  entryBuffer The byte sequence containing the encoded
   *                     entry.
   * @param  config  The configuration that may be used to control how
   *                 the entry is encoded.
   *
   * @return  A map of the decoded object classes.
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to encode the entry.
   */
  private static Map<ObjectClass,String> decodeObjectClasses(
      byte ver, ByteSequenceReader entryBuffer,
      EntryEncodeConfig config) throws DirectoryException
  {
    // Next is the set of encoded object classes.  The encoding will
    // depend on the configuration.
    if (config.compressObjectClassSets())
    {
      return config.getCompressedSchema().decodeObjectClasses(entryBuffer);
    }

    Map<ObjectClass, String> objectClasses;
    {
      if(ver < 0x03)
      {
        // Next is the length of the object classes. It may be a
        // single byte or multiple bytes.
        int ocLength = entryBuffer.readBERLength();

        // The set of object classes will be encoded as a single
        // string with the object class names separated by zeros.
        objectClasses = new LinkedHashMap<>();
        int startPos = entryBuffer.position();
        for (int i=0; i < ocLength; i++)
        {
          if (entryBuffer.readByte() == 0x00)
          {
            int endPos = entryBuffer.position() - 1;
            addObjectClass(objectClasses, entryBuffer, startPos, endPos);

            entryBuffer.skip(1);
            startPos = entryBuffer.position();
          }
        }
        int endPos = entryBuffer.position();
        addObjectClass(objectClasses, entryBuffer, startPos, endPos);
      }
      else
      {
        // Next is the number of zero terminated object classes.
        int numOC = entryBuffer.readBERLength();
        objectClasses = new LinkedHashMap<>(numOC);
        for(int i = 0; i < numOC; i++)
        {
          int startPos = entryBuffer.position();
          while(entryBuffer.readByte() != 0x00)
          {}
          int endPos = entryBuffer.position() - 1;
          addObjectClass(objectClasses, entryBuffer, startPos, endPos);
          entryBuffer.skip(1);
        }
      }
    }

    return objectClasses;
  }

  /**
   * Adds the objectClass contained in the buffer to the map of object class.
   *
   * @param objectClasses
   *          the Map where to add the objectClass
   * @param entryBuffer
   *          the buffer containing the objectClass name
   * @param startPos
   *          the starting position in the buffer
   * @param endPos
   *          the ending position in the buffer
   */
  private static void addObjectClass(Map<ObjectClass, String> objectClasses,
      ByteSequenceReader entryBuffer, int startPos, int endPos)
  {
    entryBuffer.position(startPos);
    final String ocName = entryBuffer.readStringUtf8(endPos - startPos);
    final String lowerName = toLowerCase(ocName);
    final ObjectClass oc = DirectoryServer.getObjectClass(lowerName, true);
    objectClasses.put(oc, ocName);
  }

  /**
   * Decode the attributes of an encoded entry.
   *
   * @param  ver The version of the entry encoding.
   * @param  entryBuffer The byte sequence containing the encoded
   *                     entry.
   * @param  config  The configuration that may be used to control how
   *                 the entry is encoded.
   *
   * @return  A map of the decoded object classes.
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to encode the entry.
   */
  private static Map<AttributeType, List<Attribute>>
  decodeAttributes(Byte ver, ByteSequenceReader entryBuffer,
                   EntryEncodeConfig config) throws DirectoryException
  {
    // Next is the total number of attributes.  It may be a
    // single byte or multiple bytes.
    int attrs = entryBuffer.readBERLength();


    // Now, we should iterate through the attributes and decode each one.
    Map<AttributeType, List<Attribute>> attributes = new LinkedHashMap<>(attrs);
    if (config.compressAttributeDescriptions())
    {
      for (int i=0; i < attrs; i++)
      {
        if(ver < 0x03)
        {
          // Version 2 includes a total attribute length
          entryBuffer.readBERLength();
        }
        // Decode the attribute.
        Attribute a = config.getCompressedSchema().decodeAttribute(entryBuffer);
        List<Attribute> attrList = attributes.get(a.getAttributeType());
        if (attrList == null)
        {
          attrList = new ArrayList<>(1);
          attributes.put(a.getAttributeType(), attrList);
        }
        attrList.add(a);
      }
    }
    else
    {
      AttributeBuilder builder = new AttributeBuilder();
      int startPos;
      int endPos;
      for (int i=0; i < attrs; i++)
      {

        // First, we have the zero-terminated attribute name.
        startPos = entryBuffer.position();
        while (entryBuffer.readByte() != 0x00)
        {}
        endPos = entryBuffer.position()-1;
        entryBuffer.position(startPos);
        String name = entryBuffer.readStringUtf8(endPos - startPos);
        entryBuffer.skip(1);

        AttributeType attributeType;
        int semicolonPos = name.indexOf(';');
        if (semicolonPos > 0)
        {
          builder.setAttributeType(name.substring(0, semicolonPos));
          attributeType = builder.getAttributeType();

          int nextPos = name.indexOf(';', semicolonPos+1);
          while (nextPos > 0)
          {
            String option = name.substring(semicolonPos+1, nextPos);
            if (option.length() > 0)
            {
              builder.setOption(option);
            }

            semicolonPos = nextPos;
            nextPos = name.indexOf(';', semicolonPos+1);
          }

          String option = name.substring(semicolonPos+1);
          if (option.length() > 0)
          {
            builder.setOption(option);
          }
        }
        else
        {
          builder.setAttributeType(name);
          attributeType = builder.getAttributeType();
        }


        // Next, we have the number of values.
        int numValues = entryBuffer.readBERLength();

        // Next, we have the sequence of length-value pairs.
        for (int j=0; j < numValues; j++)
        {
          int valueLength = entryBuffer.readBERLength();

          ByteString valueBytes =
              entryBuffer.readByteSequence(valueLength).toByteString();
          builder.add(valueBytes);
        }


        // Create the attribute and add it to the set of attributes.
        Attribute a = builder.toAttribute();
        List<Attribute> attrList = attributes.get(attributeType);
        if (attrList == null)
        {
          attrList = new ArrayList<>(1);
          attributes.put(attributeType, attrList);
        }
        attrList.add(a);
      }
    }

    return attributes;
  }



  /**
   * Retrieves a list of the lines for this entry in LDIF form.  Long
   * lines will not be wrapped automatically.
   *
   * @return  A list of the lines for this entry in LDIF form.
   */
  public List<StringBuilder> toLDIF()
  {
    List<StringBuilder> ldifLines = new LinkedList<>();

    // First, append the DN.
    StringBuilder dnLine = new StringBuilder("dn");
    appendLDIFSeparatorAndValue(dnLine, ByteString.valueOfUtf8(dn.toString()));
    ldifLines.add(dnLine);

    // Next, add the set of objectclasses.
    for (String s : objectClasses.values())
    {
      StringBuilder ocLine = new StringBuilder("objectClass: ").append(s);
      ldifLines.add(ocLine);
    }

    // Finally, add the set of user and operational attributes.
    addLinesForAttributes(ldifLines, userAttributes);
    addLinesForAttributes(ldifLines, operationalAttributes);

    return ldifLines;
  }


  /**
   * Add LDIF lines for each passed in attributes.
   *
   * @param ldifLines
   *          the List where to add the LDIF lines
   * @param attributes
   *          the List of attributes to convert into LDIf lines
   */
  private void addLinesForAttributes(List<StringBuilder> ldifLines,
      Map<AttributeType, List<Attribute>> attributes)
  {
    for (List<Attribute> attrList : attributes.values())
    {
      for (Attribute a : attrList)
      {
        StringBuilder attrName = new StringBuilder(a.getName());
        for (String o : a.getOptions())
        {
          attrName.append(";");
          attrName.append(o);
        }

        for (ByteString v : a)
        {
          StringBuilder attrLine = new StringBuilder(attrName);
          appendLDIFSeparatorAndValue(attrLine, v);
          ldifLines.add(attrLine);
        }
      }
    }
  }


  /**
   * Writes this entry in LDIF form according to the provided
   * configuration.
   *
   * @param  exportConfig  The configuration that specifies how the
   *                       entry should be written.
   *
   * @return  <CODE>true</CODE> if the entry is actually written, or
   *          <CODE>false</CODE> if it is not for some reason.
   *
   * @throws  IOException  If a problem occurs while writing the
   *                       information.
   *
   * @throws  LDIFException  If a problem occurs while trying to
   *                         determine whether to write the entry.
   */
  public boolean toLDIF(LDIFExportConfig exportConfig)
         throws IOException, LDIFException
  {
    // See if this entry should be included in the export at all.
    try
    {
      if (! exportConfig.includeEntry(this))
      {
        if (logger.isTraceEnabled())
        {
          logger.trace("Skipping entry %s because of the export configuration.", dn);
        }
        return false;
      }
    }
    catch (Exception e)
    {
      logger.traceException(e);
      throw new LDIFException(ERR_LDIF_COULD_NOT_EVALUATE_FILTERS_FOR_EXPORT.get(dn, e), e);
    }


    // Invoke LDIF export plugins on the entry if appropriate.
    if (exportConfig.invokeExportPlugins())
    {
      PluginConfigManager pluginConfigManager =
           DirectoryServer.getPluginConfigManager();
      PluginResult.ImportLDIF pluginResult =
           pluginConfigManager.invokeLDIFExportPlugins(exportConfig,
                                                    this);
      if (! pluginResult.continueProcessing())
      {
        return false;
      }
    }


    // Get the information necessary to write the LDIF.
    BufferedWriter writer     = exportConfig.getWriter();
    int            wrapColumn = exportConfig.getWrapColumn();
    boolean        wrapLines  = wrapColumn > 1;


    // First, write the DN.  It will always be included.
    StringBuilder dnLine = new StringBuilder("dn");
    appendLDIFSeparatorAndValue(dnLine, ByteString.valueOfUtf8(dn.toString()));
    LDIFWriter.writeLDIFLine(dnLine, writer, wrapLines, wrapColumn);


    // Next, the set of objectclasses.
    final boolean typesOnly = exportConfig.typesOnly();
    if (exportConfig.includeObjectClasses())
    {
      if (typesOnly)
      {
        StringBuilder ocLine = new StringBuilder("objectClass:");
        LDIFWriter.writeLDIFLine(ocLine, writer, wrapLines, wrapColumn);
      }
      else
      {
        for (String s : objectClasses.values())
        {
          StringBuilder ocLine = new StringBuilder("objectClass: ").append(s);
          LDIFWriter.writeLDIFLine(ocLine, writer, wrapLines, wrapColumn);
        }
      }
    }
    else
    {
      if (logger.isTraceEnabled())
      {
        logger.trace("Skipping objectclasses for entry %s because of the export configuration.", dn);
      }
    }


    // Now the set of user attributes.
    writeLDIFLines(userAttributes, typesOnly, "user", exportConfig, writer,
        wrapColumn, wrapLines);


    // Next, the set of operational attributes.
    if (exportConfig.includeOperationalAttributes())
    {
      writeLDIFLines(operationalAttributes, typesOnly, "operational",
          exportConfig, writer, wrapColumn, wrapLines);
    }
    else
    {
      if (logger.isTraceEnabled())
      {
        logger.trace(
            "Skipping all operational attributes for entry %s " +
            "because of the export configuration.", dn);
      }
    }


    // If we are not supposed to include virtual attributes, then
    // write any attributes that may normally be suppressed by a
    // virtual attribute.
    if (! exportConfig.includeVirtualAttributes())
    {
      for (AttributeType t : suppressedAttributes.keySet())
      {
        if (exportConfig.includeAttribute(t))
        {
          for (Attribute a : suppressedAttributes.get(t))
          {
            writeLDIFLine(a, typesOnly, writer, wrapLines, wrapColumn);
          }
        }
      }
    }


    // Make sure there is a blank line after the entry.
    writer.newLine();


    return true;
  }


  /**
   * Writes the provided List of attributes to LDIF using the provided
   * information.
   *
   * @param attributes
   *          the List of attributes to write as LDIF
   * @param typesOnly
   *          if true, only writes the type information, else writes the type
   *          information and values for the attribute.
   * @param attributeType
   *          the type of attribute being written to LDIF
   * @param exportConfig
   *          configures the export to LDIF
   * @param writer
   *          The writer to which the data should be written. It must not be
   *          <CODE>null</CODE>.
   * @param wrapLines
   *          Indicates whether to wrap long lines.
   * @param wrapColumn
   *          The column at which long lines should be wrapped.
   * @throws IOException
   *           If a problem occurs while writing the information.
   */
  private void writeLDIFLines(Map<AttributeType, List<Attribute>> attributes,
      final boolean typesOnly, String attributeType,
      LDIFExportConfig exportConfig, BufferedWriter writer, int wrapColumn,
      boolean wrapLines) throws IOException
  {
    for (AttributeType attrType : attributes.keySet())
    {
      if (exportConfig.includeAttribute(attrType))
      {
        List<Attribute> attrList = attributes.get(attrType);
        for (Attribute a : attrList)
        {
          if (a.isVirtual() && !exportConfig.includeVirtualAttributes())
          {
            continue;
          }

          writeLDIFLine(a, typesOnly, writer, wrapLines, wrapColumn);
        }
      }
      else
      {
        if (logger.isTraceEnabled())
        {
          logger.trace("Skipping %s attribute %s for entry %s "
              + "because of the export configuration.", attributeType, attrType.getNameOrOID(), dn);
        }
      }
    }
  }


  /**
   * Writes the provided attribute to LDIF using the provided information.
   *
   * @param attribute
   *          the attribute to write to LDIF
   * @param typesOnly
   *          if true, only writes the type information, else writes the type
   *          information and values for the attribute.
   * @param writer
   *          The writer to which the data should be written. It must not be
   *          <CODE>null</CODE>.
   * @param wrapLines
   *          Indicates whether to wrap long lines.
   * @param wrapColumn
   *          The column at which long lines should be wrapped.
   * @throws IOException
   *           If a problem occurs while writing the information.
   */
  private void writeLDIFLine(Attribute attribute, final boolean typesOnly,
      BufferedWriter writer, boolean wrapLines, int wrapColumn)
      throws IOException
  {
    StringBuilder attrName = new StringBuilder(attribute.getName());
    for (String o : attribute.getOptions())
    {
      attrName.append(";");
      attrName.append(o);
    }

    if (typesOnly)
    {
      attrName.append(":");

      LDIFWriter.writeLDIFLine(attrName, writer, wrapLines, wrapColumn);
    }
    else
    {
      for (ByteString v : attribute)
      {
        StringBuilder attrLine = new StringBuilder(attrName);
        appendLDIFSeparatorAndValue(attrLine, v);
        LDIFWriter.writeLDIFLine(attrLine, writer, wrapLines, wrapColumn);
      }
    }
  }



  /**
   * Retrieves the name of the protocol associated with this protocol
   * element.
   *
   * @return  The name of the protocol associated with this protocol
   *          element.
   */
  @Override
  public String getProtocolElementName()
  {
    return "Entry";
  }



  /**
   * Retrieves a hash code for this entry.
   *
   * @return  The hash code for this entry.
   */
  @Override
  public int hashCode()
  {
    int hashCode = dn.hashCode();
    for (ObjectClass oc : objectClasses.keySet())
    {
      hashCode += oc.hashCode();
    }

    hashCode += hashCode(userAttributes.values());
    hashCode += hashCode(operationalAttributes.values());
    return hashCode;
  }

  /**
   * Computes the hashCode for the list of attributes list.
   *
   * @param attributesLists
   *          the attributes for which to commpute the hashCode
   * @return the hashCode for the list of attributes list.
   */
  private int hashCode(Collection<List<Attribute>> attributesLists)
  {
    int result = 0;
    for (List<Attribute> attributes : attributesLists)
    {
      for (Attribute a : attributes)
      {
        result += a.hashCode();
      }
    }
    return result;
  }



  /**
   * Indicates whether the provided object is equal to this entry.  In
   * order for the object to be considered equal, it must be an entry
   * with the same DN, set of object classes, and set of user and
   * operational attributes.
   *
   * @param  o  The object for which to make the determination.
   *
   * @return  {@code true} if the provided object may be considered
   *          equal to this entry, or {@code false} if not.
   */
  @Override
  public boolean equals(Object o)
  {
    if (this == o)
    {
      return true;
    }
    if (o == null)
    {
      return false;
    }
    if (! (o instanceof Entry))
    {
      return false;
    }

    Entry e = (Entry) o;
    return dn.equals(e.dn)
        && objectClasses.keySet().equals(e.objectClasses.keySet())
        && equals(userAttributes, e.userAttributes)
        && equals(operationalAttributes, e.operationalAttributes);
  }

  /**
   * Returns whether the 2 Maps are equal.
   *
   * @param attributes1
   *          the first Map of attributes
   * @param attributes2
   *          the second Map of attributes
   * @return true if the 2 Maps are equal, false otherwise
   */
  private boolean equals(Map<AttributeType, List<Attribute>> attributes1,
      Map<AttributeType, List<Attribute>> attributes2)
  {
    for (AttributeType at : attributes1.keySet())
    {
      List<Attribute> list1 = attributes1.get(at);
      List<Attribute> list2 = attributes2.get(at);
      if (list2 == null || list1.size() != list2.size())
      {
        return false;
      }
      for (Attribute a : list1)
      {
        if (!list2.contains(a))
        {
          return false;
        }
      }
    }
    return true;
  }



  /**
   * Retrieves a string representation of this protocol element.
   *
   * @return  A string representation of this protocol element.
   */
  @Override
  public String toString()
  {
    return toLDIFString();
  }



  /**
   * Appends a string representation of this protocol element to the
   * provided buffer.
   *
   * @param  buffer  The buffer into which the string representation
   *                 should be written.
   */
  @Override
  public void toString(StringBuilder buffer)
  {
    buffer.append(this);
  }



  /**
   * Appends a string representation of this protocol element to the
   * provided buffer.
   *
   * @param  buffer  The buffer into which the string representation
   *                 should be written.
   * @param  indent  The number of spaces that should be used to
   *                 indent the resulting string representation.
   */
  @Override
  public void toString(StringBuilder buffer, int indent)
  {
    StringBuilder indentBuf = new StringBuilder(indent);
    for (int i=0 ; i < indent; i++)
    {
      indentBuf.append(' ');
    }

    for (StringBuilder b : toLDIF())
    {
      buffer.append(indentBuf);
      buffer.append(b);
      buffer.append(EOL);
    }
  }



  /**
   * Retrieves a string representation of this entry in LDIF form.
   *
   * @return  A string representation of this entry in LDIF form.
   */
  public String toLDIFString()
  {
    StringBuilder buffer = new StringBuilder();

    for (StringBuilder ldifLine : toLDIF())
    {
      buffer.append(ldifLine);
      buffer.append(EOL);
    }

    return buffer.toString();
  }



  /**
   * Appends a single-line representation of this entry to the
   * provided buffer.
   *
   * @param  buffer  The buffer to which the information should be
   *                 written.
   */
  public void toSingleLineString(StringBuilder buffer)
  {
    buffer.append("Entry(dn=\"");
    dn.toString(buffer);
    buffer.append("\",objectClasses={");

    Iterator<String> iterator = objectClasses.values().iterator();
    if (iterator.hasNext())
    {
      buffer.append(iterator.next());

      while (iterator.hasNext())
      {
        buffer.append(",");
        buffer.append(iterator.next());
      }
    }

    buffer.append("},userAttrs={");
    appendAttributes(buffer, userAttributes.values());
    buffer.append("},operationalAttrs={");
    appendAttributes(buffer, operationalAttributes.values());
    buffer.append("})");
  }

  /**
   * Appends the attributes to the StringBuilder.
   *
   * @param buffer
   *          the StringBuilder where to append
   * @param attributesLists
   *          the attributesLists to append
   */
  private void appendAttributes(StringBuilder buffer,
      Collection<List<Attribute>> attributesLists)
  {
    boolean firstAttr = true;
    for (List<Attribute> attributes : attributesLists)
    {
      for (Attribute a : attributes)
      {
        if (firstAttr)
        {
          firstAttr = false;
        }
        else
        {
          buffer.append(",");
        }

        buffer.append(a.getName());

        if (a.hasOptions())
        {
          for (String optionString : a.getOptions())
          {
            buffer.append(";");
            buffer.append(optionString);
          }
        }

        buffer.append("={");
        Iterator<ByteString> valueIterator = a.iterator();
        if (valueIterator.hasNext())
        {
          buffer.append(valueIterator.next());

          while (valueIterator.hasNext())
          {
            buffer.append(",");
            buffer.append(valueIterator.next());
          }
        }

        buffer.append("}");
      }
    }
  }



  /**
   * Retrieves the requested attribute element for the specified
   * attribute type and options or <code>null</code> if this entry
   * does not contain an attribute with the specified attribute type
   * and options.
   *
   * @param attributeType
   *          The attribute type to retrieve.
   * @param options
   *          The set of attribute options.
   * @return The requested attribute element for the specified
   *         attribute type and options, or <code>null</code> if the
   *         specified attribute type is not present in this entry
   *         with the provided set of options.
   */
  public Attribute getExactAttribute(AttributeType attributeType,
      Set<String> options)
  {
    List<Attribute> attributes = getAttributes(attributeType);
    if (attributes != null)
    {
      for (Attribute attribute : attributes)
      {
        if (attribute.optionsEqual(options))
        {
          return attribute;
        }
      }
    }
    return null;
  }



  /**
   * Adds the provided attribute to this entry. If an attribute with
   * the provided type and options already exists, then it will be
   * either merged or replaced depending on the value of
   * <code>replace</code>.
   *
   * @param attribute
   *          The attribute to add/replace in this entry.
   * @param duplicateValues
   *          A list to which any duplicate values will be added.
   * @param replace
   *          <code>true</code> if the attribute should replace any
   *          existing attribute.
   */
  private void setAttribute(Attribute attribute,
      List<ByteString> duplicateValues, boolean replace)
  {
    attachment = null;

    AttributeType attributeType = attribute.getAttributeType();

    if (attribute.getAttributeType().isObjectClass())
    {
      // We will not do any validation of the object classes - this is
      // left to the caller.
      if (replace)
      {
        objectClasses.clear();
      }

      MatchingRule rule =
          attribute.getAttributeType().getEqualityMatchingRule();
      for (ByteString v : attribute)
      {
        String name = v.toString();
        String lowerName = toLowerName(rule, v);

        // Create a default object class if necessary.
        ObjectClass oc =
          DirectoryServer.getObjectClass(lowerName, true);

        if (replace)
        {
          objectClasses.put(oc, name);
        }
        else
        {
          if (objectClasses.containsKey(oc))
          {
            duplicateValues.add(v);
          }
          else
          {
            objectClasses.put(oc, name);
          }
        }
      }

      return;
    }

    List<Attribute> attributes = getAttributes(attributeType);
    if (attributes == null)
    {
      // Do nothing if we are deleting a non-existing attribute.
      if (replace && attribute.isEmpty())
      {
        return;
      }

      // We are adding the first attribute with this attribute type.
      putAttributes(attributeType, newArrayList(attribute));
      return;
    }

    // There are already attributes with the same attribute type.
    Set<String> options = attribute.getOptions();
    for (int i = 0; i < attributes.size(); i++)
    {
      Attribute a = attributes.get(i);
      if (a.optionsEqual(options))
      {
        if (replace)
        {
          if (!attribute.isEmpty())
          {
            attributes.set(i, attribute);
          }
          else
          {
            attributes.remove(i);

            if (attributes.isEmpty())
            {
              removeAttributes(attributeType);
            }
          }
        }
        else
        {
          AttributeBuilder builder = new AttributeBuilder(a);
          for (ByteString v : attribute)
          {
            if (!builder.add(v))
            {
              duplicateValues.add(v);
            }
          }
          attributes.set(i, builder.toAttribute());
        }
        return;
      }
    }

    // There were no attributes with the same options.
    if (replace && attribute.isEmpty())
    {
      // Do nothing.
      return;
    }

    attributes.add(attribute);
  }



  /**
   * Returns an entry containing only those attributes of this entry
   * which match the provided criteria.
   *
   * @param attrNameList
   *          The list of attributes to include, may include wild
   *          cards.
   * @param omitValues
   *          Indicates whether to omit attribute values when
   *          processing.
   * @param omitReal
   *          Indicates whether to exclude real attributes.
   * @param omitVirtual
   *          Indicates whether to exclude virtual attributes.
   * @return An entry containing only those attributes of this entry
   *         which match the provided criteria.
   */
  public Entry filterEntry(Set<String> attrNameList,
      boolean omitValues, boolean omitReal, boolean omitVirtual)
  {
    final AttributeType ocType = DirectoryServer.getObjectClassAttributeType();

    Map<ObjectClass, String> objectClassesCopy;
    Map<AttributeType, List<Attribute>> userAttrsCopy;
    Map<AttributeType, List<Attribute>> operationalAttrsCopy;

    if (attrNameList == null || attrNameList.isEmpty())
    {
      // Common case: return filtered user attributes.
      userAttrsCopy = new LinkedHashMap<>(userAttributes.size());
      operationalAttrsCopy = new LinkedHashMap<>(0);

      if (omitReal)
      {
        objectClassesCopy = new LinkedHashMap<>(0);
      }
      else if (omitValues)
      {
        objectClassesCopy = new LinkedHashMap<>(0);

        // Add empty object class attribute.
        userAttrsCopy.put(ocType, newArrayList(Attributes.empty(ocType)));
      }
      else
      {
        objectClassesCopy = new LinkedHashMap<>(objectClasses);

        // First, add the objectclass attribute.
        Attribute ocAttr = getObjectClassAttribute();
        if (ocAttr != null)
        {
          userAttrsCopy.put(ocType, newArrayList(ocAttr));
        }
      }

      // Copy all user attributes.
      deepCopy(userAttributes, userAttrsCopy, omitValues, true,
          omitReal, omitVirtual, true);
    }
    else
    {
      // Incrementally build table of attributes.
      if (omitReal || omitValues)
      {
        objectClassesCopy = new LinkedHashMap<>(0);
      }
      else
      {
        objectClassesCopy = new LinkedHashMap<>(objectClasses.size());
      }

      userAttrsCopy = new LinkedHashMap<>(userAttributes.size());
      operationalAttrsCopy = new LinkedHashMap<>(operationalAttributes.size());

      for (String attrName : attrNameList)
      {
        if ("*".equals(attrName))
        {
          // This is a special placeholder indicating that all user
          // attributes should be returned.
          if (!omitReal)
          {
            if (omitValues)
            {
              // Add empty object class attribute.
              userAttrsCopy.put(ocType, newArrayList(Attributes.empty(ocType)));
            }
            else
            {
              // Add the objectclass attribute.
              objectClassesCopy.putAll(objectClasses);
              Attribute ocAttr = getObjectClassAttribute();
              if (ocAttr != null)
              {
                userAttrsCopy.put(ocType, newArrayList(ocAttr));
              }
            }
          }

          // Copy all user attributes.
          deepCopy(userAttributes, userAttrsCopy, omitValues, true,
              omitReal, omitVirtual, true);
          continue;
        }
        else if ("+".equals(attrName))
        {
          // This is a special placeholder indicating that all
          // operational attributes should be returned.
          deepCopy(operationalAttributes, operationalAttrsCopy,
              omitValues, true, omitReal, omitVirtual, true);
          continue;
        }

        String lowerName;
        Set<String> options;
        int semicolonPos = attrName.indexOf(';');
        if (semicolonPos > 0)
        {
          String tmpName = attrName.substring(0, semicolonPos);
          lowerName = toLowerCase(tmpName);
          int nextPos = attrName.indexOf(';', semicolonPos+1);
          options = new HashSet<>();
          while (nextPos > 0)
          {
            options.add(attrName.substring(semicolonPos+1, nextPos));

            semicolonPos = nextPos;
            nextPos = attrName.indexOf(';', semicolonPos+1);
          }
          options.add(attrName.substring(semicolonPos+1));
          attrName = tmpName;
        }
        else
        {
          lowerName = toLowerCase(attrName);
          options = null;
        }

        AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(lowerName);
        if (attrType == null)
        {
          // Unrecognized attribute type - do best effort search.
          for (Map.Entry<AttributeType, List<Attribute>> e :
            userAttributes.entrySet())
          {
            AttributeType t = e.getKey();
            if (t.hasNameOrOID(lowerName))
            {
              mergeAttributeLists(e.getValue(), userAttrsCopy, t,
                  attrName, options, omitValues, omitReal, omitVirtual);
              continue;
            }
          }

          for (Map.Entry<AttributeType, List<Attribute>> e :
            operationalAttributes.entrySet())
          {
            AttributeType t = e.getKey();
            if (t.hasNameOrOID(lowerName))
            {
              mergeAttributeLists(e.getValue(), operationalAttrsCopy,
                  t, attrName, options, omitValues, omitReal, omitVirtual);
              continue;
            }
          }
        }
        else
        {
          // Recognized attribute type.
          if (attrType.isObjectClass()) {
            if (!omitReal)
            {
              if (omitValues)
              {
                userAttrsCopy.put(ocType, newArrayList(Attributes.empty(ocType, attrName)));
              }
              else
              {
                Attribute ocAttr = getObjectClassAttribute();
                if (ocAttr != null)
                {
                  if (!attrName.equals(ocAttr.getName()))
                  {
                    // User requested non-default object class type name.
                    AttributeBuilder builder = new AttributeBuilder(ocAttr);
                    builder.setAttributeType(ocType, attrName);
                    ocAttr = builder.toAttribute();
                  }

                  userAttrsCopy.put(ocType, newArrayList(ocAttr));
                }
              }
            }
          }
          else
          {
            List<Attribute> attrList = getUserAttribute(attrType);
            if (attrList != null)
            {
              mergeAttributeLists(attrList, userAttrsCopy, attrType,
                  attrName, options, omitValues, omitReal, omitVirtual);
            }
            else
            {
              attrList = getOperationalAttribute(attrType);
              if (attrList != null)
              {
                mergeAttributeLists(attrList, operationalAttrsCopy,
                    attrType, attrName, options, omitValues, omitReal,
                    omitVirtual);
              }
            }
          }
        }
      }
    }

    return new Entry(dn, objectClassesCopy, userAttrsCopy,
                     operationalAttrsCopy);
  }

  /**
   * Copies the provided list of attributes into the destination
   * attribute map according to the provided criteria.
   *
   * @param sourceList
   *          The list containing the attributes to be copied.
   * @param destMap
   *          The map where the attributes should be copied to.
   * @param attrType
   *          The attribute type.
   * @param attrName
   *          The user-provided attribute name.
   * @param options
   *          The user-provided attribute options.
   * @param omitValues
   *          Indicates whether to exclude attribute values.
   * @param omitReal
   *          Indicates whether to exclude real attributes.
   * @param omitVirtual
   *          Indicates whether to exclude virtual attributes.
   */
  private void mergeAttributeLists(List<Attribute> sourceList,
      Map<AttributeType, List<Attribute>> destMap,
      AttributeType attrType, String attrName, Set<String> options,
      boolean omitValues, boolean omitReal, boolean omitVirtual)
  {
    if (sourceList == null)
    {
      return;
    }

    for (Attribute attribute : sourceList)
    {
      if (attribute.isEmpty()
          || (omitReal && attribute.isReal())
          || (omitVirtual && attribute.isVirtual())
          || !attribute.hasAllOptions(options))
      {
        continue;
      }
      else
      {
        // If a non-default attribute name was provided or if the
        // attribute has options then we will need to rebuild the
        // attribute so that it contains the user-requested names and options.
        AttributeType subAttrType = attribute.getAttributeType();

        if ((attrName != null && !attrName.equals(attribute.getName()))
            || (options != null && !options.isEmpty()))
        {
          AttributeBuilder builder = new AttributeBuilder();

          // We want to use the user-provided name only if this attribute has
          // the same type as the requested type. This might not be the case for
          // sub-types e.g. requesting "name" and getting back "cn" - we don't
          // want to rename "name" to "cn".
          if (attrName == null || !subAttrType.equals(attrType))
          {
            builder.setAttributeType(subAttrType, attribute.getName());
          }
          else
          {
            builder.setAttributeType(subAttrType, attrName);
          }

          if (options != null)
          {
            builder.setOptions(options);
          }

          // Now add in remaining options from original attribute
          // (this will not overwrite options already present).
          builder.setOptions(attribute.getOptions());

          if (!omitValues)
          {
            builder.addAll(attribute);
          }

          attribute = builder.toAttribute();
        }
        else if (omitValues)
        {
          attribute = Attributes.empty(attribute);
        }

        // Now put the attribute into the destination map.
        // Be careful of duplicates.
        List<Attribute> attrList = destMap.get(subAttrType);

        if (attrList == null)
        {
          // Assume that they'll all go in the one list. This isn't
          // always the case, for example if the list contains sub-types.
          attrList = new ArrayList<>(sourceList.size());
          attrList.add(attribute);
          destMap.put(subAttrType, attrList);
        }
        else
        {
          // The attribute may have already been put in the list.
          //
          // This may occur in two cases:
          //
          // 1) The attribute is identified by more than one attribute
          //    type description in the attribute list (e.g. in a wildcard).
          //
          // 2) The attribute has both a real and virtual component.
          //
          boolean found = false;
          for (int i = 0; i < attrList.size(); i++)
          {
            Attribute otherAttribute = attrList.get(i);
            if (otherAttribute.optionsEqual(attribute.getOptions()))
            {
              // Assume that wildcards appear first in an attribute
              // list with more specific attribute names afterwards:
              // let the attribute name and options from the later
              // attribute take preference.
              attrList.set(i, Attributes.merge(attribute, otherAttribute));
              found = true;
            }
          }

          if (!found)
          {
            attrList.add(attribute);
          }
        }
      }
    }
  }
}
