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 2009-2010 Sun Microsystems, Inc. 015 * Portions copyright 2011-2016 ForgeRock AS. 016 * Portions Copyright 2022 Wren Security. 017 */ 018package org.forgerock.opendj.ldap; 019 020import java.util.Iterator; 021import java.util.LinkedHashMap; 022import java.util.LinkedList; 023import java.util.Map; 024import java.util.Map.Entry; 025import java.util.NoSuchElementException; 026import java.util.UUID; 027 028import org.forgerock.i18n.LocalizedIllegalArgumentException; 029import org.forgerock.opendj.ldap.schema.CoreSchema; 030import org.forgerock.opendj.ldap.schema.Schema; 031import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException; 032import org.forgerock.util.Pair; 033import org.forgerock.util.Reject; 034 035import com.forgerock.opendj.util.SubstringReader; 036 037import static com.forgerock.opendj.ldap.CoreMessages.*; 038 039/** 040 * A distinguished name (DN) as defined in RFC 4512 section 2.3 is the 041 * concatenation of its relative distinguished name (RDN) and its immediate 042 * superior's DN. A DN unambiguously refers to an entry in the Directory. 043 * <p> 044 * The following are examples of string representations of DNs: 045 * 046 * <pre> 047 * UID=nobody@example.com,DC=example,DC=com CN=John 048 * Smith,OU=Sales,O=ACME Limited,L=Moab,ST=Utah,C=US 049 * </pre> 050 * 051 * @see <a href="http://tools.ietf.org/html/rfc4512#section-2.3">RFC 4512 - 052 * Lightweight Directory Access Protocol (LDAP): Directory Information 053 * Models </a> 054 */ 055public final class DN implements Iterable<RDN>, Comparable<DN> { 056 static final byte NORMALIZED_RDN_SEPARATOR = 0x00; 057 static final byte NORMALIZED_AVA_SEPARATOR = 0x01; 058 static final byte NORMALIZED_ESC_BYTE = 0x02; 059 060 static final char RDN_CHAR_SEPARATOR = ','; 061 static final char AVA_CHAR_SEPARATOR = '+'; 062 063 private static final DN ROOT_DN = new DN(CoreSchema.getInstance(), null, null); 064 065 /** 066 * This is the size of the per-thread per-schema DN cache. We should 067 * be conservative here in case there are many threads. We will only 068 * cache parent DNs, so there's no need for it to be big. 069 */ 070 private static final int DN_CACHE_SIZE = 32; 071 072 private static final ThreadLocal<Map<String, DN>> CACHE = new ThreadLocal<Map<String, DN>>() { 073 @SuppressWarnings("serial") 074 @Override 075 protected Map<String, DN> initialValue() { 076 return new LinkedHashMap<String, DN>(DN_CACHE_SIZE, 0.75f, true) { 077 @Override 078 protected boolean removeEldestEntry(Entry<String, DN> eldest) { 079 return size() > DN_CACHE_SIZE; 080 } 081 }; 082 } 083 }; 084 085 /** 086 * Returns the LDAP string representation of the provided DN attribute value 087 * in a form suitable for substitution directly into a DN string. This 088 * method may be useful in cases where a DN is to be constructed from a DN 089 * template using {@code String#format(String, Object...)}. The following 090 * example illustrates two approaches to constructing a DN: 091 * 092 * <pre> 093 * // This may contain user input. 094 * String attributeValue = ...; 095 * 096 * // Using the equality filter constructor: 097 * DN dn = DN.valueOf("ou=people,dc=example,dc=com").child("uid", attributeValue); 098 * 099 * // Using a String template: 100 * String dnTemplate = "uid=%s,ou=people,dc=example,dc=com"; 101 * String dnString = String.format(dnTemplate, 102 * DN.escapeAttributeValue(attributeValue)); 103 * DN dn = DN.valueOf(dnString); 104 * </pre> 105 * 106 * <b>Note:</b> attribute values do not and should not be escaped before 107 * passing them to constructors like {@link #child(String, Object)}. 108 * Escaping is only required when creating DN strings. 109 * 110 * @param attributeValue 111 * The attribute value. 112 * @return The LDAP string representation of the provided filter assertion 113 * value in a form suitable for substitution directly into a filter 114 * string. 115 */ 116 public static String escapeAttributeValue(final Object attributeValue) { 117 Reject.ifNull(attributeValue); 118 final String s = String.valueOf(attributeValue); 119 final StringBuilder builder = new StringBuilder(s.length()); 120 AVA.escapeAttributeValue(s, builder); 121 return builder.toString(); 122 } 123 124 /** 125 * Creates a new DN using the provided DN template and unescaped attribute 126 * values using the default schema. This method first escapes each of the 127 * attribute values and then substitutes them into the template using 128 * {@link String#format(String, Object...)}. Finally, the formatted string 129 * is parsed as an LDAP DN using {@link #valueOf(String)}. 130 * <p> 131 * This method may be useful in cases where the structure of a DN is not 132 * known at compile time, for example, it may be obtained from a 133 * configuration file. Example usage: 134 * 135 * <pre> 136 * String template = "uid=%s,ou=people,dc=example,dc=com"; 137 * DN dn = DN.format(template, "bjensen"); 138 * </pre> 139 * 140 * @param template 141 * The DN template. 142 * @param attributeValues 143 * The attribute values to be substituted into the template. 144 * @return The formatted template parsed as a {@code DN}. 145 * @throws LocalizedIllegalArgumentException 146 * If the formatted template is not a valid LDAP string 147 * representation of a DN. 148 * @see #escapeAttributeValue(Object) 149 */ 150 public static DN format(final String template, final Object... attributeValues) { 151 return format(template, Schema.getDefaultSchema(), attributeValues); 152 } 153 154 /** 155 * Creates a new DN using the provided DN template and unescaped attribute 156 * values using the provided schema. This method first escapes each of the 157 * attribute values and then substitutes them into the template using 158 * {@link String#format(String, Object...)}. Finally, the formatted string 159 * is parsed as an LDAP DN using {@link #valueOf(String)}. 160 * <p> 161 * This method may be useful in cases where the structure of a DN is not 162 * known at compile time, for example, it may be obtained from a 163 * configuration file. Example usage: 164 * 165 * <pre> 166 * String template = "uid=%s,ou=people,dc=example,dc=com"; 167 * DN dn = DN.format(template, "bjensen"); 168 * </pre> 169 * 170 * @param template 171 * The DN template. 172 * @param schema 173 * The schema to use when parsing the DN. 174 * @param attributeValues 175 * The attribute values to be substituted into the template. 176 * @return The formatted template parsed as a {@code DN}. 177 * @throws LocalizedIllegalArgumentException 178 * If the formatted template is not a valid LDAP string 179 * representation of a DN. 180 * @see #escapeAttributeValue(Object) 181 */ 182 public static DN format(final String template, final Schema schema, 183 final Object... attributeValues) { 184 final String[] attributeValueStrings = new String[attributeValues.length]; 185 for (int i = 0; i < attributeValues.length; i++) { 186 attributeValueStrings[i] = escapeAttributeValue(attributeValues[i]); 187 } 188 final String dnString = String.format(template, (Object[]) attributeValueStrings); 189 return valueOf(dnString, schema); 190 } 191 192 /** 193 * Returns the Root DN. The Root DN does not contain and RDN components and 194 * is superior to all other DNs. 195 * 196 * @return The Root DN. 197 */ 198 public static DN rootDN() { 199 return ROOT_DN; 200 } 201 202 /** 203 * Parses the provided LDAP string representation of a DN using the default schema. 204 * 205 * @param dn 206 * The LDAP string representation of a DN. 207 * @return The parsed DN. 208 * @throws LocalizedIllegalArgumentException 209 * If {@code dn} is not a valid LDAP string representation of a DN. 210 * @throws NullPointerException 211 * If {@code dn} was {@code null}. 212 * @see #format(String, Object...) 213 */ 214 public static DN valueOf(final String dn) { 215 return valueOf(dn, Schema.getDefaultSchema()); 216 } 217 218 /** 219 * Parses the provided LDAP string representation of a DN using the provided schema. 220 * 221 * @param dn 222 * The LDAP string representation of a DN. 223 * @param schema 224 * The schema to use when parsing the DN. 225 * @return The parsed DN. 226 * @throws LocalizedIllegalArgumentException 227 * If {@code dn} is not a valid LDAP string representation of a DN. 228 * @throws NullPointerException 229 * If {@code dn} or {@code schema} was {@code null}. 230 * @see #format(String, Schema, Object...) 231 */ 232 public static DN valueOf(final String dn, final Schema schema) { 233 Reject.ifNull(dn, schema); 234 if (dn.length() == 0) { 235 return ROOT_DN; 236 } 237 238 // First check if DN is already cached. 239 final Map<String, DN> cache = CACHE.get(); 240 final DN cachedDN = cache.get(dn); 241 if (cachedDN != null && cachedDN.schema == schema) { 242 return cachedDN; 243 } 244 245 // Not in cache so decode. 246 return decode(new SubstringReader(dn), schema, cache); 247 } 248 249 /** 250 * Parses the provided LDAP string representation of a DN using the default schema. 251 * 252 * @param dn 253 * The LDAP byte string representation of a DN. 254 * @return The parsed DN. 255 * @throws LocalizedIllegalArgumentException 256 * If {@code dn} is not a valid LDAP byte string representation of a DN. 257 * @throws NullPointerException 258 * If {@code dn} was {@code null}. 259 */ 260 public static DN valueOf(ByteString dn) { 261 return DN.valueOf(dn.toString()); 262 } 263 264 /** Decodes a DN using the provided reader and schema. */ 265 private static DN decode(final SubstringReader reader, final Schema schema, final Map<String, DN> cache) { 266 reader.skipWhitespaces(); 267 if (reader.remaining() == 0) { 268 return ROOT_DN; 269 } 270 271 final RDN rdn; 272 try { 273 rdn = RDN.decode(reader, schema); 274 } catch (final UnknownSchemaElementException e) { 275 throw new LocalizedIllegalArgumentException( 276 ERR_DN_TYPE_NOT_FOUND.get(reader.getString(), e.getMessageObject())); 277 } 278 279 LinkedList<Pair<Integer, RDN>> parentRDNs = null; 280 DN parent = null; 281 while (reader.remaining() > 0 && reader.read() == ',') { 282 reader.skipWhitespaces(); 283 if (reader.remaining() == 0) { 284 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME.get(reader.getString())); 285 } 286 reader.mark(); 287 final String parentString = reader.read(reader.remaining()); 288 parent = cache.get(parentString); 289 if (parent != null) { 290 break; 291 } 292 reader.reset(); 293 if (parentRDNs == null) { 294 parentRDNs = new LinkedList<>(); 295 } 296 parentRDNs.add(Pair.of(reader.pos(), RDN.decode(reader, schema))); 297 } 298 if (parent == null) { 299 parent = ROOT_DN; 300 } 301 302 if (parentRDNs != null) { 303 Iterator<Pair<Integer, RDN>> iter = parentRDNs.descendingIterator(); 304 int parentsLeft = parentRDNs.size(); 305 while (iter.hasNext()) { 306 Pair<Integer, RDN> parentRDN = iter.next(); 307 parent = new DN(schema, parent, parentRDN.getSecond()); 308 if (parentsLeft-- < DN_CACHE_SIZE) { 309 cache.put(reader.getString().substring(parentRDN.getFirst()), parent); 310 } 311 } 312 } 313 return new DN(schema, parent, rdn); 314 } 315 316 private final RDN rdn; 317 private DN parent; 318 private final int size; 319 private int hashCode = -1; 320 321 /** 322 * The normalized byte string representation of this DN, which is not 323 * a valid DN and is not reversible to a valid DN. 324 */ 325 private ByteString normalizedDN; 326 327 /** 328 * The RFC 4514 string representation of this DN. A value of {@code null} 329 * indicates that the value needs to be computed lazily. 330 */ 331 private String stringValue; 332 333 /** The schema used to create this DN. */ 334 private final Schema schema; 335 336 /** Private constructor. */ 337 private DN(final Schema schema, final DN parent, final RDN rdn) { 338 this(schema, parent, rdn, parent != null ? parent.size + 1 : 0); 339 } 340 341 /** Private constructor. */ 342 private DN(final Schema schema, final DN parent, final RDN rdn, final int size) { 343 this.schema = schema; 344 this.parent = parent; 345 this.rdn = rdn; 346 this.size = size; 347 this.stringValue = rdn == null ? "" : null; 348 } 349 350 /** 351 * Returns a DN which is subordinate to this DN and having the additional 352 * RDN components contained in the provided DN. 353 * 354 * @param dn 355 * The DN containing the RDN components to be added to this DN. 356 * @return The subordinate DN. 357 * @throws NullPointerException 358 * If {@code dn} was {@code null}. 359 */ 360 public DN child(final DN dn) { 361 Reject.ifNull(dn); 362 363 if (dn.isRootDN()) { 364 return this; 365 } else if (isRootDN()) { 366 return dn; 367 } else { 368 final RDN[] rdns = new RDN[dn.size()]; 369 int i = rdns.length; 370 for (DN next = dn; next.rdn != null; next = next.parent) { 371 rdns[--i] = next.rdn; 372 } 373 DN newDN = this; 374 for (i = 0; i < rdns.length; i++) { 375 newDN = new DN(this.schema, newDN, rdns[i]); 376 } 377 return newDN; 378 } 379 } 380 381 /** 382 * Returns a DN which is an immediate child of this DN and having the 383 * specified RDN. 384 * <p> 385 * <b>Note:</b> the child DN whose RDN is {@link RDN#maxValue()} compares 386 * greater than all other possible child DNs, and may be used to construct 387 * range queries against DN keyed sorted collections such as 388 * {@code SortedSet} and {@code SortedMap}. 389 * 390 * @param rdn 391 * The RDN for the child DN. 392 * @return The child DN. 393 * @throws NullPointerException 394 * If {@code rdn} was {@code null}. 395 * @see RDN#maxValue() 396 */ 397 public DN child(final RDN rdn) { 398 Reject.ifNull(rdn); 399 return new DN(this.schema, this, rdn); 400 } 401 402 /** 403 * Returns a DN which is subordinate to this DN and having the additional 404 * RDN components contained in the provided DN decoded using the default 405 * schema. 406 * 407 * @param dn 408 * The DN containing the RDN components to be added to this DN. 409 * @return The subordinate DN. 410 * @throws LocalizedIllegalArgumentException 411 * If {@code dn} is not a valid LDAP string representation of a 412 * DN. 413 * @throws NullPointerException 414 * If {@code dn} was {@code null}. 415 */ 416 public DN child(final String dn) { 417 Reject.ifNull(dn); 418 return child(valueOf(dn)); 419 } 420 421 /** 422 * Returns a DN which is an immediate child of this DN and with an RDN 423 * having the provided attribute type and value decoded using the default 424 * schema. 425 * <p> 426 * If {@code attributeValue} is not an instance of {@code ByteString} then 427 * it will be converted using the {@link ByteString#valueOfObject(Object)} method. 428 * 429 * @param attributeType 430 * The attribute type. 431 * @param attributeValue 432 * The attribute value. 433 * @return The child DN. 434 * @throws UnknownSchemaElementException 435 * If {@code attributeType} was not found in the default schema. 436 * @throws NullPointerException 437 * If {@code attributeType} or {@code attributeValue} was 438 * {@code null}. 439 */ 440 public DN child(final String attributeType, final Object attributeValue) { 441 return child(new RDN(attributeType, attributeValue)); 442 } 443 444 @Override 445 public int compareTo(final DN dn) { 446 return toNormalizedByteString().compareTo(dn.toNormalizedByteString()); 447 } 448 449 @Override 450 public boolean equals(final Object obj) { 451 if (this == obj) { 452 return true; 453 } 454 if (obj instanceof DN) { 455 DN otherDN = (DN) obj; 456 return toNormalizedByteString().equals(otherDN.toNormalizedByteString()); 457 } 458 return false; 459 } 460 461 @Override 462 public int hashCode() { 463 if (hashCode == -1) { 464 hashCode = toNormalizedByteString().hashCode(); 465 } 466 return hashCode; 467 } 468 469 /** 470 * Returns {@code true} if this DN is an immediate child of the provided DN. 471 * 472 * @param dn 473 * The potential parent DN. 474 * @return {@code true} if this DN is the immediate child of the provided 475 * DN, otherwise {@code false}. 476 * @throws NullPointerException 477 * If {@code dn} was {@code null}. 478 */ 479 public boolean isChildOf(final DN dn) { 480 // If this is the Root DN then parent will be null but this is ok. 481 return dn.equals(parent); 482 } 483 484 /** 485 * Returns {@code true} if this DN is an immediate child of the provided DN 486 * decoded using the default schema. 487 * 488 * @param dn 489 * The potential parent DN. 490 * @return {@code true} if this DN is the immediate child of the provided 491 * DN, otherwise {@code false}. 492 * @throws LocalizedIllegalArgumentException 493 * If {@code dn} is not a valid LDAP string representation of a 494 * DN. 495 * @throws NullPointerException 496 * If {@code dn} was {@code null}. 497 */ 498 public boolean isChildOf(final String dn) { 499 // If this is the Root DN then parent will be null but this is ok. 500 return isChildOf(valueOf(dn)); 501 } 502 503 /** 504 * Returns {@code true} if this DN matches the provided base DN and search 505 * scope. 506 * 507 * @param dn 508 * The base DN. 509 * @param scope 510 * The search scope. 511 * @return {@code true} if this DN matches the provided base DN and search 512 * scope, otherwise {@code false}. 513 * @throws NullPointerException 514 * If {@code dn} or {@code scope} was {@code null}. 515 */ 516 public boolean isInScopeOf(DN dn, SearchScope scope) { 517 switch (scope.asEnum()) { 518 case BASE_OBJECT: 519 // The base DN must equal this DN. 520 return equals(dn); 521 case SINGLE_LEVEL: 522 // The parent DN must equal the base DN. 523 return isChildOf(dn); 524 case SUBORDINATES: 525 // This DN must be a descendant of the provided base DN, but 526 // not equal to it. 527 return isSubordinateOrEqualTo(dn) && !equals(dn); 528 case WHOLE_SUBTREE: 529 // This DN must be a descendant of the provided base DN. 530 return isSubordinateOrEqualTo(dn); 531 default: 532 // This is a scope that we don't recognize. 533 return false; 534 } 535 } 536 537 /** 538 * Returns {@code true} if this DN matches the provided base DN and search 539 * scope. 540 * 541 * @param dn 542 * The base DN. 543 * @param scope 544 * The search scope. 545 * @return {@code true} if this DN matches the provided base DN and search 546 * scope, otherwise {@code false}. 547 * @throws LocalizedIllegalArgumentException 548 * If {@code dn} is not a valid LDAP string representation of a 549 * DN. 550 * @throws NullPointerException 551 * If {@code dn} or {@code scope} was {@code null}. 552 */ 553 public boolean isInScopeOf(String dn, SearchScope scope) { 554 return isInScopeOf(valueOf(dn), scope); 555 } 556 557 /** 558 * Returns {@code true} if this DN is the immediate parent of the provided 559 * DN. 560 * 561 * @param dn 562 * The potential child DN. 563 * @return {@code true} if this DN is the immediate parent of the provided 564 * DN, otherwise {@code false}. 565 * @throws NullPointerException 566 * If {@code dn} was {@code null}. 567 */ 568 public boolean isParentOf(final DN dn) { 569 // If dn is the Root DN then parent will be null but this is ok. 570 return equals(dn.parent); 571 } 572 573 /** 574 * Returns {@code true} if this DN is the immediate parent of the provided 575 * DN. 576 * 577 * @param dn 578 * The potential child DN. 579 * @return {@code true} if this DN is the immediate parent of the provided 580 * DN, otherwise {@code false}. 581 * @throws LocalizedIllegalArgumentException 582 * If {@code dn} is not a valid LDAP string representation of a 583 * DN. 584 * @throws NullPointerException 585 * If {@code dn} was {@code null}. 586 */ 587 public boolean isParentOf(final String dn) { 588 // If dn is the Root DN then parent will be null but this is ok. 589 return isParentOf(valueOf(dn)); 590 } 591 592 /** 593 * Returns {@code true} if this DN is the Root DN. 594 * 595 * @return {@code true} if this DN is the Root DN, otherwise {@code false}. 596 */ 597 public boolean isRootDN() { 598 return size == 0; 599 } 600 601 /** 602 * Returns {@code true} if this DN is subordinate to or equal to the 603 * provided DN. 604 * 605 * @param dn 606 * The potential child DN. 607 * @return {@code true} if this DN is subordinate to or equal to the 608 * provided DN, otherwise {@code false}. 609 * @throws NullPointerException 610 * If {@code dn} was {@code null}. 611 */ 612 public boolean isSubordinateOrEqualTo(final DN dn) { 613 if (size < dn.size) { 614 return false; 615 } else if (size == dn.size) { 616 return equals(dn); 617 } else { 618 // dn is a potential superior of this. 619 return parent(size - dn.size).equals(dn); 620 } 621 } 622 623 /** 624 * Returns {@code true} if this DN is subordinate to or equal to the 625 * provided DN. 626 * 627 * @param dn 628 * The potential child DN. 629 * @return {@code true} if this DN is subordinate to or equal to the 630 * provided DN, otherwise {@code false}. 631 * @throws LocalizedIllegalArgumentException 632 * If {@code dn} is not a valid LDAP string representation of a 633 * DN. 634 * @throws NullPointerException 635 * If {@code dn} was {@code null}. 636 */ 637 public boolean isSubordinateOrEqualTo(final String dn) { 638 return isSubordinateOrEqualTo(valueOf(dn)); 639 } 640 641 /** 642 * Returns {@code true} if this DN is superior to or equal to the provided 643 * DN. 644 * 645 * @param dn 646 * The potential child DN. 647 * @return {@code true} if this DN is superior to or equal to the provided 648 * DN, otherwise {@code false}. 649 * @throws NullPointerException 650 * If {@code dn} was {@code null}. 651 */ 652 public boolean isSuperiorOrEqualTo(final DN dn) { 653 if (size > dn.size) { 654 return false; 655 } else if (size == dn.size) { 656 return equals(dn); 657 } else { 658 // dn is a potential subordinate of this. 659 return dn.parent(dn.size - size).equals(this); 660 } 661 } 662 663 /** 664 * Returns {@code true} if this DN is superior to or equal to the provided 665 * DN. 666 * 667 * @param dn 668 * The potential child DN. 669 * @return {@code true} if this DN is superior to or equal to the provided 670 * DN, otherwise {@code false}. 671 * @throws LocalizedIllegalArgumentException 672 * If {@code dn} is not a valid LDAP string representation of a 673 * DN. 674 * @throws NullPointerException 675 * If {@code dn} was {@code null}. 676 */ 677 public boolean isSuperiorOrEqualTo(final String dn) { 678 return isSuperiorOrEqualTo(valueOf(dn)); 679 } 680 681 /** 682 * Returns an iterator of the RDNs contained in this DN. The RDNs will be 683 * returned in the order starting with this DN's RDN, followed by the RDN of 684 * the parent DN, and so on. 685 * <p> 686 * Attempts to remove RDNs using an iterator's {@code remove()} method are 687 * not permitted and will result in an {@code UnsupportedOperationException} 688 * being thrown. 689 * 690 * @return An iterator of the RDNs contained in this DN. 691 */ 692 @Override 693 public Iterator<RDN> iterator() { 694 return new Iterator<RDN>() { 695 private DN dn = DN.this; 696 697 @Override 698 public boolean hasNext() { 699 return dn.rdn != null; 700 } 701 702 @Override 703 public RDN next() { 704 if (dn.rdn == null) { 705 throw new NoSuchElementException(); 706 } 707 708 final RDN rdn = dn.rdn; 709 dn = dn.parent; 710 return rdn; 711 } 712 713 @Override 714 public void remove() { 715 throw new UnsupportedOperationException(); 716 } 717 }; 718 } 719 720 /** 721 * Returns the DN whose content is the specified number of RDNs from this 722 * DN. The following equivalences hold: 723 * 724 * <pre> 725 * dn.localName(0).isRootDN(); 726 * dn.localName(1).equals(rootDN.child(dn.rdn())); 727 * dn.localName(dn.size()).equals(dn); 728 * </pre> 729 * 730 * Said otherwise, a new DN is built using {@code index} RDNs, 731 * retained in the same order, starting from the left. 732 * 733 * @param index 734 * The number of RDNs to be included in the local name. 735 * @return The DN whose content is the specified number of RDNs from this 736 * DN. 737 * @throws IllegalArgumentException 738 * If {@code index} is less than zero. 739 * @see #parent(int) for the reverse operation (starting from the right) 740 */ 741 public DN localName(final int index) { 742 Reject.ifFalse(index >= 0, "index less than zero"); 743 744 if (index == 0) { 745 return ROOT_DN; 746 } else if (index >= size) { 747 return this; 748 } else { 749 final DN localName = new DN(this.schema, null, rdn, index); 750 DN nextLocalName = localName; 751 DN lastDN = parent; 752 for (int i = index - 1; i > 0; i--) { 753 nextLocalName.parent = new DN(this.schema, null, lastDN.rdn, i); 754 nextLocalName = nextLocalName.parent; 755 lastDN = lastDN.parent; 756 } 757 nextLocalName.parent = ROOT_DN; 758 return localName; 759 } 760 } 761 762 /** 763 * Returns the DN which is the immediate parent of this DN, or {@code null} 764 * if this DN is the Root DN. 765 * <p> 766 * This method is equivalent to: 767 * 768 * <pre> 769 * parent(1); 770 * </pre> 771 * 772 * @return The DN which is the immediate parent of this DN, or {@code null} 773 * if this DN is the Root DN. 774 */ 775 public DN parent() { 776 return parent; 777 } 778 779 /** 780 * Returns the DN which is equal to this DN with the specified number of 781 * RDNs removed. Note that if {@code index} is zero then this DN will be 782 * returned (identity). 783 * 784 * Said otherwise, a new DN is built using {@code index} RDNs, 785 * retained in the same order, starting from the right. 786 * 787 * @param index 788 * The number of RDNs to be removed. 789 * @return The DN which is equal to this DN with the specified number of 790 * RDNs removed, or {@code null} if the parent of the Root DN is 791 * reached. 792 * @throws IllegalArgumentException 793 * If {@code index} is less than zero. 794 * @see #localName(int) for the reverse operation (starting from the left) 795 */ 796 public DN parent(final int index) { 797 // We allow size + 1 so that we can return null as the parent of the Root DN. 798 Reject.ifTrue(index < 0, "index less than zero"); 799 800 if (index > size) { 801 return null; 802 } 803 DN parentDN = this; 804 for (int i = 0; parentDN != null && i < index; i++) { 805 parentDN = parentDN.parent; 806 } 807 return parentDN; 808 } 809 810 /** 811 * Returns the RDN of this DN, or {@code null} if this DN is the Root DN. 812 * <p> 813 * This is the equivalent of calling {@code dn.rdn(0)}. 814 * 815 * @return The RDN of this DN, or {@code null} if this DN is the Root DN. 816 */ 817 public RDN rdn() { 818 return rdn; 819 } 820 821 /** 822 * Returns the RDN at the specified index for this DN, 823 * or {@code null} if no such RDN exists. 824 * <p> 825 * Here is an example usage: 826 * <pre> 827 * DN.valueOf("ou=people,dc=example,dc=com").rdn(0) => "ou=people" 828 * DN.valueOf("ou=people,dc=example,dc=com").rdn(1) => "dc=example" 829 * DN.valueOf("ou=people,dc=example,dc=com").rdn(2) => "dc=com" 830 * DN.valueOf("ou=people,dc=example,dc=com").rdn(3) => null 831 * </pre> 832 * 833 * @param index 834 * The index of the requested RDN. 835 * @return The RDN at the specified index, or {@code null} if no such RDN exists. 836 * @throws IllegalArgumentException 837 * If {@code index} is less than zero. 838 */ 839 public RDN rdn(int index) { 840 DN parentDN = parent(index); 841 return parentDN != null ? parentDN.rdn : null; 842 } 843 844 /** 845 * Returns a copy of this DN whose parent DN, {@code fromDN}, has been 846 * renamed to the new parent DN, {@code toDN}. If this DN is not subordinate 847 * or equal to {@code fromDN} then this DN is returned (i.e. the DN is not 848 * renamed). 849 * 850 * @param fromDN 851 * The old parent DN. 852 * @param toDN 853 * The new parent DN. 854 * @return The renamed DN, or this DN if no renaming was performed. 855 * @throws NullPointerException 856 * If {@code fromDN} or {@code toDN} was {@code null}. 857 */ 858 public DN rename(final DN fromDN, final DN toDN) { 859 Reject.ifNull(fromDN, toDN); 860 861 if (!isSubordinateOrEqualTo(fromDN)) { 862 return this; 863 } else if (equals(fromDN)) { 864 return toDN; 865 } else { 866 return toDN.child(localName(size - fromDN.size)); 867 } 868 } 869 870 /** 871 * Returns the number of RDN components in this DN. 872 * 873 * @return The number of RDN components in this DN. 874 */ 875 public int size() { 876 return size; 877 } 878 879 /** 880 * Returns the RFC 4514 string representation of this DN. 881 * 882 * @return The RFC 4514 string representation of this DN. 883 * @see <a href="http://tools.ietf.org/html/rfc4514">RFC 4514 - Lightweight 884 * Directory Access Protocol (LDAP): String Representation of 885 * Distinguished Names </a> 886 */ 887 @Override 888 public String toString() { 889 // We don't care about potential race conditions here. 890 if (stringValue == null) { 891 final StringBuilder builder = new StringBuilder(); 892 builder.append(rdn); 893 for (DN dn = parent; dn.rdn != null; dn = dn.parent) { 894 builder.append(RDN_CHAR_SEPARATOR); 895 if (dn.stringValue != null) { 896 builder.append(dn.stringValue); 897 break; 898 } 899 builder.append(dn.rdn); 900 } 901 stringValue = builder.toString(); 902 } 903 return stringValue; 904 } 905 906 /** 907 * Retrieves a normalized byte string representation of this DN. 908 * <p> 909 * This representation is suitable for equality and comparisons, and 910 * for providing a natural hierarchical ordering. 911 * However, it is not a valid DN and can't be reverted to a valid DN. 912 * You should consider using a {@code CompactDn} as an alternative. 913 * 914 * @return The normalized string representation of this DN. 915 */ 916 public ByteString toNormalizedByteString() { 917 if (normalizedDN == null) { 918 if (rdn == null) { 919 normalizedDN = ByteString.empty(); 920 } else { 921 final ByteStringBuilder builder = new ByteStringBuilder(size * 8); 922 if (parent.normalizedDN != null) { 923 builder.appendBytes(parent.normalizedDN); 924 } else { 925 for (int i = size() - 1; i > 0; i--) { 926 parent(i).rdn().toNormalizedByteString(builder); 927 } 928 } 929 rdn.toNormalizedByteString(builder); 930 normalizedDN = builder.toByteString(); 931 } 932 } 933 return normalizedDN; 934 } 935 936 /** 937 * Retrieves a normalized string representation of this DN. 938 * <p> 939 * This representation is safe to use in an URL or in a file name. 940 * However, it is not a valid DN and can't be reverted to a valid DN. 941 * 942 * @return The normalized string representation of this DN. 943 */ 944 public String toNormalizedUrlSafeString() { 945 if (rdn() == null) { 946 return ""; 947 } 948 949 // This code differs from toNormalizedByteString(), 950 // because we do not care about ordering comparisons. 951 // (so we do not append the RDN_SEPARATOR first) 952 final StringBuilder builder = new StringBuilder(); 953 int i = size() - 1; 954 parent(i).rdn().toNormalizedUrlSafeString(builder); 955 for (i--; i >= 0; i--) { 956 final RDN rdn = parent(i).rdn(); 957 // Only add a separator if the RDN is not RDN.maxValue() or RDN.minValue(). 958 if (rdn.size() != 0) { 959 builder.append(RDN_CHAR_SEPARATOR); 960 } 961 rdn.toNormalizedUrlSafeString(builder); 962 } 963 return builder.toString(); 964 } 965 966 /** 967 * Returns a UUID whose content is based on the normalized content of this DN. 968 * Two equivalent DNs subject to the same schema will always yield the same UUID. 969 * 970 * @return the UUID representing this DN 971 */ 972 public UUID toUUID() { 973 ByteString normDN = toNormalizedByteString(); 974 if (!normDN.isEmpty()) { 975 // remove leading RDN separator 976 normDN = normDN.subSequence(1, normDN.length()); 977 } 978 return UUID.nameUUIDFromBytes(normDN.toByteArray()); 979 } 980 981}