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 2006-2010 Sun Microsystems, Inc. 025 * Portions Copyright 2013-2015 ForgeRock AS 026 */ 027package org.opends.server.types; 028 029import java.io.UnsupportedEncodingException; 030import java.net.URLEncoder; 031import java.nio.CharBuffer; 032import java.nio.charset.Charset; 033import java.nio.charset.CharsetDecoder; 034import java.nio.charset.CodingErrorAction; 035import java.util.*; 036 037import org.forgerock.i18n.LocalizableMessage; 038import org.forgerock.i18n.slf4j.LocalizedLogger; 039import org.forgerock.opendj.ldap.ByteString; 040import org.forgerock.opendj.ldap.ByteStringBuilder; 041import org.forgerock.opendj.ldap.DecodeException; 042import org.forgerock.opendj.ldap.ResultCode; 043import org.forgerock.opendj.ldap.schema.MatchingRule; 044import org.forgerock.util.Reject; 045import org.opends.server.core.DirectoryServer; 046 047import static org.opends.messages.CoreMessages.*; 048import static com.forgerock.opendj.util.StaticUtils.*; 049 050/** 051 * This class defines a data structure for storing and interacting 052 * with the relative distinguished names associated with entries in 053 * the Directory Server. 054 */ 055@org.opends.server.types.PublicAPI( 056 stability=org.opends.server.types.StabilityLevel.UNCOMMITTED, 057 mayInstantiate=true, 058 mayExtend=false, 059 mayInvoke=true) 060public final class RDN 061 implements Comparable<RDN> 062{ 063 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 064 065 /** The set of attribute types for the elements in this RDN. */ 066 private AttributeType[] attributeTypes; 067 068 /** The set of values for the elements in this RDN. */ 069 private ByteString[] attributeValues; 070 071 /** The set of user-provided names for the attributes in this RDN. */ 072 private String[] attributeNames; 073 074 /** Representation of the normalized form of this RDN. */ 075 private ByteString normalizedRDN; 076 077 078 /** 079 * Creates a new RDN with the provided information. 080 * 081 * @param attributeType The attribute type for this RDN. It must 082 * not be {@code null}. 083 * @param attributeValue The value for this RDN. It must not be 084 * {@code null}. 085 */ 086 @SuppressWarnings("unchecked") 087 public RDN(AttributeType attributeType, ByteString attributeValue) 088 { 089 Reject.ifNull(attributeType, attributeValue); 090 attributeTypes = new AttributeType[] { attributeType }; 091 attributeNames = new String[] { attributeType.getPrimaryName() }; 092 attributeValues = new ByteString[] { attributeValue }; 093 } 094 095 096 097 /** 098 * Creates a new RDN with the provided information. 099 * 100 * @param attributeType The attribute type for this RDN. It must 101 * not be {@code null}. 102 * @param attributeName The user-provided name for this RDN. It 103 * must not be {@code null}. 104 * @param attributeValue The value for this RDN. It must not be 105 * {@code null}. 106 */ 107 @SuppressWarnings("unchecked") 108 public RDN(AttributeType attributeType, String attributeName, ByteString attributeValue) 109 { 110 Reject.ifNull(attributeType, attributeName, attributeValue); 111 attributeTypes = new AttributeType[] { attributeType }; 112 attributeNames = new String[] { attributeName }; 113 attributeValues = new ByteString[] { attributeValue }; 114 } 115 116 117 118 /** 119 * Creates a new RDN with the provided information. The number of 120 * type, name, and value elements must be nonzero and equal. 121 * 122 * @param attributeTypes The set of attribute types for this RDN. 123 * It must not be empty or {@code null}. 124 * @param attributeNames The set of user-provided names for this 125 * RDN. It must have the same number of 126 * elements as the {@code attributeTypes} 127 * argument. 128 * @param attributeValues The set of values for this RDN. It must 129 * have the same number of elements as the 130 * {@code attributeTypes} argument. 131 */ 132 @SuppressWarnings("unchecked") 133 public RDN(List<AttributeType> attributeTypes, 134 List<String> attributeNames, 135 List<ByteString> attributeValues) 136 { 137 Reject.ifNull(attributeTypes, attributeNames, attributeValues); 138 Reject.ifTrue(attributeTypes.isEmpty(), "attributeTypes must not be empty"); 139 Reject.ifFalse(attributeNames.size() == attributeTypes.size(), 140 "attributeNames must have the same number of elements than attributeTypes"); 141 Reject.ifFalse(attributeValues.size() == attributeTypes.size(), 142 "attributeValues must have the same number of elements than attributeTypes"); 143 144 this.attributeTypes = new AttributeType[attributeTypes.size()]; 145 this.attributeNames = new String[attributeNames.size()]; 146 this.attributeValues = new ByteString[attributeValues.size()]; 147 148 attributeTypes.toArray(this.attributeTypes); 149 attributeNames.toArray(this.attributeNames); 150 attributeValues.toArray(this.attributeValues); 151 } 152 153 154 155 /** 156 * Creates a new RDN with the provided information. The number of 157 * type, name, and value elements must be nonzero and equal. 158 * 159 * @param attributeTypes The set of attribute types for this RDN. 160 * It must not be empty or {@code null}. 161 * @param attributeNames The set of user-provided names for this 162 * RDN. It must have the same number of 163 * elements as the {@code attributeTypes} 164 * argument. 165 * @param attributeValues The set of values for this RDN. It must 166 * have the same number of elements as the 167 * {@code attributeTypes} argument. 168 */ 169 @SuppressWarnings("unchecked") 170 public RDN(AttributeType[] attributeTypes, String[] attributeNames, ByteString[] attributeValues) 171 { 172 Reject.ifNull(attributeTypes, attributeNames, attributeValues); 173 Reject.ifFalse(attributeTypes.length > 0, "attributeTypes must not be empty"); 174 Reject.ifFalse(attributeNames.length == attributeTypes.length, 175 "attributeNames must have the same number of elements than attributeTypes"); 176 Reject.ifFalse(attributeValues.length == attributeTypes.length, 177 "attributeValues must have the same number of elements than attributeTypes"); 178 179 this.attributeTypes = attributeTypes; 180 this.attributeNames = attributeNames; 181 this.attributeValues = attributeValues; 182 } 183 184 185 186 /** 187 * Creates a new RDN with the provided information. 188 * 189 * @param attributeType The attribute type for this RDN. It must 190 * not be {@code null}. 191 * @param attributeValue The value for this RDN. It must not be 192 * {@code null}. 193 * 194 * @return The RDN created with the provided information. 195 */ 196 public static RDN create(AttributeType attributeType, ByteString attributeValue) 197 { 198 return new RDN(attributeType, attributeValue); 199 } 200 201 202 203 /** 204 * Retrieves the number of attribute-value pairs contained in this 205 * RDN. 206 * 207 * @return The number of attribute-value pairs contained in this 208 * RDN. 209 */ 210 public int getNumValues() 211 { 212 return attributeTypes.length; 213 } 214 215 216 217 /** 218 * Indicates whether this RDN includes the specified attribute type. 219 * 220 * @param attributeType The attribute type for which to make the 221 * determination. 222 * 223 * @return <CODE>true</CODE> if the RDN includes the specified 224 * attribute type, or <CODE>false</CODE> if not. 225 */ 226 public boolean hasAttributeType(AttributeType attributeType) 227 { 228 for (AttributeType t : attributeTypes) 229 { 230 if (t.equals(attributeType)) 231 { 232 return true; 233 } 234 } 235 236 return false; 237 } 238 239 240 241 /** 242 * Indicates whether this RDN includes the specified attribute type. 243 * 244 * @param lowerName The name or OID for the attribute type for 245 * which to make the determination, formatted in 246 * all lowercase characters. 247 * 248 * @return <CODE>true</CODE> if the RDN includes the specified 249 * attribute type, or <CODE>false</CODE> if not. 250 */ 251 public boolean hasAttributeType(String lowerName) 252 { 253 for (AttributeType t : attributeTypes) 254 { 255 if (t.hasNameOrOID(lowerName)) 256 { 257 return true; 258 } 259 } 260 261 for (String s : attributeNames) 262 { 263 if (s.equalsIgnoreCase(lowerName)) 264 { 265 return true; 266 } 267 } 268 269 return false; 270 } 271 272 273 274 /** 275 * Retrieves the attribute type at the specified position in the set 276 * of attribute types for this RDN. 277 * 278 * @param pos The position of the attribute type to retrieve. 279 * 280 * @return The attribute type at the specified position in the set 281 * of attribute types for this RDN. 282 */ 283 public AttributeType getAttributeType(int pos) 284 { 285 return attributeTypes[pos]; 286 } 287 288 289 290 /** 291 * Retrieves the name for the attribute type at the specified 292 * position in the set of attribute types for this RDN. 293 * 294 * @param pos The position of the attribute type for which to 295 * retrieve the name. 296 * 297 * @return The name for the attribute type at the specified 298 * position in the set of attribute types for this RDN. 299 */ 300 public String getAttributeName(int pos) 301 { 302 return attributeNames[pos]; 303 } 304 305 306 307 /** 308 * Retrieves the attribute value that is associated with the 309 * specified attribute type. 310 * 311 * @param attributeType The attribute type for which to retrieve 312 * the corresponding value. 313 * 314 * @return The value for the requested attribute type, or 315 * <CODE>null</CODE> if the specified attribute type is not 316 * present in the RDN. 317 */ 318 public ByteString getAttributeValue(AttributeType attributeType) 319 { 320 for (int i=0; i < attributeTypes.length; i++) 321 { 322 if (attributeTypes[i].equals(attributeType)) 323 { 324 return attributeValues[i]; 325 } 326 } 327 328 return null; 329 } 330 331 332 333 /** 334 * Retrieves the value for the attribute type at the specified 335 * position in the set of attribute types for this RDN. 336 * 337 * @param pos The position of the attribute type for which to 338 * retrieve the value. 339 * 340 * @return The value for the attribute type at the specified 341 * position in the set of attribute types for this RDN. 342 */ 343 public ByteString getAttributeValue(int pos) 344 { 345 return attributeValues[pos]; 346 } 347 348 349 350 /** 351 * Indicates whether this RDN is multivalued. 352 * 353 * @return <CODE>true</CODE> if this RDN is multivalued, or 354 * <CODE>false</CODE> if not. 355 */ 356 public boolean isMultiValued() 357 { 358 return attributeTypes.length > 1; 359 } 360 361 362 363 /** 364 * Indicates whether this RDN contains the specified type-value 365 * pair. 366 * 367 * @param type The attribute type for which to make the 368 * determination. 369 * @param value The value for which to make the determination. 370 * 371 * @return <CODE>true</CODE> if this RDN contains the specified 372 * attribute value, or <CODE>false</CODE> if not. 373 */ 374 public boolean hasValue(AttributeType type, ByteString value) 375 { 376 for (int i=0; i < attributeTypes.length; i++) 377 { 378 if (attributeTypes[i].equals(type) && 379 attributeValues[i].equals(value)) 380 { 381 return true; 382 } 383 } 384 385 return false; 386 } 387 388 389 390 /** 391 * Adds the provided type-value pair from this RDN. Note that this 392 * is intended only for internal use when constructing DN values. 393 * 394 * @param type The attribute type of the pair to add. 395 * @param name The user-provided name of the pair to add. 396 * @param value The attribute value of the pair to add. 397 * 398 * @return <CODE>true</CODE> if the type-value pair was added to 399 * this RDN, or <CODE>false</CODE> if it was not (e.g., it 400 * was already present). 401 */ 402 boolean addValue(AttributeType type, String name, ByteString value) 403 { 404 int numValues = attributeTypes.length; 405 for (int i=0; i < numValues; i++) 406 { 407 if (attributeTypes[i].equals(type) && 408 attributeValues[i].equals(value)) 409 { 410 return false; 411 } 412 } 413 numValues++; 414 AttributeType[] newTypes = new AttributeType[numValues]; 415 System.arraycopy(attributeTypes, 0, newTypes, 0, attributeTypes.length); 416 newTypes[attributeTypes.length] = type; 417 attributeTypes = newTypes; 418 419 String[] newNames = new String[numValues]; 420 System.arraycopy(attributeNames, 0, newNames, 0, attributeNames.length); 421 newNames[attributeNames.length] = name; 422 attributeNames = newNames; 423 424 ByteString[] newValues = new ByteString[numValues]; 425 System.arraycopy(attributeValues, 0, newValues, 0, attributeValues.length); 426 newValues[attributeValues.length] = value; 427 attributeValues = newValues; 428 429 return true; 430 } 431 432 433 434 /** 435 * Retrieves a version of the provided value in a form that is 436 * properly escaped for use in a DN or RDN. 437 * 438 * @param valueBS The value to be represented in a DN-safe form. 439 * 440 * @return A version of the provided value in a form that is 441 * properly escaped for use in a DN or RDN. 442 */ 443 private static String getDNValue(ByteString valueBS) { 444 final String value = valueBS.toString(); 445 if (value == null || value.length() == 0) { 446 return ""; 447 } 448 449 // Only copy the string value if required. 450 boolean needsEscaping = false; 451 int length = value.length(); 452 453 needsEscaping: { 454 char c = value.charAt(0); 455 if (c == ' ' || c == '#') { 456 needsEscaping = true; 457 break needsEscaping; 458 } 459 460 if (value.charAt(length - 1) == ' ') { 461 needsEscaping = true; 462 break needsEscaping; 463 } 464 465 for (int i = 0; i < length; i++) { 466 c = value.charAt(i); 467 if (c < ' ') { 468 needsEscaping = true; 469 break needsEscaping; 470 } else { 471 switch (c) { 472 case ',': 473 case '+': 474 case '"': 475 case '\\': 476 case '<': 477 case '>': 478 case ';': 479 needsEscaping = true; 480 break needsEscaping; 481 } 482 } 483 } 484 } 485 486 if (!needsEscaping) { 487 return value; 488 } 489 490 // We need to copy and escape the string (allow for at least one 491 // escaped character). 492 StringBuilder buffer = new StringBuilder(length + 3); 493 494 // If the lead character is a space or a # it must be escaped. 495 int start = 0; 496 char c = value.charAt(0); 497 if (c == ' ' || c == '#') { 498 buffer.append('\\'); 499 buffer.append(c); 500 if (length == 1) { 501 return buffer.toString(); 502 } 503 start = 1; 504 } 505 506 // Escape remaining characters as necessary. 507 for (int i = start; i < length; i++) { 508 c = value.charAt(i); 509 if (c < ' ') { 510 for (byte b : getBytes(String.valueOf(c))) { 511 buffer.append('\\'); 512 buffer.append(byteToLowerHex(b)); 513 } 514 } else { 515 switch (value.charAt(i)) { 516 case ',': 517 case '+': 518 case '"': 519 case '\\': 520 case '<': 521 case '>': 522 case ';': 523 buffer.append('\\'); 524 buffer.append(c); 525 break; 526 default: 527 buffer.append(c); 528 break; 529 } 530 } 531 } 532 533 // If the last character is a space it must be escaped. 534 if (value.charAt(length - 1) == ' ') { 535 length = buffer.length(); 536 buffer.insert(length - 1, '\\'); 537 } 538 539 return buffer.toString(); 540 } 541 542 543 544 /** 545 * Decodes the provided string as an RDN. 546 * 547 * @param rdnString 548 * The string to decode as an RDN. 549 * @return The decoded RDN. 550 * @throws DirectoryException 551 * If a problem occurs while trying to decode the provided 552 * string as a RDN. 553 */ 554 public static RDN decode(String rdnString) throws DirectoryException 555 { 556 // A null or empty RDN is not acceptable. 557 if (rdnString == null) 558 { 559 LocalizableMessage message = ERR_RDN_DECODE_NULL.get(); 560 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 561 } 562 563 int length = rdnString.length(); 564 if (length == 0) 565 { 566 LocalizableMessage message = ERR_RDN_DECODE_NULL.get(); 567 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 568 } 569 570 571 // Iterate through the RDN string. The first thing to do is to 572 // get rid of any leading spaces. 573 int pos = 0; 574 char c = rdnString.charAt(pos); 575 while (c == ' ') 576 { 577 pos++; 578 if (pos == length) 579 { 580 // This means that the RDN was completely comprised of spaces, 581 // which is not valid. 582 LocalizableMessage message = ERR_RDN_DECODE_NULL.get(); 583 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 584 } 585 else 586 { 587 c = rdnString.charAt(pos); 588 } 589 } 590 591 592 // We know that it's not an empty RDN, so we can do the real processing. 593 // First, parse the attribute name. We can borrow the DN code for this. 594 boolean allowExceptions = DirectoryServer.allowAttributeNameExceptions(); 595 StringBuilder attributeName = new StringBuilder(); 596 pos = DN.parseAttributeName(rdnString, pos, attributeName, allowExceptions); 597 598 599 // Make sure that we're not at the end of the RDN string because 600 // that would be invalid. 601 if (pos >= length) 602 { 603 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 604 ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); 605 } 606 607 608 // Skip over any spaces between the attribute name and its value. 609 c = rdnString.charAt(pos); 610 while (c == ' ') 611 { 612 pos++; 613 if (pos >= length) 614 { 615 // This means that we hit the end of the string before finding a '='. 616 // This is illegal because there is no attribute-value separator. 617 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 618 ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); 619 } 620 else 621 { 622 c = rdnString.charAt(pos); 623 } 624 } 625 626 627 // The next character must be an equal sign. If it is not, then 628 // that's an error. 629 if (c == '=') 630 { 631 pos++; 632 } 633 else 634 { 635 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 636 ERR_RDN_NO_EQUAL.get(rdnString, attributeName, c)); 637 } 638 639 640 // Skip over any spaces between the equal sign and the value. 641 while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) 642 { 643 pos++; 644 } 645 646 647 // If we are at the end of the RDN string, then that must mean 648 // that the attribute value was empty. 649 if (pos >= length) 650 { 651 String name = attributeName.toString(); 652 String lowerName = toLowerCase(name); 653 LocalizableMessage message = ERR_RDN_MISSING_ATTRIBUTE_VALUE.get(rdnString, 654 lowerName); 655 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 656 } 657 658 659 // Parse the value for this RDN component. This can be done using 660 // the DN code. 661 ByteStringBuilder parsedValue = new ByteStringBuilder(0); 662 pos = DN.parseAttributeValue(rdnString, pos, parsedValue); 663 664 665 // Create the new RDN with the provided information. However, 666 // don't return it yet because this could be a multi-valued RDN. 667 String name = attributeName.toString(); 668 String lowerName = toLowerCase(name); 669 AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(lowerName); 670 if (attrType == null) 671 { 672 // This must be an attribute type that we don't know about. 673 // In that case, we'll create a new attribute using the default 674 // syntax. If this is a problem, it will be caught later either 675 // by not finding the target entry or by not allowing the entry 676 // to be added. 677 attrType = DirectoryServer.getAttributeTypeOrDefault(name); 678 } 679 680 RDN rdn = new RDN(attrType, name, parsedValue.toByteString()); 681 682 683 // Skip over any spaces that might be after the attribute value. 684 while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) 685 { 686 pos++; 687 } 688 689 690 // Most likely, this is the end of the RDN. If so, then return it. 691 if (pos >= length) 692 { 693 return rdn; 694 } 695 696 697 // If the next character is a comma or semicolon, then that is not 698 // allowed. It would be legal for a DN but not an RDN. 699 if (c == ',' || c == ';') 700 { 701 LocalizableMessage message = ERR_RDN_UNEXPECTED_COMMA.get(rdnString, pos); 702 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 703 } 704 705 706 // If the next character is anything but a plus sign, then it is illegal. 707 if (c != '+') 708 { 709 LocalizableMessage message = ERR_RDN_ILLEGAL_CHARACTER.get(rdnString, c, pos); 710 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 711 } 712 713 714 // If we have gotten here, then it is a multi-valued RDN. Parse 715 // the remaining attribute/value pairs and add them to the RDN 716 // that we've already created. 717 while (true) 718 { 719 // Skip over the plus sign and any spaces that may follow it 720 // before the next attribute name. 721 pos++; 722 while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) 723 { 724 pos++; 725 } 726 727 728 // Parse the attribute name. 729 attributeName = new StringBuilder(); 730 pos = DN.parseAttributeName(rdnString, pos, attributeName, 731 allowExceptions); 732 733 734 // Make sure we're not at the end of the RDN. 735 if (pos >= length) 736 { 737 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 738 ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); 739 } 740 741 742 // Skip over any spaces between the attribute name and the equal sign. 743 c = rdnString.charAt(pos); 744 while (c == ' ') 745 { 746 pos++; 747 if (pos >= length) 748 { 749 // This means that we hit the end of the string before finding a '='. 750 // This is illegal because there is no attribute-value separator. 751 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 752 ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); 753 } 754 else 755 { 756 c = rdnString.charAt(pos); 757 } 758 } 759 760 761 // The next character must be an equal sign. 762 if (c == '=') 763 { 764 pos++; 765 } 766 else 767 { 768 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 769 ERR_RDN_NO_EQUAL.get(rdnString, attributeName, c)); 770 } 771 772 773 // Skip over any spaces after the equal sign. 774 while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) 775 { 776 pos++; 777 } 778 779 780 // If we are at the end of the RDN string, then that must mean 781 // that the attribute value was empty. This will probably never 782 // happen in a real-world environment, but technically isn't 783 // illegal. If it does happen, then go ahead and return the RDN. 784 if (pos >= length) 785 { 786 name = attributeName.toString(); 787 lowerName = toLowerCase(name); 788 attrType = DirectoryServer.getAttributeTypeOrNull(lowerName); 789 790 if (attrType == null) 791 { 792 // This must be an attribute type that we don't know about. 793 // In that case, we'll create a new attribute using the 794 // default syntax. If this is a problem, it will be caught 795 // later either by not finding the target entry or by not 796 // allowing the entry to be added. 797 attrType = DirectoryServer.getAttributeTypeOrDefault(name); 798 } 799 800 rdn.addValue(attrType, name, ByteString.empty()); 801 return rdn; 802 } 803 804 805 // Parse the value for this RDN component. 806 parsedValue.clear(); 807 pos = DN.parseAttributeValue(rdnString, pos, parsedValue); 808 809 810 // Update the RDN to include the new attribute/value. 811 name = attributeName.toString(); 812 lowerName = toLowerCase(name); 813 attrType = DirectoryServer.getAttributeTypeOrNull(lowerName); 814 if (attrType == null) 815 { 816 // This must be an attribute type that we don't know about. 817 // In that case, we'll create a new attribute using the 818 // default syntax. If this is a problem, it will be caught 819 // later either by not finding the target entry or by not 820 // allowing the entry to be added. 821 attrType = DirectoryServer.getAttributeTypeOrDefault(name); 822 } 823 824 rdn.addValue(attrType, name, parsedValue.toByteString()); 825 826 827 // Skip over any spaces that might be after the attribute value. 828 while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) 829 { 830 pos++; 831 } 832 833 834 // If we're at the end of the string, then return the RDN. 835 if (pos >= length) 836 { 837 return rdn; 838 } 839 840 841 // If the next character is a comma or semicolon, then that is 842 // not allowed. It would be legal for a DN but not an RDN. 843 if (c == ',' || c == ';') 844 { 845 LocalizableMessage message = ERR_RDN_UNEXPECTED_COMMA.get(rdnString, pos); 846 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 847 } 848 849 850 // If the next character is anything but a plus sign, then it is illegal. 851 if (c != '+') 852 { 853 LocalizableMessage message = ERR_RDN_ILLEGAL_CHARACTER.get(rdnString, c, pos); 854 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 855 } 856 } 857 } 858 859 860 861 /** 862 * Creates a duplicate of this RDN that can be modified without 863 * impacting this RDN. 864 * 865 * @return A duplicate of this RDN that can be modified without 866 * impacting this RDN. 867 */ 868 public RDN duplicate() 869 { 870 int numValues = attributeTypes.length; 871 AttributeType[] newTypes = new AttributeType[numValues]; 872 System.arraycopy(attributeTypes, 0, newTypes, 0, numValues); 873 874 String[] newNames = new String[numValues]; 875 System.arraycopy(attributeNames, 0, newNames, 0, numValues); 876 877 ByteString[] newValues = new ByteString[numValues]; 878 System.arraycopy(attributeValues, 0, newValues, 0, numValues); 879 880 return new RDN(newTypes, newNames, newValues); 881 } 882 883 884 885 /** 886 * Indicates whether the provided object is equal to this RDN. It 887 * will only be considered equal if it is an RDN object that 888 * contains the same number of elements in the same order with the 889 * same types and normalized values. 890 * 891 * @param o The object for which to make the determination. 892 * 893 * @return <CODE>true</CODE> if it is determined that the provided 894 * object is equal to this RDN, or <CODE>false</CODE> if 895 * not. 896 */ 897 @Override 898 public boolean equals(Object o) 899 { 900 if (this == o) 901 { 902 return true; 903 } 904 if (o instanceof RDN) 905 { 906 RDN otherRDN = (RDN) o; 907 return toNormalizedByteString().equals(otherRDN.toNormalizedByteString()); 908 } 909 return false; 910 } 911 912 /** 913 * Retrieves the hash code for this RDN. It will be calculated as 914 * the sum of the hash codes of the types and values. 915 * 916 * @return The hash code for this RDN. 917 */ 918 @Override 919 public int hashCode() 920 { 921 return toNormalizedByteString().hashCode(); 922 } 923 924 /** Returns normalized value for attribute at provided position. */ 925 private ByteString getEqualityNormalizedValue(int position) 926 { 927 final MatchingRule matchingRule = attributeTypes[position].getEqualityMatchingRule(); 928 ByteString attributeValue = attributeValues[position]; 929 if (matchingRule != null) 930 { 931 try 932 { 933 attributeValue = matchingRule.normalizeAttributeValue(attributeValue); 934 } 935 catch (final DecodeException de) 936 { 937 // Unable to normalize, use default 938 attributeValue = attributeValues[position]; 939 } 940 } 941 return attributeValue; 942 } 943 944 945 946 /** 947 * Retrieves a string representation of this RDN. 948 * 949 * @return A string representation of this RDN. 950 */ 951 @Override 952 public String toString() 953 { 954 StringBuilder buffer = new StringBuilder(); 955 buffer.append(attributeNames[0]); 956 buffer.append("="); 957 buffer.append(getDNValue(attributeValues[0])); 958 for (int i = 1; i < attributeTypes.length; i++) { 959 buffer.append("+"); 960 buffer.append(attributeNames[i]); 961 buffer.append("="); 962 buffer.append(getDNValue(attributeValues[i])); 963 } 964 return buffer.toString(); 965 } 966 967 /** 968 * Appends a string representation of this RDN to the provided 969 * buffer. 970 * 971 * @param buffer The buffer to which the string representation 972 * should be appended. 973 */ 974 public void toString(StringBuilder buffer) 975 { 976 buffer.append(this); 977 } 978 979 /** 980 * Retrieves a normalized string representation of this RDN. 981 * <p> 982 * 983 * This representation is safe to use in an URL or in a file name. 984 * However, it is not a valid RDN and can't be reverted to a valid RDN. 985 * 986 * @return The normalized string representation of this RDN. 987 */ 988 String toNormalizedUrlSafeString() 989 { 990 final StringBuilder buffer = new StringBuilder(); 991 if (attributeNames.length == 1) 992 { 993 normalizeAVAToUrlSafeString(0, buffer); 994 } 995 else 996 { 997 // Normalization sorts RDNs alphabetically 998 SortedSet<String> avaStrings = new TreeSet<>(); 999 for (int i=0; i < attributeNames.length; i++) 1000 { 1001 StringBuilder builder = new StringBuilder(); 1002 normalizeAVAToUrlSafeString(i, builder); 1003 avaStrings.add(builder.toString()); 1004 } 1005 1006 Iterator<String> iterator = avaStrings.iterator(); 1007 buffer.append(iterator.next()); 1008 while (iterator.hasNext()) 1009 { 1010 buffer.append('+'); 1011 buffer.append(iterator.next()); 1012 } 1013 } 1014 return buffer.toString(); 1015 } 1016 1017 private ByteString toNormalizedByteString() 1018 { 1019 if (normalizedRDN == null) 1020 { 1021 computeNormalizedByteString(new ByteStringBuilder()); 1022 } 1023 return normalizedRDN; 1024 } 1025 1026 /** 1027 * Adds a normalized byte string representation of this RDN to the provided builder. 1028 * <p> 1029 * The representation is suitable for equality and comparisons, and for providing a 1030 * natural hierarchical ordering. 1031 * However, it is not a valid RDN and can't be reverted to a valid RDN. 1032 * 1033 * @param builder 1034 * Builder to add this representation to. 1035 * @return the builder 1036 */ 1037 public ByteStringBuilder toNormalizedByteString(ByteStringBuilder builder) 1038 { 1039 if (normalizedRDN != null) 1040 { 1041 return builder.appendBytes(normalizedRDN); 1042 } 1043 return computeNormalizedByteString(builder); 1044 } 1045 1046 private ByteStringBuilder computeNormalizedByteString(ByteStringBuilder builder) 1047 { 1048 final int startPos = builder.length(); 1049 1050 if (attributeNames.length == 1) 1051 { 1052 normalizeAVAToByteString(0, builder); 1053 } 1054 else 1055 { 1056 // Normalization sorts RDNs 1057 SortedSet<ByteString> avaStrings = new TreeSet<>(); 1058 for (int i = 0; i < attributeNames.length; i++) 1059 { 1060 ByteStringBuilder b = new ByteStringBuilder(); 1061 normalizeAVAToByteString(i, b); 1062 avaStrings.add(b.toByteString()); 1063 } 1064 1065 Iterator<ByteString> iterator = avaStrings.iterator(); 1066 builder.appendBytes(iterator.next()); 1067 while (iterator.hasNext()) 1068 { 1069 builder.appendByte(DN.NORMALIZED_AVA_SEPARATOR); 1070 builder.appendBytes(iterator.next()); 1071 } 1072 } 1073 1074 if (normalizedRDN == null) 1075 { 1076 normalizedRDN = builder.subSequence(startPos, builder.length()).toByteString(); 1077 } 1078 1079 return builder; 1080 } 1081 1082 /** 1083 * Adds a normalized byte string representation of the AVA corresponding 1084 * to provided position in this RDN to the provided builder. 1085 * 1086 * @param position 1087 * Position of AVA in this RDN. 1088 * @param builder 1089 * Builder to add the representation to. 1090 * @return the builder 1091 */ 1092 private ByteStringBuilder normalizeAVAToByteString(int position, final ByteStringBuilder builder) 1093 { 1094 builder.appendUtf8(attributeTypes[position].getNormalizedPrimaryNameOrOID()); 1095 builder.appendUtf8("="); 1096 final ByteString value = getEqualityNormalizedValue(position); 1097 if (value.length() > 0) 1098 { 1099 builder.appendBytes(escapeBytes(value)); 1100 } 1101 return builder; 1102 } 1103 1104 /** 1105 * Return a new byte string with bytes 0x00, 0x01 and 0x02 escaped. 1106 * <p> 1107 * These bytes are reserved to represent respectively the RDN separator, the 1108 * AVA separator and the escape byte in a normalized byte string. 1109 */ 1110 private ByteString escapeBytes(final ByteString value) 1111 { 1112 if (!needEscaping(value)) 1113 { 1114 return value; 1115 } 1116 1117 final ByteStringBuilder builder = new ByteStringBuilder(); 1118 for (int i = 0; i < value.length(); i++) 1119 { 1120 final byte b = value.byteAt(i); 1121 if (isByteToEscape(b)) 1122 { 1123 builder.appendByte(DN.NORMALIZED_ESC_BYTE); 1124 } 1125 builder.appendByte(b); 1126 } 1127 return builder.toByteString(); 1128 } 1129 1130 private boolean needEscaping(final ByteString value) 1131 { 1132 for (int i = 0; i < value.length(); i++) 1133 { 1134 if (isByteToEscape(value.byteAt(i))) 1135 { 1136 return true; 1137 } 1138 } 1139 return false; 1140 } 1141 1142 private boolean isByteToEscape(final byte b) 1143 { 1144 return b == DN.NORMALIZED_RDN_SEPARATOR || b == DN.NORMALIZED_AVA_SEPARATOR || b == DN.NORMALIZED_ESC_BYTE; 1145 } 1146 1147 1148 /** 1149 * Appends a normalized string representation of this RDN to the 1150 * provided buffer. 1151 * 1152 * @param position The position of the attribute type and value to 1153 * retrieve. 1154 * @param builder The buffer to which to append the information. 1155 * @return the builder 1156 */ 1157 private StringBuilder normalizeAVAToUrlSafeString(int position, StringBuilder builder) 1158 { 1159 builder.append(attributeTypes[position].getNormalizedPrimaryNameOrOID()); 1160 builder.append('='); 1161 1162 ByteString value = getEqualityNormalizedValue(position); 1163 if (value.length() == 0) 1164 { 1165 return builder; 1166 } 1167 final boolean hasAttributeName = attributeTypes[position].getPrimaryName() != null; 1168 final boolean isHumanReadable = attributeTypes[position].getSyntax().isHumanReadable(); 1169 if (!hasAttributeName || !isHumanReadable) 1170 { 1171 builder.append(value.toPercentHexString()); 1172 } 1173 else 1174 { 1175 // try to decode value as UTF-8 string 1176 final CharBuffer buffer = CharBuffer.allocate(value.length()); 1177 final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder() 1178 .onMalformedInput(CodingErrorAction.REPORT) 1179 .onUnmappableCharacter(CodingErrorAction.REPORT); 1180 if (value.copyTo(buffer, decoder)) 1181 { 1182 buffer.flip(); 1183 try 1184 { 1185 // URL encoding encodes space char as '+' instead of using hex code 1186 final String val = URLEncoder.encode(buffer.toString(), "UTF-8").replaceAll("\\+", "%20"); 1187 builder.append(val); 1188 } 1189 catch (UnsupportedEncodingException e) 1190 { 1191 // should never happen 1192 builder.append(value.toPercentHexString()); 1193 } 1194 } 1195 else 1196 { 1197 builder.append(value.toPercentHexString()); 1198 } 1199 } 1200 return builder; 1201 } 1202 1203 /** 1204 * Compares this RDN with the provided RDN based on natural ordering defined 1205 * by the toNormalizedByteString() method. 1206 * 1207 * @param rdn The RDN against which to compare this RDN. 1208 * 1209 * @return A negative integer if this RDN should come before the 1210 * provided RDN, a positive integer if this RDN should come 1211 * after the provided RDN, or zero if there is no 1212 * difference with regard to ordering. 1213 */ 1214 @Override 1215 public int compareTo(RDN rdn) 1216 { 1217 return toNormalizedByteString().compareTo(rdn.toNormalizedByteString()); 1218 } 1219 1220}