From 42dd9eb626b81e5f370f3d66f932a72fc2bdc0d5 Mon Sep 17 00:00:00 2001
From: Jean-Noël Rouvignac <jean-noel.rouvignac@forgerock.com>
Date: Mon, 07 Mar 2016 15:44:49 +0000
Subject: [PATCH] OPENDJ-1342 Migrate AVA, RDN, and DN classes

---
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java                                              |  106 ++++++++++++++---
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java |    9 +
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java                                               |  105 +++++++++++------
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java                                       |  139 ++++++++++++++++++++--
 4 files changed, 285 insertions(+), 74 deletions(-)

diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
index 5956133..d93d03f 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
+++ b/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>
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java
index aef0808..85854ac 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java
+++ b/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;
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
index 33c3f71..c497245 100644
--- a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
+++ b/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);
+    }
 }
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java
index c893363..5a5a79e 100644
--- a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java
+++ b/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();
+    }
 }

--
Gitblit v1.10.0