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

Nicolas Capponi
11.44.2014 4ccf2e43d0714e5fda04ce64071ccab0961147f9
OPENDJ-1585 CR-5594 Re-implement DN normalization in SDK

- Re-implement DN.toIrreversibleNormalizedByteString() using a
ByteStringBuilder, assuming attribute values can be arbitrary
bytes and escaping significant bytes

- Add DN.toIrreversibleReadableString() to return a normalized
readable representation of the DN

- Move intermediate methods to appropriate classes RDN and AVA

- Add two methods to ByteSequence
copyTo(ByteBuffer)
copyTo(CharBuffer, CharsetDecoder)

- Update DN tests with more complete data providers for
normalization
10 files modified
658 ■■■■ changed files
opendj-core/clirr-ignored-api-changes.xml 12 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.java 117 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequence.java 30 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteString.java 44 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteStringBuilder.java 25 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java 173 ●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java 74 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringTestCase.java 53 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java 114 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java 16 ●●●●● patch | view | raw | blame | history
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>
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;
    }
}
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}.
     *
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();
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);
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;
    }
}
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();
    }
}
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(
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());
    }
}
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() {