001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2010 Sun Microsystems, Inc.
015 * Portions copyright 2011-2016 ForgeRock AS.
016 */
017package org.forgerock.opendj.ldap;
018
019import static com.forgerock.opendj.ldap.CoreMessages.*;
020import static com.forgerock.opendj.util.StaticUtils.*;
021
022import static org.forgerock.util.Reject.*;
023
024import java.io.UnsupportedEncodingException;
025import java.net.URLEncoder;
026import java.nio.CharBuffer;
027import java.nio.charset.Charset;
028import java.nio.charset.CharsetDecoder;
029import java.nio.charset.CodingErrorAction;
030
031import org.forgerock.i18n.LocalizableMessage;
032import org.forgerock.i18n.LocalizedIllegalArgumentException;
033import org.forgerock.opendj.ldap.schema.AttributeType;
034import org.forgerock.opendj.ldap.schema.MatchingRule;
035import org.forgerock.opendj.ldap.schema.Schema;
036import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
037
038import com.forgerock.opendj.util.StaticUtils;
039import com.forgerock.opendj.util.SubstringReader;
040
041/**
042 * An attribute value assertion (AVA) as defined in RFC 4512 section 2.3
043 * consists of an attribute description with zero options and an attribute
044 * value.
045 * <p>
046 * The following are examples of string representations of AVAs:
047 *
048 * <pre>
049 * uid=12345
050 * ou=Engineering
051 * cn=Kurt Zeilenga
052 * </pre>
053 *
054 * Note: The name <em>AVA</em> is historical, coming from X500/LDAPv2.
055 * However, in LDAP context, this class actually represents an
056 * <code>AttributeTypeAndValue</code>.
057 *
058 * @see <a href="http://tools.ietf.org/html/rfc4512#section-2.3">RFC 4512 -
059 *      Lightweight Directory Access Protocol (LDAP): Directory Information
060 *      Models </a>
061 */
062public final class AVA implements Comparable<AVA> {
063
064    /**
065     * Parses the provided LDAP string representation of an AVA using the
066     * default schema.
067     *
068     * @param ava
069     *            The LDAP string representation of an AVA.
070     * @return The parsed RDN.
071     * @throws LocalizedIllegalArgumentException
072     *             If {@code ava} is not a valid LDAP string representation of a
073     *             AVA.
074     * @throws NullPointerException
075     *             If {@code ava} was {@code null}.
076     */
077    public static AVA valueOf(final String ava) {
078        return valueOf(ava, Schema.getDefaultSchema());
079    }
080
081    /**
082     * Parses the provided LDAP string representation of an AVA using the
083     * provided schema.
084     *
085     * @param ava
086     *            The LDAP string representation of a AVA.
087     * @param schema
088     *            The schema to use when parsing the AVA.
089     * @return The parsed AVA.
090     * @throws LocalizedIllegalArgumentException
091     *             If {@code ava} is not a valid LDAP string representation of a
092     *             AVA.
093     * @throws NullPointerException
094     *             If {@code ava} or {@code schema} was {@code null}.
095     */
096    public static AVA valueOf(final String ava, final Schema schema) {
097        final SubstringReader reader = new SubstringReader(ava);
098        try {
099            return decode(reader, schema);
100        } catch (final UnknownSchemaElementException e) {
101            final LocalizableMessage message =
102                    ERR_RDN_TYPE_NOT_FOUND.get(ava, e.getMessageObject());
103            throw new LocalizedIllegalArgumentException(message);
104        }
105    }
106
107    static AVA decode(final SubstringReader reader, final Schema schema) {
108        // Skip over any spaces at the beginning.
109        reader.skipWhitespaces();
110
111        if (reader.remaining() == 0) {
112            final LocalizableMessage message =
113                    ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME.get(reader.getString());
114            throw new LocalizedIllegalArgumentException(message);
115        }
116
117        final String nameOrOid = readAttributeName(reader);
118        final AttributeType attribute = schema.getAttributeType(nameOrOid);
119
120        // Skip over any spaces if we have.
121        reader.skipWhitespaces();
122
123        // Make sure that we're not at the end of the DN string because
124        // that would be invalid.
125        if (reader.remaining() == 0) {
126            final LocalizableMessage message =
127                    ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME.get(reader.getString(), attribute
128                            .getNameOrOID());
129            throw new LocalizedIllegalArgumentException(message);
130        }
131
132        // The next character must be an equal sign. If it is not, then
133        // that's an error.
134        final char c = reader.read();
135        if (c != '=') {
136            final LocalizableMessage message =
137                    ERR_ATTR_SYNTAX_DN_NO_EQUAL
138                            .get(reader.getString(), attribute.getNameOrOID(), c);
139            throw new LocalizedIllegalArgumentException(message);
140        }
141
142        // Skip over any spaces after the equal sign.
143        reader.skipWhitespaces();
144
145        // Parse the value for this RDN component.
146        final ByteString value = readAttributeValue(reader);
147        return new AVA(attribute, nameOrOid, value);
148    }
149
150    static void escapeAttributeValue(final String str, final StringBuilder builder) {
151        if (str.length() > 0) {
152            char c = str.charAt(0);
153            int startPos = 0;
154            if (c == ' ' || c == '#') {
155                builder.append('\\');
156                builder.append(c);
157                startPos = 1;
158            }
159            final int length = str.length();
160            for (int si = startPos; si < length; si++) {
161                c = str.charAt(si);
162                if (c < ' ') {
163                    for (final byte b : getBytes(String.valueOf(c))) {
164                        builder.append('\\');
165                        builder.append(StaticUtils.byteToLowerHex(b));
166                    }
167                } else {
168                    if ((c == ' ' && si == length - 1)
169                            || (c == '"' || c == '+' || c == ',' || c == ';' || c == '<'
170                            || c == '>' || c == '\\' || c == '\u0000')) {
171                        builder.append('\\');
172                    }
173                    builder.append(c);
174                }
175            }
176        }
177    }
178
179    private static String readAttributeName(final SubstringReader reader) {
180        int length = 1;
181        reader.mark();
182
183        // The next character must be either numeric (for an OID) or
184        // alphabetic (for an attribute description).
185        char c = reader.read();
186        if (isDigit(c)) {
187            boolean lastWasPeriod = false;
188            while (reader.remaining() > 0) {
189                c = reader.read();
190                if (c == '=' || c == ' ') {
191                    // This signals the end of the OID.
192                    break;
193                } else if (c == '.') {
194                    if (lastWasPeriod) {
195                        throw illegalCharacter(reader, c);
196                    }
197                    lastWasPeriod = true;
198                } else if (!isDigit(c)) {
199                    throw illegalCharacter(reader, c);
200                } else {
201                    lastWasPeriod = false;
202                }
203                length++;
204            }
205            if (lastWasPeriod) {
206                throw illegalCharacter(reader, '.');
207            }
208        } else if (isAlpha(c)) {
209            // This must be an attribute description. In this case, we will
210            // only accept alphabetic characters, numeric digits, and the
211            // hyphen.
212            while (reader.remaining() > 0) {
213                c = reader.read();
214                if (c == '=' || c == ' ') {
215                    // This signals the end of the OID.
216                    break;
217                } else if (!isAlpha(c) && !isDigit(c) && c != '-') {
218                    throw illegalCharacter(reader, c);
219                }
220                length++;
221            }
222        } else {
223            throw illegalCharacter(reader, c);
224        }
225        // Return the position of the first non-space character after the token
226        reader.reset();
227        return reader.read(length);
228    }
229
230    private static LocalizedIllegalArgumentException illegalCharacter(
231            final SubstringReader reader, final char c) {
232        return new LocalizedIllegalArgumentException(
233                ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(reader.getString(), c, reader.pos() - 1));
234    }
235
236    private static ByteString readAttributeValue(final SubstringReader reader) {
237        // All leading spaces have already been stripped so we can start
238        // reading the value. However, it may be empty so check for that.
239        if (reader.remaining() == 0) {
240            return ByteString.empty();
241        }
242
243        // Decide how to parse based on the first character.
244        reader.mark();
245        char c = reader.read();
246        if (c == '+') {
247            // Value is empty and followed by another AVA.
248            reader.reset();
249            return ByteString.empty();
250        } else if (c == '#') {
251            // Value is HEX encoded BER.
252            return readAttributeValueAsBER(reader);
253        } else if (c == '"') {
254            // Legacy support for RFC 2253. The value should continue until the
255            // corresponding closing quotation mark and has the same format as
256            // RFC 4514 attribute values, except that special characters,
257            // excluding double quote and back-slash, do not need escaping.
258            reader.mark();
259            return readAttributeValue(reader, true);
260        } else {
261            // Otherwise, use general parsing to find the end of the value.
262            return readAttributeValue(reader, false);
263        }
264    }
265
266    private static ByteString readAttributeValueAsBER(final SubstringReader reader) {
267        // The first two characters must be hex characters.
268        reader.mark();
269        if (reader.remaining() < 2) {
270            throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT.get(reader.getString()));
271        }
272
273        int length = 0;
274        for (int i = 0; i < 2; i++) {
275            final char c = reader.read();
276            if (isHexDigit(c)) {
277                length++;
278            } else {
279                throw new LocalizedIllegalArgumentException(
280                        ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(reader.getString(), c));
281            }
282        }
283
284        // The rest of the value must be a multiple of two hex
285        // characters. The end of the value may be designated by the
286        // end of the DN, a comma or semicolon, or a space.
287        while (reader.remaining() > 0) {
288            char c = reader.read();
289            if (isHexDigit(c)) {
290                length++;
291
292                if (reader.remaining() > 0) {
293                    c = reader.read();
294                    if (isHexDigit(c)) {
295                        length++;
296                    } else {
297                        throw new LocalizedIllegalArgumentException(
298                                ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(reader.getString(), c));
299                    }
300                } else {
301                    throw new LocalizedIllegalArgumentException(
302                            ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT.get(reader.getString()));
303                }
304            } else if (c == ' ' || c == ',' || c == ';') {
305                // This denotes the end of the value.
306                break;
307            } else {
308                throw new LocalizedIllegalArgumentException(
309                        ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(reader.getString(), c));
310            }
311        }
312
313        // At this point, we should have a valid hex string. Convert it
314        // to a byte array and set that as the value of the provided
315        // octet string.
316        try {
317            reader.reset();
318            return ByteString.valueOfHex(reader.read(length));
319        } catch (final LocalizedIllegalArgumentException e) {
320            throw new LocalizedIllegalArgumentException(
321                    ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE.get(reader.getString(), e.getMessageObject()));
322        }
323    }
324
325    private static ByteString readAttributeValue(final SubstringReader reader, final boolean isQuoted) {
326        reader.reset();
327        final ByteString bytes = delimitAndEvaluateEscape(reader, isQuoted);
328        if (bytes.length() == 0) {
329            // We don't allow an empty attribute value.
330            final LocalizableMessage message =
331                    ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR.get(reader.getString(), reader.pos());
332            throw new LocalizedIllegalArgumentException(message);
333        }
334        return bytes;
335    }
336
337    private static ByteString delimitAndEvaluateEscape(final SubstringReader reader, final boolean isQuoted) {
338        final StringBuilder valueBuffer = new StringBuilder();
339        StringBuilder hexBuffer = null;
340        boolean escaped = false;
341        int trailingSpaces = 0;
342        while (reader.remaining() > 0) {
343            final char c = reader.read();
344            if (escaped) {
345                // This character is escaped.
346                if (isHexDigit(c)) {
347                    // Unicode characters.
348                    if (reader.remaining() <= 0) {
349                        throw new LocalizedIllegalArgumentException(
350                                ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID.get(reader.getString()));
351                    }
352
353                    // Check the next byte for hex.
354                    final char c2 = reader.read();
355                    if (isHexDigit(c2)) {
356                        if (hexBuffer == null) {
357                            hexBuffer = new StringBuilder();
358                        }
359                        hexBuffer.append(c);
360                        hexBuffer.append(c2);
361                        // We may be at the end.
362                        if (reader.remaining() == 0) {
363                            appendHexChars(reader, valueBuffer, hexBuffer);
364                        }
365                    } else {
366                        throw new LocalizedIllegalArgumentException(
367                                ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID.get(reader.getString()));
368                    }
369                } else {
370                    appendHexChars(reader, valueBuffer, hexBuffer);
371                    valueBuffer.append(c);
372                }
373                escaped = false;
374            } else if (c == '\\') {
375                escaped = true;
376                trailingSpaces = 0;
377            } else if (isQuoted && c == '"') {
378                appendHexChars(reader, valueBuffer, hexBuffer);
379                reader.skipWhitespaces();
380                return ByteString.valueOfUtf8(valueBuffer);
381            } else if (!isQuoted && (c == '+' || c == ',' || c == ';')) {
382                reader.reset();
383                appendHexChars(reader, valueBuffer, hexBuffer);
384                valueBuffer.setLength(valueBuffer.length() - trailingSpaces);
385                return ByteString.valueOfUtf8(valueBuffer);
386            } else {
387                // It is definitely not a delimiter at this point.
388                appendHexChars(reader, valueBuffer, hexBuffer);
389                valueBuffer.append(c);
390                trailingSpaces = c != ' ' ? 0 : trailingSpaces + 1;
391            }
392            reader.mark();
393        }
394        if (isQuoted) {
395            // We hit the end of the AVA before the closing quote. That's an error.
396            throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE.get(reader.getString()));
397        }
398        reader.reset();
399        valueBuffer.setLength(valueBuffer.length() - trailingSpaces);
400        return ByteString.valueOfUtf8(valueBuffer);
401    }
402
403    private static void appendHexChars(final SubstringReader reader,
404                                       final StringBuilder valueBuffer,
405                                       final StringBuilder hexBuffer) {
406        if (hexBuffer == null) {
407            return;
408        }
409        final ByteString bytes = ByteString.valueOfHex(hexBuffer.toString());
410        try {
411            valueBuffer.append(new String(bytes.toByteArray(), "UTF-8"));
412        } catch (final Exception e) {
413            throw new LocalizedIllegalArgumentException(
414                    ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE.get(reader.getString(), String.valueOf(e)));
415        }
416        // Clean up the hex buffer.
417        hexBuffer.setLength(0);
418    }
419
420    private final AttributeType attributeType;
421    private final String attributeName;
422    private final ByteString attributeValue;
423
424    /** Cached normalized value using equality matching rule. */
425    private ByteString equalityNormalizedAttributeValue;
426    /** Cached normalized value using ordering matching rule. */
427    private ByteString orderingNormalizedAttributeValue;
428
429    /**
430     * Creates a new attribute value assertion (AVA) using the provided
431     * attribute type and value.
432     * <p>
433     * If {@code attributeValue} is not an instance of {@code ByteString} then
434     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
435     *
436     * @param attributeType
437     *            The attribute type.
438     * @param attributeValue
439     *            The attribute value.
440     * @throws NullPointerException
441     *             If {@code attributeType} or {@code attributeValue} was
442     *             {@code null}.
443     */
444    public AVA(final AttributeType attributeType, final Object attributeValue) {
445        this(attributeType, null, attributeValue);
446    }
447
448    /**
449     * Creates a new attribute value assertion (AVA) using the provided
450     * attribute type, name and value.
451     * <p>
452     * If {@code attributeValue} is not an instance of {@code ByteString} then
453     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
454     *
455     * @param attributeType
456     *            The attribute type.
457     * @param attributeName
458     *            The user provided attribute name.
459     * @param attributeValue
460     *            The attribute value.
461     * @throws NullPointerException
462     *             If {@code attributeType}, {@code attributeName} or {@code attributeValue} was {@code null}.
463     */
464    public AVA(final AttributeType attributeType, final String attributeName, final Object attributeValue) {
465        this.attributeType = checkNotNull(attributeType);
466        this.attributeName = computeAttributeName(attributeName, attributeType);
467        this.attributeValue = ByteString.valueOfObject(checkNotNull(attributeValue));
468    }
469
470    /**
471     * Creates a new attribute value assertion (AVA) using the provided
472     * attribute type and value decoded using the default schema.
473     * <p>
474     * If {@code attributeValue} is not an instance of {@code ByteString} then
475     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
476     *
477     * @param attributeType
478     *            The attribute type.
479     * @param attributeValue
480     *            The attribute value.
481     * @throws UnknownSchemaElementException
482     *             If {@code attributeType} was not found in the default schema.
483     * @throws NullPointerException
484     *             If {@code attributeType} or {@code attributeValue} was
485     *             {@code null}.
486     */
487    public AVA(final String attributeType, final Object attributeValue) {
488        this.attributeName = checkNotNull(attributeType);
489        this.attributeType = Schema.getDefaultSchema().getAttributeType(attributeType);
490        this.attributeValue = ByteString.valueOfObject(checkNotNull(attributeValue));
491    }
492
493    private String computeAttributeName(final String attributeName, final AttributeType attributeType) {
494        return attributeName != null ? attributeName : attributeType.getNameOrOID();
495    }
496
497    @Override
498    public int compareTo(final AVA ava) {
499        final int result = attributeType.compareTo(ava.attributeType);
500        if (result != 0) {
501            return result > 0 ? 1 : -1;
502        }
503
504        final ByteString normalizedValue = getOrderingNormalizedValue();
505        final ByteString otherNormalizedValue = ava.getOrderingNormalizedValue();
506        return normalizedValue.compareTo(otherNormalizedValue);
507    }
508
509    @Override
510    public boolean equals(final Object obj) {
511        if (this == obj) {
512            return true;
513        } else if (obj instanceof AVA) {
514            final AVA ava = (AVA) obj;
515
516            if (!attributeType.equals(ava.attributeType)) {
517                return false;
518            }
519
520            final ByteString normalizedValue = getEqualityNormalizedValue();
521            final ByteString otherNormalizedValue = ava.getEqualityNormalizedValue();
522            return normalizedValue.equals(otherNormalizedValue);
523        } else {
524            return false;
525        }
526    }
527
528    /**
529     * Returns the attribute type associated with this AVA.
530     *
531     * @return The attribute type associated with this AVA.
532     */
533    public AttributeType getAttributeType() {
534        return attributeType;
535    }
536
537    /**
538     * Returns the attribute name associated with this AVA.
539     *
540     * @return The attribute name associated with this AVA.
541     */
542    public String getAttributeName() {
543        return attributeName;
544    }
545
546    /**
547     * Returns the attribute value associated with this AVA.
548     *
549     * @return The attribute value associated with this AVA.
550     */
551    public ByteString getAttributeValue() {
552        return attributeValue;
553    }
554
555    @Override
556    public int hashCode() {
557        return attributeType.hashCode() * 31 + getEqualityNormalizedValue().hashCode();
558    }
559
560    /**
561     * Returns a single valued attribute having the same attribute type and
562     * value as this AVA.
563     *
564     * @return A single valued attribute having the same attribute type and
565     *         value as this AVA.
566     */
567    public Attribute toAttribute() {
568        AttributeDescription ad = AttributeDescription.create(attributeType);
569        return new LinkedAttribute(ad, attributeValue);
570    }
571
572    @Override
573    public String toString() {
574        final StringBuilder builder = new StringBuilder();
575        return toString(builder).toString();
576    }
577
578    StringBuilder toString(final StringBuilder builder) {
579        if (attributeName.equals(attributeType.getOID())) {
580            builder.append(attributeType.getOID());
581            builder.append("=#");
582            builder.append(attributeValue.toHexString());
583        } else {
584            builder.append(attributeName);
585            builder.append("=");
586
587            if (!attributeType.getSyntax().isHumanReadable()) {
588                builder.append("#");
589                builder.append(attributeValue.toHexString());
590            } else {
591                escapeAttributeValue(attributeValue.toString(), builder);
592            }
593        }
594        return builder;
595    }
596
597    private ByteString getEqualityNormalizedValue() {
598        final ByteString normalizedValue = equalityNormalizedAttributeValue;
599
600        if (normalizedValue != null) {
601            return normalizedValue;
602        }
603
604        final MatchingRule matchingRule = attributeType.getEqualityMatchingRule();
605        if (matchingRule != null) {
606            try {
607                equalityNormalizedAttributeValue =
608                        matchingRule.normalizeAttributeValue(attributeValue);
609            } catch (final DecodeException de) {
610                // Unable to normalize, so default to byte-wise comparison.
611                equalityNormalizedAttributeValue = attributeValue;
612            }
613        } else {
614            // No matching rule, so default to byte-wise comparison.
615            equalityNormalizedAttributeValue = attributeValue;
616        }
617
618        return equalityNormalizedAttributeValue;
619    }
620
621    private ByteString getOrderingNormalizedValue() {
622        final ByteString normalizedValue = orderingNormalizedAttributeValue;
623
624        if (normalizedValue != null) {
625            return normalizedValue;
626        }
627
628        final MatchingRule matchingRule = attributeType.getEqualityMatchingRule();
629        if (matchingRule != null) {
630            try {
631                orderingNormalizedAttributeValue =
632                        matchingRule.normalizeAttributeValue(attributeValue);
633            } catch (final DecodeException de) {
634                // Unable to normalize, so default to equality matching.
635                orderingNormalizedAttributeValue = getEqualityNormalizedValue();
636            }
637        } else {
638            // No matching rule, so default to equality matching.
639            orderingNormalizedAttributeValue = getEqualityNormalizedValue();
640        }
641
642        return orderingNormalizedAttributeValue;
643    }
644
645    /**
646     * Returns the normalized byte string representation of this AVA.
647     * <p>
648     * The representation is not a valid AVA.
649     *
650     * @param builder
651     *            The builder to use to construct the normalized byte string.
652     * @return The normalized byte string representation.
653     * @see DN#toNormalizedByteString()
654     */
655    ByteStringBuilder toNormalizedByteString(final ByteStringBuilder builder) {
656        builder.appendUtf8(toLowerCase(attributeType.getNameOrOID()));
657        builder.appendUtf8("=");
658        final ByteString value = getEqualityNormalizedValue();
659        if (value.length() > 0) {
660            builder.appendBytes(escapeBytes(value));
661        }
662        return builder;
663    }
664
665    /**
666     * Returns the normalized readable string representation of this AVA.
667     * <p>
668     * The representation is not a valid AVA.
669     *
670     * @param builder
671     *            The builder to use to construct the normalized string.
672     * @return The normalized readable string representation.
673     * @see DN#toNormalizedUrlSafeString()
674     */
675    StringBuilder toNormalizedUrlSafe(final StringBuilder builder) {
676        builder.append(toLowerCase(attributeType.getNameOrOID()));
677        builder.append('=');
678        final ByteString value = getEqualityNormalizedValue();
679
680        if (value.length() == 0) {
681            return builder;
682        }
683        final boolean hasAttributeName = !attributeType.getNames().isEmpty();
684        final boolean isHumanReadable = attributeType.getSyntax().isHumanReadable();
685        if (!hasAttributeName || !isHumanReadable) {
686            builder.append(value.toPercentHexString());
687        } else {
688            // try to decode value as UTF-8 string
689            final CharBuffer buffer = CharBuffer.allocate(value.length());
690            final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder()
691                .onMalformedInput(CodingErrorAction.REPORT)
692                .onUnmappableCharacter(CodingErrorAction.REPORT);
693            if (value.copyTo(buffer, decoder)) {
694                buffer.flip();
695                try {
696                    // URL encoding encodes space char as '+' instead of using hex code
697                    final String val = URLEncoder.encode(buffer.toString(), "UTF-8").replaceAll("\\+", "%20");
698                    builder.append(val);
699                } catch (UnsupportedEncodingException e) {
700                    // should never happen
701                    builder.append(value.toPercentHexString());
702                }
703            } else {
704                builder.append(value.toPercentHexString());
705            }
706        }
707        return builder;
708    }
709
710    /**
711     * Return a new byte string with bytes 0x00, 0x01 and 0x02 escaped.
712     * <p>
713     * These bytes are reserved to represent respectively the RDN separator,
714     * the AVA separator and the escape byte in a normalized byte string.
715     */
716    private ByteString escapeBytes(final ByteString value) {
717        if (!needEscaping(value)) {
718            return value;
719        }
720
721        final ByteStringBuilder builder = new ByteStringBuilder();
722        for (int i = 0; i < value.length(); i++) {
723            final byte b = value.byteAt(i);
724            if (isByteToEscape(b)) {
725                builder.appendByte(DN.NORMALIZED_ESC_BYTE);
726            }
727            builder.appendByte(b);
728        }
729        return builder.toByteString();
730    }
731
732    private boolean needEscaping(final ByteString value) {
733        for (int i = 0; i < value.length(); i++) {
734            if (isByteToEscape(value.byteAt(i))) {
735                return true;
736            }
737        }
738        return false;
739    }
740
741    private boolean isByteToEscape(final byte b) {
742        return b == DN.NORMALIZED_RDN_SEPARATOR || b == DN.NORMALIZED_AVA_SEPARATOR || b == DN.NORMALIZED_ESC_BYTE;
743    }
744}