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

Jean-Noël Rouvignac
29.30.2016 42dd9eb626b81e5f370f3d66f932a72fc2bdc0d5
OPENDJ-1342 Migrate AVA, RDN, and DN classes

These changes are required to complete the migration of the server to the SDK's DN and RDN classes.

DN.java:
In toNormalizedByteString(), changed the representation to include a leading NORMALIZED_RDN_SEPARATOR
when for all DNs other than the root DN.
This is consistent with how the server stores data in dn2id or dn2uri.
Moved RDN.AVA_CHAR_SEPARATOR here, to be consistent with NORMALIZED_* constants.
Added RDN_CHAR_SEPARATOR and used it throughout this class.
Added valueOf(ByteString), rdn(int) and toUUID().
Moved hasAttributeType() here from server's RDN.
Inlined static method compareTo(DN, DN)

RDN.java:
Added MIN_VALUE and minValue(), and modified code to take care of it.
Moved AVA_CHAR_SEPARATOR to DN class and used it throughout this class.

DNTestCase.java:
Added test cases for using RDN.maxValue() and DN.minValue() with a Map keyed by DNs

DistinguishedNameEqualityMatchingRuleTest.java:
Consequence of the change to DN.toNormalizedByteString()
4 files modified
359 ■■■■ changed files
opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java 105 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java 106 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java 139 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java 9 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
@@ -21,6 +21,7 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.WeakHashMap;
import org.forgerock.i18n.LocalizableMessage;
@@ -56,6 +57,9 @@
    static final byte NORMALIZED_AVA_SEPARATOR = 0x01;
    static final byte NORMALIZED_ESC_BYTE = 0x02;
    static final char RDN_CHAR_SEPARATOR = ',';
    static final char AVA_CHAR_SEPARATOR = '+';
    private static final DN ROOT_DN = new DN(CoreSchema.getInstance(), null, null, "");
    /**
@@ -191,15 +195,13 @@
    }
    /**
     * Parses the provided LDAP string representation of a DN using the default
     * schema.
     * Parses the provided LDAP string representation of a DN using the default schema.
     *
     * @param dn
     *            The LDAP string representation of a DN.
     * @return The parsed DN.
     * @throws LocalizedIllegalArgumentException
     *             If {@code dn} is not a valid LDAP string representation of a
     *             DN.
     *             If {@code dn} is not a valid LDAP string representation of a DN.
     * @throws NullPointerException
     *             If {@code dn} was {@code null}.
     * @see #format(String, Object...)
@@ -209,8 +211,7 @@
    }
    /**
     * Parses the provided LDAP string representation of a DN using the provided
     * schema.
     * Parses the provided LDAP string representation of a DN using the provided schema.
     *
     * @param dn
     *            The LDAP string representation of a DN.
@@ -218,8 +219,7 @@
     *            The schema to use when parsing the DN.
     * @return The parsed DN.
     * @throws LocalizedIllegalArgumentException
     *             If {@code dn} is not a valid LDAP string representation of a
     *             DN.
     *             If {@code dn} is not a valid LDAP string representation of a DN.
     * @throws NullPointerException
     *             If {@code dn} or {@code schema} was {@code null}.
     * @see #format(String, Schema, Object...)
@@ -243,21 +243,18 @@
    }
    /**
     * Compares the provided DN values to determine their relative order in a
     * sorted list. The order is the natural order as defined by the
     * {@code toNormalizedByteString()} method.
     * Parses the provided LDAP string representation of a DN using the default schema.
     *
     * @param dn1
     *            The first DN to be compared. It must not be {@code null}.
     * @param dn2
     *            The second DN to be compared. It must not be {@code null}.
     * @return A negative integer if the first DN should come before the second
     *         DN in a sorted list, a positive integer if the first DN should
     *         come after the second DN in a sorted list, or zero if the two DN
     *         values can be considered equal.
     * @param dn
     *            The LDAP byte string representation of a DN.
     * @return The parsed DN.
     * @throws LocalizedIllegalArgumentException
     *             If {@code dn} is not a valid LDAP byte string representation of a DN.
     * @throws NullPointerException
     *             If {@code dn} was {@code null}.
     */
    private static int compareTo(final DN dn1, final DN dn2) {
        return dn1.toNormalizedByteString().compareTo(dn2.toNormalizedByteString());
    public static DN valueOf(ByteString dn) {
        return DN.valueOf(dn.toString());
    }
    /** Decodes a DN using the provided reader and schema. */
@@ -446,7 +443,7 @@
    @Override
    public int compareTo(final DN dn) {
        return compareTo(this, dn);
        return toNormalizedByteString().compareTo(dn.toNormalizedByteString());
    }
    @Override
@@ -814,6 +811,29 @@
    }
    /**
     * Returns the RDN at the specified index for this DN,
     * or {@code null} if no such RDN exists.
     * <p>
     * Here is an example usage:
     * <pre>
     * DN.valueOf("ou=people,dc=example,dc=com").rdn(0) => "ou=people"
     * DN.valueOf("ou=people,dc=example,dc=com").rdn(1) => "dc=example"
     * DN.valueOf("ou=people,dc=example,dc=com").rdn(2) => "dc=com"
     * DN.valueOf("ou=people,dc=example,dc=com").rdn(3) => null
     * </pre>
     *
     * @param index
     *            The index of the requested RDN.
     * @return The RDN at the specified index, or {@code null} if no such RDN exists.
     * @throws IllegalArgumentException
     *             If {@code index} is less than zero.
     */
    public RDN rdn(int index) {
        DN parentDN = parent(index);
        return parentDN != null ? parentDN.rdn : null;
    }
    /**
     * Returns a copy of this DN whose parent DN, {@code fromDN}, has been
     * renamed to the new parent DN, {@code toDN}. If this DN is not subordinate
     * or equal to {@code fromDN} then this DN is returned (i.e. the DN is not
@@ -863,7 +883,7 @@
            final StringBuilder builder = new StringBuilder();
            rdn.toString(builder);
            if (!parent.isRootDN()) {
                builder.append(',');
                builder.append(RDN_CHAR_SEPARATOR);
                builder.append(parent);
            }
            stringValue = builder.toString();
@@ -883,20 +903,13 @@
     */
    public ByteString toNormalizedByteString() {
        if (normalizedDN == null) {
            if (rdn() == null) {
            if (rdn == null) {
                normalizedDN = ByteString.empty();
            } else {
                final ByteStringBuilder builder = new ByteStringBuilder();
                int i = size() - 1;
                parent(i).rdn().toNormalizedByteString(builder);
                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.appendByte(DN.NORMALIZED_RDN_SEPARATOR);
                    }
                    rdn.toNormalizedByteString(builder);
                }
                final ByteString normalizedParent = parent.toNormalizedByteString();
                final ByteStringBuilder builder = new ByteStringBuilder(normalizedParent.length() + 16);
                builder.appendBytes(normalizedParent);
                rdn.toNormalizedByteString(builder);
                normalizedDN = builder.toByteString();
            }
        }
@@ -916,14 +929,17 @@
            return "";
        }
        // This code differs from toNormalizedByteString(),
        // because we do not care about ordering comparisons.
        // (so we do not append the RDN_SEPARATOR first)
        final StringBuilder builder = new StringBuilder();
        int i = size() - 1;
        parent(i).rdn().toNormalizedUrlSafeString(builder);
        for (i--; i >= 0; i--) {
            final RDN rdn = parent(i).rdn();
            // Only add a separator if the RDN is not RDN.maxValue().
            // Only add a separator if the RDN is not RDN.maxValue() or RDN.minValue().
            if (rdn.size() != 0) {
                builder.append(',');
                builder.append(RDN_CHAR_SEPARATOR);
            }
            rdn.toNormalizedUrlSafeString(builder);
        }
@@ -931,6 +947,21 @@
    }
    /**
     * Returns a UUID whose content is based on the normalized content of this DN.
     * Two equivalent DNs subject to the same schema will always yield the same UUID.
     *
     * @return the UUID representing this DN
     */
    public UUID toUUID() {
        ByteString normDN = toNormalizedByteString();
        if (!normDN.isEmpty()) {
            // remove leading RDN separator
            normDN = normDN.subSequence(1, normDN.length());
        }
        return UUID.nameUUIDFromBytes(normDN.toByteArray());
    }
    /**
     * A compact representation of a DN, suitable for equality and comparisons, and providing a natural hierarchical
     * ordering.
     * <p>
opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java
@@ -16,6 +16,11 @@
 */
package org.forgerock.opendj.ldap;
import static org.forgerock.opendj.ldap.DN.AVA_CHAR_SEPARATOR;
import static org.forgerock.opendj.ldap.DN.RDN_CHAR_SEPARATOR;
import static org.forgerock.opendj.ldap.DN.NORMALIZED_AVA_SEPARATOR;
import static org.forgerock.opendj.ldap.DN.NORMALIZED_RDN_SEPARATOR;
import static com.forgerock.opendj.ldap.CoreMessages.ERR_RDN_TYPE_NOT_FOUND;
import java.util.ArrayList;
@@ -30,10 +35,10 @@
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
import org.forgerock.util.Reject;
import com.forgerock.opendj.util.Iterators;
import com.forgerock.opendj.util.SubstringReader;
import org.forgerock.util.Reject;
/**
 * A relative distinguished name (RDN) as defined in RFC 4512 section 2.3 is the
@@ -63,18 +68,44 @@
 */
public final class RDN implements Iterable<AVA>, Comparable<RDN> {
    /** Separator for AVAs. */
    private static final char AVA_CHAR_SEPARATOR = '+';
    /**
     * A constant holding a special RDN having zero AVAs and which always
     * compares greater than any other RDN other than itself.
     * A constant holding a special RDN having zero AVAs
     * and which sorts before any RDN other than itself.
     */
    private static final RDN MIN_VALUE = new RDN(new AVA[0], "");
    /**
     * A constant holding a special RDN having zero AVAs
     * and which sorts after any RDN other than itself.
     */
    private static final RDN MAX_VALUE = new RDN(new AVA[0], "");
    /**
     * Returns a constant containing a special RDN which is greater than any
     * other RDN other than itself. This RDN may be used in order to perform
     * Returns a constant containing a special RDN which sorts before any
     * RDN other than itself. This RDN may be used in order to perform
     * range queries on DN keyed collections such as {@code SortedSet}s and
     * {@code SortedMap}s. For example, the following code can be used to
     * construct a range whose contents is a sub-tree of entries, excluding the base entry:
     *
     * <pre>
     * SortedMap<DN, Entry> entries = ...;
     * DN baseDN = ...;
     *
     * // Returns a map containing the baseDN and all of its subordinates.
     * SortedMap<DN,Entry> subtree = entries.subMap(
     *     baseDN.child(RDN.minValue()), baseDN.child(RDN.maxValue()));
     * </pre>
     *
     * @return A constant containing a special RDN which sorts before any
     *         RDN other than itself.
     * @see #maxValue()
     */
    public static RDN minValue() {
        return MIN_VALUE;
    }
    /**
     * Returns a constant containing a special RDN which sorts after any
     * RDN other than itself. This RDN may be used in order to perform
     * range queries on DN keyed collections such as {@code SortedSet}s and
     * {@code SortedMap}s. For example, the following code can be used to
     * construct a range whose contents is a sub-tree of entries:
@@ -84,11 +115,12 @@
     * DN baseDN = ...;
     *
     * // Returns a map containing the baseDN and all of its subordinates.
     * SortedMap<DN,Entry> subtree = entries.subMap(baseDN, baseDN.child(RDN.maxValue));
     * SortedMap<DN,Entry> subtree = entries.subMap(baseDN, baseDN.child(RDN.maxValue()));
     * </pre>
     *
     * @return A constant containing a special RDN which is greater than any
     *         other RDN other than itself.
     * @return A constant containing a special RDN which sorts after any
     *         RDN other than itself.
     * @see #minValue()
     */
    public static RDN maxValue() {
        return MAX_VALUE;
@@ -244,6 +276,9 @@
    @Override
    public int compareTo(final RDN rdn) {
        // FIXME how about replacing this method body with the following code?
        // return toNormalizedByteString().compareTo(rdn.toNormalizedByteString())
        // Identity.
        if (this == rdn) {
            return 0;
@@ -253,11 +288,18 @@
        if (this == MAX_VALUE) {
            return 1;
        }
        if (rdn == MAX_VALUE) {
            return -1;
        }
        // MIN_VALUE is always less than any other RDN other than itself.
        if (this == MIN_VALUE) {
            return -1;
        }
        if (rdn == MIN_VALUE) {
            return 1;
        }
        // Compare number of AVAs first as this is quick and easy.
        final int sz1 = avas.length;
        final int sz2 = rdn.avas.length;
@@ -348,6 +390,22 @@
    }
    /**
     * Indicates whether this RDN includes the specified attribute type.
     *
     * @param attributeType  The attribute type for which to make the determination.
     * @return {@code true} if the RDN includes the specified attribute type,
     *         or {@code false} if not.
     */
    public boolean hasAttributeType(AttributeType attributeType) {
        for (AVA ava : avas) {
            if (ava.getAttributeType().equals(attributeType)) {
                return true;
            }
        }
        return false;
    }
    /**
     * Returns an iterator of the AVAs contained in this RDN. The AVAs will be
     * returned in the user provided order.
     * <p>
@@ -386,7 +444,7 @@
            final StringBuilder builder = new StringBuilder();
            avas[0].toString(builder);
            for (int i = 1; i < avas.length; i++) {
                builder.append('+');
                builder.append(AVA_CHAR_SEPARATOR);
                avas[i].toString(builder);
            }
            stringValue = builder.toString();
@@ -411,17 +469,22 @@
    ByteStringBuilder toNormalizedByteString(final ByteStringBuilder builder) {
        switch (size()) {
        case 0:
            // Handle RDN.maxValue().
            builder.appendByte(DN.NORMALIZED_AVA_SEPARATOR);
            if (this == MIN_VALUE) {
                builder.appendByte(NORMALIZED_RDN_SEPARATOR);
            } else { // can only be MAX_VALUE
                builder.appendByte(NORMALIZED_AVA_SEPARATOR);
            }
            break;
        case 1:
            builder.appendByte(NORMALIZED_RDN_SEPARATOR);
            getFirstAVA().toNormalizedByteString(builder);
            break;
        default:
            builder.appendByte(NORMALIZED_RDN_SEPARATOR);
            Iterator<AVA> it = getSortedAvas();
            it.next().toNormalizedByteString(builder);
            while (it.hasNext()) {
                builder.appendByte(DN.NORMALIZED_AVA_SEPARATOR);
                builder.appendByte(NORMALIZED_AVA_SEPARATOR);
                it.next().toNormalizedByteString(builder);
            }
            break;
@@ -441,8 +504,13 @@
    StringBuilder toNormalizedUrlSafeString(final StringBuilder builder) {
        switch (size()) {
        case 0:
            // Handle RDN.maxValue().
            builder.append(RDN.AVA_CHAR_SEPARATOR);
            // since MIN_VALUE and MAX_VALUE are only used for sorting DNs,
            // it is strange to call toNormalizedUrlSafeString() on one of them
            if (this == MIN_VALUE) {
                builder.append(RDN_CHAR_SEPARATOR);
            } else { // can only be MAX_VALUE
                builder.append(AVA_CHAR_SEPARATOR);
            }
            break;
        case 1:
            getFirstAVA().toNormalizedUrlSafe(builder);
@@ -451,7 +519,7 @@
            Iterator<AVA> it = getSortedAvas();
            it.next().toNormalizedUrlSafe(builder);
            while (it.hasNext()) {
                builder.append(RDN.AVA_CHAR_SEPARATOR);
                builder.append(AVA_CHAR_SEPARATOR);
                it.next().toNormalizedUrlSafe(builder);
            }
            break;
opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
@@ -12,17 +12,22 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2010 Sun Microsystems, Inc.
 * Portions copyright 2011-2015 ForgeRock AS.
 * Portions copyright 2011-2016 ForgeRock AS.
 */
package org.forgerock.opendj.ldap;
import static java.lang.Integer.*;
import static org.fest.assertions.Assertions.*;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.Assertions.*;
import static org.testng.Assert.*;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.TreeMap;
import java.util.UUID;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.testng.annotations.DataProvider;
@@ -594,18 +599,22 @@
        dn.isChildOf((String) null);
    }
    /**
     * Tests the parent method that require iteration.
     */
    /** Tests the parent and rdn method that require iteration. */
    @Test
    public void testIterableParent() {
    public void testIterableParentAndRdn() {
        final String str = "ou=people,dc=example,dc=com";
        final DN dn = DN.valueOf(str);
        // Parent at index 0 is self.
        assertEquals(dn, dn.parent(0));
        assertEquals(dn.parent(0), dn);
        assertEquals(dn.parent(1), DN.valueOf("dc=example,dc=com"));
        assertEquals(dn.parent(2), DN.valueOf("dc=com"));
        assertEquals(dn.parent(3), DN.rootDN());
        assertEquals(dn.parent(4), null);
        assertEquals(dn.rdn(0), RDN.valueOf("ou=people"));
        assertEquals(dn.rdn(1), RDN.valueOf("dc=example"));
        assertEquals(dn.rdn(2), RDN.valueOf("dc=com"));
        assertEquals(dn.rdn(3), null);
    }
    /**
@@ -674,8 +683,6 @@
        assertEquals(p.rdn(), RDN.valueOf("dc=bar"));
        assertEquals(p.rdn(), RDN.valueOf("dc=bar"));
        assertEquals(p.parent(), DN.valueOf("dc=opendj,dc=org"));
        assertEquals(p.parent(), e.parent());
@@ -719,15 +726,25 @@
    }
    /**
     * Tests the root DN.
     * Tests {@link DN#valueOf(String)}.
     *
     * @throws Exception
     *             If the test failed unexpectedly.
     */
    @Test(expectedExceptions = { NullPointerException.class, AssertionError.class })
    public void testRootDN2() throws Exception {
        final DN dn = DN.valueOf(null);
        assertEquals(dn, DN.rootDN());
    public void testValueOfString() throws Exception {
        DN.valueOf((String) null);
    }
    /**
     * Tests {@link DN#valueOf(ByteString)}.
     *
     * @throws Exception
     *             If the test failed unexpectedly.
     */
    @Test(expectedExceptions = { NullPointerException.class, AssertionError.class })
    public void testValueOfByteString() throws Exception {
        DN.valueOf((ByteString) null);
    }
    /**
@@ -1030,7 +1047,7 @@
    }
    @DataProvider
    public Object[][] toIrreversibleNormalizedByteStringDataProvider() {
    public Object[][] toNormalizedByteStringDataProvider() {
        // @formatter:off
        return new Object[][] {
            // first value to normalize, second value to normalize, expected sign of comparison between the two
@@ -1038,6 +1055,13 @@
            { "dc=example,dc=com", "dc=example,dc=com", 0 },
            { "cn=test+dc=example,dc=com", "cn=test+dc=example,dc=com", 0 },
            { "dc=example+cn=test,dc=com", "cn=test+dc=example,dc=com", 0 },
            // siblings
            { "cn=test,dc=com", "cn=test+dc=example,dc=com", -1 },
            { "cn=test+dc=example,dc=com", "cn=test,dc=com", 1 },
            { "dc=example,dc=com", "cn=test+dc=example,dc=com", 1 },
            { "cn=test+dc=example,dc=com", "dc=example,dc=com", -1 },
            { "dc=example,dc=com", "dc=example+cn=test,dc=com", 1 },
            { "dc=example+cn=test,dc=com", "dc=example,dc=com", -1 },
            // parent entry is followed by its children, not its siblings
            { "dc=com", "dc=example,dc=com", -1 },
            { "dc=com", "dc=test,dc=example,dc=com", -1},
@@ -1071,7 +1095,7 @@
        // @formatter:on
    }
    @Test(dataProvider = "toIrreversibleNormalizedByteStringDataProvider")
    @Test(dataProvider = "toNormalizedByteStringDataProvider")
    public void testToNormalizedByteString(String first, String second, int expectedCompareResult) {
        DN actual = DN.valueOf(first);
        DN expected = DN.valueOf(second);
@@ -1092,7 +1116,81 @@
    }
    @DataProvider
    public Object[][] toIrreversibleReadableStringDataProvider() {
    private Object[][] minAndMaxRdnsDataProvider() {
        DN dcCom          = DN.valueOf("dc=com");
        DN dcExampleDcCom = DN.valueOf("dc=example,dc=com");
        DN cnTestDcCom    = DN.valueOf("cn=test,dc=com");
        return new Object[][] {
            { dcCom,          dcCom.child(RDN.minValue()),          -1 },
            { dcCom,          dcCom.child(RDN.maxValue()),          -1 },
            { dcExampleDcCom, dcExampleDcCom.child(RDN.minValue()), -1 },
            { dcExampleDcCom, dcExampleDcCom.child(RDN.maxValue()), -1 },
            { dcExampleDcCom, dcCom.child(RDN.minValue()),           1 },
            { dcExampleDcCom, dcCom.child(RDN.maxValue()),          -1 },
            // siblings
            { DN.valueOf("cn=test+dc=example,dc=com"), cnTestDcCom.child(RDN.minValue()), 1 },
            { DN.valueOf("dc=example+cn=test,dc=com"), cnTestDcCom.child(RDN.minValue()), 1 },
            { DN.valueOf("cn=test+dc=example,dc=com"), cnTestDcCom.child(RDN.maxValue()), 1 },
            { DN.valueOf("dc=example+cn=test,dc=com"), cnTestDcCom.child(RDN.maxValue()), 1 },
        };
    }
    /** Using DN as a Map key depends on this behaviour. In particular MemoryBackend depends on this behaviour. */
    @Test(dataProvider = "minAndMaxRdnsDataProvider")
    public void testToNormalizedByteStringWithMinAndMaxRdns(DN dn1, DN dn2, int expectedCompareResult) {
        int cmp = dn1.toNormalizedByteString().compareTo(dn2.toNormalizedByteString());
        assertThat(signum(cmp)).isEqualTo(expectedCompareResult);
    }
    @Test
    public void testToNormalizedByteStringWithMinAndMaxRdnsInOrderedCollection() {
        DN dcCom = DN.valueOf("dc=com");
        DN cnTestDcCom = DN.valueOf("cn=test,dc=com");
        DN cnDeeperCnTestDcCom = DN.valueOf("cn=deeper,cn=test,dc=com");
        DN cnTestAndDcExampleDcCom = DN.valueOf("cn=test+dc=example,dc=com");
        DN dcExampleDcCom = DN.valueOf("dc=example,dc=com");
        TreeMap<ByteString, DN> map = new TreeMap<>();
        putAll(map, dcCom, cnTestDcCom, cnDeeperCnTestDcCom, cnTestAndDcExampleDcCom, dcExampleDcCom);
        assertThat(subordinates(map, dcCom))
            .containsExactly(cnTestDcCom, cnDeeperCnTestDcCom, cnTestAndDcExampleDcCom, dcExampleDcCom);
        assertThat(subordinates(map, cnTestDcCom))
            .containsExactly(cnDeeperCnTestDcCom);
        assertThat(after(map, cnTestDcCom))
            .containsExactly(cnDeeperCnTestDcCom, cnTestAndDcExampleDcCom, dcExampleDcCom);
        assertThat(after(map, cnDeeperCnTestDcCom))
            .containsExactly(cnTestAndDcExampleDcCom, dcExampleDcCom);
        assertThat(before(map, cnTestDcCom))
            .containsExactly(dcCom);
        assertThat(before(map, cnDeeperCnTestDcCom))
            .containsExactly(dcCom, cnTestDcCom);
    }
    private void putAll(Map<ByteString, DN> map, DN... dns) {
        for (DN dn : dns) {
            map.put(dn.toNormalizedByteString(), dn);
        }
    }
    private Collection<DN> subordinates(TreeMap<ByteString, DN> map, DN dn) {
        return map.subMap(
            dn.child(RDN.minValue()).toNormalizedByteString(),
            dn.child(RDN.maxValue()).toNormalizedByteString()).values();
    }
    private Collection<DN> before(TreeMap<ByteString, DN> map, DN dn) {
        return map.headMap(dn.toNormalizedByteString(), false).values();
    }
    private Collection<DN> after(TreeMap<ByteString, DN> map, DN dn) {
        return map.tailMap(dn.toNormalizedByteString(), false).values();
    }
    @DataProvider
    public Object[][] toNormalizedUrlSafeStringDataProvider() {
        // @formatter:off
        return new Object[][] {
            // first value = string used to build DN, second value = expected readable string
@@ -1125,7 +1223,7 @@
        // @formatter:on
    }
    @Test(dataProvider = "toIrreversibleReadableStringDataProvider")
    @Test(dataProvider = "toNormalizedUrlSafeStringDataProvider")
    public void testToNormalizedUrlSafeString(String dnAsString, String expectedReadableString) {
        DN actual = DN.valueOf(dnAsString);
        assertEquals(actual.toNormalizedUrlSafeString(), expectedReadableString);
@@ -1141,4 +1239,11 @@
        assertEquals(irreversibleReadableString, dn2.toNormalizedUrlSafeString());
        assertEquals(irreversibleReadableString, dn3.toNormalizedUrlSafeString());
    }
    @Test
    public void toUUID() {
        UUID uuid1 = DN.valueOf("dc=example+cn=test,dc=com").toUUID();
        UUID uuid2 = DN.valueOf("cn=test+dc=example,dc=com").toUUID();
        assertEquals(uuid1, uuid2);
    }
}
opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java
@@ -21,6 +21,7 @@
import static org.testng.Assert.assertEquals;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.ConditionResult;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -194,8 +195,14 @@
        final MatchingRule rule = getRule();
        final ByteString normalizedValue1 =
                rule.normalizeAttributeValue(ByteString.valueOfUtf8(value1));
        final ByteString expectedValue = ByteString.valueOfUtf8(value2);
        final ByteString expectedValue = toExpectedNormalizedByteString(value2);
        assertEquals(normalizedValue1, expectedValue);
    }
    private ByteString toExpectedNormalizedByteString(final String s) {
        if (s.isEmpty()) {
            return ByteString.valueOfUtf8(s);
        }
        return new ByteStringBuilder().appendByte(0).appendUtf8(s).toByteString();
    }
}