From 60f3502563cd2c71a751b74f0b34bfe2f5d17330 Mon Sep 17 00:00:00 2001
From: Violette Roche-Montane <violette.roche-montane@forgerock.com>
Date: Thu, 08 Nov 2012 15:40:09 +0000
Subject: [PATCH] Fix OPENDJ-619 : Reduce the memory utilization of LDIF.diff(EntryReader, EntryReader) in the SDK

---
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/DN.java                                               |  136 ++++
 opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldif/LDIFTestCase.java                                     | 1336 ++++++++++++++++++++++++++++++++++++++++++-
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIF.java                                             |  221 +++++-
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java |  135 ----
 4 files changed, 1,614 insertions(+), 214 deletions(-)

diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/DN.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/DN.java
index e2ed752..a32ce02 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/DN.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/DN.java
@@ -27,19 +27,24 @@
 
 package org.forgerock.opendj.ldap;
 
+import static com.forgerock.opendj.util.StaticUtils.getBytes;
 import static org.forgerock.opendj.ldap.CoreMessages.ERR_DN_TYPE_NOT_FOUND;
 
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.TreeSet;
 import java.util.WeakHashMap;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.schema.MatchingRule;
 import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.Syntax;
 import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
 
+import com.forgerock.opendj.util.StaticUtils;
 import com.forgerock.opendj.util.SubstringReader;
 import com.forgerock.opendj.util.Validator;
 
@@ -799,4 +804,135 @@
         }
         return stringValue;
     }
+
+    /**
+     * Returns the normalized string representation of a DN.
+     *
+     * @return The normalized string representation of the provided DN.
+     */
+    public String toNormalizedString() {
+        final StringBuilder builder = new StringBuilder(this.size());
+        if (rdn() == null) {
+            return builder.toString();
+        }
+
+        int i = this.size() - 1;
+        normalizeRDN(builder, parent(i).rdn());
+        for (i--; i >= 0; i--) {
+            final RDN rdn = parent(i).rdn();
+            // Only add a separator if the RDN is not RDN.maxValue().
+            if (rdn.size() != 0) {
+                builder.append('\u0000');
+            }
+            normalizeRDN(builder, rdn);
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Returns the normalized string representation of a RDN.
+     *
+     * @param builder
+     *            The StringBuilder to use to construct the normalized string.
+     * @param rdn
+     *            The RDN.
+     * @return The normalized string representation of the provided RDN.
+     */
+    private static StringBuilder normalizeRDN(final StringBuilder builder, final RDN rdn) {
+        final int sz = rdn.size();
+        switch (sz) {
+        case 0:
+            // Handle RDN.maxValue().
+            builder.append('\u0001');
+            break;
+        case 1:
+            normalizeAVA(builder, rdn.getFirstAVA());
+            break;
+        default:
+            // Need to sort the AVAs before comparing.
+            TreeSet<AVA> a = new TreeSet<AVA>();
+            for (AVA ava : rdn) {
+                a.add(ava);
+            }
+            Iterator<AVA> i = a.iterator();
+            // Normalize the first AVA.
+            normalizeAVA(builder, i.next());
+            while (i.hasNext()) {
+                builder.append('\u0001');
+                normalizeAVA(builder, i.next());
+            }
+            break;
+        }
+        return builder;
+    }
+
+    /**
+     * Returns the normalized string representation of an AVA.
+     *
+     * @param builder
+     *            The StringBuilder to use to construct the normalized string.
+     * @param ava
+     *            The AVA.
+     * @return The normalized string representation of the provided AVA.
+     */
+    private static StringBuilder normalizeAVA(final StringBuilder builder, final AVA ava) {
+        ByteString value = ava.getAttributeValue();
+        final MatchingRule matchingRule = ava.getAttributeType().getEqualityMatchingRule();
+        if (matchingRule != null) {
+            try {
+                value = matchingRule.normalizeAttributeValue(ava.getAttributeValue());
+            } catch (final DecodeException de) {
+                // Ignore - we'll drop back to the user provided value.
+            }
+        }
+
+        if (!ava.getAttributeType().getNames().iterator().hasNext()) {
+            builder.append(ava.getAttributeType().getOID());
+            builder.append("=#");
+            StaticUtils.toHex(value, builder);
+        } else {
+            final String name = ava.getAttributeType().getNameOrOID();
+            // Normalizing.
+            StaticUtils.toLowerCase(name, builder);
+
+            builder.append("=");
+
+            final Syntax syntax = ava.getAttributeType().getSyntax();
+            if (!syntax.isHumanReadable()) {
+                builder.append("#");
+                StaticUtils.toHex(value, builder);
+            } else {
+                final String str = value.toString();
+                if (str.length() == 0) {
+                    return builder;
+                }
+                char c = str.charAt(0);
+                int startPos = 0;
+                if ((c == ' ') || (c == '#')) {
+                    builder.append('\\');
+                    builder.append(c);
+                    startPos = 1;
+                }
+                final int length = str.length();
+                for (int si = startPos; si < length; si++) {
+                    c = str.charAt(si);
+                    if (c < ' ') {
+                        for (final byte b : getBytes(String.valueOf(c))) {
+                            builder.append('\\');
+                            builder.append(StaticUtils.byteToLowerHex(b));
+                        }
+                    } else {
+                        if ((c == ' ' && si == length - 1)
+                                || (c == '"' || c == '+' || c == ',' || c == ';' || c == '<'
+                                        || c == '=' || c == '>' || c == '\\' || c == '\u0000')) {
+                            builder.append('\\');
+                        }
+                        builder.append(c);
+                    }
+                }
+            }
+        }
+        return builder;
+    }
+
 }
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java
index b005f62..b2a7fb0 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java
@@ -22,151 +22,21 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
+ *      Portions copyright 2011-2012 ForgeRock AS.
  */
 package org.forgerock.opendj.ldap.schema;
 
-import static com.forgerock.opendj.util.StaticUtils.getBytes;
-
-import java.util.Iterator;
-import java.util.TreeSet;
-
 import org.forgerock.i18n.LocalizedIllegalArgumentException;
-import org.forgerock.opendj.ldap.AVA;
 import org.forgerock.opendj.ldap.ByteSequence;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.DecodeException;
-import org.forgerock.opendj.ldap.RDN;
-
-import com.forgerock.opendj.util.StaticUtils;
 
 /**
  * This class defines the distinguishedNameMatch matching rule defined in X.520
  * and referenced in RFC 2252.
  */
 final class DistinguishedNameEqualityMatchingRuleImpl extends AbstractMatchingRuleImpl {
-    /**
-     * Returns the normalized string representation of an AVA.
-     *
-     * @param builder
-     *            The StringBuilder to use to construct the normalized string.
-     * @param ava
-     *            The AVA.
-     * @return The normalized string representation of the provided AVA.
-     */
-    private static StringBuilder normalizeAVA(final StringBuilder builder, final AVA ava) {
-        ByteString value = ava.getAttributeValue();
-        final MatchingRule matchingRule = ava.getAttributeType().getEqualityMatchingRule();
-        if (matchingRule != null) {
-            try {
-                value = matchingRule.normalizeAttributeValue(ava.getAttributeValue());
-            } catch (final DecodeException de) {
-                // Ignore - we'll drop back to the user provided value.
-            }
-        }
-
-        if (!ava.getAttributeType().getNames().iterator().hasNext()) {
-            builder.append(ava.getAttributeType().getOID());
-            builder.append("=#");
-            StaticUtils.toHex(value, builder);
-        } else {
-            final String name = ava.getAttributeType().getNameOrOID();
-            // Normalizing.
-            StaticUtils.toLowerCase(name, builder);
-
-            builder.append("=");
-
-            final Syntax syntax = ava.getAttributeType().getSyntax();
-            if (!syntax.isHumanReadable()) {
-                builder.append("#");
-                StaticUtils.toHex(value, builder);
-            } else {
-                final String str = value.toString();
-                if (str.length() == 0) {
-                    return builder;
-                }
-                char c = str.charAt(0);
-                int startPos = 0;
-                if ((c == ' ') || (c == '#')) {
-                    builder.append('\\');
-                    builder.append(c);
-                    startPos = 1;
-                }
-                final int length = str.length();
-                for (int si = startPos; si < length; si++) {
-                    c = str.charAt(si);
-                    if (c < ' ') {
-                        for (final byte b : getBytes(String.valueOf(c))) {
-                            builder.append('\\');
-                            builder.append(StaticUtils.byteToLowerHex(b));
-                        }
-                    } else {
-                        if ((c == ' ' && si == length - 1)
-                                || (c == '"' || c == '+' || c == ',' || c == ';' || c == '<'
-                                        || c == '=' || c == '>' || c == '\\' || c == '\u0000')) {
-                            builder.append('\\');
-                        }
-                        builder.append(c);
-                    }
-                }
-            }
-        }
-        return builder;
-    }
-
-    /**
-     * Returns the normalized string representation of a DN.
-     *
-     * @param builder
-     *            The StringBuilder to use to construct the normalized string.
-     * @param dn
-     *            The DN.
-     * @return The normalized string representation of the provided DN.
-     */
-    private static StringBuilder normalizeDN(final StringBuilder builder, final DN dn) {
-        if (dn.rdn() == null) {
-            return builder;
-        }
-
-        int i = dn.size() - 1;
-        normalizeRDN(builder, dn.parent(i).rdn());
-        for (i--; i >= 0; i--) {
-            builder.append('\u0000');
-            normalizeRDN(builder, dn.parent(i).rdn());
-        }
-        return builder;
-    }
-
-    /**
-     * Returns the normalized string representation of a RDN.
-     *
-     * @param builder
-     *            The StringBuilder to use to construct the normalized string.
-     * @param rdn
-     *            The RDN.
-     * @return The normalized string representation of the provided RDN.
-     */
-    private static StringBuilder normalizeRDN(final StringBuilder builder, final RDN rdn) {
-        final int sz = rdn.size();
-        if (sz == 1) {
-            return normalizeAVA(builder, rdn.getFirstAVA());
-        } else {
-            // Need to sort the AVAs before comparing.
-            TreeSet<AVA> a = new TreeSet<AVA>();
-            for (AVA ava : rdn) {
-                a.add(ava);
-            }
-            Iterator<AVA> i = a.iterator();
-            // Normalize the first AVA.
-            normalizeAVA(builder, i.next());
-            while (i.hasNext()) {
-                builder.append('\u0001');
-                normalizeAVA(builder, i.next());
-            }
-
-            return builder;
-        }
-    }
 
     /**
      * {@inheritDoc}
@@ -175,8 +45,7 @@
             throws DecodeException {
         try {
             DN dn = DN.valueOf(value.toString(), schema.asNonStrictSchema());
-            StringBuilder builder = new StringBuilder(value.length());
-            return ByteString.valueOf(normalizeDN(builder, dn));
+            return ByteString.valueOf(dn.toNormalizedString());
         } catch (final LocalizedIllegalArgumentException e) {
             throw DecodeException.error(e.getMessageObject());
         }
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIF.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIF.java
index d6276db..ffa1db7 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIF.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIF.java
@@ -28,19 +28,29 @@
 import static org.forgerock.opendj.ldap.CoreMessages.REJECTED_CHANGE_FAIL_DELETE;
 import static org.forgerock.opendj.ldap.CoreMessages.REJECTED_CHANGE_FAIL_MODIFY;
 import static org.forgerock.opendj.ldap.CoreMessages.REJECTED_CHANGE_FAIL_MODIFYDN;
+import static com.forgerock.opendj.util.StaticUtils.getBytes;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.SortedMap;
 import java.util.TreeMap;
 
+import org.forgerock.opendj.asn1.ASN1;
+import org.forgerock.opendj.asn1.ASN1Reader;
+import org.forgerock.opendj.asn1.ASN1Writer;
 import org.forgerock.opendj.ldap.AVA;
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
 import org.forgerock.opendj.ldap.Attributes;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.DecodeException;
 import org.forgerock.opendj.ldap.DecodeOptions;
@@ -58,9 +68,13 @@
 import org.forgerock.opendj.ldap.requests.ModifyRequest;
 import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
 import org.forgerock.opendj.ldap.schema.AttributeUsage;
 import org.forgerock.opendj.ldap.schema.Schema;
 
+import com.forgerock.opendj.ldap.LDAPUtils;
+
 /**
  * This class contains common utility methods for creating and manipulating
  * readers and writers.
@@ -77,6 +91,26 @@
     // @formatter:on
 
     /**
+     * Comparator ordering the DN ASC.
+     */
+    private static final Comparator<byte[][]> DN_ORDER2 = new Comparator<byte[][]>() {
+        public int compare(byte[][] b1, byte[][] b2) {
+            return DN_ORDER.compare(b1[0], b2[0]);
+        }
+    };
+
+    /**
+     * Comparator ordering the DN ASC.
+     */
+    private static final Comparator<byte[]> DN_ORDER = new Comparator<byte[]>() {
+        public int compare(byte[] b1, byte[] b2) {
+            final ByteString bs = ByteString.valueOf(b1);
+            final ByteString bs2 = ByteString.valueOf(b2);
+            return bs.compareTo(bs2);
+        }
+    };
+
+    /**
      * Copies the content of {@code input} to {@code output}. This method does
      * not close {@code input} or {@code output}.
      *
@@ -137,10 +171,11 @@
      */
     public static ChangeRecordReader diff(final EntryReader source, final EntryReader target)
             throws IOException {
-        final SortedMap<DN, Entry> sourceEntries = readEntries(source);
-        final SortedMap<DN, Entry> targetEntries = readEntries(target);
-        final Iterator<Entry> sourceIterator = sourceEntries.values().iterator();
-        final Iterator<Entry> targetIterator = targetEntries.values().iterator();
+
+        final List<byte[][]> source2 = readEntriesAsList(source);
+        final List<byte[][]> target2 = readEntriesAsList(target);
+        final Iterator<byte[][]> sourceIterator = source2.iterator();
+        final Iterator<byte[][]> targetIterator = target2.iterator();
 
         return new ChangeRecordReader() {
             private Entry sourceEntry = nextEntry(sourceIterator);
@@ -201,11 +236,13 @@
                 }
             }
 
-            private Entry nextEntry(final Iterator<Entry> i) {
-                return i.hasNext() ? i.next() : null;
+            private Entry nextEntry(final Iterator<byte[][]> i) throws IOException {
+                if (i.hasNext()) {
+                    return decodeEntry(i.next()[1]);
+                }
+                return null;
             }
         };
-
     }
 
     /**
@@ -240,6 +277,12 @@
      * <b>NOTE:</b> this method reads the content of {@code input} into memory
      * before applying the changes, and is therefore not suited for use in cases
      * where a very large number of entries are to be patched.
+     * <p>
+     * <b>NOTE:</b> this method will not perform modifications required in order
+     * to maintain referential integrity. In particular, if an entry references
+     * another entry using a DN valued attribute and the referenced entry is
+     * deleted, then the DN reference will not be removed. The same applies to
+     * renamed entries and their references.
      *
      * @param input
      *            The entry reader containing the set of entries to be patched.
@@ -264,6 +307,12 @@
      * <b>NOTE:</b> this method reads the content of {@code input} into memory
      * before applying the changes, and is therefore not suited for use in cases
      * where a very large number of entries are to be patched.
+     * <p>
+     * <b>NOTE:</b> this method will not perform modifications required in order
+     * to maintain referential integrity. In particular, if an entry references
+     * another entry using a DN valued attribute and the referenced entry is
+     * deleted, then the DN reference will not be removed. The same applies to
+     * renamed entries and their references.
      *
      * @param input
      *            The entry reader containing the set of entries to be patched.
@@ -278,10 +327,12 @@
      */
     public static EntryReader patch(final EntryReader input, final ChangeRecordReader patch,
             final RejectedChangeRecordListener listener) throws IOException {
-        final SortedMap<DN, Entry> entries = readEntries(input);
+        final SortedMap<byte[], byte[]> entries = readEntriesAsMap(input);
 
         while (patch.hasNext()) {
             final ChangeRecord change = patch.readChangeRecord();
+            final DN changeDN = change.getName();
+            final byte[] changeNormDN = getBytes(change.getName().toNormalizedString());
 
             final DecodeException de =
                     change.accept(new ChangeRecordVisitor<DecodeException, Void>() {
@@ -289,17 +340,19 @@
                         @Override
                         public DecodeException visitChangeRecord(final Void p,
                                 final AddRequest change) {
-                            final Entry existingEntry = entries.get(change.getName());
-                            if (existingEntry != null) {
+
+                            if (entries.get(changeNormDN) != null) {
+                                final Entry existingEntry = decodeEntry(entries.get(changeNormDN));
                                 try {
                                     final Entry entry =
                                             listener.handleDuplicateEntry(change, existingEntry);
-                                    entries.put(entry.getName(), entry);
+                                    entries.put(getBytes(entry.getName().toNormalizedString()),
+                                            encodeEntry(entry)[1]);
                                 } catch (final DecodeException e) {
                                     return e;
                                 }
                             } else {
-                                entries.put(change.getName(), change);
+                                entries.put(changeNormDN, encodeEntry(change)[1]);
                             }
                             return null;
                         }
@@ -307,7 +360,7 @@
                         @Override
                         public DecodeException visitChangeRecord(final Void p,
                                 final DeleteRequest change) {
-                            if (!entries.containsKey(change.getName())) {
+                            if (entries.get(changeNormDN) == null) {
                                 try {
                                     listener.handleRejectedChangeRecord(change,
                                             REJECTED_CHANGE_FAIL_DELETE.get(change.getName()
@@ -319,10 +372,12 @@
                                 try {
                                     if (change.getControl(SubtreeDeleteRequestControl.DECODER,
                                             new DecodeOptions()) != null) {
-                                        entries.subMap(change.getName(),
-                                                change.getName().child(RDN.maxValue())).clear();
+                                        entries.subMap(
+                                                getBytes(change.getName().toNormalizedString()),
+                                                getBytes(change.getName().child(RDN.maxValue())
+                                                        .toNormalizedString())).clear();
                                     } else {
-                                        entries.remove(change.getName());
+                                        entries.remove(changeNormDN);
                                     }
                                 } catch (final DecodeException e) {
                                     return e;
@@ -335,7 +390,7 @@
                         @Override
                         public DecodeException visitChangeRecord(final Void p,
                                 final ModifyDNRequest change) {
-                            if (!entries.containsKey(change.getName())) {
+                            if (entries.get(changeNormDN) == null) {
                                 try {
                                     listener.handleRejectedChangeRecord(change,
                                             REJECTED_CHANGE_FAIL_MODIFYDN.get(change.getName()
@@ -345,7 +400,7 @@
                                 }
                             } else {
                                 // Calculate the old and new DN.
-                                final DN oldDN = change.getName();
+                                final DN oldDN = changeDN;
 
                                 DN newSuperior = change.getNewSuperior();
                                 if (newSuperior == null) {
@@ -357,51 +412,72 @@
                                 final DN newDN = newSuperior.child(change.getNewRDN());
 
                                 // Move the renamed entries into a separate map
-                                // in order to
-                                // avoid cases where the renamed subtree
-                                // overlaps.
-                                final SortedMap<DN, Entry> renamedEntries =
-                                        new TreeMap<DN, Entry>();
-                                final Iterator<Map.Entry<DN, Entry>> i =
-                                        entries.subMap(change.getName(),
-                                                change.getName().child(RDN.maxValue())).entrySet()
-                                                .iterator();
-                                while (i.hasNext()) {
-                                    final Map.Entry<DN, Entry> e = i.next();
-                                    i.remove();
+                                // in order to avoid cases where the renamed subtree overlaps.
+                                final SortedMap<byte[], byte[]> renamedEntries =
+                                        new TreeMap<byte[], byte[]>(DN_ORDER);
 
-                                    final DN renamedDN = e.getKey().rename(oldDN, newDN);
-                                    final Entry entry = e.getValue().setName(renamedDN);
-                                    renamedEntries.put(renamedDN, entry);
+                                // @formatter:off
+                                final Iterator<Map.Entry<byte[], byte[]>> i =
+                                    entries.subMap(changeNormDN,
+                                        getBytes((changeDN.child(RDN.maxValue())).
+                                                toNormalizedString())).entrySet().iterator();
+                                // @formatter:on
+
+                                while (i.hasNext()) {
+                                    final Map.Entry<byte[], byte[]> e = i.next();
+                                    final Entry entry = decodeEntry(e.getValue());
+                                    final DN renamedDN = entry.getName().rename(oldDN, newDN);
+                                    entry.setName(renamedDN);
+                                    renamedEntries.put(getBytes(renamedDN.toNormalizedString()),
+                                            encodeEntry(entry)[1]);
+                                    i.remove();
                                 }
 
-                                // Modify the target entry.
-                                final Entry entry = entries.values().iterator().next();
+                                // Modify target entry
+                                final Entry targetEntry =
+                                        decodeEntry(renamedEntries.values().iterator().next());
+
                                 if (change.isDeleteOldRDN()) {
                                     for (final AVA ava : oldDN.rdn()) {
-                                        entry.removeAttribute(ava.toAttribute(), null);
+                                        targetEntry.removeAttribute(ava.toAttribute(), null);
                                     }
                                 }
                                 for (final AVA ava : newDN.rdn()) {
-                                    entry.addAttribute(ava.toAttribute());
+                                    targetEntry.addAttribute(ava.toAttribute());
                                 }
 
+                                renamedEntries.remove(getBytes(targetEntry.getName()
+                                        .toNormalizedString()));
+                                renamedEntries.put(getBytes(targetEntry.getName()
+                                        .toNormalizedString()), encodeEntry(targetEntry)[1]);
+
                                 // Add the renamed entries.
-                                for (final Entry renamedEntry : renamedEntries.values()) {
-                                    final Entry existingEntry = entries.get(renamedEntry.getName());
-                                    if (existingEntry != null) {
+                                final Iterator<byte[]> j = renamedEntries.values().iterator();
+                                while (j.hasNext()) {
+                                    final Entry renamedEntry = decodeEntry(j.next());
+                                    final byte[] existingEntryDn =
+                                            entries.get(getBytes(renamedEntry.getName()
+                                                    .toNormalizedString()));
+
+                                    if (existingEntryDn != null) {
+                                        final Entry existingEntry = decodeEntry(existingEntryDn);
                                         try {
                                             final Entry tmp =
                                                     listener.handleDuplicateEntry(change,
                                                             existingEntry, renamedEntry);
-                                            entries.put(tmp.getName(), tmp);
+                                            entries.put(
+                                                    getBytes(tmp.getName().toNormalizedString()),
+                                                    encodeEntry(tmp)[1]);
                                         } catch (final DecodeException e) {
                                             return e;
                                         }
                                     } else {
-                                        entries.put(renamedEntry.getName(), renamedEntry);
+                                        entries.put(getBytes(renamedEntry.getName()
+                                                .toNormalizedString()),
+                                                encodeEntry(renamedEntry)[1]);
                                     }
                                 }
+                                renamedEntries.clear();
                             }
                             return null;
                         }
@@ -409,7 +485,7 @@
                         @Override
                         public DecodeException visitChangeRecord(final Void p,
                                 final ModifyRequest change) {
-                            if (!entries.containsKey(change.getName())) {
+                            if (entries.get(changeNormDN) == null) {
                                 try {
                                     listener.handleRejectedChangeRecord(change,
                                             REJECTED_CHANGE_FAIL_MODIFY.get(change.getName()
@@ -418,7 +494,7 @@
                                     return e;
                                 }
                             } else {
-                                final Entry entry = entries.get(change.getName());
+                                final Entry entry = decodeEntry(entries.get(changeNormDN));
                                 for (final Modification modification : change.getModifications()) {
                                     final ModificationType modType =
                                             modification.getModificationType();
@@ -434,6 +510,7 @@
                                                 + "\": modification type not supported");
                                     }
                                 }
+                                entries.put(changeNormDN, encodeEntry(entry)[1]);
                             }
                             return null;
                         }
@@ -446,7 +523,7 @@
         }
 
         return new EntryReader() {
-            private final Iterator<Entry> iterator = entries.values().iterator();
+            private final Iterator<byte[]> iterator = entries.values().iterator();
 
             @Override
             public void close() throws IOException {
@@ -464,7 +541,7 @@
 
             @Override
             public Entry readEntry() throws IOException {
-                return iterator.next();
+                return decodeEntry(iterator.next());
             }
         };
     }
@@ -612,15 +689,61 @@
         };
     }
 
-    private static SortedMap<DN, Entry> readEntries(final EntryReader reader) throws IOException {
-        final SortedMap<DN, Entry> entries = new TreeMap<DN, Entry>();
+    private static List<byte[][]> readEntriesAsList(final EntryReader reader) throws IOException {
+        final List<byte[][]> entries = new ArrayList<byte[][]>();
+
         while (reader.hasNext()) {
             final Entry entry = reader.readEntry();
-            entries.put(entry.getName(), entry);
+            entries.add(encodeEntry(entry));
         }
+        // Sorting the list by DN
+        Collections.sort(entries, DN_ORDER2);
+
         return entries;
     }
 
+    private static TreeMap<byte[], byte[]> readEntriesAsMap(final EntryReader reader)
+            throws IOException {
+        final TreeMap<byte[], byte[]> entries = new TreeMap<byte[], byte[]>(DN_ORDER);
+
+        while (reader.hasNext()) {
+            final Entry entry = reader.readEntry();
+            final byte[][] bEntry = encodeEntry(entry);
+            entries.put(bEntry[0], bEntry[1]);
+        }
+
+        return entries;
+    }
+
+    private static SearchResultEntry decodeEntry(final byte[] asn1EntryFormat) {
+        final ASN1Reader readerASN1 = ASN1.getReader(asn1EntryFormat);
+        try {
+            final SearchResultEntry sr =
+                    LDAPUtils.decodeSearchResultEntry(readerASN1, new DecodeOptions());
+            readerASN1.close();
+            return sr;
+        } catch (IOException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    private static byte[][] encodeEntry(final Entry entry) {
+        final byte[][] bEntry = new byte[2][];
+
+        final ByteStringBuilder bsb = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(bsb);
+        // Store normalized DN
+        bEntry[0] = getBytes(entry.getName().toNormalizedString());
+        try {
+            // Store ASN1 representation of the entry.
+            LDAPUtils.encodeSearchResultEntry(writer, Responses.newSearchResultEntry(entry));
+            bEntry[1] = bsb.toByteArray();
+            return bEntry;
+        } catch (final IOException ioe) {
+            throw new IllegalStateException(ioe);
+        }
+    }
+
     // Prevent instantiation.
     private LDIF() {
         // Do nothing.
diff --git a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldif/LDIFTestCase.java b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldif/LDIFTestCase.java
index 0d614da..0a2302b 100644
--- a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldif/LDIFTestCase.java
+++ b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldif/LDIFTestCase.java
@@ -29,6 +29,7 @@
 
 import static org.fest.assertions.Assertions.assertThat;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
@@ -202,12 +203,12 @@
 
         // @formatter:off
         final EntryReader reader = new LDIFEntryReader(
-                "dn: uid=user.0,ou=People,dc=example,dc=com",
-                "objectClass: person",
-                "objectClass: inetorgperson",
-                "objectClass: organizationalperson",
-                "objectClass: top",
-                "uid: user.0"
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "uid: user.0"
         );
         // @formatter:on
 
@@ -306,7 +307,7 @@
      *
      * @throws Exception
      */
-    @Test()
+    @Test(expectedExceptions = NoSuchElementException.class)
     public final void testLdifSearchWithSchemaMatchFullAttributesTypeOnly() throws Exception {
 
         // @formatter:off
@@ -335,13 +336,8 @@
             assertThat(entry.getAttribute("uid")).isNotNull();
             assertThat(entry.getAttribute("uid").getAttributeDescription()).isNotNull();
             assertThat(entry.getAttribute("uid")).isEmpty();
-
-            try {
-                assertThat(entry.getAttribute("uid").firstValueAsString()).isNull();
-            } catch (NoSuchElementException ex) {
-                // No values, only type.
-                // Expected exception on entry.getAttribute("uid").firstValueAsString()
-            }
+            // The following assert throws an exception because no values contained in, only type.
+            assertThat(entry.getAttribute("uid").firstValueAsString()).isNull();
         }
         resultReader.close();
     }
@@ -470,7 +466,7 @@
      *
      * @throws Exception
      */
-    @Test()
+    @Test(expectedExceptions = NoSuchElementException.class)
     public final void testLdifSearchWithSchemaMatchSpecifiedAttributeTypeOnly() throws Exception {
 
         // @formatter:off
@@ -496,12 +492,9 @@
             assertThat(entry.getAttributeCount()).isEqualTo(1);
             assertThat(entry.getAttribute("uid").getAttributeDescription()).isNotNull();
             assertThat(entry.getAttribute("uid")).isEmpty();
-            try {
-                assertThat(entry.getAttribute("uid").firstValueAsString()).isNull();
-            } catch (NoSuchElementException ex) {
-                // No values, only type.
-                // Expected exception on entry.getAttribute("uid").firstValueAsString()
-            }
+            // The following assert throws an exception because no values contained in, only type.
+            assertThat(entry.getAttribute("uid").firstValueAsString()).isNull();
+
         }
         resultReader.close();
     }
@@ -637,7 +630,10 @@
             "objectClass: person",
             "objectClass: inetorgperson"
         );
-        final Entry e1 = new LinkedHashMapEntry("dn: uid=user.1,ou=People,dc=example,dc=com", "objectClass: person");
+        final Entry e1 = new LinkedHashMapEntry(
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: person"
+        );
         // @formatter:on
 
         final SortedMap<DN, Entry> sourceEntries = new TreeMap<DN, Entry>();
@@ -646,10 +642,9 @@
         final Iterator<Entry> sourceIterator = sourceEntries.values().iterator();
 
         final EntryReader resultReader = LDIF.newEntryIteratorReader(sourceIterator);
-        Entry entry = null;
         int cCount = 0;
         while (resultReader.hasNext()) {
-            entry = resultReader.readEntry();
+            final Entry entry = resultReader.readEntry();
             assertThat(entry.getName().toString()).isNotNull();
             assertThat(entry.getName().toString()).contains("ou=People,dc=example,dc=com");
             assertThat(entry.getAttributeCount()).isGreaterThanOrEqualTo(1);
@@ -862,7 +857,7 @@
         assertThat(cr).isInstanceOf(ModifyRequest.class);
 
         // @formatter:off
-        /* Expected : 2 add / 1 delete.
+        /* Expected : 2 add / 1 delete - output :
          * dn: uid=newuser,ou=People,dc=example,dc=com
          * changetype: modify
          * add: userPassword
@@ -1025,6 +1020,199 @@
     }
 
     /**
+     * Differences between two short ldif examples.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifDiffEntriesShortExamples() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader source = new LDIFEntryReader(
+                "dn: uid=user.0,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aaccf",
+                "sn: Amar",
+                "cn: Aaccf Amar",
+                "initials: APA",
+                "employeeNumber: 0",
+                "uid: user.0",
+                "mail: user.0@example.com",
+                "description: This is the description for Aaccf Amar.",
+                "",
+                "dn: uid=user.1,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aaren",
+                "sn: Atp",
+                "cn: Aaren Atp",
+                "initials: AFA",
+                "employeeNumber: 1",
+                "uid: user.1",
+                "mail: user.1@example.com",
+                "description: This is the description for Aaren Atp.",
+                "",
+                "dn: uid=user.2,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aarika",
+                "sn: Atpco",
+                "cn: Aarika Atpco",
+                "initials: AVA",
+                "employeeNumber: 2",
+                "uid: user.2",
+                "mail: user.2@example.com",
+                "description: This is the description for Aarika Atpco.",
+                "",
+                "dn: uid=user.3,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aaron",
+                "sn: Atrc",
+                "cn: Aaron Atrc",
+                "initials: ATA",
+                "employeeNumber: 3",
+                "uid: user.3",
+                "mail: user.3@example.com",
+                "description: This is the description for Aaron Atrc.",
+                "",
+                "dn: uid=user.4,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aartjan",
+                "sn: Aalders",
+                "cn: Aartjan Aalders",
+                "initials: AAA",
+                "employeeNumber: 4",
+                "uid: user.4",
+                "mail: user.4@example.com",
+                "description: This is the description for Aartjan Aalders."
+        );
+
+        final LDIFEntryReader target = new LDIFEntryReader(
+                "dn: uid=user.0,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Amar", // diff
+                "sn: Amar",
+                "cn: Aaccf Amar",
+                "initials: APA",
+                "employeeNumber: 55", // diff
+                "uid: user.0",
+                "mail: user.0@example.com",
+                "description: This is the description for Aaccf Amar.",
+                "work-phone: 650/506-0666", // diff
+                "",
+                "dn: uid=user.1,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aaren",
+                "sn: Atp",
+                "cn: Aaren Atp",
+                "initials: AFA",
+                "employeeNumber: 1",
+                "uid: user.1",
+                "mail: aaren@example.com", // diff
+                "description: This is the description for Aaren Atp.",
+                "",
+                "dn: uid=user.2,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aarika",
+                "sn: Atpco",
+                "cn: Aarika Atpco",
+                "initials: AVA",
+                "employeeNumber: 2",
+                "uid: user.2",
+                "mail: user.2@example.com", // diff (delete description)
+                "",
+                "dn: uid=user.3,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aaron",
+                "sn: Atrc",
+                "cn: Aaron Atrc",
+                "initials: ATA",
+                "employeeNumber: 3",
+                "uid: user.999", // diff
+                "mail: user.999@example.com", // diff
+                "description: This is the description for Aaron Atrc.",
+                "",
+                "dn: uid=user.4,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aartjan",
+                "sn: Aalders",
+                "cn: Aartjan Aalders",
+                "initials: AAA",
+                "employeeNumber: 4",
+                "uid: user.4",
+                "mail: user.4@example.com",
+                "description: This is the description for Aartjan Aalders."
+        );
+        // @formatter:on
+
+        final ChangeRecordReader reader = LDIF.diff(source, target);
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications()).isNotEmpty();
+        // 1st entry : 2 add/delete + 1 add(work-phone)
+        assertThat(((ModifyRequest) cr).getModifications().size()).isEqualTo(5);
+        // 2nd entry : 1 add/delete
+        cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications().size()).isEqualTo(2);
+        // 3rd entry : 1 delete
+        cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=user.2,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications().size()).isEqualTo(1);
+        assertThat(((ModifyRequest) cr).getModifications().get(0).getModificationType().toString())
+                .isEqualTo("delete");
+        assertThat(
+                ((ModifyRequest) cr).getModifications().get(0).getAttribute()
+                        .getAttributeDescriptionAsString()).isEqualTo("description");
+        // 4th entry : 2 add/delete
+        cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=user.3,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications().size()).isEqualTo(4);
+        // 5th entry : 0 modifications
+        cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=user.4,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications().size()).isEqualTo(0);
+        assertThat(reader.hasNext()).isFalse();
+
+        reader.close();
+        target.close();
+    }
+
+    /**
      * Diff between two same entries : no modifications expected.
      *
      * @throws Exception
@@ -1081,13 +1269,12 @@
     }
 
     /**
-     * Test the patch function. Apply simple patch to replace/add data to the
-     * input.
+     * Create a patch without any differences with the original.
      *
      * @throws Exception
      */
     @Test()
-    public final void testLdifPatch() throws Exception {
+    public final void testLdifPatchAddNoDiff() throws Exception {
         // @formatter:off
         final LDIFEntryReader input = new LDIFEntryReader(
             "dn: uid=scarter,ou=People,dc=example,dc=com",
@@ -1095,6 +1282,1012 @@
             "sn: new user",
             "mail: mail@mailme.org"
         );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(3); // objectclass - sn - mail
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * The patch add successfully an attribute 'manager' to the entry.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchAddDiff() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "add: manager",
+            "manager: uid=joneill,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5); // objectclass - sn - mail
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * The patch add two new entries to the original.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchAddDiffNewEntry() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=joneill,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "add: manager",
+            "manager: uid=hamond,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "",
+            "dn: uid=djackson,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "add: manager",
+            "manager: uid=joneill,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString())
+                .isEqualTo("uid=djackson,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=joneill,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+        reader.close();
+    }
+
+    /**
+     * Try to modify an nonexistent entry. The patch throw an error via the
+     * listener which is in RejectedChangeRecordListener.OVERWRITE.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchAddModifyNonExistantEntryDoNothing() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: cn=Lisa Jangles,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "add: telephonenumber",
+            "telephonenumber: (408) 555-2468"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Testing to delete in entry.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchDeleteEntry() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Testing to delete in entry.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchDeleteEntryAmongSeveral() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "uid: user.1",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2",
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaron",
+            "sn: Atrc",
+            "cn: Aaron Atrc",
+            "uid: user.3"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.3,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Testing to delete attributes in an entry.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchDeleteAttributesEntry() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Carter",
+            "sn: Sam",
+            "cn: Sam Carter",
+            "uid: scarter",
+            "mail: user.1@mail.com",
+            "postalAdress: 42 Shepherd Street",
+            "work-phone: 650/506-7000",
+            "work-phone: 650/506-0666",
+            "home-fax: 650-7001"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "delete: work-phone",
+            "work-phone: 650/506-0666",
+            "-",
+            "delete: home-fax"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(8);
+        assertThat(entry.getAttribute("work-phone").firstValueAsString()).isEqualTo("650/506-7000");
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Modifying an entry : add
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchModifyEntry() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "add: work-phone",
+            "work-phone: 650/506-7000"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * The patch attempts to modify the dn adding uppercase.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchModifyDNEntryUppercaseUid() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: scarter",
+            "",
+            "dn: uid=djackson,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: djackson"
+
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: uid=Scarter",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        // does not work with a single entry && ...
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString())
+                .isEqualTo("uid=djackson,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=Scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4); //The patch create uid attribute on the selected entry.
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Attemp to modify the entry adding uppercase in cn.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchModifyDNEntryUpperCaseDnNameSurname() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: cn=sam carter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: scarter",
+            "",
+            "dn: uid=djackson,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: djackson"
+
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: cn=sam carter,ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: cn=Sam Carter",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        // does not work with a single entry && ...
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "cn=Sam Carter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString())
+                .isEqualTo("uid=djackson,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4); //The patch create uid attribute on the selected entry.
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * The patch attempts to modify a rdn of a specific entry.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchModifyDNEntry() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: scarter",
+            "",
+            "dn: uid=djackson,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: djackson"
+
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: uid=Susan Jacobs",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        // does not work with a single entry && ...
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString())
+                .isEqualTo("uid=djackson,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=Susan Jacobs,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4); //The patch create uid attribute on the selected entry.
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    @Test()
+    public final void testLdifPatchModifyDnEntry2() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "uid: user.1",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2",
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaron",
+            "sn: Atrc",
+            "cn: Aaron Atrc",
+            "uid: user.3"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: uid=user.22",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.22,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.3,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Rename a branch : from ou=People,dc=example,dc=com to ou=Human
+     * Resources,dc=example,dc=com.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchModifyDnEntryBranch() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: organizationalunit",
+            "ou: People",
+            "",
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "uid: user.1",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2",
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaron",
+            "sn: Atrc",
+            "cn: Aaron Atrc",
+            "uid: user.3",
+            "",
+            "dn: uid=user.4,ou=People,dc=example,dc=org",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Allan",
+            "sn: Zorg",
+            "cn: Allan Zorg",
+            "uid: user.4"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: ou=Human Resources,dc=example,dc=com",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAllAttributes("ou").iterator().next().firstValueAsString()).isEqualTo(
+                "Human Resources");
+        assertThat(entry.getAttributeCount()).isEqualTo(2);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.1,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.2,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.3,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.4,ou=People,dc=example,dc=org");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Rename a branch : from ou=People,dc=example,dc=com to ou=Human
+     * Resources,dc=example,dc=com. In this example deleteoldrdn is set to 0.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchModifyDnEntryBranchKeepsOldRdn() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: organizationalunit",
+            "ou: People",
+            "",
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "uid: user.1",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2",
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaron",
+            "sn: Atrc",
+            "cn: Aaron Atrc",
+            "uid: user.3",
+            "",
+            "dn: uid=user.4,ou=People,dc=example,dc=org",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Allan",
+            "sn: Zorg",
+            "cn: Allan Zorg",
+            "uid: user.4"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: ou=Human Resources,dc=example,dc=com",
+            "deleteoldrdn: 0"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAllAttributes("ou").iterator().next().firstValueAsString()).isEqualTo(
+                "People");
+        assertThat(entry.getAttributeCount()).isEqualTo(2);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.1,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.2,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.3,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.4,ou=People,dc=example,dc=org");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Moves an entry.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchModifyDnEntryNewSuperior() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "uid: user.1",
+            "mail: user.1@mail.com",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "# moves the entry from ou=People, dc=example,dc=com to Marketing",
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: uid=user.1",
+            "deleteoldrdn: 1",
+            "newsuperior: ou=Marketing,dc=example,dc=com"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.1,ou=Marketing,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(6);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.2,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Apply simple patch to replace/add data to the input.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchAddReplace() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace:sn",
+            "sn: scarter",
+            "-",
+            "add: manager",
+            "manager: uid=joneill,ou=People,dc=example,dc=com"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4); // objectclass - sn - mail - manager
+        assertThat(entry.getAttribute("manager").firstValueAsString()).isEqualTo(
+                "uid=joneill,ou=People,dc=example,dc=com");
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Replace / add postalAdress.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchAddReplaceLanguageTagExample() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "postalAdress;lang-en: Shepherd Street"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace: postalAdress;lang-fr",
+            "postalAdress;lang-fr: 355 avenue Leon Blum",
+            "-",
+            "replace: postalAdress;lang-en",
+            "postalAdress;lang-en: 42 Shepherd Street"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5); // objectclass - sn - mail - manager - postalAdress
+        assertThat(entry.getAttribute("postalAdress;lang-fr").firstValueAsString()).isEqualTo(
+                "355 avenue Leon Blum");
+        assertThat(entry.getAttribute("postalAdress;lang-en").firstValueAsString()).isEqualTo(
+                "42 Shepherd Street");
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Test some changes : add/replace/delete...
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchVariousChanges() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "",
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaccf",
+            "sn: Amar",
+            "cn: Aaccf Amar",
+            "",
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaren",
+            "sn: Atp",
+            "cn: Aaren Atp",
+            "mail: AarenAtp@mail.org",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "description:: ZnVubnkgZGVzY3JpcHRpb24gISA6RA==",
+            "mail:: QWFyaWthQXRwY29AbWFpbC5vcmc=",
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Kadja",
+            "sn: Atpcol",
+            "cn: Kadja Atpcol"
+        );
+        // @formatter:on
+
+        final File file = File.createTempFile("sdk", ".png");
+        final String url = file.toURI().toURL().toString();
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace:sn",
+            "sn: scarter",
+            "-",
+            "add: manager",
+            "manager: uid=joneill,ou=People,dc=example,dc=com",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace:description",
+            "description:: QWFyaWthIEF0cGNvIGRlc2NyaXB0aW9uIDogbG9yZW0gaXBzdW0uLi4=",
+            "-",
+            "add: jpegphoto",
+            "jpegphoto:< " + url,
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(entry.getAttribute("manager").firstValueAsString()).isEqualTo(
+                "uid=joneill,ou=People,dc=example,dc=com");
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getAttributeCount()).isEqualTo(7);
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.2,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttribute("mail").firstValueAsString()).isEqualTo(
+                "AarikaAtpco@mail.org");
+        assertThat(entry.getAttribute("description").firstValueAsString()).isEqualTo(
+                "Aarika Atpco description : lorem ipsum...");
+        assertThat(entry.getAttribute("jpegphoto")).isNotEmpty();
+        assertThat(reader.hasNext()).isFalse();
+
+        file.delete();
+        reader.close();
+    }
+
+    /**
+     * An example for illustrate an LDIFChangeRecordReader containing changes on
+     * previous ldif.
+     *
+     * @throws Exception
+     */
+    @Test()
+    public final void testLdifPatchContainingChanges() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "",
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaccf",
+            "sn: Amar",
+            "cn: Aaccf Amar",
+            "",
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaren",
+            "sn: Atp",
+            "cn: Aaren Atp",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco"
+        );
+        // @formatter:on
 
         // @formatter:off
         final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
@@ -1104,21 +2297,101 @@
                 "sn: scarter",
                 "-",
                 "add: manager",
-                "manager: uid=joneill,ou=People,dc=example,dc=com"
+                "manager: uid=joneill,ou=People,dc=example,dc=com",
+                "",
+                "dn: uid=user.0,ou=People,dc=example,dc=com",
+                "changetype: modify",
+                "replace:sn",
+                "sn: Amarr",
+                "-",
+                "delete: givenName",
+                "",
+                "dn: uid=user.1,ou=People,dc=example,dc=com",
+                "changetype: modify",
+                "replace:givenName",
+                "givenName: Aarwen",
+                "-",
+                "add: manager",
+                "manager: uid=joneill,ou=People,dc=example,dc=com",
+                "-",
+                "add: mail",
+                "mail: Aarwen@mail.com",
+                "-",
+                "add: fax",
+                "fax: 555 555-5555",
+                "-",
+                "add: description",
+                "description:: QWFyd2VuIGRlc2NyaXB0aW9uLg=="
         );
         // @formatter:on
 
-        // @formatter:on
         final EntryReader reader = LDIF.patch(input, patch);
+
         Entry entry = reader.readEntry();
         assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
-        assertThat(entry.getAttributeCount()).isEqualTo(4); // objectclass - sn - mail - manager
+        // Attr. list : objectclass - sn - mail - manager
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
         assertThat(entry.getAttribute("manager").firstValueAsString()).isEqualTo(
                 "uid=joneill,ou=People,dc=example,dc=com");
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        // Attr. list : objectclass - sn - cn
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        // Attr. list : objectclass - sn - cn - givenName - manager - mail - fax - description
+        assertThat(entry.getAttributeCount()).isEqualTo(8);
+        assertThat(entry.getAttribute("description").firstValueAsString()).isEqualTo(
+                "Aarwen description.");
+        assertThat(reader.hasNext()).isTrue();
+        // Last entry, no modification on it.
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.2,ou=People,dc=example,dc=com");
+        // Attr. list : objectClass - givenname - sn - cn
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(reader.hasNext()).isFalse();
         reader.close();
     }
 
     /**
+     * Try to apply a patch which data are not valid. Exception expected.
+     *
+     * @throws Exception
+     */
+    @SuppressWarnings("resource")
+    @Test(expectedExceptions = DecodeException.class)
+    public final void testLdifPatchInvalidChangeRecord() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+                "dn: uid=scarter,ou=People,dc=example,dc=com",
+                "changetype: modif\u0000",
+                "replace:sn",
+                "sn: scarter",
+                "-",
+                "add: manager\u0000",
+                "manager: uid=joneill,ou=People,dc=example,dc=com"
+        );
+        // @formatter:on
+        EntryReader reader = new LDIFEntryReader();
+        try {
+            reader = LDIF.patch(input, patch);
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
      * LDIF patch - EntryReader/ChangeRecordReader doesn't allow null. Exception
      * expected.
      *
@@ -1128,5 +2401,4 @@
     public final void testLdifPatchDoesntAllowNull() throws Exception {
         LDIF.patch(null, null);
     }
-
 }

--
Gitblit v1.10.0