From 8d673dd2b125d0b974eb1e8376e053731c628354 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 19 Sep 2012 22:52:22 +0000
Subject: [PATCH] Fix OPENDJ-157: Make methods like Entry.getAttribute(String) more user friendly

---
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java                |  217 ++++---
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java           |    2 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractMapEntry.java             |   41 
 opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AttributeDescriptionTestCase.java |   56 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java         |  100 ++-
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/AttributeType.java         |  115 +++
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entry.java                        |   50 +
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java                   |  220 +++++++
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java       |    2 
 opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/EntryTestCase.java                |  794 ++++++++++++++++++++++++++-
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java                |  109 +-
 11 files changed, 1,402 insertions(+), 304 deletions(-)

diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java
index 9aa3359..0112e4c 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java
@@ -28,6 +28,7 @@
 package org.forgerock.opendj.ldap;
 
 import java.util.Collection;
+import java.util.Iterator;
 
 import com.forgerock.opendj.util.Iterables;
 import com.forgerock.opendj.util.Predicate;
@@ -43,6 +44,7 @@
     private static final Predicate<Attribute, AttributeDescription> FIND_ATTRIBUTES_PREDICATE =
             new Predicate<Attribute, AttributeDescription>() {
 
+                @Override
                 public boolean matches(final Attribute value, final AttributeDescription p) {
                     return value.getAttributeDescription().isSubTypeOf(p);
                 }
@@ -50,94 +52,6 @@
             };
 
     /**
-     * Returns {@code true} if {@code object} is an entry which is equal to
-     * {@code entry}. Two entry are considered equal if their distinguished
-     * names are equal, they both have the same number of attributes, and every
-     * attribute contained in the first entry is also contained in the second
-     * entry.
-     *
-     * @param entry
-     *            The entry to be tested for equality.
-     * @param object
-     *            The object to be tested for equality with the entry.
-     * @return {@code true} if {@code object} is an entry which is equal to
-     *         {@code entry}, or {@code false} if not.
-     */
-    static boolean equals(final Entry entry, final Object object) {
-        if (entry == object) {
-            return true;
-        }
-
-        if (!(object instanceof Entry)) {
-            return false;
-        }
-
-        final Entry other = (Entry) object;
-        if (!entry.getName().equals(other.getName())) {
-            return false;
-        }
-
-        // Distinguished name is the same, compare attributes.
-        if (entry.getAttributeCount() != other.getAttributeCount()) {
-            return false;
-        }
-
-        for (final Attribute attribute : entry.getAllAttributes()) {
-            final Attribute otherAttribute =
-                    other.getAttribute(attribute.getAttributeDescription());
-
-            if (!attribute.equals(otherAttribute)) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Returns the hash code for {@code entry}. It will be calculated as the sum
-     * of the hash codes of the distinguished name and all of the attributes.
-     *
-     * @param entry
-     *            The entry whose hash code should be calculated.
-     * @return The hash code for {@code entry}.
-     */
-    static int hashCode(final Entry entry) {
-        int hashCode = entry.getName().hashCode();
-        for (final Attribute attribute : entry.getAllAttributes()) {
-            hashCode += attribute.hashCode();
-        }
-        return hashCode;
-    }
-
-    /**
-     * Returns a string representation of {@code entry}.
-     *
-     * @param entry
-     *            The entry whose string representation should be returned.
-     * @return The string representation of {@code entry}.
-     */
-    static String toString(final Entry entry) {
-        final StringBuilder builder = new StringBuilder();
-        builder.append("Entry(");
-        builder.append(entry.getName());
-        builder.append(", {");
-
-        boolean firstValue = true;
-        for (final Attribute attribute : entry.getAllAttributes()) {
-            if (!firstValue) {
-                builder.append(", ");
-            }
-
-            builder.append(attribute);
-            firstValue = false;
-        }
-
-        builder.append("})");
-        return builder.toString();
-    }
-
-    /**
      * Sole constructor.
      */
     protected AbstractEntry() {
@@ -147,6 +61,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public boolean addAttribute(final Attribute attribute) {
         return addAttribute(attribute, null);
     }
@@ -154,6 +69,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public Entry addAttribute(final String attributeDescription, final Object... values) {
         addAttribute(new LinkedAttribute(attributeDescription, values), null);
         return this;
@@ -162,6 +78,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public boolean containsAttribute(final Attribute attribute,
             final Collection<ByteString> missingValues) {
         final Attribute a = getAttribute(attribute.getAttributeDescription());
@@ -187,6 +104,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public boolean containsAttribute(final String attributeDescription, final Object... values) {
         return containsAttribute(new LinkedAttribute(attributeDescription, values), null);
     }
@@ -196,12 +114,34 @@
      */
     @Override
     public boolean equals(final Object object) {
-        return equals(this, object);
+        if (this == object) {
+            return true;
+        } else if (object instanceof Entry) {
+            final Entry other = (Entry) object;
+            if (!this.getName().equals(other.getName())) {
+                return false;
+            }
+            // Distinguished name is the same, compare attributes.
+            if (this.getAttributeCount() != other.getAttributeCount()) {
+                return false;
+            }
+            for (final Attribute attribute : this.getAllAttributes()) {
+                final Attribute otherAttribute =
+                        other.getAttribute(attribute.getAttributeDescription());
+                if (!attribute.equals(otherAttribute)) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            return false;
+        }
     }
 
     /**
      * {@inheritDoc}
      */
+    @Override
     public Iterable<Attribute> getAllAttributes(final AttributeDescription attributeDescription) {
         Validator.ensureNotNull(attributeDescription);
 
@@ -212,6 +152,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public Iterable<Attribute> getAllAttributes(final String attributeDescription) {
         return getAllAttributes(AttributeDescription.valueOf(attributeDescription));
     }
@@ -219,6 +160,21 @@
     /**
      * {@inheritDoc}
      */
+    @Override
+    public Attribute getAttribute(final AttributeDescription attributeDescription) {
+        for (final Attribute attribute : getAllAttributes()) {
+            final AttributeDescription ad = attribute.getAttributeDescription();
+            if (isAssignable(attributeDescription, ad)) {
+                return attribute;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public Attribute getAttribute(final String attributeDescription) {
         return getAttribute(AttributeDescription.valueOf(attributeDescription));
     }
@@ -228,26 +184,64 @@
      */
     @Override
     public int hashCode() {
-        return hashCode(this);
+        int hashCode = this.getName().hashCode();
+        for (final Attribute attribute : this.getAllAttributes()) {
+            hashCode += attribute.hashCode();
+        }
+        return hashCode;
     }
 
     /**
      * {@inheritDoc}
      */
-    public AttributeParser parseAttribute(AttributeDescription attributeDescription) {
+    @Override
+    public AttributeParser parseAttribute(final AttributeDescription attributeDescription) {
         return AttributeParser.parseAttribute(getAttribute(attributeDescription));
     }
 
     /**
      * {@inheritDoc}
      */
-    public AttributeParser parseAttribute(String attributeDescription) {
+    @Override
+    public AttributeParser parseAttribute(final String attributeDescription) {
         return AttributeParser.parseAttribute(getAttribute(attributeDescription));
     }
 
     /**
      * {@inheritDoc}
      */
+    @Override
+    public boolean removeAttribute(final Attribute attribute,
+            final Collection<ByteString> missingValues) {
+        final Iterator<Attribute> i = getAllAttributes().iterator();
+        final AttributeDescription attributeDescription = attribute.getAttributeDescription();
+        while (i.hasNext()) {
+            final Attribute oldAttribute = i.next();
+            if (isAssignable(attributeDescription, oldAttribute.getAttributeDescription())) {
+                if (attribute.isEmpty()) {
+                    i.remove();
+                    return true;
+                } else {
+                    final boolean modified = oldAttribute.removeAll(attribute, missingValues);
+                    if (oldAttribute.isEmpty()) {
+                        i.remove();
+                        return true;
+                    }
+                    return modified;
+                }
+            }
+        }
+        // Not found.
+        if (missingValues != null) {
+            missingValues.addAll(attribute);
+        }
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public boolean removeAttribute(final AttributeDescription attributeDescription) {
         return removeAttribute(Attributes.emptyAttribute(attributeDescription), null);
     }
@@ -255,6 +249,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public Entry removeAttribute(final String attributeDescription, final Object... values) {
         removeAttribute(new LinkedAttribute(attributeDescription, values), null);
         return this;
@@ -263,12 +258,20 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public boolean replaceAttribute(final Attribute attribute) {
         if (attribute.isEmpty()) {
             return removeAttribute(attribute.getAttributeDescription());
         } else {
-            removeAttribute(attribute.getAttributeDescription());
-            addAttribute(attribute, null);
+            // For consistency with addAttribute and removeAttribute, preserve
+            // the existing attribute if it already exists.
+            final Attribute oldAttribute = getAttribute(attribute.getAttributeDescription());
+            if (oldAttribute != null) {
+                oldAttribute.clear();
+                oldAttribute.addAll(attribute);
+            } else {
+                addAttribute(attribute, null);
+            }
             return true;
         }
     }
@@ -276,6 +279,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public Entry replaceAttribute(final String attributeDescription, final Object... values) {
         replaceAttribute(new LinkedAttribute(attributeDescription, values));
         return this;
@@ -284,6 +288,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public Entry setName(final String dn) {
         return setName(DN.valueOf(dn));
     }
@@ -293,7 +298,29 @@
      */
     @Override
     public String toString() {
-        return toString(this);
+        final StringBuilder builder = new StringBuilder();
+        builder.append("Entry(");
+        builder.append(this.getName());
+        builder.append(", {");
+        boolean firstValue = true;
+        for (final Attribute attribute : this.getAllAttributes()) {
+            if (!firstValue) {
+                builder.append(", ");
+            }
+
+            builder.append(attribute);
+            firstValue = false;
+        }
+        builder.append("})");
+        return builder.toString();
+    }
+
+    private boolean isAssignable(final AttributeDescription from, final AttributeDescription to) {
+        if (!from.isPlaceHolder()) {
+            return from.equals(to);
+        } else {
+            return from.matches(to);
+        }
     }
 
 }
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractMapEntry.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractMapEntry.java
index 9825e45..e6d23e1 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractMapEntry.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractMapEntry.java
@@ -37,7 +37,6 @@
  */
 abstract class AbstractMapEntry extends AbstractEntry {
     private final Map<AttributeDescription, Attribute> attributes;
-
     private DN name;
 
     /**
@@ -57,12 +56,11 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public final boolean addAttribute(final Attribute attribute,
             final Collection<ByteString> duplicateValues) {
-        Validator.ensureNotNull(attribute);
-
         final AttributeDescription attributeDescription = attribute.getAttributeDescription();
-        final Attribute oldAttribute = attributes.get(attributeDescription);
+        final Attribute oldAttribute = getAttribute(attributeDescription);
         if (oldAttribute != null) {
             return oldAttribute.addAll(attribute, duplicateValues);
         } else {
@@ -74,6 +72,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public final Entry clearAttributes() {
         attributes.clear();
         return this;
@@ -82,6 +81,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public final Iterable<Attribute> getAllAttributes() {
         return attributes.values();
     }
@@ -89,15 +89,21 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public final Attribute getAttribute(final AttributeDescription attributeDescription) {
-        Validator.ensureNotNull(attributeDescription);
-
-        return attributes.get(attributeDescription);
+        final Attribute attribute = attributes.get(attributeDescription);
+        if (attribute == null && attributeDescription.isPlaceHolder()) {
+            // Fall-back to inefficient search using place-holder.
+            return super.getAttribute(attributeDescription);
+        } else {
+            return attribute;
+        }
     }
 
     /**
      * {@inheritDoc}
      */
+    @Override
     public final int getAttributeCount() {
         return attributes.size();
     }
@@ -105,6 +111,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public final DN getName() {
         return name;
     }
@@ -112,20 +119,27 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public final boolean removeAttribute(final Attribute attribute,
             final Collection<ByteString> missingValues) {
-        Validator.ensureNotNull(attribute);
-
         final AttributeDescription attributeDescription = attribute.getAttributeDescription();
-
         if (attribute.isEmpty()) {
-            return attributes.remove(attributeDescription) != null;
+            if (attributes.remove(attributeDescription) != null) {
+                return true;
+            } else if (attributeDescription.isPlaceHolder()) {
+                // Fall-back to inefficient remove using place-holder.
+                return super.removeAttribute(attribute, missingValues);
+            } else {
+                return false;
+            }
         } else {
-            final Attribute oldAttribute = attributes.get(attributeDescription);
+            final Attribute oldAttribute = getAttribute(attributeDescription);
             if (oldAttribute != null) {
                 final boolean modified = oldAttribute.removeAll(attribute, missingValues);
                 if (oldAttribute.isEmpty()) {
-                    attributes.remove(attributeDescription);
+                    // Use old attribute's description in case it is different
+                    // (e.g. this may be the case when using place-holders).
+                    attributes.remove(oldAttribute.getAttributeDescription());
                     return true;
                 }
                 return modified;
@@ -141,6 +155,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public final Entry setName(final DN dn) {
         Validator.ensureNotNull(dn);
         this.name = dn;
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java
index ebefad2..d7c9d6b 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java
@@ -67,7 +67,7 @@
 
         public abstract int compareTo(Impl other);
 
-        public abstract boolean containsOption(String normalizedOption);
+        public abstract boolean hasOption(String normalizedOption);
 
         public abstract boolean equals(Impl other);
 
@@ -128,7 +128,7 @@
         }
 
         @Override
-        public boolean containsOption(final String normalizedOption) {
+        public boolean hasOption(final String normalizedOption) {
             final int sz = normalizedOptions.length;
             for (int i = 0; i < sz; i++) {
                 if (normalizedOptions[i].equals(normalizedOption)) {
@@ -169,7 +169,7 @@
             if (other == ZERO_OPTION_IMPL) {
                 return true;
             } else if (other.size() == 1) {
-                return containsOption(other.firstNormalizedOption());
+                return hasOption(other.firstNormalizedOption());
             } else if (other.size() > size()) {
                 return false;
             } else {
@@ -179,7 +179,7 @@
                 // not worth it.
                 final MultiOptionImpl tmp = (MultiOptionImpl) other;
                 for (final String normalizedOption : tmp.normalizedOptions) {
-                    if (!containsOption(normalizedOption)) {
+                    if (!hasOption(normalizedOption)) {
                         return false;
                     }
                 }
@@ -191,7 +191,7 @@
         public boolean isSuperTypeOf(final Impl other) {
             // Must contain a sub-set of other's options.
             for (final String normalizedOption : normalizedOptions) {
-                if (!other.containsOption(normalizedOption)) {
+                if (!other.hasOption(normalizedOption)) {
                     return false;
                 }
             }
@@ -235,13 +235,13 @@
         }
 
         @Override
-        public boolean containsOption(final String normalizedOption) {
+        public boolean hasOption(final String normalizedOption) {
             return this.normalizedOption.equals(normalizedOption);
         }
 
         @Override
         public boolean equals(final Impl other) {
-            return other.size() == 1 && other.containsOption(normalizedOption);
+            return other.size() == 1 && other.hasOption(normalizedOption);
         }
 
         @Override
@@ -272,7 +272,7 @@
         @Override
         public boolean isSuperTypeOf(final Impl other) {
             // Other must have this option.
-            return other.containsOption(normalizedOption);
+            return other.hasOption(normalizedOption);
         }
 
         public Iterator<String> iterator() {
@@ -298,7 +298,7 @@
         }
 
         @Override
-        public boolean containsOption(final String normalizedOption) {
+        public boolean hasOption(final String normalizedOption) {
             return false;
         }
 
@@ -391,7 +391,7 @@
         Validator.ensureNotNull(option);
 
         final String normalizedOption = toLowerCase(option);
-        if (pimpl.containsOption(normalizedOption)) {
+        if (pimpl.hasOption(normalizedOption)) {
             return this;
         }
 
@@ -480,7 +480,7 @@
         Validator.ensureNotNull(option);
 
         final String normalizedOption = toLowerCase(option);
-        if (!pimpl.containsOption(normalizedOption)) {
+        if (!pimpl.hasOption(normalizedOption)) {
             return this;
         }
 
@@ -489,8 +489,7 @@
                 new StringBuilder(oldAttributeDescription.length() - option.length() - 1);
 
         final String normalizedOldAttributeDescription = toLowerCase(oldAttributeDescription);
-        final int index =
-                normalizedOldAttributeDescription.indexOf(normalizedOption);
+        final int index = normalizedOldAttributeDescription.indexOf(normalizedOption);
         builder.append(oldAttributeDescription, 0, index - 1 /* to semi-colon */);
         builder.append(oldAttributeDescription, index + option.length(), oldAttributeDescription
                 .length());
@@ -1033,15 +1032,16 @@
      * @throws NullPointerException
      *             If {@code option} was {@code null}.
      */
-    public boolean containsOption(final String option) {
+    public boolean hasOption(final String option) {
         final String normalizedOption = toLowerCase(option);
-        return pimpl.containsOption(normalizedOption);
+        return pimpl.hasOption(normalizedOption);
     }
 
     /**
      * Indicates whether the provided object is an attribute description which
      * is equal to this attribute description. It will be considered equal if
-     * the attribute type and normalized sorted list of options are identical.
+     * the attribute types are {@link AttributeType#equals equal} and normalized
+     * sorted list of options are identical.
      *
      * @param o
      *            The object for which to make the determination.
@@ -1053,19 +1053,12 @@
     public boolean equals(final Object o) {
         if (this == o) {
             return true;
-        }
-
-        if (!(o instanceof AttributeDescription)) {
+        } else if (o instanceof AttributeDescription) {
+            final AttributeDescription other = (AttributeDescription) o;
+            return attributeType.equals(other.attributeType) && pimpl.equals(other.pimpl);
+        } else {
             return false;
         }
-
-        final AttributeDescription other = (AttributeDescription) o;
-        if (!attributeType.equals(other.attributeType)) {
-            return false;
-        }
-
-        // Attribute type is the same, compare options.
-        return pimpl.equals(other.pimpl);
     }
 
     /**
@@ -1125,14 +1118,34 @@
     }
 
     /**
+     * Indicates whether this attribute description is a temporary place-holder
+     * allocated dynamically by a non-strict schema when no corresponding
+     * registered attribute type was found.
+     * <p>
+     * Place holder attribute descriptions have an attribute type whose OID is
+     * the normalized attribute name with the string {@code -oid} appended. In
+     * addition, they will use the directory string syntax and case ignore
+     * matching rule.
+     *
+     * @return {@code true} if this is a temporary place-holder attribute
+     *         description allocated dynamically by a non-strict schema when no
+     *         corresponding registered attribute type was found.
+     * @see Schema#getAttributeType(String)
+     * @see AttributeType#isPlaceHolder()
+     */
+    public boolean isPlaceHolder() {
+        return attributeType.isPlaceHolder();
+    }
+
+    /**
      * Indicates whether or not this attribute description is a sub-type of the
      * provided attribute description as defined in RFC 4512 section 2.5.
      * Specifically, this method will return {@code true} if and only if the
      * following conditions are both {@code true}:
      * <ul>
-     * <li>This attribute description has an attribute type which is equal to,
-     * or is a sub-type of, the attribute type in the provided attribute
-     * description.
+     * <li>This attribute description has an attribute type which
+     * {@link AttributeType#matches matches}, or is a sub-type of, the attribute
+     * type in the provided attribute description.
      * <li>This attribute description contains all of the options contained in
      * the provided attribute description.
      * </ul>
@@ -1160,9 +1173,9 @@
      * Specifically, this method will return {@code true} if and only if the
      * following conditions are both {@code true}:
      * <ul>
-     * <li>This attribute description has an attribute type which is equal to,
-     * or is a super-type of, the attribute type in the provided attribute
-     * description.
+     * <li>This attribute description has an attribute type which
+     * {@link AttributeType#matches matches}, or is a super-type of, the
+     * attribute type in the provided attribute description.
      * <li>This attribute description contains a sub-set of the options
      * contained in the provided attribute description.
      * </ul>
@@ -1177,7 +1190,7 @@
      *             If {@code name} was {@code null}.
      */
     public boolean isSuperTypeOf(final AttributeDescription other) {
-        if (!other.attributeType.isSubTypeOf(attributeType)) {
+        if (!attributeType.isSuperTypeOf(other.attributeType)) {
             return false;
         } else {
             return pimpl.isSuperTypeOf(other.pimpl);
@@ -1185,6 +1198,25 @@
     }
 
     /**
+     * Indicates whether the provided attribute description matches this
+     * attribute description. It will be considered a match if the attribute
+     * types {@link AttributeType#matches match} and the normalized sorted list
+     * of options are identical.
+     *
+     * @param other
+     *            The attribute description for which to make the determination.
+     * @return {@code true} if the provided attribute description matches this
+     *         attribute description, or {@code false} if not.
+     */
+    public boolean matches(final AttributeDescription other) {
+        if (this == other) {
+            return true;
+        } else {
+            return attributeType.matches(other.attributeType) && pimpl.equals(other.pimpl);
+        }
+    }
+
+    /**
      * Returns the string representation of this attribute description as
      * defined in RFC4512 section 2.5.
      *
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java
index ff43d3d..49360e3 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java
@@ -30,6 +30,8 @@
 import java.util.Collection;
 import java.util.Iterator;
 
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+
 import com.forgerock.opendj.util.Iterators;
 import com.forgerock.opendj.util.Validator;
 
@@ -97,7 +99,6 @@
     private static final class RenamedAttribute implements Attribute {
 
         private final Attribute attribute;
-
         private final AttributeDescription attributeDescription;
 
         private RenamedAttribute(final Attribute attribute,
@@ -106,31 +107,38 @@
             this.attributeDescription = attributeDescription;
         }
 
+        @Override
         public boolean add(final ByteString value) {
             return attribute.add(value);
         }
 
+        @Override
         public boolean add(final Object firstValue, final Object... remainingValues) {
             return attribute.add(firstValue, remainingValues);
         }
 
+        @Override
         public boolean addAll(final Collection<? extends ByteString> values) {
             return attribute.addAll(values);
         }
 
+        @Override
         public boolean addAll(final Collection<? extends ByteString> values,
                 final Collection<? super ByteString> duplicateValues) {
             return attribute.addAll(values, duplicateValues);
         }
 
+        @Override
         public void clear() {
             attribute.clear();
         }
 
+        @Override
         public boolean contains(final Object value) {
             return attribute.contains(value);
         }
 
+        @Override
         public boolean containsAll(final Collection<?> values) {
             return attribute.containsAll(values);
         }
@@ -140,18 +148,22 @@
             return AbstractAttribute.equals(this, object);
         }
 
+        @Override
         public ByteString firstValue() {
             return attribute.firstValue();
         }
 
+        @Override
         public String firstValueAsString() {
             return attribute.firstValueAsString();
         }
 
+        @Override
         public AttributeDescription getAttributeDescription() {
             return attributeDescription;
         }
 
+        @Override
         public String getAttributeDescriptionAsString() {
             return attributeDescription.toString();
         }
@@ -161,48 +173,59 @@
             return AbstractAttribute.hashCode(this);
         }
 
-        public AttributeParser parse() {
-            return attribute.parse();
-        }
-
+        @Override
         public boolean isEmpty() {
             return attribute.isEmpty();
         }
 
+        @Override
         public Iterator<ByteString> iterator() {
             return attribute.iterator();
         }
 
+        @Override
+        public AttributeParser parse() {
+            return attribute.parse();
+        }
+
+        @Override
         public boolean remove(final Object value) {
             return attribute.remove(value);
         }
 
+        @Override
         public boolean removeAll(final Collection<?> values) {
             return attribute.removeAll(values);
         }
 
+        @Override
         public <T> boolean removeAll(final Collection<T> values,
                 final Collection<? super T> missingValues) {
             return attribute.removeAll(values, missingValues);
         }
 
+        @Override
         public boolean retainAll(final Collection<?> values) {
             return attribute.retainAll(values);
         }
 
+        @Override
         public <T> boolean retainAll(final Collection<T> values,
                 final Collection<? super T> missingValues) {
             return attribute.retainAll(values, missingValues);
         }
 
+        @Override
         public int size() {
             return attribute.size();
         }
 
+        @Override
         public ByteString[] toArray() {
             return attribute.toArray();
         }
 
+        @Override
         public <T> T[] toArray(final T[] array) {
             return attribute.toArray(array);
         }
@@ -215,6 +238,72 @@
     }
 
     /**
+     * Singleton attribute.
+     */
+    private static final class SingletonAttribute extends AbstractAttribute {
+
+        private final AttributeDescription attributeDescription;
+        private ByteString normalizedValue;
+        private final ByteString value;
+
+        private SingletonAttribute(final AttributeDescription attributeDescription,
+                final Object value) {
+            this.attributeDescription = attributeDescription;
+            this.value = ByteString.valueOf(value);
+        }
+
+        @Override
+        public boolean add(final ByteString value) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void clear() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean contains(final Object value) {
+            final ByteString normalizedValue = normalizeValue(this, ByteString.valueOf(value));
+            return normalizedSingleValue().equals(normalizedValue);
+        }
+
+        @Override
+        public AttributeDescription getAttributeDescription() {
+            return attributeDescription;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+
+        @Override
+        public Iterator<ByteString> iterator() {
+            return Iterators.singletonIterator(value);
+        }
+
+        @Override
+        public boolean remove(final Object value) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int size() {
+            return 1;
+        }
+
+        // Lazily computes the normalized single value.
+        private ByteString normalizedSingleValue() {
+            if (normalizedValue == null) {
+                normalizedValue = normalizeValue(this, value);
+            }
+            return normalizedValue;
+        }
+
+    }
+
+    /**
      * Unmodifiable attribute.
      */
     private static final class UnmodifiableAttribute implements Attribute {
@@ -225,31 +314,38 @@
             this.attribute = attribute;
         }
 
+        @Override
         public boolean add(final ByteString value) {
             throw new UnsupportedOperationException();
         }
 
+        @Override
         public boolean add(final Object firstValue, final Object... remainingValues) {
             throw new UnsupportedOperationException();
         }
 
+        @Override
         public boolean addAll(final Collection<? extends ByteString> values) {
             throw new UnsupportedOperationException();
         }
 
+        @Override
         public boolean addAll(final Collection<? extends ByteString> values,
                 final Collection<? super ByteString> duplicateValues) {
             throw new UnsupportedOperationException();
         }
 
+        @Override
         public void clear() {
             throw new UnsupportedOperationException();
         }
 
+        @Override
         public boolean contains(final Object value) {
             return attribute.contains(value);
         }
 
+        @Override
         public boolean containsAll(final Collection<?> values) {
             return attribute.containsAll(values);
         }
@@ -259,18 +355,22 @@
             return (object == this || attribute.equals(object));
         }
 
+        @Override
         public ByteString firstValue() {
             return attribute.firstValue();
         }
 
+        @Override
         public String firstValueAsString() {
             return attribute.firstValueAsString();
         }
 
+        @Override
         public AttributeDescription getAttributeDescription() {
             return attribute.getAttributeDescription();
         }
 
+        @Override
         public String getAttributeDescriptionAsString() {
             return attribute.getAttributeDescriptionAsString();
         }
@@ -280,48 +380,59 @@
             return attribute.hashCode();
         }
 
+        @Override
         public boolean isEmpty() {
             return attribute.isEmpty();
         }
 
+        @Override
         public Iterator<ByteString> iterator() {
             return Iterators.unmodifiableIterator(attribute.iterator());
         }
 
+        @Override
         public AttributeParser parse() {
             return attribute.parse();
         }
 
+        @Override
         public boolean remove(final Object value) {
             throw new UnsupportedOperationException();
         }
 
+        @Override
         public boolean removeAll(final Collection<?> values) {
             throw new UnsupportedOperationException();
         }
 
+        @Override
         public <T> boolean removeAll(final Collection<T> values,
                 final Collection<? super T> missingValues) {
             throw new UnsupportedOperationException();
         }
 
+        @Override
         public boolean retainAll(final Collection<?> values) {
             throw new UnsupportedOperationException();
         }
 
+        @Override
         public <T> boolean retainAll(final Collection<T> values,
                 final Collection<? super T> missingValues) {
             throw new UnsupportedOperationException();
         }
 
+        @Override
         public int size() {
             return attribute.size();
         }
 
+        @Override
         public ByteString[] toArray() {
             return attribute.toArray();
         }
 
+        @Override
         public <T> T[] toArray(final T[] array) {
             return attribute.toArray(array);
         }
@@ -330,12 +441,13 @@
         public String toString() {
             return attribute.toString();
         }
-
     }
 
     /**
      * Returns a read-only empty attribute having the specified attribute
-     * description.
+     * description. Attempts to modify the returned attribute either directly,
+     * or indirectly via an iterator, result in an
+     * {@code UnsupportedOperationException}.
      *
      * @param attributeDescription
      *            The attribute description.
@@ -348,6 +460,26 @@
     }
 
     /**
+     * Returns a read-only empty attribute having the specified attribute
+     * description. The attribute description will be decoded using the default
+     * schema. Attempts to modify the returned attribute either directly, or
+     * indirectly via an iterator, result in an
+     * {@code UnsupportedOperationException}.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @return The empty attribute.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    public static final Attribute emptyAttribute(final String attributeDescription) {
+        return emptyAttribute(AttributeDescription.valueOf(attributeDescription));
+    }
+
+    /**
      * Returns a view of {@code attribute} having a different attribute
      * description. All operations on the returned attribute "pass-through" to
      * the underlying attribute.
@@ -368,6 +500,80 @@
     }
 
     /**
+     * Returns a view of {@code attribute} having a different attribute
+     * description. All operations on the returned attribute "pass-through" to
+     * the underlying attribute. The attribute description will be decoded using
+     * the default schema.
+     *
+     * @param attribute
+     *            The attribute to be renamed.
+     * @param attributeDescription
+     *            The new attribute description for {@code attribute}.
+     * @return A renamed view of {@code attribute}.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws NullPointerException
+     *             If {@code attribute} or {@code attributeDescription} was
+     *             {@code null}.
+     */
+    public static final Attribute renameAttribute(final Attribute attribute,
+            final String attributeDescription) {
+        Validator.ensureNotNull(attribute, attributeDescription);
+        return renameAttribute(attribute, AttributeDescription.valueOf(attributeDescription));
+    }
+
+    /**
+     * Returns a read-only single-valued attribute having the specified
+     * attribute description and value. Attempts to modify the returned
+     * attribute either directly, or indirectly via an iterator, result in an
+     * {@code UnsupportedOperationException}.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOf(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param value
+     *            The single attribute value.
+     * @return The single-valued attribute.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code value} was
+     *             {@code null}.
+     */
+    public static final Attribute singletonAttribute(
+            final AttributeDescription attributeDescription, final Object value) {
+        return new SingletonAttribute(attributeDescription, value);
+    }
+
+    /**
+     * Returns a read-only single-valued attribute having the specified
+     * attribute description. The attribute description will be decoded using
+     * the default schema. Attempts to modify the returned attribute either
+     * directly, or indirectly via an iterator, result in an
+     * {@code UnsupportedOperationException}.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOf(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param value
+     *            The single attribute value.
+     * @return The single-valued attribute.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code value} was
+     *             {@code null}.
+     */
+    public static final Attribute singletonAttribute(final String attributeDescription,
+            final Object value) {
+        return singletonAttribute(AttributeDescription.valueOf(attributeDescription), value);
+    }
+
+    /**
      * Returns a read-only view of {@code attribute}. Query operations on the
      * returned attribute "read-through" to the underlying attribute, and
      * attempts to modify the returned attribute either directly or indirectly
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entry.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entry.java
index 3c1c382..0688857 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entry.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entry.java
@@ -38,8 +38,12 @@
  * {@link #addAttribute(String, Object...)} and {@link #setName(String)}). In
  * these cases the default schema is used unless an alternative schema is
  * specified in the {@code Entry} constructor. The default schema is not used
- * for any other purpose. In particular, an {@code Entry} will permit attributes
- * to be added which have been decoded using multiple schemas.
+ * for any other purpose. In particular, an {@code Entry} may contain attributes
+ * which have been decoded using different schemas.
+ * <p>
+ * When determining whether or not an entry already contains a particular
+ * attribute, attribute descriptions will be compared using
+ * {@link AttributeDescription#matches}.
  * <p>
  * Full LDAP modify semantics are provided via the {@link #addAttribute},
  * {@link #removeAttribute}, and {@link #replaceAttribute} methods.
@@ -64,12 +68,14 @@
      * Ensures that this entry contains the provided attribute and values
      * (optional operation). This method has the following semantics:
      * <ul>
-     * <li>If this entry does not already contain an attribute with the same
-     * attribute description, then this entry will be modified such that it
-     * contains {@code attribute}, even if it is empty.
-     * <li>If this entry already contains an attribute with the same attribute
-     * description, then the attribute values contained in {@code attribute}
-     * will be merged with the existing attribute values.
+     * <li>If this entry does not already contain an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * this entry will be modified such that it contains {@code attribute}, even
+     * if it is empty.
+     * <li>If this entry already contains an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * the attribute values contained in {@code attribute} will be merged with
+     * the existing attribute values.
      * </ul>
      * <p>
      * <b>NOTE:</b> When {@code attribute} is non-empty, this method implements
@@ -91,12 +97,14 @@
      * Ensures that this entry contains the provided attribute and values
      * (optional operation). This method has the following semantics:
      * <ul>
-     * <li>If this entry does not already contain an attribute with the same
-     * attribute description, then this entry will be modified such that it
-     * contains {@code attribute}, even if it is empty.
-     * <li>If this entry already contains an attribute with the same attribute
-     * description, then the attribute values contained in {@code attribute}
-     * will be merged with the existing attribute values.
+     * <li>If this entry does not already contain an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * this entry will be modified such that it contains {@code attribute}, even
+     * if it is empty.
+     * <li>If this entry already contains an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * the attribute values contained in {@code attribute} will be merged with
+     * the existing attribute values.
      * </ul>
      * <p>
      * <b>NOTE:</b> When {@code attribute} is non-empty, this method implements
@@ -121,12 +129,14 @@
      * Ensures that this entry contains the provided attribute and values
      * (optional operation). This method has the following semantics:
      * <ul>
-     * <li>If this entry does not already contain an attribute with the same
-     * attribute description, then this entry will be modified such that it
-     * contains {@code attribute}, even if it is empty.
-     * <li>If this entry already contains an attribute with the same attribute
-     * description, then the attribute values contained in {@code attribute}
-     * will be merged with the existing attribute values.
+     * <li>If this entry does not already contain an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * this entry will be modified such that it contains {@code attribute}, even
+     * if it is empty.
+     * <li>If this entry already contains an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * the attribute values contained in {@code attribute} will be merged with
+     * the existing attribute values.
      * </ul>
      * <p>
      * The attribute description will be decoded using the schema associated
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/AttributeType.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/AttributeType.java
index 589d1db..e5aa80b 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/AttributeType.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/AttributeType.java
@@ -74,6 +74,9 @@
     // Indicates whether this definition is declared "obsolete".
     private final boolean isObsolete;
 
+    // Indicates whether this definition is a temporary place-holder.
+    private final boolean isPlaceHolder;
+
     // Indicates whether this attribute type is declared "single-value".
     private final boolean isSingleValue;
 
@@ -162,34 +165,45 @@
         }
 
         this.isObjectClassType = oid.equals("2.5.4.0");
+        this.isPlaceHolder = false;
         this.normalizedName = StaticUtils.toLowerCase(getNameOrOID());
     }
 
-    AttributeType(final String oid, final List<String> names, final String description,
-            final MatchingRule equalityMatchingRule, final Syntax syntax) {
-        super(description, Collections.<String, List<String>> emptyMap());
+    /**
+     * Creates a new place-holder attribute type having the specified name,
+     * default syntax, and default matching rule. The OID of the place-holder
+     * attribute will be the normalized attribute type name followed by the
+     * suffix "-oid".
+     *
+     * @param name
+     *            The name of the place-holder attribute type.
+     */
+    AttributeType(final String name) {
+        super("", Collections.<String, List<String>> emptyMap());
 
-        Validator.ensureNotNull(oid, names, description);
+        final StringBuilder builder = new StringBuilder(name.length() + 4);
+        StaticUtils.toLowerCase(name, builder);
+        builder.append("-oid");
 
-        this.oid = oid;
-        this.names = names;
+        this.oid = builder.toString();
+        this.names = Collections.singletonList(name);
         this.isObsolete = false;
         this.superiorTypeOID = null;
         this.superiorType = null;
+        this.equalityMatchingRule = Schema.getDefaultMatchingRule();
         this.equalityMatchingRuleOID = equalityMatchingRule.getOID();
-        this.equalityMatchingRule = equalityMatchingRule;
         this.orderingMatchingRuleOID = null;
         this.substringMatchingRuleOID = null;
         this.approximateMatchingRuleOID = null;
+        this.syntax = Schema.getDefaultSyntax();
         this.syntaxOID = syntax.getOID();
-        this.syntax = syntax;
         this.isSingleValue = false;
         this.isCollective = false;
         this.isNoUserModification = false;
         this.attributeUsage = AttributeUsage.USER_APPLICATIONS;
         this.definition = buildDefinition();
-
-        this.isObjectClassType = oid.equals("2.5.4.0");
+        this.isObjectClassType = false;
+        this.isPlaceHolder = true;
         this.normalizedName = StaticUtils.toLowerCase(getNameOrOID());
     }
 
@@ -200,7 +214,8 @@
      * <li>The {@code objectClass} attribute is less than all other attribute
      * types.
      * <li>User attributes are less than operational attributes.
-     * <li>Lexicographic comparison of the primary name or OID.
+     * <li>Lexicographic comparison of the primary name and then, if equal, the
+     * OID.
      * </ul>
      *
      * @param type
@@ -219,9 +234,13 @@
         } else {
             final boolean isOperational = getUsage().isOperational();
             final boolean typeIsOperational = type.getUsage().isOperational();
-
             if (isOperational == typeIsOperational) {
-                return normalizedName.compareTo(type.normalizedName);
+                final int tmp = normalizedName.compareTo(type.normalizedName);
+                if (tmp == 0) {
+                    return oid.compareTo(type.oid);
+                } else {
+                    return tmp;
+                }
             } else {
                 return isOperational ? 1 : -1;
             }
@@ -452,6 +471,24 @@
     }
 
     /**
+     * Indicates whether this attribute type is a temporary place-holder
+     * allocated dynamically by a non-strict schema when no registered attribute
+     * type was found.
+     * <p>
+     * Place holder attribute types have an OID which is the normalized
+     * attribute name with the string {@code -oid} appended. In addition, they
+     * will use the directory string syntax and case ignore matching rule.
+     *
+     * @return {@code true} if this is a temporary place-holder attribute type
+     *         allocated dynamically by a non-strict schema when no registered
+     *         attribute type was found.
+     * @see Schema#getAttributeType(String)
+     */
+    public boolean isPlaceHolder() {
+        return isPlaceHolder;
+    }
+
+    /**
      * Indicates whether this attribute type is declared "single-value".
      *
      * @return {@code true} if this attribute type is declared "single-value",
@@ -475,7 +512,7 @@
     public boolean isSubTypeOf(final AttributeType type) {
         AttributeType tmp = this;
         do {
-            if (tmp.equals(type)) {
+            if (tmp.matches(type)) {
                 return true;
             }
             tmp = tmp.getSuperiorType();
@@ -484,6 +521,50 @@
     }
 
     /**
+     * Indicates whether or not this attribute type is a super-type of the
+     * provided attribute type.
+     *
+     * @param type
+     *            The attribute type for which to make the determination.
+     * @return {@code true} if this attribute type is a super-type of the
+     *         provided attribute type, or {@code false} if not.
+     * @throws NullPointerException
+     *             If {@code type} was {@code null}.
+     */
+    public boolean isSuperTypeOf(final AttributeType type) {
+        return type.isSubTypeOf(this);
+    }
+
+    /**
+     * Implements a place-holder tolerant version of {@link #equals}. This
+     * method returns {@true} in the following cases:
+     * <ul>
+     * <li>this attribute type is equal to the provided attribute type as
+     * specified by {@link #equals}
+     * <li>this attribute type is a place-holder and the provided attribute type
+     * has a name which matches the name of this attribute type
+     * <li>the provided attribute type is a place-holder and this attribute type
+     * has a name which matches the name of the provided attribute type.
+     * </ul>
+     *
+     * @param type
+     *            The attribute type for which to make the determination.
+     * @return {@code true} if the provided attribute type matches this
+     *         attribute type.
+     */
+    public boolean matches(final AttributeType type) {
+        if (this == type) {
+            return true;
+        } else if (oid.equals(type.oid)) {
+            return true;
+        } else if (isPlaceHolder != type.isPlaceHolder) {
+            return isPlaceHolder ? type.hasName(normalizedName) : hasName(type.normalizedName);
+        } else {
+            return false;
+        }
+    }
+
+    /**
      * Returns the string representation of this schema definition in the form
      * specified in RFC 2252.
      *
@@ -591,8 +672,7 @@
     boolean validate(final Schema schema, final List<AttributeType> invalidSchemaElements,
             final List<LocalizableMessage> warnings) {
         // Avoid validating this schema element more than once. This may occur
-        // if
-        // multiple attributes specify the same superior.
+        // if multiple attributes specify the same superior.
         if (!needsValidating) {
             return isValid;
         }
@@ -612,8 +692,7 @@
             }
 
             // First ensure that the superior has been validated and fail if it
-            // is
-            // invalid.
+            // is invalid.
             if (!superiorType.validate(schema, invalidSchemaElements, warnings)) {
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_ATTRTYPE_INVALID_SUPERIOR_TYPE.get(getNameOrOID(),
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java
index ad4db1e..8ccd84a 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java
@@ -73,8 +73,10 @@
 public final class Schema {
     private static final class EmptyImpl implements Impl {
 
-        private EmptyImpl() {
-            // Nothing to do.
+        private final boolean isStrict;
+
+        private EmptyImpl(final boolean isStrict) {
+            this.isStrict = isStrict;
         }
 
         public boolean allowMalformedNamesAndOptions() {
@@ -95,17 +97,12 @@
 
         @Override
         public AttributeType getAttributeType(final String name) {
-            // Construct an placeholder attribute type with the given name,
-            // the default matching rule, and the default syntax. The OID of
-            // the attribute will be the normalized OID alias with "-oid"
-            // appended to the given name.
-            final StringBuilder builder = new StringBuilder(name.length() + 4);
-            StaticUtils.toLowerCase(name, builder);
-            builder.append("-oid");
-            final String noid = builder.toString();
-
-            return new AttributeType(noid, Collections.singletonList(name), "", Schema
-                    .getDefaultMatchingRule(), Schema.getDefaultSyntax());
+            if (isStrict) {
+                throw new UnknownSchemaElementException(WARN_ATTR_TYPE_UNKNOWN.get(name));
+            } else {
+                // Return a place-holder.
+                return new AttributeType(name);
+            }
         }
 
         @Override
@@ -298,7 +295,7 @@
 
         @Override
         public boolean isStrict() {
-            return false;
+            return isStrict;
         }
     }
 
@@ -389,9 +386,9 @@
     }
 
     private static final class NonStrictImpl implements Impl {
-        private final Impl strictImpl;
+        private final StrictImpl strictImpl;
 
-        private NonStrictImpl(final Impl strictImpl) {
+        private NonStrictImpl(final StrictImpl strictImpl) {
             this.strictImpl = strictImpl;
         }
 
@@ -413,20 +410,8 @@
 
         @Override
         public AttributeType getAttributeType(final String name) {
-            if (!strictImpl.hasAttributeType(name)) {
-                // Construct an placeholder attribute type with the given name,
-                // the default matching rule, and the default syntax. The OID of
-                // the attribute will be the normalized OID alias with "-oid"
-                // appended to the given name.
-                final StringBuilder builder = new StringBuilder(name.length() + 4);
-                StaticUtils.toLowerCase(name, builder);
-                builder.append("-oid");
-                final String noid = builder.toString();
-
-                return new AttributeType(noid, Collections.singletonList(name), "", Schema
-                        .getDefaultMatchingRule(), Schema.getDefaultSyntax());
-            }
-            return strictImpl.getAttributeType(name);
+            final AttributeType type = strictImpl.getAttributeType0(name);
+            return type != null ? type : new AttributeType(name);
         }
 
         @Override
@@ -735,19 +720,12 @@
 
         @Override
         public AttributeType getAttributeType(final String name) {
-            final AttributeType type = numericOID2AttributeTypes.get(name);
+            final AttributeType type = getAttributeType0(name);
             if (type != null) {
                 return type;
+            } else {
+                throw new UnknownSchemaElementException(WARN_ATTR_TYPE_UNKNOWN.get(name));
             }
-            final List<AttributeType> attributes =
-                    name2AttributeTypes.get(StaticUtils.toLowerCase(name));
-            if (attributes != null) {
-                if (attributes.size() == 1) {
-                    return attributes.get(0);
-                }
-                throw new UnknownSchemaElementException(WARN_ATTR_TYPE_AMBIGIOUS.get(name));
-            }
-            throw new UnknownSchemaElementException(WARN_ATTR_TYPE_UNKNOWN.get(name));
         }
 
         @Override
@@ -1073,28 +1051,37 @@
         public boolean isStrict() {
             return true;
         }
+
+        AttributeType getAttributeType0(final String name) {
+            final AttributeType type = numericOID2AttributeTypes.get(name);
+            if (type != null) {
+                return type;
+            }
+            final List<AttributeType> attributes =
+                    name2AttributeTypes.get(StaticUtils.toLowerCase(name));
+            if (attributes != null) {
+                if (attributes.size() == 1) {
+                    return attributes.get(0);
+                }
+                throw new UnknownSchemaElementException(WARN_ATTR_TYPE_AMBIGIOUS.get(name));
+            }
+            return null;
+        }
     }
 
     /*
      * WARNING: do not reference the core schema in the following declarations.
      */
 
-    private static final Schema EMPTY_SCHEMA = new Schema(new EmptyImpl());
-
+    private static final Schema EMPTY_STRICT_SCHEMA = new Schema(new EmptyImpl(true));
+    private static final Schema EMPTY_NON_STRICT_SCHEMA = new Schema(new EmptyImpl(false));
     static final String ATTR_ATTRIBUTE_TYPES = "attributeTypes";
-
     static final String ATTR_DIT_CONTENT_RULES = "dITContentRules";
-
     static final String ATTR_DIT_STRUCTURE_RULES = "dITStructureRules";
-
     static final String ATTR_LDAP_SYNTAXES = "ldapSyntaxes";
-
     static final String ATTR_MATCHING_RULE_USE = "matchingRuleUse";
-
     static final String ATTR_MATCHING_RULES = "matchingRules";
-
     static final String ATTR_NAME_FORMS = "nameForms";
-
     static final String ATTR_OBJECT_CLASSES = "objectClasses";
 
     /**
@@ -1138,7 +1125,7 @@
      * @return The empty schema.
      */
     public static Schema getEmptySchema() {
-        return EMPTY_SCHEMA;
+        return EMPTY_NON_STRICT_SCHEMA;
     }
 
     /**
@@ -1457,10 +1444,13 @@
      * @see Schema#isStrict()
      */
     public Schema asNonStrictSchema() {
-        if (impl.isStrict()) {
-            return new Schema(new NonStrictImpl(impl));
-        } else {
+        if (!impl.isStrict()) {
             return this;
+        } else if (impl instanceof StrictImpl) {
+            return new Schema(new NonStrictImpl((StrictImpl) impl));
+        } else {
+            // EmptyImpl
+            return EMPTY_NON_STRICT_SCHEMA;
         }
     }
 
@@ -1475,13 +1465,23 @@
     public Schema asStrictSchema() {
         if (impl.isStrict()) {
             return this;
-        } else {
+        } else if (impl instanceof NonStrictImpl) {
             return new Schema(((NonStrictImpl) impl).strictImpl);
+        } else {
+            // EmptyImpl
+            return EMPTY_STRICT_SCHEMA;
         }
     }
 
     /**
      * Returns the attribute type with the specified name or numeric OID.
+     * <p>
+     * If the requested attribute type is not registered in this schema and this
+     * schema is non-strict then a temporary "place-holder" attribute type will
+     * be created and returned. Place holder attribute types have an OID which
+     * is the normalized attribute name with the string {@code -oid} appended.
+     * In addition, they will use the directory string syntax and case ignore
+     * matching rule.
      *
      * @param name
      *            The name or OID of the attribute type to retrieve.
@@ -1489,6 +1489,7 @@
      * @throws UnknownSchemaElementException
      *             If this is a strict schema and the requested attribute type
      *             was not found or if the provided name is ambiguous.
+     * @see AttributeType#isPlaceHolder()
      */
     public AttributeType getAttributeType(final String name) {
         return impl.getAttributeType(name);
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java
index 0b6caf2..b455c06 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java
@@ -501,7 +501,7 @@
         // Ensure that the binary option is present if required.
         if (!syntax.isBEREncodingRequired()) {
             if (schemaValidationPolicy.checkAttributeValues().needsChecking()
-                    && attributeDescription.containsOption("binary")) {
+                    && attributeDescription.hasOption("binary")) {
                 final LocalizableMessage message =
                         ERR_LDIF_UNEXPECTED_BINARY_OPTION.get(record.lineNumber, entry.getName()
                                 .toString(), attrDescr);
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
index 57af7f8..8f3edde 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
@@ -538,7 +538,7 @@
             // Ensure that the binary option is present if required.
             if (!syntax.isBEREncodingRequired()) {
                 if (schemaValidationPolicy.checkAttributeValues().needsChecking()
-                        && attributeDescription.containsOption("binary")) {
+                        && attributeDescription.hasOption("binary")) {
                     final LocalizableMessage message =
                             ERR_LDIF_UNEXPECTED_BINARY_OPTION.get(record.lineNumber, entryDN
                                     .toString(), pair.value);
diff --git a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AttributeDescriptionTestCase.java b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AttributeDescriptionTestCase.java
index 1f6d5b2..619c7a2 100644
--- a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AttributeDescriptionTestCase.java
+++ b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AttributeDescriptionTestCase.java
@@ -219,7 +219,7 @@
         assertEquals(attributeDescription.isObjectClass(), isObjectClass);
 
         assertFalse(attributeDescription.hasOptions());
-        assertFalse(attributeDescription.containsOption("dummy"));
+        assertFalse(attributeDescription.hasOption("dummy"));
 
         final Iterator<String> iterator = attributeDescription.getOptions().iterator();
         assertFalse(iterator.hasNext());
@@ -250,19 +250,19 @@
             assertTrue(attributeDescription.hasOptions());
         }
 
-        assertFalse(attributeDescription.containsOption("dummy"));
+        assertFalse(attributeDescription.hasOption("dummy"));
         if (containsFoo) {
-            assertTrue(attributeDescription.containsOption("foo"));
-            assertTrue(attributeDescription.containsOption("FOO"));
-            assertTrue(attributeDescription.containsOption("FoO"));
+            assertTrue(attributeDescription.hasOption("foo"));
+            assertTrue(attributeDescription.hasOption("FOO"));
+            assertTrue(attributeDescription.hasOption("FoO"));
         } else {
-            assertFalse(attributeDescription.containsOption("foo"));
-            assertFalse(attributeDescription.containsOption("FOO"));
-            assertFalse(attributeDescription.containsOption("FoO"));
+            assertFalse(attributeDescription.hasOption("foo"));
+            assertFalse(attributeDescription.hasOption("FOO"));
+            assertFalse(attributeDescription.hasOption("FoO"));
         }
 
         for (final String option : options) {
-            assertTrue(attributeDescription.containsOption(option));
+            assertTrue(attributeDescription.hasOption(option));
         }
 
         final Iterator<String> iterator = attributeDescription.getOptions().iterator();
@@ -278,8 +278,8 @@
         AttributeDescription ad1 = AttributeDescription.valueOf("cn");
         AttributeDescription ad2 = ad1.withOption("test");
         assertTrue(ad2.hasOptions());
-        assertTrue(ad2.containsOption("test"));
-        assertFalse(ad2.containsOption("dummy"));
+        assertTrue(ad2.hasOption("test"));
+        assertFalse(ad2.hasOption("dummy"));
         assertEquals(ad2.toString(), "cn;test");
         assertEquals(ad2.getOptions().iterator().next(), "test");
     }
@@ -296,9 +296,9 @@
         AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1");
         AttributeDescription ad2 = ad1.withOption("test2");
         assertTrue(ad2.hasOptions());
-        assertTrue(ad2.containsOption("test1"));
-        assertTrue(ad2.containsOption("test2"));
-        assertFalse(ad2.containsOption("dummy"));
+        assertTrue(ad2.hasOption("test1"));
+        assertTrue(ad2.hasOption("test2"));
+        assertFalse(ad2.hasOption("dummy"));
         assertEquals(ad2.toString(), "cn;test1;test2");
         Iterator<String> i = ad2.getOptions().iterator();
         assertEquals(i.next(), "test1");
@@ -326,7 +326,7 @@
         AttributeDescription ad1 = AttributeDescription.valueOf("cn;test");
         AttributeDescription ad2 = ad1.withoutOption("test");
         assertFalse(ad2.hasOptions());
-        assertFalse(ad2.containsOption("test"));
+        assertFalse(ad2.hasOption("test"));
         assertEquals(ad2.toString(), "cn");
         assertFalse(ad2.getOptions().iterator().hasNext());
     }
@@ -343,8 +343,8 @@
         AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2");
         AttributeDescription ad2 = ad1.withoutOption("test1");
         assertTrue(ad2.hasOptions());
-        assertFalse(ad2.containsOption("test1"));
-        assertTrue(ad2.containsOption("test2"));
+        assertFalse(ad2.hasOption("test1"));
+        assertTrue(ad2.hasOption("test2"));
         assertEquals(ad2.toString(), "cn;test2");
         assertEquals(ad2.getOptions().iterator().next(), "test2");
     }
@@ -354,8 +354,8 @@
         AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2");
         AttributeDescription ad2 = ad1.withoutOption("test2");
         assertTrue(ad2.hasOptions());
-        assertTrue(ad2.containsOption("test1"));
-        assertFalse(ad2.containsOption("test2"));
+        assertTrue(ad2.hasOption("test1"));
+        assertFalse(ad2.hasOption("test2"));
         assertEquals(ad2.toString(), "cn;test1");
         assertEquals(ad2.getOptions().iterator().next(), "test1");
     }
@@ -372,9 +372,9 @@
         AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2;test3");
         AttributeDescription ad2 = ad1.withoutOption("test1");
         assertTrue(ad2.hasOptions());
-        assertFalse(ad2.containsOption("test1"));
-        assertTrue(ad2.containsOption("test2"));
-        assertTrue(ad2.containsOption("test3"));
+        assertFalse(ad2.hasOption("test1"));
+        assertTrue(ad2.hasOption("test2"));
+        assertTrue(ad2.hasOption("test3"));
         assertEquals(ad2.toString(), "cn;test2;test3");
         Iterator<String> i = ad2.getOptions().iterator();
         assertEquals(i.next(), "test2");
@@ -386,9 +386,9 @@
         AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2;test3");
         AttributeDescription ad2 = ad1.withoutOption("test2");
         assertTrue(ad2.hasOptions());
-        assertTrue(ad2.containsOption("test1"));
-        assertFalse(ad2.containsOption("test2"));
-        assertTrue(ad2.containsOption("test3"));
+        assertTrue(ad2.hasOption("test1"));
+        assertFalse(ad2.hasOption("test2"));
+        assertTrue(ad2.hasOption("test3"));
         assertEquals(ad2.toString(), "cn;test1;test3");
         Iterator<String> i = ad2.getOptions().iterator();
         assertEquals(i.next(), "test1");
@@ -400,9 +400,9 @@
         AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2;test3");
         AttributeDescription ad2 = ad1.withoutOption("test3");
         assertTrue(ad2.hasOptions());
-        assertTrue(ad2.containsOption("test1"));
-        assertTrue(ad2.containsOption("test2"));
-        assertFalse(ad2.containsOption("test3"));
+        assertTrue(ad2.hasOption("test1"));
+        assertTrue(ad2.hasOption("test2"));
+        assertFalse(ad2.hasOption("test3"));
         assertEquals(ad2.toString(), "cn;test1;test2");
         Iterator<String> i = ad2.getOptions().iterator();
         assertEquals(i.next(), "test1");
diff --git a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/EntryTestCase.java b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/EntryTestCase.java
index 5476659..5721f28 100644
--- a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/EntryTestCase.java
+++ b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/EntryTestCase.java
@@ -22,11 +22,21 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
+ *      Portions copyright 2012 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap;
 
-import org.testng.Assert;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.Attributes.emptyAttribute;
+import static org.forgerock.opendj.ldap.Attributes.singletonAttribute;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaBuilder;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
@@ -35,60 +45,778 @@
  */
 @SuppressWarnings("javadoc")
 public final class EntryTestCase extends SdkTestCase {
+
     private static interface EntryFactory {
-        Entry newEntry(String... ldifLines);
+        Entry newEntry(String... ldifLines) throws Exception;
     }
 
     private static final class LinkedHashMapEntryFactory implements EntryFactory {
-        public Entry newEntry(final String... ldifLines) {
-            return new LinkedHashMapEntry(ldifLines);
+        @Override
+        public Entry newEntry(final String... ldifLines) throws Exception {
+            final LDIFEntryReader reader = new LDIFEntryReader(ldifLines).setSchema(SCHEMA);
+            final Entry entry = reader.readEntry();
+            assertThat(reader.hasNext()).isFalse();
+            return new LinkedHashMapEntry(entry);
         }
     }
 
     private static final class TreeMapEntryFactory implements EntryFactory {
-        public Entry newEntry(final String... ldifLines) {
-            return new TreeMapEntry(ldifLines);
+        @Override
+        public Entry newEntry(final String... ldifLines) throws Exception {
+            final LDIFEntryReader reader = new LDIFEntryReader(ldifLines).setSchema(SCHEMA);
+            final Entry entry = reader.readEntry();
+            assertThat(reader.hasNext()).isFalse();
+            return new TreeMapEntry(entry);
         }
     }
 
+    private static final AttributeDescription AD_CN;
+    private static final AttributeDescription AD_CUSTOM1;
+    private static final AttributeDescription AD_CUSTOM2;
+    private static final AttributeDescription AD_NAME;
+
+    private static final AttributeDescription AD_SN;
+
+    private static final Schema SCHEMA;
+
+    static {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addAttributeType("( 9.9.9.1 NAME 'custom1' SUP name )", false);
+        builder.addAttributeType("( 9.9.9.2 NAME 'custom2' SUP name )", false);
+        SCHEMA = builder.toSchema();
+        AD_CUSTOM1 = AttributeDescription.valueOf("custom1", SCHEMA);
+        AD_CUSTOM2 = AttributeDescription.valueOf("custom2", SCHEMA);
+        AD_CN = AttributeDescription.valueOf("cn");
+        AD_SN = AttributeDescription.valueOf("sn");
+        AD_NAME = AttributeDescription.valueOf("name");
+    }
+
     @DataProvider(name = "EntryFactory")
-    public Object[][] entryFactory() {
+    Object[][] entryFactory() {
         // Value, type, options, containsOptions("foo")
         return new Object[][] { { new TreeMapEntryFactory() }, { new LinkedHashMapEntryFactory() } };
     }
 
     @Test(dataProvider = "EntryFactory")
-    public void smokeTest(final EntryFactory factory) throws Exception {
-        final Entry entry1 =
-                factory.newEntry("dn: cn=Joe Bloggs,dc=example,dc=com", "objectClass: top",
-                        "objectClass: person", "cn: Joe Bloggs", "sn: Bloggs", "givenName: Joe",
-                        "description: A description");
+    public void testAddAttributeAttribute(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute(new LinkedAttribute("sn", "sn"))).isTrue();
+        assertThat(entry.getAttribute(AD_SN)).hasSize(1);
+    }
 
-        final Entry entry2 =
-                factory.newEntry("dn: cn=Joe Bloggs,dc=example,dc=com", "changetype: add",
-                        "objectClass: top", "objectClass: person", "cn: Joe Bloggs", "sn: Bloggs",
-                        "givenName: Joe", "description: A description");
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeAttributeCollection(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> duplicateValues = new LinkedList<ByteString>();
+        assertThat(entry.addAttribute(new LinkedAttribute("sn", "sn"), duplicateValues)).isTrue();
+        assertThat(entry.getAttribute(AD_SN)).hasSize(1);
+        assertThat(duplicateValues).hasSize(0);
+    }
 
-        Assert.assertEquals(entry1, entry2);
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeAttributeCollectionValueMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> duplicateValues = new LinkedList<ByteString>();
+        assertThat(entry.addAttribute(new LinkedAttribute("cn", "newcn"), duplicateValues))
+                .isTrue();
+        assertThat(entry.getAttribute(AD_CN)).hasSize(2);
+        assertThat(duplicateValues).hasSize(0);
+    }
 
-        for (final Entry e : new Entry[] { entry1, entry2 }) {
-            Assert.assertEquals(e.getName(), DN.valueOf("cn=Joe Bloggs,dc=example,dc=com"));
-            Assert.assertEquals(e.getAttributeCount(), 5);
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeAttributeCollectionValuePresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> duplicateValues = new LinkedList<ByteString>();
+        assertThat(entry.addAttribute(new LinkedAttribute("cn", "test"), duplicateValues))
+                .isFalse();
+        assertThat(entry.getAttribute(AD_CN)).hasSize(1);
+        assertThat(duplicateValues).hasSize(1);
+        assertThat(duplicateValues).contains(ByteString.valueOf("test"));
+    }
 
-            Assert.assertEquals(e.getAttribute("objectClass").size(), 2);
-            Assert.assertTrue(e.containsAttribute("objectClass", "top", "person"));
-            Assert.assertFalse(e.containsAttribute("objectClass", "top", "person", "foo"));
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeAttributeValueMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute(new LinkedAttribute("cn", "newcn"))).isTrue();
+        assertThat(entry.getAttribute(AD_CN)).hasSize(2);
+    }
 
-            Assert.assertTrue(e.containsAttribute("objectClass"));
-            Assert.assertTrue(e.containsAttribute("cn"));
-            Assert.assertTrue(e.containsAttribute("cn", "Joe Bloggs"));
-            Assert.assertFalse(e.containsAttribute("cn", "Jane Bloggs"));
-            Assert.assertTrue(e.containsAttribute("sn"));
-            Assert.assertTrue(e.containsAttribute("givenName"));
-            Assert.assertTrue(e.containsAttribute("description"));
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeAttributeValuePresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute(new LinkedAttribute("cn", "test"))).isFalse();
+        assertThat(entry.getAttribute(AD_CN)).hasSize(1);
+    }
 
-            Assert.assertEquals(e.getAttribute("cn").firstValueAsString(), "Joe Bloggs");
-            Assert.assertEquals(e.getAttribute("sn").firstValueAsString(), "Bloggs");
-        }
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeString(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("sn", "sn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_SN)).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeStringCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("custom2", "custom2")).isSameAs(entry);
+        // This is expected to be null since the type was decoded using the
+        // default schema and a temporary oid was allocated.
+        assertThat(entry.getAttribute(AD_CUSTOM2)).isNull();
+        assertThat(entry.getAttribute("custom2")).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeStringCustomValueMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("custom1", "xxxx")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CUSTOM1)).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeStringCustomValuePresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("custom1", "custom1")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CUSTOM1)).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeStringValueMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("cn", "newcn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeStringValuePresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("cn", "test")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testClearAttributes(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.clearAttributes()).isSameAs(entry);
+        assertThat(entry.getAttributeCount()).isEqualTo(0);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeCustomMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.containsAttribute(emptyAttribute(AD_CUSTOM2), missingValues)).isFalse();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeCustomPresent1(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.containsAttribute(emptyAttribute(AD_CUSTOM1), missingValues)).isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeCustomPresent2(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.containsAttribute(emptyAttribute("custom1"), missingValues)).isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeCustomValueMissing1(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(
+                entry.containsAttribute(singletonAttribute(AD_CUSTOM2, "missing"), missingValues))
+                .isFalse();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.containsAttribute(emptyAttribute(AD_SN), missingValues)).isFalse();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributePresent1(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.containsAttribute(emptyAttribute(AD_CN), missingValues)).isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributePresent2(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.containsAttribute(emptyAttribute("cn"), missingValues)).isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeValueCustomMissing2(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(
+                entry.containsAttribute(singletonAttribute(AD_CUSTOM1, "missing"), missingValues))
+                .isFalse();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeValueCustomPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(
+                entry.containsAttribute(singletonAttribute(AD_CUSTOM1, "custom1"), missingValues))
+                .isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeValueMissing1(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.containsAttribute(singletonAttribute(AD_SN, "missing"), missingValues))
+                .isFalse();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeValueMissing2(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.containsAttribute(singletonAttribute(AD_CN, "missing"), missingValues))
+                .isFalse();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeValuePresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.containsAttribute(singletonAttribute(AD_CN, "test"), missingValues))
+                .isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringCustomMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("custom2")).isFalse();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringCustomPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("custom1")).isTrue();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("sn")).isFalse();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringPresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("cn")).isTrue();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringValueCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("custom1", "custom1")).isTrue();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringValueMissing1(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("cn", "missing")).isFalse();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringValueMissing2(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("sn", "missing")).isFalse();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringValuePresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("cn", "test")).isTrue();
+    }
+
+    @Test
+    public void testEqualsHashCodeDifferentContentDifferentTypes1() throws Exception {
+        final Entry e1 = createTestEntry(new TreeMapEntryFactory());
+        // Extra attributes.
+        final Entry e2 = createTestEntry(new LinkedHashMapEntryFactory()).addAttribute("sn", "sn");
+        assertThat(e1).isNotEqualTo(e2);
+        assertThat(e2).isNotEqualTo(e1);
+        assertThat(e1.hashCode()).isNotEqualTo(e2.hashCode());
+    }
+
+    @Test
+    public void testEqualsHashCodeDifferentContentDifferentTypes2() throws Exception {
+        final Entry e1 = createTestEntry(new TreeMapEntryFactory());
+        // Same attributes, extra values.
+        final Entry e2 =
+                createTestEntry(new LinkedHashMapEntryFactory()).addAttribute("cn", "newcn");
+        assertThat(e1).isNotEqualTo(e2);
+        assertThat(e2).isNotEqualTo(e1);
+        assertThat(e1.hashCode()).isNotEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testEqualsHashCodeDifferentContentSameTypes1(final EntryFactory factory)
+            throws Exception {
+        final Entry e1 = createTestEntry(factory);
+        // Extra attributes.
+        final Entry e2 = createTestEntry(factory).addAttribute("sn", "sn");
+        assertThat(e1).isNotEqualTo(e2);
+        assertThat(e2).isNotEqualTo(e1);
+        assertThat(e1.hashCode()).isNotEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testEqualsHashCodeDifferentContentSameTypes2(final EntryFactory factory)
+            throws Exception {
+        final Entry e1 = createTestEntry(factory);
+        // Same attributes, extra values.
+        final Entry e2 = createTestEntry(factory).addAttribute("cn", "newcn");
+        assertThat(e1).isNotEqualTo(e2);
+        assertThat(e2).isNotEqualTo(e1);
+        assertThat(e1.hashCode()).isNotEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testEqualsHashCodeDifferentDN(final EntryFactory factory) throws Exception {
+        final Entry e1 = createTestEntry(factory);
+        final Entry e2 = createTestEntry(factory).setName("cn=foobar");
+        assertThat(e1).isNotEqualTo(e2);
+        assertThat(e1.hashCode()).isNotEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testEqualsHashCodeMutates(final EntryFactory factory) throws Exception {
+        final Entry e = createTestEntry(factory);
+        final int hc1 = e.hashCode();
+        e.addAttribute("sn", "sn");
+        final int hc2 = e.hashCode();
+        assertThat(hc1).isNotEqualTo(hc2);
+    }
+
+    @Test
+    public void testEqualsHashCodeSameContentDifferentTypes() throws Exception {
+        final Entry e1 = createTestEntry(new TreeMapEntryFactory());
+        final Entry e2 = createTestEntry(new LinkedHashMapEntryFactory());
+        assertThat(e1).isEqualTo(e2);
+        assertThat(e2).isEqualTo(e1);
+        assertThat(e1.hashCode()).isEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testEqualsHashCodeSameContentSameTypes(final EntryFactory factory) throws Exception {
+        final Entry e1 = createTestEntry(factory);
+        final Entry e2 = createTestEntry(factory);
+        assertThat(e1).isEqualTo(e1);
+        assertThat(e1).isEqualTo(e2);
+        assertThat(e1.hashCode()).isEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributes(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes().iterator()).hasSize(3);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesAttributeDescriptionMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes(AD_SN)).hasSize(0);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesAttributeDescriptionPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes(AD_CN)).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesAttributeDescriptionPresentOptions(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        entry.addAttribute(singletonAttribute(AD_CN.withOption("lang-fr"), "xxxx"));
+        assertThat(entry.getAllAttributes(AD_CN)).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesAttributeDescriptionSupertype(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes(AD_NAME)).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesStringCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        entry.addAttribute(singletonAttribute(AD_CUSTOM1.withOption("lang-fr"), "xxxx"));
+        assertThat(entry.getAllAttributes("custom1")).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesStringCustomOptions(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        entry.addAttribute("custom2", "value1");
+        entry.addAttribute("custom2;lang-fr", "value2");
+        assertThat(entry.getAllAttributes("custom2")).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesStringMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes("sn")).hasSize(0);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesStringPresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes("cn")).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesStringSupertype(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes("name")).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeAttributeDescriptionMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttribute(AD_SN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeAttributeDescriptionPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttribute(AD_CN)).isNotNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeCount(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeStringCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttribute("custom1")).isNotNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeStringMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttribute("sn")).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeStringPresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttribute("cn")).isNotNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetName(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat((Object) entry.getName()).isEqualTo(DN.valueOf("cn=test"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeAttributeDescriptionCustom(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute(AD_CUSTOM1).asString()).isEqualTo("custom1");
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeAttributeDescriptionMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute(AD_SN).asString()).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeAttributeDescriptionPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute(AD_CN).asString()).isEqualTo("test");
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeStringCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute("custom1").asString()).isEqualTo("custom1");
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeStringMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute("sn").asString()).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeStringPresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute("cn").asString()).isEqualTo("test");
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeDescriptionMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute(AD_SN)).isFalse();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeDescriptionPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute(AD_CN)).isTrue();
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.removeAttribute(emptyAttribute(AD_SN), missingValues)).isFalse();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributePresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.removeAttribute(emptyAttribute(AD_CN), missingValues)).isTrue();
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeValueMissing1(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.removeAttribute(singletonAttribute(AD_CN, "missing"), missingValues))
+                .isFalse();
+        assertThat(entry.getAttribute(AD_CN)).isNotNull();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeValueMissing2(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.removeAttribute(singletonAttribute(AD_SN, "missing"), missingValues))
+                .isFalse();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeValuePresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<ByteString>();
+        assertThat(entry.removeAttribute(singletonAttribute(AD_CN, "test"), missingValues))
+                .isTrue();
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("custom1")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CUSTOM1)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("sn")).isSameAs(entry);
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringPresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("cn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringValueMissing1(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("cn", "missing")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).isNotNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringValueMissing2(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("sn", "missing")).isSameAs(entry);
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringValuePresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("cn", "test")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeAttributeMissingEmpty(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute(emptyAttribute(AD_SN))).isFalse();
+        assertThat(entry.getAttribute(AD_SN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeAttributeMissingValue(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute(singletonAttribute(AD_SN, "sn"))).isTrue();
+        assertThat(entry.getAttribute(AD_SN)).isEqualTo(singletonAttribute(AD_SN, "sn"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeAttributePresentEmpty(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute(emptyAttribute(AD_CN))).isTrue();
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeAttributePresentValue(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute(singletonAttribute(AD_CN, "newcn"))).isTrue();
+        assertThat(entry.getAttribute(AD_CN)).isEqualTo(singletonAttribute(AD_CN, "newcn"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringCustomEmpty(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("custom1")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CUSTOM1)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringCustomMissingValue(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("custom2", "xxxx")).isSameAs(entry);
+        // This is expected to be null since the type was decoded using the
+        // default schema and a temporary oid was allocated.
+        assertThat(entry.getAttribute(AD_CUSTOM2)).isNull();
+        assertThat(entry.getAttribute("custom2")).isEqualTo(singletonAttribute("custom2", "xxxx"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringCustomValue(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("custom1", "xxxx")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CUSTOM1))
+                .isEqualTo(singletonAttribute(AD_CUSTOM1, "xxxx"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringMissingEmpty(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("sn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_SN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringMissingValue(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("sn", "sn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_SN)).isEqualTo(singletonAttribute(AD_SN, "sn"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringPresentEmpty(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("cn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringPresentValue(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("cn", "newcn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).isEqualTo(singletonAttribute(AD_CN, "newcn"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testSetNameDN(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.setName(DN.valueOf("cn=foobar"))).isSameAs(entry);
+        assertThat((Object) entry.getName()).isEqualTo(DN.valueOf("cn=foobar"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testSetNameString(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.setName("cn=foobar")).isSameAs(entry);
+        assertThat((Object) entry.getName()).isEqualTo(DN.valueOf("cn=foobar"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testToString(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        // The String representation is unspecified but we should at least
+        // expect the DN to be present.
+        assertThat(entry.toString()).contains("cn=test");
+    }
+
+    private Entry createTestEntry(final EntryFactory factory) throws Exception {
+        return factory.newEntry("dn: cn=test", "objectClass: top", "objectClass: extensibleObject",
+                "cn: test", "custom1: custom1");
     }
 }

--
Gitblit v1.10.0