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.
| | |
| | | * |
| | | * |
| | | * 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; |
| | |
| | | import java.util.Comparator; |
| | | import java.util.HashSet; |
| | | import java.util.Iterator; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | |
| | | * @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; |
| | |
| | | } |
| | | }; |
| | | |
| | | 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>() { |
| | | |
| | |
| | | * 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: |
| | |
| | | * 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(); |
| | | |
| | |
| | | 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 Attribute deletedValues = new LinkedAttribute(afrom); |
| | | deletedValues.removeAll(ato); |
| | | if (!deletedValues.isEmpty()) { |
| | | request.addModification(new Modification(ModificationType.DELETE, deletedValues)); |
| | | 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); |
| | | 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> |
| | |
| | | * <p> |
| | | * Sample usage: |
| | | * <pre> |
| | | * List<Entry> smiths = TestCaseUtils.makeEntries( |
| | | * List<Entry> smiths = TestCaseUtils.makeEntries( |
| | | * "dn: cn=John Smith,dc=example,dc=com", |
| | | * "objectclass: inetorgperson", |
| | | * "cn: John Smith", |
| | |
| | | } |
| | | } |
| | | |
| | | 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. |
| | |
| | | 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. |
| | |
| | | * 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 |
| | |
| | | * 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) |
| | |
| | | * |
| | | * |
| | | * 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; |
| | | |
| | |
| | | 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; |
| | | |
| | | /** |
| | |
| | | */ |
| | | @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"))); |
| | | } |
| | | } |