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}