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 org.opends.server.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 start = 1; 501 } 502 503 // Escape remaining characters as necessary. 504 for (int i = start; i < length; i++) { 505 c = value.charAt(i); 506 if (c < ' ') { 507 for (byte b : getBytes(String.valueOf(c))) { 508 buffer.append('\\'); 509 buffer.append(byteToLowerHex(b)); 510 } 511 } else { 512 switch (value.charAt(i)) { 513 case ',': 514 case '+': 515 case '"': 516 case '\\': 517 case '<': 518 case '>': 519 case ';': 520 buffer.append('\\'); 521 buffer.append(c); 522 break; 523 default: 524 buffer.append(c); 525 break; 526 } 527 } 528 } 529 530 // If the last character is a space it must be escaped. 531 if (value.charAt(length - 1) == ' ') { 532 length = buffer.length(); 533 buffer.insert(length - 1, '\\'); 534 } 535 536 return buffer.toString(); 537 } 538 539 540 541 /** 542 * Decodes the provided string as an RDN. 543 * 544 * @param rdnString 545 * The string to decode as an RDN. 546 * @return The decoded RDN. 547 * @throws DirectoryException 548 * If a problem occurs while trying to decode the provided 549 * string as a RDN. 550 */ 551 public static RDN decode(String rdnString) throws DirectoryException 552 { 553 // A null or empty RDN is not acceptable. 554 if (rdnString == null) 555 { 556 LocalizableMessage message = ERR_RDN_DECODE_NULL.get(); 557 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 558 } 559 560 int length = rdnString.length(); 561 if (length == 0) 562 { 563 LocalizableMessage message = ERR_RDN_DECODE_NULL.get(); 564 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 565 } 566 567 568 // Iterate through the RDN string. The first thing to do is to 569 // get rid of any leading spaces. 570 int pos = 0; 571 char c = rdnString.charAt(pos); 572 while (c == ' ') 573 { 574 pos++; 575 if (pos == length) 576 { 577 // This means that the RDN was completely comprised of spaces, 578 // which is not valid. 579 LocalizableMessage message = ERR_RDN_DECODE_NULL.get(); 580 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 581 } 582 else 583 { 584 c = rdnString.charAt(pos); 585 } 586 } 587 588 589 // We know that it's not an empty RDN, so we can do the real processing. 590 // First, parse the attribute name. We can borrow the DN code for this. 591 boolean allowExceptions = DirectoryServer.allowAttributeNameExceptions(); 592 StringBuilder attributeName = new StringBuilder(); 593 pos = DN.parseAttributeName(rdnString, pos, attributeName, allowExceptions); 594 595 596 // Make sure that we're not at the end of the RDN string because 597 // that would be invalid. 598 if (pos >= length) 599 { 600 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 601 ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); 602 } 603 604 605 // Skip over any spaces between the attribute name and its value. 606 c = rdnString.charAt(pos); 607 while (c == ' ') 608 { 609 pos++; 610 if (pos >= length) 611 { 612 // This means that we hit the end of the string before finding a '='. 613 // This is illegal because there is no attribute-value separator. 614 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 615 ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); 616 } 617 else 618 { 619 c = rdnString.charAt(pos); 620 } 621 } 622 623 624 // The next character must be an equal sign. If it is not, then 625 // that's an error. 626 if (c == '=') 627 { 628 pos++; 629 } 630 else 631 { 632 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 633 ERR_RDN_NO_EQUAL.get(rdnString, attributeName, c)); 634 } 635 636 637 // Skip over any spaces between the equal sign and the value. 638 while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) 639 { 640 pos++; 641 } 642 643 644 // If we are at the end of the RDN string, then that must mean 645 // that the attribute value was empty. 646 if (pos >= length) 647 { 648 String name = attributeName.toString(); 649 String lowerName = toLowerCase(name); 650 LocalizableMessage message = ERR_RDN_MISSING_ATTRIBUTE_VALUE.get(rdnString, 651 lowerName); 652 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 653 } 654 655 656 // Parse the value for this RDN component. This can be done using 657 // the DN code. 658 ByteStringBuilder parsedValue = new ByteStringBuilder(0); 659 pos = DN.parseAttributeValue(rdnString, pos, parsedValue); 660 661 662 // Create the new RDN with the provided information. However, 663 // don't return it yet because this could be a multi-valued RDN. 664 String name = attributeName.toString(); 665 String lowerName = toLowerCase(name); 666 AttributeType attrType = DirectoryServer.getAttributeType(lowerName); 667 if (attrType == null) 668 { 669 // This must be an attribute type that we don't know about. 670 // In that case, we'll create a new attribute using the default 671 // syntax. If this is a problem, it will be caught later either 672 // by not finding the target entry or by not allowing the entry 673 // to be added. 674 attrType = DirectoryServer.getDefaultAttributeType(name); 675 } 676 677 RDN rdn = new RDN(attrType, name, parsedValue.toByteString()); 678 679 680 // Skip over any spaces that might be after the attribute value. 681 while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) 682 { 683 pos++; 684 } 685 686 687 // Most likely, this is the end of the RDN. If so, then return it. 688 if (pos >= length) 689 { 690 return rdn; 691 } 692 693 694 // If the next character is a comma or semicolon, then that is not 695 // allowed. It would be legal for a DN but not an RDN. 696 if (c == ',' || c == ';') 697 { 698 LocalizableMessage message = ERR_RDN_UNEXPECTED_COMMA.get(rdnString, pos); 699 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 700 } 701 702 703 // If the next character is anything but a plus sign, then it is illegal. 704 if (c != '+') 705 { 706 LocalizableMessage message = ERR_RDN_ILLEGAL_CHARACTER.get(rdnString, c, pos); 707 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 708 } 709 710 711 // If we have gotten here, then it is a multi-valued RDN. Parse 712 // the remaining attribute/value pairs and add them to the RDN 713 // that we've already created. 714 while (true) 715 { 716 // Skip over the plus sign and any spaces that may follow it 717 // before the next attribute name. 718 pos++; 719 while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) 720 { 721 pos++; 722 } 723 724 725 // Parse the attribute name. 726 attributeName = new StringBuilder(); 727 pos = DN.parseAttributeName(rdnString, pos, attributeName, 728 allowExceptions); 729 730 731 // Make sure we're not at the end of the RDN. 732 if (pos >= length) 733 { 734 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 735 ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); 736 } 737 738 739 // Skip over any spaces between the attribute name and the equal sign. 740 c = rdnString.charAt(pos); 741 while (c == ' ') 742 { 743 pos++; 744 if (pos >= length) 745 { 746 // This means that we hit the end of the string before finding a '='. 747 // This is illegal because there is no attribute-value separator. 748 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 749 ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); 750 } 751 else 752 { 753 c = rdnString.charAt(pos); 754 } 755 } 756 757 758 // The next character must be an equal sign. 759 if (c == '=') 760 { 761 pos++; 762 } 763 else 764 { 765 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 766 ERR_RDN_NO_EQUAL.get(rdnString, attributeName, c)); 767 } 768 769 770 // Skip over any spaces after the equal sign. 771 while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) 772 { 773 pos++; 774 } 775 776 777 // If we are at the end of the RDN string, then that must mean 778 // that the attribute value was empty. This will probably never 779 // happen in a real-world environment, but technically isn't 780 // illegal. If it does happen, then go ahead and return the RDN. 781 if (pos >= length) 782 { 783 name = attributeName.toString(); 784 lowerName = toLowerCase(name); 785 attrType = DirectoryServer.getAttributeType(lowerName); 786 787 if (attrType == null) 788 { 789 // This must be an attribute type that we don't know about. 790 // In that case, we'll create a new attribute using the 791 // default syntax. If this is a problem, it will be caught 792 // later either by not finding the target entry or by not 793 // allowing the entry to be added. 794 attrType = DirectoryServer.getDefaultAttributeType(name); 795 } 796 797 rdn.addValue(attrType, name, ByteString.empty()); 798 return rdn; 799 } 800 801 802 // Parse the value for this RDN component. 803 parsedValue.clear(); 804 pos = DN.parseAttributeValue(rdnString, pos, parsedValue); 805 806 807 // Update the RDN to include the new attribute/value. 808 name = attributeName.toString(); 809 lowerName = toLowerCase(name); 810 attrType = DirectoryServer.getAttributeType(lowerName); 811 if (attrType == null) 812 { 813 // This must be an attribute type that we don't know about. 814 // In that case, we'll create a new attribute using the 815 // default syntax. If this is a problem, it will be caught 816 // later either by not finding the target entry or by not 817 // allowing the entry to be added. 818 attrType = DirectoryServer.getDefaultAttributeType(name); 819 } 820 821 rdn.addValue(attrType, name, parsedValue.toByteString()); 822 823 824 // Skip over any spaces that might be after the attribute value. 825 while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) 826 { 827 pos++; 828 } 829 830 831 // If we're at the end of the string, then return the RDN. 832 if (pos >= length) 833 { 834 return rdn; 835 } 836 837 838 // If the next character is a comma or semicolon, then that is 839 // not allowed. It would be legal for a DN but not an RDN. 840 if (c == ',' || c == ';') 841 { 842 LocalizableMessage message = ERR_RDN_UNEXPECTED_COMMA.get(rdnString, pos); 843 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 844 } 845 846 847 // If the next character is anything but a plus sign, then it is illegal. 848 if (c != '+') 849 { 850 LocalizableMessage message = ERR_RDN_ILLEGAL_CHARACTER.get(rdnString, c, pos); 851 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 852 } 853 } 854 } 855 856 857 858 /** 859 * Creates a duplicate of this RDN that can be modified without 860 * impacting this RDN. 861 * 862 * @return A duplicate of this RDN that can be modified without 863 * impacting this RDN. 864 */ 865 public RDN duplicate() 866 { 867 int numValues = attributeTypes.length; 868 AttributeType[] newTypes = new AttributeType[numValues]; 869 System.arraycopy(attributeTypes, 0, newTypes, 0, numValues); 870 871 String[] newNames = new String[numValues]; 872 System.arraycopy(attributeNames, 0, newNames, 0, numValues); 873 874 ByteString[] newValues = new ByteString[numValues]; 875 System.arraycopy(attributeValues, 0, newValues, 0, numValues); 876 877 return new RDN(newTypes, newNames, newValues); 878 } 879 880 881 882 /** 883 * Indicates whether the provided object is equal to this RDN. It 884 * will only be considered equal if it is an RDN object that 885 * contains the same number of elements in the same order with the 886 * same types and normalized values. 887 * 888 * @param o The object for which to make the determination. 889 * 890 * @return <CODE>true</CODE> if it is determined that the provided 891 * object is equal to this RDN, or <CODE>false</CODE> if 892 * not. 893 */ 894 @Override 895 public boolean equals(Object o) 896 { 897 if (this == o) 898 { 899 return true; 900 } 901 if (o instanceof RDN) 902 { 903 RDN otherRDN = (RDN) o; 904 return toNormalizedByteString().equals(otherRDN.toNormalizedByteString()); 905 } 906 return false; 907 } 908 909 /** 910 * Retrieves the hash code for this RDN. It will be calculated as 911 * the sum of the hash codes of the types and values. 912 * 913 * @return The hash code for this RDN. 914 */ 915 @Override 916 public int hashCode() 917 { 918 return toNormalizedByteString().hashCode(); 919 } 920 921 /** Returns normalized value for attribute at provided position. */ 922 private ByteString getEqualityNormalizedValue(int position) 923 { 924 final MatchingRule matchingRule = attributeTypes[position].getEqualityMatchingRule(); 925 ByteString attributeValue = attributeValues[position]; 926 if (matchingRule != null) 927 { 928 try 929 { 930 attributeValue = matchingRule.normalizeAttributeValue(attributeValue); 931 } 932 catch (final DecodeException de) 933 { 934 // Unable to normalize, use default 935 attributeValue = attributeValues[position]; 936 } 937 } 938 return attributeValue; 939 } 940 941 942 943 /** 944 * Retrieves a string representation of this RDN. 945 * 946 * @return A string representation of this RDN. 947 */ 948 @Override 949 public String toString() 950 { 951 StringBuilder buffer = new StringBuilder(); 952 buffer.append(attributeNames[0]); 953 buffer.append("="); 954 buffer.append(getDNValue(attributeValues[0])); 955 for (int i = 1; i < attributeTypes.length; i++) { 956 buffer.append("+"); 957 buffer.append(attributeNames[i]); 958 buffer.append("="); 959 buffer.append(getDNValue(attributeValues[i])); 960 } 961 return buffer.toString(); 962 } 963 964 /** 965 * Appends a string representation of this RDN to the provided 966 * buffer. 967 * 968 * @param buffer The buffer to which the string representation 969 * should be appended. 970 */ 971 public void toString(StringBuilder buffer) 972 { 973 buffer.append(this); 974 } 975 976 /** 977 * Retrieves a normalized string representation of this RDN. 978 * <p> 979 * 980 * This representation is safe to use in an URL or in a file name. 981 * However, it is not a valid RDN and can't be reverted to a valid RDN. 982 * 983 * @return The normalized string representation of this RDN. 984 */ 985 String toNormalizedUrlSafeString() 986 { 987 final StringBuilder buffer = new StringBuilder(); 988 if (attributeNames.length == 1) 989 { 990 normalizeAVAToUrlSafeString(0, buffer); 991 } 992 else 993 { 994 // Normalization sorts RDNs alphabetically 995 SortedSet<String> avaStrings = new TreeSet<>(); 996 for (int i=0; i < attributeNames.length; i++) 997 { 998 StringBuilder builder = new StringBuilder(); 999 normalizeAVAToUrlSafeString(i, builder); 1000 avaStrings.add(builder.toString()); 1001 } 1002 1003 Iterator<String> iterator = avaStrings.iterator(); 1004 buffer.append(iterator.next()); 1005 while (iterator.hasNext()) 1006 { 1007 buffer.append('+'); 1008 buffer.append(iterator.next()); 1009 } 1010 } 1011 return buffer.toString(); 1012 } 1013 1014 private ByteString toNormalizedByteString() 1015 { 1016 if (normalizedRDN == null) 1017 { 1018 computeNormalizedByteString(new ByteStringBuilder()); 1019 } 1020 return normalizedRDN; 1021 } 1022 1023 /** 1024 * Adds a normalized byte string representation of this RDN to the provided builder. 1025 * <p> 1026 * The representation is suitable for equality and comparisons, and for providing a 1027 * natural hierarchical ordering. 1028 * However, it is not a valid RDN and can't be reverted to a valid RDN. 1029 * 1030 * @param builder 1031 * Builder to add this representation to. 1032 * @return the builder 1033 */ 1034 public ByteStringBuilder toNormalizedByteString(ByteStringBuilder builder) 1035 { 1036 if (normalizedRDN != null) 1037 { 1038 return builder.append(normalizedRDN); 1039 } 1040 return computeNormalizedByteString(builder); 1041 } 1042 1043 private ByteStringBuilder computeNormalizedByteString(ByteStringBuilder builder) 1044 { 1045 final int startPos = builder.length(); 1046 1047 if (attributeNames.length == 1) 1048 { 1049 normalizeAVAToByteString(0, builder); 1050 } 1051 else 1052 { 1053 // Normalization sorts RDNs 1054 SortedSet<ByteString> avaStrings = new TreeSet<>(); 1055 for (int i = 0; i < attributeNames.length; i++) 1056 { 1057 ByteStringBuilder b = new ByteStringBuilder(); 1058 normalizeAVAToByteString(i, b); 1059 avaStrings.add(b.toByteString()); 1060 } 1061 1062 Iterator<ByteString> iterator = avaStrings.iterator(); 1063 builder.append(iterator.next()); 1064 while (iterator.hasNext()) 1065 { 1066 builder.append(DN.NORMALIZED_AVA_SEPARATOR); 1067 builder.append(iterator.next()); 1068 } 1069 } 1070 1071 if (normalizedRDN == null) 1072 { 1073 normalizedRDN = builder.subSequence(startPos, builder.length()).toByteString(); 1074 } 1075 1076 return builder; 1077 } 1078 1079 /** 1080 * Adds a normalized byte string representation of the AVA corresponding 1081 * to provided position in this RDN to the provided builder. 1082 * 1083 * @param position 1084 * Position of AVA in this RDN. 1085 * @param builder 1086 * Builder to add the representation to. 1087 * @return the builder 1088 */ 1089 private ByteStringBuilder normalizeAVAToByteString(int position, final ByteStringBuilder builder) 1090 { 1091 builder.append(attributeTypes[position].getNormalizedPrimaryNameOrOID()); 1092 builder.append("="); 1093 final ByteString value = getEqualityNormalizedValue(position); 1094 if (value.length() > 0) 1095 { 1096 builder.append(escapeBytes(value)); 1097 } 1098 return builder; 1099 } 1100 1101 /** 1102 * Return a new byte string with bytes 0x00, 0x01 and 0x02 escaped. 1103 * <p> 1104 * These bytes are reserved to represent respectively the RDN separator, the 1105 * AVA separator and the escape byte in a normalized byte string. 1106 */ 1107 private ByteString escapeBytes(final ByteString value) 1108 { 1109 if (!needEscaping(value)) 1110 { 1111 return value; 1112 } 1113 1114 final ByteStringBuilder builder = new ByteStringBuilder(); 1115 for (int i = 0; i < value.length(); i++) 1116 { 1117 final byte b = value.byteAt(i); 1118 if (isByteToEscape(b)) 1119 { 1120 builder.append(DN.NORMALIZED_ESC_BYTE); 1121 } 1122 builder.append(b); 1123 } 1124 return builder.toByteString(); 1125 } 1126 1127 private boolean needEscaping(final ByteString value) 1128 { 1129 for (int i = 0; i < value.length(); i++) 1130 { 1131 if (isByteToEscape(value.byteAt(i))) 1132 { 1133 return true; 1134 } 1135 } 1136 return false; 1137 } 1138 1139 private boolean isByteToEscape(final byte b) 1140 { 1141 return b == DN.NORMALIZED_RDN_SEPARATOR || b == DN.NORMALIZED_AVA_SEPARATOR || b == DN.NORMALIZED_ESC_BYTE; 1142 } 1143 1144 1145 /** 1146 * Appends a normalized string representation of this RDN to the 1147 * provided buffer. 1148 * 1149 * @param position The position of the attribute type and value to 1150 * retrieve. 1151 * @param builder The buffer to which to append the information. 1152 * @return the builder 1153 */ 1154 private StringBuilder normalizeAVAToUrlSafeString(int position, StringBuilder builder) 1155 { 1156 builder.append(attributeTypes[position].getNormalizedPrimaryNameOrOID()); 1157 builder.append('='); 1158 1159 ByteString value = getEqualityNormalizedValue(position); 1160 if (value.length() == 0) 1161 { 1162 return builder; 1163 } 1164 final boolean hasAttributeName = attributeTypes[position].getPrimaryName() != null; 1165 final boolean isHumanReadable = attributeTypes[position].getSyntax().isHumanReadable(); 1166 if (!hasAttributeName || !isHumanReadable) 1167 { 1168 builder.append(value.toPercentHexString()); 1169 } 1170 else 1171 { 1172 // try to decode value as UTF-8 string 1173 final CharBuffer buffer = CharBuffer.allocate(value.length()); 1174 final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder() 1175 .onMalformedInput(CodingErrorAction.REPORT) 1176 .onUnmappableCharacter(CodingErrorAction.REPORT); 1177 if (value.copyTo(buffer, decoder)) 1178 { 1179 try 1180 { 1181 // URL encoding encodes space char as '+' instead of using hex code 1182 final String val = URLEncoder.encode(buffer.toString(), "UTF-8").replaceAll("\\+", "%20"); 1183 builder.append(val); 1184 } 1185 catch (UnsupportedEncodingException e) 1186 { 1187 // should never happen 1188 builder.append(value.toPercentHexString()); 1189 } 1190 } 1191 else 1192 { 1193 builder.append(value.toPercentHexString()); 1194 } 1195 } 1196 return builder; 1197 } 1198 1199 /** 1200 * Compares this RDN with the provided RDN based on natural ordering defined 1201 * by the toNormalizedByteString() method. 1202 * 1203 * @param rdn The RDN against which to compare this RDN. 1204 * 1205 * @return A negative integer if this RDN should come before the 1206 * provided RDN, a positive integer if this RDN should come 1207 * after the provided RDN, or zero if there is no 1208 * difference with regard to ordering. 1209 */ 1210 @Override 1211 public int compareTo(RDN rdn) 1212 { 1213 return toNormalizedByteString().compareTo(rdn.toNormalizedByteString()); 1214 } 1215 1216}