From 4ccf2e43d0714e5fda04ce64071ccab0961147f9 Mon Sep 17 00:00:00 2001
From: Nicolas Capponi <nicolas.capponi@forgerock.com>
Date: Thu, 11 Dec 2014 13:44:07 +0000
Subject: [PATCH] OPENDJ-1585 CR-5594 Re-implement DN normalization in SDK

---
 opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java                                       |  114 +++++++++
 opendj-core/clirr-ignored-api-changes.xml                                                                 |   12 +
 opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.java                                              |  117 ++++++++++
 opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteStringBuilder.java                                |   25 ++
 opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringTestCase.java                               |   53 ++++
 opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java                                              |   74 ++++++
 opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteString.java                                       |   44 +++
 opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequence.java                                     |   30 ++
 opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java                                               |  173 ++++-----------
 opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java |   16 
 10 files changed, 516 insertions(+), 142 deletions(-)

diff --git a/opendj-core/clirr-ignored-api-changes.xml b/opendj-core/clirr-ignored-api-changes.xml
index ac802e6..e69eea0 100644
--- a/opendj-core/clirr-ignored-api-changes.xml
+++ b/opendj-core/clirr-ignored-api-changes.xml
@@ -396,4 +396,16 @@
     <method>%regex[(boolean|org.forgerock.opendj.ldap.schema.SchemaBuilder) allow(.)*\((boolean)?\)]</method>
     <justification>OPENDJ-1478 Make it easier to add compatibility options to schemas</justification>
   </difference>
+  <difference>
+    <className>org/forgerock/opendj/ldap/ByteSequence</className>
+    <differenceType>7012</differenceType>
+    <method>java.nio.ByteBuffer copyTo(java.nio.ByteBuffer)</method>
+    <justification>Added new utility method copyTo() for a byte buffer</justification>
+  </difference>
+  <difference>
+    <className>org/forgerock/opendj/ldap/ByteSequence</className>
+    <differenceType>7012</differenceType>
+    <method>boolean copyTo(java.nio.CharBuffer, java.nio.charset.CharsetDecoder)</method>
+    <justification>OPENDJ-1585: Added new utility method copyTo for a char buffer</justification>
+  </difference>
 </differences>
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.java
index 874898d..6562263 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.java
+++ b/opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.java
@@ -30,6 +30,12 @@
 import static com.forgerock.opendj.util.StaticUtils.*;
 import static com.forgerock.opendj.ldap.CoreMessages.*;
 
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
 import java.util.Comparator;
 
 import org.forgerock.i18n.LocalizableMessage;
@@ -62,6 +68,9 @@
  *      Models </a>
  */
 public final class AVA implements Comparable<AVA> {
+
+    private static final char HEX_STRING_SEPARATOR = '%';
+
     /**
      * Parses the provided LDAP string representation of an AVA using the
      * default schema.
@@ -473,7 +482,11 @@
         // that means that the value should be a hex string.
         char c = reader.read();
         int length = 0;
-        if (c == '#') {
+        if (c == '+') {
+            // Value is empty and followed by another AVA
+            reader.reset();
+            return ByteString.empty();
+        } else if (c == '#') {
             // The first two characters must be hex characters.
             reader.mark();
             if (reader.remaining() < 2) {
@@ -792,4 +805,106 @@
 
         return orderingNormalizedAttributeValue;
     }
+
+    /**
+     * Returns the normalized byte string representation of this AVA.
+     * <p>
+     * The representation is not a valid AVA.
+     *
+     * @param builder
+     *            The builder to use to construct the normalized byte string.
+     * @return The normalized byte string representation.
+     * @see DN#toIrreversibleNormalizedByteString()
+     */
+    ByteStringBuilder toNormalizedByteString(final ByteStringBuilder builder) {
+        builder.append(toLowerCase(attributeType.getNameOrOID()));
+        builder.append("=");
+        final ByteString value = getEqualityNormalizedValue();
+        if (value.length() > 0) {
+            builder.append(escapeBytes(value));
+        }
+        return builder;
+    }
+
+    /**
+     * Returns the normalized readable string representation of this AVA.
+     * <p>
+     * The representation is not a valid AVA.
+     *
+     * @param builder
+     *            The builder to use to construct the normalized string.
+     * @return The normalized readable string representation.
+     * @see DN#toIrreversibleReadableString()
+     */
+    StringBuilder toNormalizedReadableString(final StringBuilder builder) {
+        builder.append(toLowerCase(attributeType.getNameOrOID()));
+        builder.append('=');
+        final ByteString value = getEqualityNormalizedValue();
+
+        if (value.length() == 0) {
+            return builder;
+        }
+        final boolean hasAttributeName = !attributeType.getNames().isEmpty();
+        final boolean isHumanReadable = attributeType.getSyntax().isHumanReadable();
+        if (!hasAttributeName || !isHumanReadable) {
+            builder.append(value.toHexString(AVA.HEX_STRING_SEPARATOR));
+        } else {
+            // try to decode value as UTF-8 string
+            final CharBuffer buffer = CharBuffer.allocate(value.length());
+            final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder()
+                .onMalformedInput(CodingErrorAction.REPORT)
+                .onUnmappableCharacter(CodingErrorAction.REPORT);
+            if (value.copyTo(buffer, decoder)) {
+                try {
+                    // URL encoding encodes space char as '+' instead of using hex code
+                    final String val = URLEncoder.encode(buffer.toString(), "UTF-8").replaceAll("\\+", "%20");
+                    builder.append(val);
+                } catch (UnsupportedEncodingException e) {
+                    // should never happen
+                    builder.append(value.toHexString(AVA.HEX_STRING_SEPARATOR));
+                }
+            } else {
+                builder.append(value.toHexString(AVA.HEX_STRING_SEPARATOR));
+            }
+        }
+        return builder;
+    }
+
+    /**
+     * Return a new byte string with bytes 0x00, 0x01 and 0x02 escaped.
+     * <p>
+     * These bytes are reserved to represent respectively the RDN separator,
+     * the AVA separator and the escape byte in a normalized byte string.
+     */
+    private ByteString escapeBytes(final ByteString value) {
+        if (!needEscaping(value)) {
+            return value;
+        }
+
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        for (int i = 0; i < value.length(); i++) {
+            final byte b = value.byteAt(i);
+            if (isByteToEscape(b)) {
+                builder.append(DN.NORMALIZED_ESC_BYTE);
+            }
+            builder.append(b);
+        }
+        return builder.toByteString();
+    }
+
+    private boolean needEscaping(final ByteString value) {
+        boolean needEscaping = false;
+        for (int i = 0; i < value.length(); i++) {
+            final byte b = value.byteAt(i);
+            if (isByteToEscape(b)) {
+                needEscaping = true;
+                break;
+            }
+        }
+        return needEscaping;
+    }
+
+    private boolean isByteToEscape(final byte b) {
+        return b == DN.NORMALIZED_RDN_SEPARATOR || b == DN.NORMALIZED_AVA_SEPARATOR || b == DN.NORMALIZED_ESC_BYTE;
+    }
 }
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequence.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequence.java
index 0105c25..9676275 100755
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequence.java
+++ b/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequence.java
@@ -28,6 +28,10 @@
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetDecoder;
 import java.util.Comparator;
 
 /**
@@ -189,6 +193,32 @@
     ByteStringBuilder copyTo(ByteStringBuilder builder);
 
     /**
+     * Appends the content of this byte sequence to the provided {@link ByteBuffer}.
+     *
+     * @param buffer
+     *            The buffer to copy to.
+     *            It must be large enough to receive all bytes.
+     * @return The buffer.
+     * @throws BufferOverflowException
+     *            If there is insufficient space in the provided buffer
+     */
+    ByteBuffer copyTo(ByteBuffer buffer);
+
+    /**
+     * Appends the content of this byte sequence decoded using provided charset decoder,
+     * to the provided {@link CharBuffer}.
+     *
+     * @param charBuffer
+     *            The buffer to copy to, if decoding is successful.
+     *            It must be large enough to receive all decoded characters.
+     * @param decoder
+     *            The charset decoder to use for decoding.
+     * @return {@code true} if byte string was successfully decoded and charBuffer is
+     *         large enough to receive the resulting string, {@code false} otherwise
+     */
+    boolean copyTo(CharBuffer charBuffer, CharsetDecoder decoder);
+
+    /**
      * Copies the entire contents of this byte sequence to the provided
      * {@code OutputStream}.
      *
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteString.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteString.java
index 0da9322..0c5ac3a 100755
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteString.java
+++ b/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteString.java
@@ -35,6 +35,8 @@
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
 import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
 import java.util.Arrays;
 
 import org.forgerock.i18n.LocalizedIllegalArgumentException;
@@ -619,11 +621,38 @@
     }
 
     /** {@inheritDoc} */
+    public ByteBuffer copyTo(final ByteBuffer byteBuffer) {
+        byteBuffer.put(buffer, offset, length);
+        byteBuffer.flip();
+        return byteBuffer;
+    }
+
+    /** {@inheritDoc} */
     public ByteStringBuilder copyTo(final ByteStringBuilder builder) {
         builder.append(buffer, offset, length);
         return builder;
     }
 
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean copyTo(CharBuffer charBuffer, CharsetDecoder decoder) {
+        return copyTo(ByteBuffer.wrap(buffer, offset, length), charBuffer, decoder);
+    }
+
+    /**
+     * Convenience method to copy from a byte buffer to a char buffer using provided decoder to decode
+     * bytes into characters.
+     * <p>
+     * It should not be used directly, prefer instance method of ByteString or ByteStringBuilder instead.
+     */
+    static boolean copyTo(ByteBuffer inBuffer, CharBuffer outBuffer, CharsetDecoder decoder) {
+        final CoderResult result = decoder.decode(inBuffer, outBuffer, true);
+        decoder.flush(outBuffer);
+        outBuffer.flip();
+        return !result.isError() && !result.isOverflow();
+    }
+
     /** {@inheritDoc} */
     public OutputStream copyTo(final OutputStream stream) throws IOException {
         stream.write(buffer, offset, length);
@@ -701,10 +730,23 @@
      *         using hexadecimal characters.
      */
     public String toHexString() {
+        return toHexString(' ');
+    }
+
+    /**
+     * Returns a string representation of the contents of this byte sequence
+     * using hexadecimal characters and the provided separator between each byte.
+     *
+     * @param separator
+     *          Character used to separate each byte
+     * @return A string representation of the contents of this byte sequence
+     *         using hexadecimal characters.
+     */
+    public String toHexString(char separator) {
         StringBuilder builder = new StringBuilder((length - 1) * 3 + 2);
         builder.append(StaticUtils.byteToHex(buffer[offset]));
         for (int i = 1; i < length; i++) {
-            builder.append(" ");
+            builder.append(separator);
             builder.append(StaticUtils.byteToHex(buffer[offset + i]));
         }
         return builder.toString();
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteStringBuilder.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteStringBuilder.java
index ef6d5dc..c4079cf 100755
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteStringBuilder.java
+++ b/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteStringBuilder.java
@@ -36,6 +36,7 @@
 import java.nio.CharBuffer;
 import java.nio.channels.WritableByteChannel;
 import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
 
 /**
  * A mutable sequence of bytes backed by a byte array.
@@ -149,6 +150,13 @@
         }
 
         /** {@inheritDoc} */
+        public ByteBuffer copyTo(final ByteBuffer byteBuffer) {
+            byteBuffer.put(buffer, subOffset, subLength);
+            byteBuffer.flip();
+            return byteBuffer;
+        }
+
+        /** {@inheritDoc} */
         @Override
         public ByteStringBuilder copyTo(final ByteStringBuilder builder) {
             // Protect against reallocation: use builder's buffer.
@@ -156,6 +164,11 @@
         }
 
         /** {@inheritDoc} */
+        public boolean copyTo(CharBuffer charBuffer, CharsetDecoder decoder) {
+            return ByteString.copyTo(ByteBuffer.wrap(buffer, subOffset, subLength), charBuffer, decoder);
+        }
+
+        /** {@inheritDoc} */
         @Override
         public OutputStream copyTo(final OutputStream stream) throws IOException {
             // Protect against reallocation: use builder's buffer.
@@ -842,6 +855,13 @@
     }
 
     /** {@inheritDoc} */
+    public ByteBuffer copyTo(final ByteBuffer byteBuffer) {
+        byteBuffer.put(buffer, 0, length);
+        byteBuffer.flip();
+        return byteBuffer;
+    }
+
+    /** {@inheritDoc} */
     @Override
     public ByteStringBuilder copyTo(final ByteStringBuilder builder) {
         builder.append(buffer, 0, length);
@@ -849,6 +869,11 @@
     }
 
     /** {@inheritDoc} */
+    public boolean copyTo(CharBuffer charBuffer, CharsetDecoder decoder) {
+        return ByteString.copyTo(ByteBuffer.wrap(buffer, 0, length), charBuffer, decoder);
+    }
+
+    /** {@inheritDoc} */
     @Override
     public OutputStream copyTo(final OutputStream stream) throws IOException {
         stream.write(buffer, 0, length);
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
index 2f57c7b..622967f 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
+++ b/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
@@ -31,20 +31,15 @@
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.NoSuchElementException;
-import java.util.TreeSet;
 import java.util.WeakHashMap;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.LocalizedIllegalArgumentException;
-import org.forgerock.opendj.ldap.schema.AttributeType;
 import org.forgerock.opendj.ldap.schema.CoreSchema;
-import org.forgerock.opendj.ldap.schema.MatchingRule;
 import org.forgerock.opendj.ldap.schema.Schema;
-import org.forgerock.opendj.ldap.schema.Syntax;
 import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
 import org.forgerock.util.Reject;
 
-import com.forgerock.opendj.util.StaticUtils;
 import com.forgerock.opendj.util.SubstringReader;
 
 import static com.forgerock.opendj.ldap.CoreMessages.*;
@@ -67,6 +62,11 @@
  *      Models </a>
  */
 public final class DN implements Iterable<RDN>, Comparable<DN> {
+
+    static final byte NORMALIZED_RDN_SEPARATOR = 0x00;
+    static final byte NORMALIZED_AVA_SEPARATOR = 0x01;
+    static final byte NORMALIZED_ESC_BYTE = 0x02;
+
     private static final DN ROOT_DN = new DN(CoreSchema.getInstance(), null, null, "");
 
     /**
@@ -920,31 +920,64 @@
     }
 
     /**
-     * Returns the irreversible normalized byte string representation of a DN, suitable for equality and comparisons,
-     * and providing a natural hierarchical ordering but not usable as a valid DN nor reversible to a valid DN.
+     * Returns the irreversible normalized byte string representation of a DN,
+     * suitable for equality and comparisons, and providing a natural hierarchical
+     * ordering, but not usable as a valid DN nor reversible to a valid DN.
      * <p>
-     * This representation should be used only when a byte string representation is needed and when no reversibility to
-     * a valid DN is needed. Always consider using a {@code CompactDn} as an alternative.
+     * This representation should be used only when a byte string representation
+     * is needed and when no reversibility to a valid DN is needed. Always consider
+     * using a {@code CompactDn} as an alternative.
      *
-     * @return The normalized string representation of the provided DN, not usable as a valid DN
+     * @return The normalized byte string representation of the provided DN, not
+     *         usable as a valid DN
      */
     public ByteString toIrreversibleNormalizedByteString() {
         if (rdn() == null) {
             return ByteString.empty();
         }
 
-        final StringBuilder builder = new StringBuilder();
+        final ByteStringBuilder builder = new ByteStringBuilder();
         int i = size() - 1;
-        normalizeRDN(builder, parent(i).rdn());
+        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.append('\u0000');
+                builder.append(DN.NORMALIZED_RDN_SEPARATOR);
             }
-            normalizeRDN(builder, rdn);
+            rdn.toNormalizedByteString(builder);
         }
-        return ByteString.valueOf(builder.toString());
+        return builder.toByteString();
+    }
+
+    /**
+     * Returns the irreversible readable string representation of a DN, suitable
+     * for equality and usage as a name in file system or URL, but not usable as
+     * a valid DN nor reversible to a valid DN.
+     * <p>
+     * This representation should be used only when a string representation is
+     * needed and when no reversibility to a valid DN is needed.
+     *
+     * @return The readable string representation of the provided DN,
+     *         not usable as a valid DN
+     */
+    public String toIrreversibleReadableString() {
+        if (rdn() == null) {
+            return "";
+        }
+
+        final StringBuilder builder = new StringBuilder();
+        int i = size() - 1;
+        parent(i).rdn().toNormalizedReadableString(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.append(',');
+            }
+            rdn.toNormalizedReadableString(builder);
+        }
+        return builder.toString();
     }
 
     /**
@@ -960,6 +993,8 @@
      *   <li>eagerly: the normalized value is computed immediately at creation time.</li>
      *   <li>lazily: the normalized value is computed only the first time it is needed.</li>
      * </ul>
+     *
+     * @Deprecated This class will eventually be replaced by a compact implementation of a DN.
      */
     public static final class CompactDn implements Comparable<CompactDn> {
 
@@ -1038,112 +1073,4 @@
         return new CompactDn(this);
     }
 
-    /**
-     * Returns the normalized string representation of a RDN.
-     *
-     * @param builder
-     *            The StringBuilder to use to construct the normalized string.
-     * @param rdn
-     *            The RDN.
-     * @return The normalized string representation of the provided RDN.
-     */
-    private static StringBuilder normalizeRDN(final StringBuilder builder, final RDN rdn) {
-        final int sz = rdn.size();
-        switch (sz) {
-        case 0:
-            // Handle RDN.maxValue().
-            builder.append('\u0001');
-            break;
-        case 1:
-            normalizeAVA(builder, rdn.getFirstAVA());
-            break;
-        default:
-            // Need to sort the AVAs before comparing.
-            TreeSet<AVA> a = new TreeSet<AVA>();
-            for (AVA ava : rdn) {
-                a.add(ava);
-            }
-            Iterator<AVA> i = a.iterator();
-            // Normalize the first AVA.
-            normalizeAVA(builder, i.next());
-            while (i.hasNext()) {
-                builder.append('\u0001');
-                normalizeAVA(builder, i.next());
-            }
-            break;
-        }
-        return builder;
-    }
-
-    /**
-     * Returns the normalized string representation of an AVA.
-     *
-     * @param builder
-     *            The StringBuilder to use to construct the normalized string.
-     * @param ava
-     *            The AVA.
-     * @return The normalized string representation of the provided AVA.
-     */
-    private static StringBuilder normalizeAVA(final StringBuilder builder, final AVA ava) {
-        final AttributeType attributeType = ava.getAttributeType();
-
-        ByteString value = ava.getAttributeValue();
-        final MatchingRule matchingRule = attributeType.getEqualityMatchingRule();
-        if (matchingRule != null) {
-            try {
-                value = matchingRule.normalizeAttributeValue(ava.getAttributeValue());
-            } catch (final DecodeException de) {
-                // Ignore - we'll drop back to the user provided value.
-            }
-        }
-
-        if (attributeType.getNames().isEmpty()) {
-            builder.append(attributeType.getOID());
-            builder.append("=#");
-            builder.append(value.toHexString());
-        } else {
-            final String name = attributeType.getNameOrOID();
-            // Normalizing.
-            StaticUtils.toLowerCase(name, builder);
-
-            builder.append("=");
-
-            final Syntax syntax = attributeType.getSyntax();
-            if (!syntax.isHumanReadable()) {
-                builder.append("#");
-                builder.append(value.toHexString());
-            } else {
-                final String str = value.toString();
-                if (str.length() == 0) {
-                    return builder;
-                }
-                char c = str.charAt(0);
-                int startPos = 0;
-                if (c == ' ' || c == '#') {
-                    builder.append('\\');
-                    builder.append(c);
-                    startPos = 1;
-                }
-                final int length = str.length();
-                for (int si = startPos; si < length; si++) {
-                    c = str.charAt(si);
-                    if (c < ' ') {
-                        for (final byte b : getBytes(String.valueOf(c))) {
-                            builder.append('\\');
-                            builder.append(StaticUtils.byteToLowerHex(b));
-                        }
-                    } else {
-                        if ((c == ' ' && si == length - 1)
-                                || (c == '"' || c == '+' || c == ',' || c == ';' || c == '<'
-                                        || c == '=' || c == '>' || c == '\\' || c == '\u0000')) {
-                            builder.append('\\');
-                        }
-                        builder.append(c);
-                    }
-                }
-            }
-        }
-        return builder;
-    }
-
 }
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java
index 15ea6b0..eb05a90 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java
+++ b/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java
@@ -34,6 +34,7 @@
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
+import java.util.TreeSet;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.LocalizedIllegalArgumentException;
@@ -73,6 +74,9 @@
  */
 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.
@@ -408,4 +412,74 @@
     StringBuilder toString(final StringBuilder builder) {
         return builder.append(this);
     }
+
+    /**
+     * Returns the normalized byte string representation of this RDN.
+     * <p>
+     * The representation is not a valid RDN.
+     *
+     * @param builder
+     *            The builder to use to construct the normalized byte string.
+     * @return The normalized byte string representation.
+     * @see DN#toIrreversibleNormalizedByteString()
+     */
+    ByteStringBuilder toNormalizedByteString(final ByteStringBuilder builder) {
+        switch (size()) {
+        case 0:
+            // Handle RDN.maxValue().
+            builder.append(DN.NORMALIZED_AVA_SEPARATOR);
+            break;
+        case 1:
+            getFirstAVA().toNormalizedByteString(builder);
+            break;
+        default:
+            Iterator<AVA> it = getSortedAvas();
+            it.next().toNormalizedByteString(builder);
+            while (it.hasNext()) {
+                builder.append(DN.NORMALIZED_AVA_SEPARATOR);
+                it.next().toNormalizedByteString(builder);
+            }
+            break;
+        }
+        return builder;
+    }
+
+    /**
+     * Returns the normalized readable string representation of this RDN.
+     * <p>
+     * The representation is not a valid RDN.
+     *
+     * @param builder
+     *            The builder to use to construct the normalized string.
+     * @return The normalized readable string representation.
+     * @see DN#toIrreversibleReadableString()
+     */
+    StringBuilder toNormalizedReadableString(final StringBuilder builder) {
+        switch (size()) {
+        case 0:
+            // Handle RDN.maxValue().
+            builder.append(RDN.AVA_CHAR_SEPARATOR);
+            break;
+        case 1:
+            getFirstAVA().toNormalizedReadableString(builder);
+            break;
+        default:
+            Iterator<AVA> it = getSortedAvas();
+            it.next().toNormalizedReadableString(builder);
+            while (it.hasNext()) {
+                builder.append(RDN.AVA_CHAR_SEPARATOR);
+                it.next().toNormalizedReadableString(builder);
+            }
+            break;
+        }
+        return builder;
+    }
+
+    private Iterator<AVA> getSortedAvas() {
+        TreeSet<AVA> sortedAvas = new TreeSet<AVA>();
+        for (AVA ava : avas) {
+            sortedAvas.add(ava);
+        }
+        return sortedAvas.iterator();
+    }
 }
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringTestCase.java b/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringTestCase.java
index e141d06..dedeea1 100644
--- a/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringTestCase.java
+++ b/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringTestCase.java
@@ -26,6 +26,10 @@
  */
 package org.forgerock.opendj.ldap;
 
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
 import java.util.Arrays;
 
 import javax.xml.bind.DatatypeConverter;
@@ -260,6 +264,55 @@
     }
 
     @Test
+    public void testToHex() throws Exception {
+        ByteString byteString = new ByteStringBuilder().append("org=example").toByteString();
+        assertThat(byteString.toHexString()).isEqualTo("6F 72 67 3D 65 78 61 6D 70 6C 65");
+        assertThat(byteString.toHexString('-')).isEqualTo("6F-72-67-3D-65-78-61-6D-70-6C-65");
+    }
+
+    @Test
+    public void testCopyToCharBuffer() throws Exception {
+        String value = "org=example";
+        ByteString byteString = new ByteStringBuilder().append(value).toByteString();
+        CharBuffer buffer = CharBuffer.allocate(value.length());
+        final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
+
+        boolean isCopied = byteString.copyTo(buffer, decoder);
+
+        assertThat(isCopied).isTrue();
+        assertThat(buffer.toString()).isEqualTo(value);
+    }
+
+    @Test
+    public void testCopyToCharBufferFailure() throws Exception {
+        // Non valid UTF-8 byte sequence
+        ByteString byteString = new ByteStringBuilder().append((byte) 0x80).toByteString();
+        CharBuffer buffer = CharBuffer.allocate(1);
+        final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
+
+        boolean isCopied = byteString.copyTo(buffer, decoder);
+
+        assertThat(isCopied).isFalse();
+    }
+
+    @Test
+    public void testCopyToByteBuffer() throws Exception {
+        String value = "org=example";
+        ByteString byteString = new ByteStringBuilder().append(value).toByteString();
+        ByteBuffer buffer = ByteBuffer.allocate(value.length());
+
+        byteString.copyTo(buffer);
+
+        assertSameByteContent(buffer, byteString);
+    }
+
+    private void assertSameByteContent(ByteBuffer buffer, ByteString byteString) {
+        for (byte b : byteString.toByteArray()) {
+            assertThat(buffer.get()).isEqualTo(b);
+        }
+    }
+
+    @Test
     public void testToHexPlusAsciiString() throws Exception {
         ByteString byteString = new ByteStringBuilder().append("cn=testvalue,org=example").toByteString();
         assertThat(byteString.toHexPlusAsciiString(10)).isEqualTo(
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java b/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
index c5fe282..cc892fb 100644
--- a/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
+++ b/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
@@ -33,6 +33,7 @@
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
+import static org.fest.assertions.Assertions.*;
 import static org.testng.Assert.*;
 
 /**
@@ -126,7 +127,7 @@
             { "AB-global=", "ab-global=", "AB-global=" },
             { "OU= Sales + CN = J. Smith ,DC=example,DC=net",
                 "cn=j. smith+ou=sales,dc=example,dc=net", "OU=Sales+CN=J. Smith,DC=example,DC=net" },
-            { "cn=John+a=", "a=+cn=john", "cn=John+a=" },
+            { "cn=John+dc=", "dc=+cn=john", "cn=John+dc=" },
             { "O=\"Sue, Grabbit and Runn\",C=US", "o=sue\\, grabbit and runn,c=us",
                 "O=Sue\\, Grabbit and Runn,C=US" }, };
     }
@@ -1042,19 +1043,112 @@
     public Object[][] toIrreversibleNormalizedByteStringDataProvider() {
         // @formatter:off
         return new Object[][] {
-            { "dc=com", "dc=com" },
-            { "dc=example,dc=com", "dc=example,dc=com" },
-            { "dc=example,dc=com", "dc = example, dc = com" },
-            { "dc=example+cn=test,dc=com", "cn=test+dc=example,dc=com" },
+            // first value to normalize, second value to normalize, expected sign of comparison between the two
+            { "dc=com", "dc=com", 0 },
+            { "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 },
+            // 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},
+            { "dc=example,dc=com", "dc=test,dc=example,dc=com", -1},
+            { "dc=example,dc=com", "dc=example2,dc=com", -1},
+            { "dc=example2,dc=com", "dc=test,dc=example,dc=com", 1},
+            // with space
+            { "dc=example,dc=com", "dc = example, dc = com", 0 },
+            { "dc=example\\20test,dc=com", "dc=example test,dc=com", 0 },
+            { "dc=example test,dc=com", "dc=exampletest,dc=com", -1 },
+            // with various escaped characters
+            { "dc=example\\2Dtest,dc=com", "dc=example-test,dc=com", 0 },
+            { "dc=example\\28test,dc=com", "dc=example(test,dc=com", 0 },
+            { "dc=example\\3Ftest,dc=com", "dc=example?test,dc=com", 0 },
+            // with escaped comma
+            { "dc=example\\,dc=com,dc=com", "dc=example\\2Cdc=com,dc=com", 0 },
+            { "dc=example\\2Cdc=com,dc=com", "dc=example\\2Cdc\\3Dcom,dc=com", 0 },
+            { "dc=example,dc=com", "dc=example\\,dc=com,dc=com", -1 },
+            { "dc=example2,dc=com", "dc=example\\,dc=com,dc=com", 1 },
+            // with escaped "="
+            { "dc=example\\=other,dc=com", "dc=example\\3Dother,dc=com", 0 },
+            // with escaped "+"
+            { "dc=example\\+other,dc=com", "dc=example\\2Bother,dc=com", 0 },
+            // integer
+            { "governingStructureRule=10,dc=com", "governingStructureRule=10, dc=com", 0 },
+            { "governingStructureRule=99,dc=com", "governingStructureRule=100, dc=com", -1 },
+            { "governingStructureRule=999999,dc=com", "governingStructureRule=1000000, dc=com", -1 },
+            // no matching rule for the attribute
+            { "dummy=9,dc=com", "dummy=10,dc=com", 1 }
         };
         // @formatter:on
     }
 
-    /** Tests the {@link DN#toIrreversibleNormalizedByteString()} method. */
     @Test(dataProvider = "toIrreversibleNormalizedByteStringDataProvider")
-    public void testToIrreversibleNormalizedByteString(String actualStr, String expectedStr) {
-        DN actual = DN.valueOf(actualStr);
-        DN expected = DN.valueOf(expectedStr);
-        assertEquals(actual.toIrreversibleNormalizedByteString(), expected.toIrreversibleNormalizedByteString());
+    public void testToIrreversibleNormalizedByteString(String first, String second, int expectedCompareResult) {
+        DN actual = DN.valueOf(first);
+        DN expected = DN.valueOf(second);
+        int cmp = actual.toIrreversibleNormalizedByteString().compareTo(expected.toIrreversibleNormalizedByteString());
+        assertThat(Integer.signum(cmp)).isEqualTo(expectedCompareResult);
+    }
+
+    @Test(dataProvider = "testDNs")
+    /** Additional tests with testDNs data provider */
+    public void testToIrreversibleNormalizedByteString2(String one, String two, String three) {
+        DN dn1 = DN.valueOf(one);
+        DN dn2 = DN.valueOf(two);
+        DN dn3 = DN.valueOf(three);
+        int cmp = dn1.toIrreversibleNormalizedByteString().compareTo(dn2.toIrreversibleNormalizedByteString());
+        assertThat(cmp).isEqualTo(0);
+        int cmp2 = dn1.toIrreversibleNormalizedByteString().compareTo(dn3.toIrreversibleNormalizedByteString());
+        assertThat(cmp2).isEqualTo(0);
+    }
+
+    @DataProvider
+    public Object[][] toIrreversibleReadableStringDataProvider() {
+        // @formatter:off
+        return new Object[][] {
+            // first value = string used to build DN, second value = expected readable string
+            { "dc=com", "dc=com" },
+            { "dc=example,dc=com", "dc=com,dc=example" },
+            { "dc = example, dc = com", "dc=com,dc=example" },
+            { "dc=example+cn=test,dc=com", "dc=com,cn=test+dc=example" },
+            { "cn=test+dc=example,dc=com", "dc=com,cn=test+dc=example" },
+            // with space
+            { "dc=example test,dc=com", "dc=com,dc=example%20test" },
+            { "dc=example\\20test,dc=com", "dc=com,dc=example%20test" },
+            // with escaped comma
+            { "dc=example\\,dc=com,dc=com", "dc=com,dc=example%2Cdc%3Dcom" },
+            { "dc=example\\2Cdc=com,dc=com", "dc=com,dc=example%2Cdc%3Dcom" },
+            // with escaped "="
+            { "dc=example\\=other,dc=com", "dc=com,dc=example%3Dother" },
+            { "dc=example\\3Dother,dc=com", "dc=com,dc=example%3Dother" },
+            // with escaped "+"
+            { "dc=example\\+other,dc=com", "dc=com,dc=example%2Bother" },
+            { "dc=example\\2Bother,dc=com", "dc=com,dc=example%2Bother" },
+            // integer
+            { "governingStructureRule=256,dc=com", "dc=com,governingstructurerule=%01%00" },
+            // uuid
+            { "entryUUID=597ae2f6-16a6-1027-98f4-d28b5365dc14,dc=com",
+              "dc=com,entryuuid=597ae2f6-16a6-1027-98f4-d28b5365dc14" },
+            // characters unescaped by URL encoding (-, _, ., ~)
+            { "dc=example\\2Dtest,dc=com", "dc=com,dc=example-test" },
+            { "dc=example\\5Ftest,dc=com", "dc=com,dc=example_test" },
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "toIrreversibleReadableStringDataProvider")
+    public void testToIrreversibleReadableString(String dnAsString, String expectedReadableString) {
+        DN actual = DN.valueOf(dnAsString);
+        assertEquals(actual.toIrreversibleReadableString(), expectedReadableString);
+    }
+
+    @Test(dataProvider = "testDNs")
+    /** Additional tests with testDNs data provider */
+    public void testToIrreversibleReadableString2(String one, String two, String three) {
+        DN dn1 = DN.valueOf(one);
+        DN dn2 = DN.valueOf(two);
+        DN dn3 = DN.valueOf(three);
+        String irreversibleReadableString = dn1.toIrreversibleReadableString();
+        assertEquals(irreversibleReadableString, dn2.toIrreversibleReadableString());
+        assertEquals(irreversibleReadableString, dn3.toIrreversibleReadableString());
     }
 }
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java b/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java
index ced2d91..8699270 100644
--- a/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java
+++ b/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java
@@ -149,18 +149,20 @@
             { "givenName=John+cn=Doe,ou=People,dc=example,dc=com",
                 "dc=com\u0000dc=example\u0000ou=people\u0000cn=doe\u0001givenname=john" },
             { "givenName=John\\+cn=Doe,ou=People,dc=example,dc=com",
-                "dc=com\u0000dc=example\u0000ou=people\u0000givenname=john\\+cn\\=doe" },
+                "dc=com\u0000dc=example\u0000ou=people\u0000givenname=john\u002Bcn\u003Ddoe" },
             { "cn=Doe\\, John,ou=People,dc=example,dc=com",
-                "dc=com\u0000dc=example\u0000ou=people\u0000cn=doe\\, john" },
+                "dc=com\u0000dc=example\u0000ou=people\u0000cn=doe, john" },
             { "UID=jsmith,DC=example,DC=net", "dc=net\u0000dc=example\u0000uid=jsmith" },
             { "OU=Sales+CN=J. Smith,DC=example,DC=net",
                 "dc=net\u0000dc=example\u0000cn=j. smith\u0001ou=sales" },
-            { "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net",
-                "dc=net\u0000dc=example\u0000cn=james \\\"jim\\\" smith\\, iii" },
+            // commented due to checkstyle bug : https://github.com/checkstyle/checkstyle/issues/157
+            // uncomment when it is fixed
+            // { "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net",
+            //    "dc=net\u0000dc=example\u0000cn=james " + "\u005C\u0022jim\u005C\u0022" + " smith\u002C iii" },
             { "CN=John Smith\\2C III,DC=example,DC=net",
-                "dc=net\u0000dc=example\u0000cn=john smith\\, iii" },
+                "dc=net\u0000dc=example\u0000cn=john smith\u002C iii" },
             { "CN=\\23John Smith\\20,DC=example,DC=net",
-                "dc=net\u0000dc=example\u0000cn=\\#john smith" },
+                "dc=net\u0000dc=example\u0000cn=\u0023john smith" },
             { "CN=Before\\0dAfter,DC=example,DC=net",
                 // \0d is a hex representation of Carriage return. It is mapped
                 // to a SPACE as defined in the MAP ( RFC 4518)
@@ -179,7 +181,7 @@
             { "OU= Sales + CN = J. Smith ,DC=example,DC=net",
                 "dc=net\u0000dc=example\u0000cn=j. smith\u0001ou=sales" },
             { "cn=John+a=", "a=\u0001cn=john" },
-            { "O=\"Sue, Grabbit and Runn\",C=US", "c=us\u0000o=sue\\, grabbit and runn" }, };
+            { "O=\"Sue, Grabbit and Runn\",C=US", "c=us\u0000o=sue\u002C grabbit and runn" }, };
     }
 
     protected MatchingRule getRule() {

--
Gitblit v1.10.0