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}