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