mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Matthew Swift
05.01.2014 19f4078a1c392ef3a8423139745c474ee8facd59
Fix OPENDJ-1481: Consider adding method for comparing entries for exact differences, ignoring matching rules

Introduced a new object, DiffOptions, which can be used to control the behavior of diffEntries:

* the ability to perform byte-by-byte comparisons
* the ability to select which attributes are compared using an AttributeFilter
* the ability to control whether reversible (DELETE+ADD) modifications are generated, or non-reversible REPLACE modifications. For small attributes REPLACE modifications are desirable because the consume less bandwidth and result in less replication meta-data.
3 files modified
983 ■■■■ changed files
opendj-core/src/main/java/org/forgerock/opendj/ldap/Entries.java 361 ●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java 18 ●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/ldap/EntriesTestCase.java 604 ●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/Entries.java
@@ -22,15 +22,13 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2011-2013 ForgeRock AS
 *      Portions copyright 2011-2014 ForgeRock AS
 */
package org.forgerock.opendj.ldap;
import static org.forgerock.opendj.ldap.AttributeDescription.objectClass;
import static com.forgerock.opendj.ldap.CoreMessages.*;
import static org.forgerock.opendj.ldap.AttributeDescription.objectClass;
import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
import java.util.ArrayList;
@@ -39,6 +37,7 @@
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@@ -63,6 +62,142 @@
 * @see Entry
 */
public final class Entries {
    /**
     * Options for controlling the behavior of the
     * {@link Entries#diffEntries(Entry, Entry, DiffOptions) diffEntries}
     * method. {@code DiffOptions} specify which attributes are compared, how
     * they are compared, and the type of modifications generated.
     *
     * @see Entries#diffEntries(Entry, Entry, DiffOptions)
     */
    public static final class DiffOptions {
        /**
         * Selects which attributes will be compared. By default all user
         * attributes will be compared.
         */
        private AttributeFilter attributeFilter = USER_ATTRIBUTES_ONLY_FILTER;
        /**
         * When true, attribute values are compared byte for byte, otherwise
         * they are compared using their matching rules.
         */
        private boolean useExactMatching = false;
        /**
         * When greater than 0, modifications with REPLACE type will be
         * generated for the new attributes containing at least
         * "useReplaceMaxValues" attribute values. Otherwise, modifications with
         * DELETE + ADD types will be generated.
         */
        private int useReplaceMaxValues = 0;
        private DiffOptions() {
            // Nothing to do.
        }
        /**
         * Specifies an attribute filter which will be used to determine which
         * attributes will be compared. By default only user attributes will be
         * compared.
         *
         * @param attributeFilter
         *            The filter which will be used to determine which
         *            attributes will be compared.
         * @return A reference to this set of options.
         */
        public DiffOptions attributes(final AttributeFilter attributeFilter) {
            Reject.ifNull(attributeFilter);
            this.attributeFilter = attributeFilter;
            return this;
        }
        /**
         * Specifies the list of attributes to be compared. By default only user
         * attributes will be compared.
         *
         * @param attributeDescriptions
         *            The names of the attributes to be compared.
         * @return A reference to this set of options.
         */
        public DiffOptions attributes(final String... attributeDescriptions) {
            return attributes(new AttributeFilter(attributeDescriptions));
        }
        /**
         * Requests that attribute values should be compared byte for byte,
         * rather than using their matching rules. This is useful when a client
         * wishes to perform trivial changes to an attribute value which would
         * otherwise be ignored by the matching rule, such as removing extra
         * white space from an attribute, or capitalizing a user's name.
         *
         * @return A reference to this set of options.
         */
        public DiffOptions useExactMatching() {
            this.useExactMatching = true;
            return this;
        }
        /**
         * Requests that all generated changes should use the
         * {@link ModificationType#REPLACE REPLACE} modification type, rather
         * than a combination of {@link ModificationType#DELETE DELETE} and
         * {@link ModificationType#ADD ADD}.
         * <p>
         * Note that the generated changes will not be reversible, nor will they
         * be efficient for attributes containing many values (such as groups).
         * Enabling this option may result in more efficient updates for single
         * valued attributes and reduce the amount of replication meta-data that
         * needs to be maintained..
         *
         * @return A reference to this set of options.
         */
        public DiffOptions alwaysReplaceAttributes() {
            return replaceMaxValues(Integer.MAX_VALUE);
        }
        /**
         * Requests that the generated changes should use the
         * {@link ModificationType#REPLACE REPLACE} modification type when the
         * new attribute contains at most one attribute value. All other changes
         * will use a combination of {@link ModificationType#DELETE DELETE} then
         * {@link ModificationType#ADD ADD}.
         * <p>
         * Specifying this option will usually provide the best overall
         * performance for single and multi-valued attribute updates, but the
         * generated changes will probably not be reversible.
         *
         * @return A reference to this set of options.
         */
        public DiffOptions replaceSingleValuedAttributes() {
            return replaceMaxValues(1);
        }
        /**
         * Requests that the generated changes should use the
         * {@link ModificationType#REPLACE REPLACE} modification type when the
         * new attribute contains {@code maxValues} attribute values or less.
         * All other changes will use a combination of
         * {@link ModificationType#DELETE DELETE} then
         * {@link ModificationType#ADD ADD}.
         *
         * @param maxValues
         *            The maximum number of attribute values a modified
         *            attribute can contain before reversible changes will be
         *            generated.
         * @return A reference to this set of options.
         */
        private DiffOptions replaceMaxValues(final int maxValues) {
            // private until we can think of a good use case and better name.
            Reject.ifFalse(maxValues >= 0, "maxValues must be >= 0");
            this.useReplaceMaxValues = maxValues;
            return this;
        }
        private Entry filter(final Entry entry) {
            return attributeFilter.filteredViewOf(entry);
        }
    }
    private static final class UnmodifiableEntry implements Entry {
        private final Entry entry;
@@ -269,6 +404,9 @@
        }
    };
    private static final AttributeFilter USER_ATTRIBUTES_ONLY_FILTER = new AttributeFilter();
    private static final DiffOptions DEFAULT_DIFF_OPTIONS = new DiffOptions();
    private static final Function<Attribute, Attribute, Void> UNMODIFIABLE_ATTRIBUTE_FUNCTION =
            new Function<Attribute, Attribute, Void>() {
@@ -409,12 +547,15 @@
     * Creates a new modify request containing a list of modifications which can
     * be used to transform {@code fromEntry} into entry {@code toEntry}.
     * <p>
     * The modify request is reversible: it will contain only modifications of
     * type {@link ModificationType#ADD ADD} and {@link ModificationType#DELETE
     * DELETE}.
     * The changes will be generated using a default set of {@link DiffOptions
     * options}. More specifically, only user attributes will be compared,
     * attributes will be compared using their matching rules, and all generated
     * changes will be reversible: it will contain only modifications of type
     * {@link ModificationType#DELETE DELETE} then {@link ModificationType#ADD
     * ADD}.
     * <p>
     * Finally, the modify request will use the distinguished name taken from
     * {@code fromEntry}. Moreover, this method will not check to see if both
     * {@code fromEntry}. This method will not check to see if both
     * {@code fromEntry} and {@code toEntry} have the same distinguished name.
     * <p>
     * This method is equivalent to:
@@ -423,35 +564,62 @@
     * ModifyRequest request = Requests.newModifyRequest(fromEntry, toEntry);
     * </pre>
     *
     * Or:
     *
     * <pre>
     * ModifyRequest request = diffEntries(fromEntry, toEntry, Entries.diffOptions());
     * </pre>
     *
     * @param fromEntry
     *            The source entry.
     * @param toEntry
     *            The destination entry.
     * @return A modify request containing a list of modifications which can be
     *         used to transform {@code fromEntry} into entry {@code toEntry}.
     *         The returned request will always be non-{@code null} but may not
     *         contain any modifications.
     * @throws NullPointerException
     *             If {@code fromEntry} or {@code toEntry} were {@code null}.
     * @see Requests#newModifyRequest(Entry, Entry)
     */
    public static ModifyRequest diffEntries(final Entry fromEntry, final Entry toEntry) {
        Reject.ifNull(fromEntry, toEntry);
        return diffEntries(fromEntry, toEntry, DEFAULT_DIFF_OPTIONS);
    }
    /**
     * Creates a new modify request containing a list of modifications which can
     * be used to transform {@code fromEntry} into entry {@code toEntry}.
     * <p>
     * The changes will be generated using the provided set of
     * {@link DiffOptions}.
     * <p>
     * Finally, the modify request will use the distinguished name taken from
     * {@code fromEntry}. This method will not check to see if both
     * {@code fromEntry} and {@code toEntry} have the same distinguished name.
     *
     * @param fromEntry
     *            The source entry.
     * @param toEntry
     *            The destination entry.
     * @param options
     *            The set of options which will control which attributes are
     *            compared, how they are compared, and the type of modifications
     *            generated.
     * @return A modify request containing a list of modifications which can be
     *         used to transform {@code fromEntry} into entry {@code toEntry}.
     *         The returned request will always be non-{@code null} but may not
     *         contain any modifications.
     * @throws NullPointerException
     *             If {@code fromEntry}, {@code toEntry}, or {@code options}
     *             were {@code null}.
     */
    public static ModifyRequest diffEntries(final Entry fromEntry, final Entry toEntry,
            final DiffOptions options) {
        Reject.ifNull(fromEntry, toEntry, options);
        final ModifyRequest request = Requests.newModifyRequest(fromEntry.getName());
        final TreeMapEntry tfrom;
        if (fromEntry instanceof TreeMapEntry) {
            tfrom = (TreeMapEntry) fromEntry;
        } else {
            tfrom = new TreeMapEntry(fromEntry);
        }
        final TreeMapEntry tto;
        if (toEntry instanceof TreeMapEntry) {
            tto = (TreeMapEntry) toEntry;
        } else {
            tto = new TreeMapEntry(toEntry);
        }
        final Entry tfrom = toFilteredTreeMapEntry(fromEntry, options);
        final Entry tto = toFilteredTreeMapEntry(toEntry, options);
        final Iterator<Attribute> ifrom = tfrom.getAllAttributes().iterator();
        final Iterator<Attribute> ito = tto.getAllAttributes().iterator();
@@ -461,62 +629,101 @@
        while (afrom != null && ato != null) {
            final AttributeDescription adfrom = afrom.getAttributeDescription();
            final AttributeDescription adto = ato.getAttributeDescription();
            final int cmp = adfrom.compareTo(adto);
            if (cmp == 0) {
                /*
                 * Attribute is in both entries. Compute the set of values to be
                 * added and removed. We won't replace the attribute because
                 * this is not reversible.
                 * Attribute is in both entries so compute the differences
                 * between the old and new.
                 */
                final Attribute addedValues = new LinkedAttribute(ato);
                addedValues.removeAll(afrom);
                if (!addedValues.isEmpty()) {
                    request.addModification(new Modification(ModificationType.ADD, addedValues));
                if (options.useReplaceMaxValues > ato.size()) {
                    // This attribute is a candidate for replacing.
                    if (diffAttributeNeedsReplacing(afrom, ato, options)) {
                        request.addModification(new Modification(ModificationType.REPLACE, ato));
                }
                } else if (afrom.size() == 1 && ato.size() == 1) {
                    // Fast-path for single valued attributes.
                    if (diffFirstValuesAreDifferent(options, afrom, ato)) {
                        diffDeleteValues(request, afrom);
                        diffAddValues(request, ato);
                    }
                } else if (options.useExactMatching) {
                    /*
                     * Compare multi-valued attributes using exact matching. Use
                     * a hash sets for membership checking rather than the
                     * attributes in order to avoid matching rule based
                     * comparisons.
                     */
                    final Set<ByteString> oldValues = new LinkedHashSet<ByteString>(afrom);
                    final Set<ByteString> newValues = new LinkedHashSet<ByteString>(ato);
                    final Set<ByteString> deletedValues = new LinkedHashSet<ByteString>(oldValues);
                    deletedValues.removeAll(newValues);
                    diffDeleteValues(request, deletedValues.size() == afrom.size() ? afrom
                            : new LinkedAttribute(adfrom, deletedValues));
                    final Set<ByteString> addedValues = newValues;
                    addedValues.removeAll(oldValues);
                    diffAddValues(request, addedValues.size() == ato.size() ? ato
                            : new LinkedAttribute(adto, addedValues));
                } else {
                    // Compare multi-valued attributes using matching rules.
                final Attribute deletedValues = new LinkedAttribute(afrom);
                deletedValues.removeAll(ato);
                if (!deletedValues.isEmpty()) {
                    request.addModification(new Modification(ModificationType.DELETE, deletedValues));
                    diffDeleteValues(request, deletedValues);
                    final Attribute addedValues = new LinkedAttribute(ato);
                    addedValues.removeAll(afrom);
                    diffAddValues(request, addedValues);
                }
                afrom = ifrom.hasNext() ? ifrom.next() : null;
                ato = ito.hasNext() ? ito.next() : null;
            } else if (cmp < 0) {
                // afrom in source, but not destination.
                request.addModification(new Modification(ModificationType.DELETE, afrom));
                diffDeleteAttribute(request, afrom, options);
                afrom = ifrom.hasNext() ? ifrom.next() : null;
            } else {
                // ato in destination, but not in source.
                request.addModification(new Modification(ModificationType.ADD, ato));
                diffAddAttribute(request, ato, options);
                ato = ito.hasNext() ? ito.next() : null;
            }
        }
        // Additional attributes in source entry: these must be deleted.
        if (afrom != null) {
            request.addModification(new Modification(ModificationType.DELETE, afrom));
            diffDeleteAttribute(request, afrom, options);
        }
        while (ifrom.hasNext()) {
            final Attribute a = ifrom.next();
            request.addModification(new Modification(ModificationType.DELETE, a));
            diffDeleteAttribute(request, ifrom.next(), options);
        }
        // Additional attributes in destination entry: these must be added.
        if (ato != null) {
            request.addModification(new Modification(ModificationType.ADD, ato));
            diffAddAttribute(request, ato, options);
        }
        while (ito.hasNext()) {
            final Attribute a = ito.next();
            request.addModification(new Modification(ModificationType.ADD, a));
            diffAddAttribute(request, ito.next(), options);
        }
        return request;
    }
    /**
     * Returns a new set of options which may be used to control how entries are
     * compared and changes generated using
     * {@link #diffEntries(Entry, Entry, DiffOptions)}. By default only user
     * attributes will be compared, matching rules will be used for comparisons,
     * and all generated changes will be reversible.
     *
     * @return A new set of options which may be used to control how entries are
     *         compared and changes generated.
     */
    public static DiffOptions diffOptions() {
        return new DiffOptions();
    }
    /**
     * Returns an unmodifiable set containing the object classes associated with
     * the provided entry. This method will ignore unrecognized object classes.
     * <p>
@@ -617,7 +824,7 @@
     * <p>
     * Sample usage:
     * <pre>
     * List<Entry> smiths = TestCaseUtils.makeEntries(
     * List&lt;Entry&gt; smiths = TestCaseUtils.makeEntries(
     *   "dn: cn=John Smith,dc=example,dc=com",
     *   "objectclass: inetorgperson",
     *   "cn: John Smith",
@@ -816,6 +1023,64 @@
        }
    }
    private static void diffAddAttribute(final ModifyRequest request, final Attribute ato,
            final DiffOptions diffOptions) {
        if (diffOptions.useReplaceMaxValues > 0) {
            request.addModification(new Modification(ModificationType.REPLACE, ato));
        } else {
            request.addModification(new Modification(ModificationType.ADD, ato));
        }
    }
    private static void diffAddValues(final ModifyRequest request, final Attribute addedValues) {
        if (addedValues != null && !addedValues.isEmpty()) {
            request.addModification(new Modification(ModificationType.ADD, addedValues));
        }
    }
    private static boolean diffAttributeNeedsReplacing(final Attribute afrom, final Attribute ato,
            final DiffOptions options) {
        if (afrom.size() != ato.size()) {
            return true;
        } else if (afrom.size() == 1) {
            return diffFirstValuesAreDifferent(options, afrom, ato);
        } else if (options.useExactMatching) {
            /*
             * Use a hash set for membership checking rather than the attribute
             * in order to avoid matching rule based comparisons.
             */
            final Set<ByteString> oldValues = new LinkedHashSet<ByteString>(afrom);
            return !oldValues.containsAll(ato);
        } else {
            return !afrom.equals(ato);
        }
    }
    private static void diffDeleteAttribute(final ModifyRequest request, final Attribute afrom,
            final DiffOptions diffOptions) {
        if (diffOptions.useReplaceMaxValues > 0) {
            request.addModification(new Modification(ModificationType.REPLACE, Attributes
                    .emptyAttribute(afrom.getAttributeDescription())));
        } else {
            request.addModification(new Modification(ModificationType.DELETE, afrom));
        }
    }
    private static void diffDeleteValues(final ModifyRequest request, final Attribute deletedValues) {
        if (deletedValues != null && !deletedValues.isEmpty()) {
            request.addModification(new Modification(ModificationType.DELETE, deletedValues));
        }
    }
    private static boolean diffFirstValuesAreDifferent(final DiffOptions diffOptions,
            final Attribute afrom, final Attribute ato) {
        if (diffOptions.useExactMatching) {
            return !afrom.firstValue().equals(ato.firstValue());
        } else {
            return !afrom.contains(ato.firstValue());
        }
    }
    private static void incrementAttribute(final Entry entry, final Attribute change)
            throws ErrorResultException {
        // First parse the change.
@@ -893,6 +1158,14 @@
        return entry;
    }
    private static Entry toFilteredTreeMapEntry(final Entry entry, final DiffOptions options) {
        if (entry instanceof TreeMapEntry) {
            return options.filter(entry);
        } else {
            return new TreeMapEntry(options.filter(entry));
        }
    }
    // Prevent instantiation.
    private Entries() {
        // Nothing to do.
opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java
@@ -893,9 +893,13 @@
     * Creates a new modify request containing a list of modifications which can
     * be used to transform {@code fromEntry} into entry {@code toEntry}.
     * <p>
     * The modify request is reversible: it will contain only modifications of
     * type {@link ModificationType#ADD ADD} and {@link ModificationType#DELETE
     * DELETE}.
     * The changes will be generated using a default set of
     * {@link org.forgerock.opendj.ldap.Entries.DiffOptions options}. More
     * specifically, only user attributes will be compared, attributes will be
     * compared using their matching rules, and all generated changes will be
     * reversible: it will contain only modifications of type
     * {@link ModificationType#DELETE DELETE} then {@link ModificationType#ADD
     * ADD}.
     * <p>
     * Finally, the modify request will use the distinguished name taken from
     * {@code fromEntry}. Moreover, this method will not check to see if both
@@ -907,12 +911,20 @@
     * ModifyRequest request = Entries.diffEntries(fromEntry, toEntry);
     * </pre>
     *
     * Or:
     *
     * <pre>
     * ModifyRequest request = Entries.diffEntries(fromEntry, toEntry, Entries.diffOptions());
     * </pre>
     *
     * @param fromEntry
     *            The source entry.
     * @param toEntry
     *            The destination entry.
     * @return A modify request containing a list of modifications which can be
     *         used to transform {@code fromEntry} into entry {@code toEntry}.
     *         The returned request will always be non-{@code null} but may not
     *         contain any modifications.
     * @throws NullPointerException
     *             If {@code fromEntry} or {@code toEntry} were {@code null}.
     * @see Entries#diffEntries(Entry, Entry)
opendj-core/src/test/java/org/forgerock/opendj/ldap/EntriesTestCase.java
@@ -22,10 +22,14 @@
 *
 *
 *      Copyright 2009-2010 Sun Microsystems, Inc.
 *      Portions copyright 2014 ForgeRock AS.
 */
package org.forgerock.opendj.ldap;
import static org.fest.assertions.Assertions.assertThat;
import static org.forgerock.opendj.ldap.Entries.diffEntries;
import static org.forgerock.opendj.ldap.Entries.diffOptions;
import static org.testng.AssertJUnit.assertFalse;
import static org.testng.AssertJUnit.assertTrue;
@@ -34,8 +38,6 @@
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.schema.Schema;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
@@ -43,188 +45,442 @@
 */
@SuppressWarnings("javadoc")
public final class EntriesTestCase extends SdkTestCase {
    /**
     * Creates test data for {@link #testDiffEntries}.
     *
     * @return The test data.
     */
    @DataProvider(name = "createTestDiffEntriesData")
    public Object[][] createTestDiffEntriesData() {
        // @formatter:off
        Entry empty = new LinkedHashMapEntry(
            "dn: cn=test",
            "objectClass: top",
            "objectClass: test"
        );
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "objectClass: top",
            "objectClass: test",
            "fromOnly: fromOnlyValue",
            "bothSame: one",
            "bothSame: two",
            "bothSame: three",
            "bothDifferentDeletes: common",
            "bothDifferentDeletes: fromOnly1",
            "bothDifferentDeletes: fromOnly2",
            "bothDifferentAdds: common",
            "bothDifferentAddsAndDeletes: common",
            "bothDifferentAddsAndDeletes: fromOnly",
            "bothDifferentReplace: fromOnly1",
            "bothDifferentReplace: fromOnly2"
        );
    @Test
    public void testContainsObjectClass() throws Exception {
        Entry entry =
                new LinkedHashMapEntry("dn: cn=test", "objectClass: top", "objectClass: person");
        Schema schema = Schema.getDefaultSchema();
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "objectClass: top",
            "objectClass: test",
            "toOnly: toOnlyValue",
            "bothSame: one",
            "bothSame: two",
            "bothSame: three",
            "bothDifferentDeletes: common",
            "bothDifferentAdds: common",
            "bothDifferentAdds: toOnly1",
            "bothDifferentAdds: toOnly2",
            "bothDifferentAddsAndDeletes: common",
            "bothDifferentAddsAndDeletes: toOnly",
            "bothDifferentReplace: toOnly1",
            "bothDifferentReplace: toOnly2"
        );
        ModifyRequest diffFromEmpty = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "delete: bothDifferentAdds",
            "bothDifferentAdds: common",
            "-",
            "delete: bothDifferentAddsAndDeletes",
            "bothDifferentAddsAndDeletes: common",
            "bothDifferentAddsAndDeletes: fromOnly",
            "-",
            "delete: bothDifferentDeletes",
            "bothDifferentDeletes: common",
            "bothDifferentDeletes: fromOnly1",
            "bothDifferentDeletes: fromOnly2",
            "-",
            "delete: bothDifferentReplace",
            "bothDifferentReplace: fromOnly1",
            "bothDifferentReplace: fromOnly2",
            "-",
            "delete: bothSame",
            "bothSame: one",
            "bothSame: two",
            "bothSame: three",
            "-",
            "delete: fromOnly",
            "fromOnly: fromOnlyValue"
        );
        ModifyRequest diffEmptyTo = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "add: bothDifferentAdds",
            "bothDifferentAdds: common",
            "bothDifferentAdds: toOnly1",
            "bothDifferentAdds: toOnly2",
            "-",
            "add: bothDifferentAddsAndDeletes",
            "bothDifferentAddsAndDeletes: common",
            "bothDifferentAddsAndDeletes: toOnly",
            "-",
            "add: bothDifferentDeletes",
            "bothDifferentDeletes: common",
            "-",
            "add: bothDifferentReplace",
            "bothDifferentReplace: toOnly1",
            "bothDifferentReplace: toOnly2",
            "-",
            "add: bothSame",
            "bothSame: one",
            "bothSame: two",
            "bothSame: three",
            "-",
            "add: toOnly",
            "toOnly: toOnlyValue"
        );
        ModifyRequest diffFromTo = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "add: bothDifferentAdds",
            "bothDifferentAdds: toOnly1",
            "bothDifferentAdds: toOnly2",
            "-",
            "add: bothDifferentAddsAndDeletes",
            "bothDifferentAddsAndDeletes: toOnly",
            "-",
            "delete: bothDifferentAddsAndDeletes",
            "bothDifferentAddsAndDeletes: fromOnly",
            "-",
            "delete: bothDifferentDeletes",
            "bothDifferentDeletes: fromOnly1",
            "bothDifferentDeletes: fromOnly2",
            "-",
            "add: bothDifferentReplace",
            "bothDifferentReplace: toOnly1",
            "bothDifferentReplace: toOnly2",
            "-",
            "delete: bothDifferentReplace",
            "bothDifferentReplace: fromOnly1",
            "bothDifferentReplace: fromOnly2",
            "-",
            "delete: fromOnly",
            "fromOnly: fromOnlyValue",
            "-",
            "add: toOnly",
            "toOnly: toOnlyValue"
        );
        // From, to, diff.
        return new Object[][] {
            { from,  empty, diffFromEmpty },
            { empty, to,    diffEmptyTo   },
            { from,  to,    diffFromTo    }
        };
        // @formatter:on
        assertTrue("should contain top", Entries.containsObjectClass(entry, schema
                .getObjectClass("top")));
        assertTrue("should contain person", Entries.containsObjectClass(entry, schema
                .getObjectClass("person")));
        assertFalse("should not contain country", Entries.containsObjectClass(entry, schema
                .getObjectClass("country")));
    }
    /**
     * Tests {@link Entries#diffEntries(Entry, Entry)}.
     *
     * @param from
     *            Source entry.
     * @param to
     *            Destination entry.
     * @param expected
     *            Expected modifications.
     */
    @Test(dataProvider = "createTestDiffEntriesData")
    public void testDiffEntries(final Entry from, final Entry to, final ModifyRequest expected) {
        ModifyRequest actual = Entries.diffEntries(from, to);
    @Test
    public void testDiffEntriesAddDeleteAddIntermediateAttribute() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "sn: ignore");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value",
            "sn: ignore");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "add: description",
            "description: value");
        // @formatter:on
        assertEquals(diffEntries(from, to), expected);
    }
        Assert.assertEquals(from.getName(), actual.getName());
        Assert.assertEquals(actual.getModifications().size(), expected.getModifications().size());
    @Test
    public void testDiffEntriesAddDeleteAddTrailingAttributes() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "cn: ignore");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "cn: ignore",
            "description: value",
            "sn: value");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "add: description",
            "description: value",
            "-",
            "add: sn",
            "sn: value");
        // @formatter:on
        assertEquals(diffEntries(from, to), expected);
    }
    @Test
    public void testDiffEntriesAddDeleteDeleteIntermediateAttribute() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value",
            "sn: ignore");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "sn: ignore");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "delete: description",
            "description: value");
        // @formatter:on
        assertEquals(diffEntries(from, to), expected);
    }
    @Test
    public void testDiffEntriesAddDeleteDeleteTrailingAttributes() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "cn: ignore",
            "description: value",
            "sn: value");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "cn: ignore");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "delete: description",
            "description: value",
            "-",
            "delete: sn",
            "sn: value");
        // @formatter:on
        assertEquals(diffEntries(from, to), expected);
    }
    @Test
    public void testDiffEntriesAddDeleteMultiValueAddSingleValue() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value1");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value1",
            "description: value2");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "add: description",
            "description: value2");
        // @formatter:on
        assertEquals(diffEntries(from, to), expected);
    }
    @Test
    public void testDiffEntriesAddDeleteMultiValueDeleteSingleValue() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value1",
            "description: value2");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value1");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "delete: description",
            "description: value2");
        // @formatter:on
        assertEquals(diffEntries(from, to), expected);
    }
    @Test
    public void testDiffEntriesAddDeleteMultiValueSameSizeDifferentValues() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value1",
            "description: value2");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: VALUE2",
            "description: VALUE3");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "delete: description",
            "description: value1",
            "-",
            "add: description",
            "description: VALUE3");
        // @formatter:on
        assertEquals(diffEntries(from, to), expected);
    }
    @Test
    public void testDiffEntriesAddDeleteMultiValueSameSizeDifferentValuesExact() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value1",
            "description: value2");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: VALUE2",
            "description: VALUE3");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "delete: description",
            "description: value1",
            "description: value2",
            "-",
            "add: description",
            "description: VALUE2",
            "description: VALUE3");
        // @formatter:on
        assertEquals(diffEntries(from, to, diffOptions().useExactMatching()), expected);
    }
    @Test
    public void testDiffEntriesAddDeleteSingleValue() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: from");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: to");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "delete: description",
            "description: from",
            "-",
            "add: description",
            "description: to");
        // @formatter:on
        assertEquals(diffEntries(from, to), expected);
    }
    @Test
    public void testDiffEntriesAddDeleteSingleValueExactMatch() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: VALUE");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "delete: description",
            "description: value",
            "-",
            "add: description",
            "description: VALUE");
        // @formatter:on
        assertEquals(diffEntries(from, to, diffOptions().useExactMatching()), expected);
    }
    @Test
    public void testDiffEntriesAddDeleteSingleValueNoChange() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: VALUE");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify");
        // @formatter:on
        assertEquals(diffEntries(from, to), expected);
    }
    @Test
    public void testDiffEntriesReplaceAddTrailingAttributes() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "cn: ignore");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "cn: ignore",
            "description: value",
            "sn: value");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "replace: description",
            "description: value",
            "-",
            "replace: sn",
            "sn: value");
        // @formatter:on
        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes()), expected);
    }
    @Test
    public void testDiffEntriesReplaceDeleteTrailingAttributes() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "cn: ignore",
            "description: value",
            "sn: value");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "cn: ignore");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "replace: description",
            "-",
            "replace: sn");
        // @formatter:on
        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes()), expected);
    }
    @Test
    public void testDiffEntriesReplaceFilteredAttributes() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "cn: from");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "cn: to",
            "description: value",
            "sn: value");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "replace: cn",
            "cn: to",
            "-",
            "replace: sn",
            "sn: value");
        // @formatter:on
        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes().attributes("cn", "sn")),
                expected);
    }
    @Test
    public void testDiffEntriesReplaceMultiValueChangeSize() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value1");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value1",
            "description: value2");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "replace: description",
            "description: value1",
            "description: value2");
        // @formatter:on
        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes()), expected);
    }
    @Test
    public void testDiffEntriesReplaceMultiValueSameSize() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value1",
            "description: value2");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: VALUE2",
            "description: VALUE3");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "replace: description",
            "description: VALUE2",
            "description: VALUE3");
        // @formatter:on
        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes()), expected);
    }
    @Test
    public void testDiffEntriesReplaceMultiValueSameSizeExact() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value1",
            "description: value2");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value2",
            "description: value3");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "replace: description",
            "description: value2",
            "description: value3");
        // @formatter:on
        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes().useExactMatching()), expected);
    }
    @Test
    public void testDiffEntriesReplaceSingleValue() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: from");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: to");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "replace: description",
            "description: to");
        // @formatter:on
        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes()), expected);
    }
    @Test
    public void testDiffEntriesReplaceSingleValueExactMatch() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: VALUE");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify",
            "replace: description",
            "description: VALUE");
        // @formatter:on
        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes().useExactMatching()), expected);
    }
    @Test
    public void testDiffEntriesReplaceSingleValueNoChange() {
        // @formatter:off
        Entry from = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: value");
        Entry to = new LinkedHashMapEntry(
            "dn: cn=test",
            "description: VALUE");
        ModifyRequest expected = Requests.newModifyRequest(
            "dn: cn=test",
            "changetype: modify");
        // @formatter:on
        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes()), expected);
    }
    private void assertEquals(ModifyRequest actual, ModifyRequest expected) {
        assertThat((Object) actual.getName()).isEqualTo(expected.getName());
        assertThat(actual.getModifications()).hasSize(expected.getModifications().size());
        Iterator<Modification> i1 = actual.getModifications().iterator();
        Iterator<Modification> i2 = expected.getModifications().iterator();
        while (i1.hasNext()) {
            Modification m1 = i1.next();
            Modification m2 = i2.next();
            Assert.assertEquals(m1.getModificationType(), m2.getModificationType());
            Assert.assertEquals(m1.getAttribute(), m2.getAttribute());
            assertThat(m1.getModificationType()).isEqualTo(m2.getModificationType());
            assertThat(m1.getAttribute()).isEqualTo(m2.getAttribute());
        }
    }
    @Test
    public void testContainsObjectClass() throws Exception {
        Entry entry = new LinkedHashMapEntry("dn: cn=test", "objectClass: top", "objectClass: person");
        Schema schema = Schema.getDefaultSchema();
        assertTrue("should contain top", Entries.containsObjectClass(entry, schema.getObjectClass("top")));
        assertTrue("should contain person", Entries.containsObjectClass(entry, schema.getObjectClass("person")));
        assertFalse("should not contain country", Entries.containsObjectClass(entry, schema.getObjectClass("country")));
    }
}