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
| | |
| | | <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> |
| | |
| | | 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; |
| | |
| | | * 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. |
| | |
| | | // 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) { |
| | |
| | | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | |
| | | 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; |
| | | |
| | | /** |
| | |
| | | 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}. |
| | | * |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | /** {@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); |
| | |
| | | * 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(); |
| | |
| | | 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. |
| | |
| | | } |
| | | |
| | | /** {@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. |
| | |
| | | } |
| | | |
| | | /** {@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. |
| | |
| | | } |
| | | |
| | | /** {@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); |
| | |
| | | } |
| | | |
| | | /** {@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); |
| | |
| | | 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.*; |
| | |
| | | * 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, ""); |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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(); |
| | | } |
| | | |
| | | /** |
| | |
| | | * <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> { |
| | | |
| | |
| | | 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; |
| | | } |
| | | |
| | | } |
| | |
| | | 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; |
| | |
| | | */ |
| | | 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. |
| | |
| | | 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(); |
| | | } |
| | | } |
| | |
| | | */ |
| | | 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; |
| | |
| | | } |
| | | |
| | | @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( |
| | |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | import static org.fest.assertions.Assertions.*; |
| | | import static org.testng.Assert.*; |
| | | |
| | | /** |
| | |
| | | { "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" }, }; |
| | | } |
| | |
| | | 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()); |
| | | } |
| | | } |
| | |
| | | { "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) |
| | |
| | | { "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() { |