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 static com.forgerock.opendj.ldap.CoreMessages.ERR_RDN_DUPLICATE_AVA_TYPES; 021import static com.forgerock.opendj.ldap.CoreMessages.ERR_RDN_NO_AVAS; 022import static com.forgerock.opendj.ldap.CoreMessages.ERR_RDN_TRAILING_GARBAGE; 023import static com.forgerock.opendj.ldap.CoreMessages.ERR_RDN_TYPE_NOT_FOUND; 024import static org.forgerock.opendj.ldap.DN.AVA_CHAR_SEPARATOR; 025import static org.forgerock.opendj.ldap.DN.NORMALIZED_AVA_SEPARATOR; 026import static org.forgerock.opendj.ldap.DN.NORMALIZED_RDN_SEPARATOR; 027import static org.forgerock.opendj.ldap.DN.RDN_CHAR_SEPARATOR; 028 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.Collection; 032import java.util.Collections; 033import java.util.Iterator; 034import java.util.List; 035import java.util.TreeSet; 036 037import org.forgerock.i18n.LocalizedIllegalArgumentException; 038import org.forgerock.opendj.ldap.schema.AttributeType; 039import org.forgerock.opendj.ldap.schema.Schema; 040import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException; 041import org.forgerock.util.Reject; 042 043import com.forgerock.opendj.util.Iterators; 044import com.forgerock.opendj.util.SubstringReader; 045 046/** 047 * A relative distinguished name (RDN) as defined in RFC 4512 section 2.3 is the 048 * name of an entry relative to its immediate superior. An RDN is composed of an 049 * unordered set of one or more attribute value assertions (AVA) consisting of 050 * an attribute description with zero options and an attribute value. These AVAs 051 * are chosen to match attribute values (each a distinguished value) of the 052 * entry. 053 * <p> 054 * An entry's relative distinguished name must be unique among all immediate 055 * subordinates of the entry's immediate superior (i.e. all siblings). 056 * <p> 057 * The following are examples of string representations of RDNs: 058 * 059 * <pre> 060 * uid=12345 061 * ou=Engineering 062 * cn=Kurt Zeilenga+L=Redwood Shores 063 * </pre> 064 * 065 * The last is an example of a multi-valued RDN; that is, an RDN composed of 066 * multiple AVAs. 067 * 068 * @see <a href="http://tools.ietf.org/html/rfc4512#section-2.3">RFC 4512 - 069 * Lightweight Directory Access Protocol (LDAP): Directory Information 070 * Models </a> 071 */ 072public final class RDN implements Iterable<AVA>, Comparable<RDN> { 073 074 /** 075 * A constant holding a special RDN having zero AVAs 076 * and which sorts before any RDN other than itself. 077 */ 078 private static final RDN MIN_VALUE = new RDN(); 079 /** 080 * A constant holding a special RDN having zero AVAs 081 * and which sorts after any RDN other than itself. 082 */ 083 private static final RDN MAX_VALUE = new RDN(); 084 085 /** 086 * Returns a constant containing a special RDN which sorts before any 087 * RDN other than itself. This RDN may be used in order to perform 088 * range queries on DN keyed collections such as {@code SortedSet}s and 089 * {@code SortedMap}s. For example, the following code can be used to 090 * construct a range whose contents is a sub-tree of entries, excluding the base entry: 091 * 092 * <pre> 093 * SortedMap<DN, Entry> entries = ...; 094 * DN baseDN = ...; 095 * 096 * // Returns a map containing the baseDN and all of its subordinates. 097 * SortedMap<DN,Entry> subtree = entries.subMap( 098 * baseDN.child(RDN.minValue()), baseDN.child(RDN.maxValue())); 099 * </pre> 100 * 101 * @return A constant containing a special RDN which sorts before any 102 * RDN other than itself. 103 * @see #maxValue() 104 */ 105 public static RDN minValue() { 106 return MIN_VALUE; 107 } 108 109 /** 110 * Returns a constant containing a special RDN which sorts after any 111 * RDN other than itself. This RDN may be used in order to perform 112 * range queries on DN keyed collections such as {@code SortedSet}s and 113 * {@code SortedMap}s. For example, the following code can be used to 114 * construct a range whose contents is a sub-tree of entries: 115 * 116 * <pre> 117 * SortedMap<DN, Entry> entries = ...; 118 * DN baseDN = ...; 119 * 120 * // Returns a map containing the baseDN and all of its subordinates. 121 * SortedMap<DN,Entry> subtree = entries.subMap(baseDN, baseDN.child(RDN.maxValue())); 122 * </pre> 123 * 124 * @return A constant containing a special RDN which sorts after any 125 * RDN other than itself. 126 * @see #minValue() 127 */ 128 public static RDN maxValue() { 129 return MAX_VALUE; 130 } 131 132 /** 133 * Parses the provided LDAP string representation of an RDN using the 134 * default schema. 135 * 136 * @param rdn 137 * The LDAP string representation of a RDN. 138 * @return The parsed RDN. 139 * @throws LocalizedIllegalArgumentException 140 * If {@code rdn} is not a valid LDAP string representation of a 141 * RDN. 142 * @throws NullPointerException 143 * If {@code rdn} was {@code null}. 144 */ 145 public static RDN valueOf(final String rdn) { 146 return valueOf(rdn, Schema.getDefaultSchema()); 147 } 148 149 /** 150 * Parses the provided LDAP string representation of a RDN using the 151 * provided schema. 152 * 153 * @param rdn 154 * The LDAP string representation of a RDN. 155 * @param schema 156 * The schema to use when parsing the RDN. 157 * @return The parsed RDN. 158 * @throws LocalizedIllegalArgumentException 159 * If {@code rdn} is not a valid LDAP string representation of a 160 * RDN. 161 * @throws NullPointerException 162 * If {@code rdn} or {@code schema} was {@code null}. 163 */ 164 public static RDN valueOf(final String rdn, final Schema schema) { 165 final SubstringReader reader = new SubstringReader(rdn); 166 final RDN parsedRdn; 167 try { 168 parsedRdn = decode(reader, schema); 169 } catch (final UnknownSchemaElementException e) { 170 throw new LocalizedIllegalArgumentException(ERR_RDN_TYPE_NOT_FOUND.get(rdn, e.getMessageObject())); 171 } 172 if (reader.remaining() > 0) { 173 throw new LocalizedIllegalArgumentException( 174 ERR_RDN_TRAILING_GARBAGE.get(rdn, reader.read(reader.remaining()))); 175 } 176 return parsedRdn; 177 } 178 179 static RDN decode(final SubstringReader reader, final Schema schema) { 180 final AVA firstAVA = AVA.decode(reader, schema); 181 182 // Skip over any spaces that might be after the attribute value. 183 reader.skipWhitespaces(); 184 185 reader.mark(); 186 if (reader.remaining() > 0 && reader.read() == '+') { 187 final List<AVA> avas = new ArrayList<>(); 188 avas.add(firstAVA); 189 190 do { 191 avas.add(AVA.decode(reader, schema)); 192 193 // Skip over any spaces that might be after the attribute value. 194 reader.skipWhitespaces(); 195 196 reader.mark(); 197 } while (reader.remaining() > 0 && reader.read() == '+'); 198 199 reader.reset(); 200 return new RDN(avas); 201 } else { 202 reader.reset(); 203 return new RDN(firstAVA); 204 } 205 } 206 207 /** In original order. */ 208 private final AVA[] avas; 209 210 /** 211 * We need to store the original string value if provided in order to 212 * preserve the original whitespace. 213 */ 214 private String stringValue; 215 216 /** 217 * Creates a new RDN using the provided attribute type and value. 218 * <p> 219 * If {@code attributeValue} is not an instance of {@code ByteString} then 220 * it will be converted using the {@link ByteString#valueOfObject(Object)} method. 221 * 222 * @param attributeType 223 * The attribute type. 224 * @param attributeValue 225 * The attribute value. 226 * @throws NullPointerException 227 * If {@code attributeType} or {@code attributeValue} was 228 * {@code null}. 229 */ 230 public RDN(final AttributeType attributeType, final Object attributeValue) { 231 this.avas = new AVA[] { new AVA(attributeType, attributeValue) }; 232 } 233 234 /** 235 * Creates a new RDN using the provided attribute type and value decoded 236 * using the default schema. 237 * <p> 238 * If {@code attributeValue} is not an instance of {@code ByteString} then 239 * it will be converted using the {@link ByteString#valueOfObject(Object)} method. 240 * 241 * @param attributeType 242 * The attribute type. 243 * @param attributeValue 244 * The attribute value. 245 * @throws UnknownSchemaElementException 246 * If {@code attributeType} was not found in the default schema. 247 * @throws NullPointerException 248 * If {@code attributeType} or {@code attributeValue} was 249 * {@code null}. 250 */ 251 public RDN(final String attributeType, final Object attributeValue) { 252 this.avas = new AVA[] { new AVA(attributeType, attributeValue) }; 253 } 254 255 /** 256 * Creates a new RDN using the provided AVAs. 257 * 258 * @param avas 259 * The attribute-value assertions used to build this RDN. 260 * @throws NullPointerException 261 * If {@code avas} is {@code null} or contains a null ava. 262 * @throws IllegalArgumentException 263 * If {@code avas} is empty. 264 */ 265 public RDN(final AVA... avas) { 266 Reject.ifNull(avas); 267 this.avas = validateAvas(avas); 268 } 269 270 private AVA[] validateAvas(final AVA[] avas) { 271 switch (avas.length) { 272 case 0: 273 throw new LocalizedIllegalArgumentException(ERR_RDN_NO_AVAS.get()); 274 case 1: 275 // Guaranteed to be valid. 276 break; 277 case 2: 278 if (avas[0].getAttributeType().equals(avas[1].getAttributeType())) { 279 throw new LocalizedIllegalArgumentException( 280 ERR_RDN_DUPLICATE_AVA_TYPES.get(avas[0].getAttributeName())); 281 } 282 break; 283 default: 284 final AVA[] sortedAVAs = Arrays.copyOf(avas, avas.length); 285 Arrays.sort(sortedAVAs); 286 AttributeType previousAttributeType = null; 287 for (AVA ava : sortedAVAs) { 288 if (ava.getAttributeType().equals(previousAttributeType)) { 289 throw new LocalizedIllegalArgumentException( 290 ERR_RDN_DUPLICATE_AVA_TYPES.get(ava.getAttributeName())); 291 } 292 previousAttributeType = ava.getAttributeType(); 293 } 294 } 295 return avas; 296 } 297 298 /** 299 * Creates a new RDN using the provided AVAs. 300 * 301 * @param avas 302 * The attribute-value assertions used to build this RDN. 303 * @throws NullPointerException 304 * If {@code ava} is {@code null} or contains null ava. 305 * @throws IllegalArgumentException 306 * If {@code avas} is empty. 307 */ 308 public RDN(Collection<AVA> avas) { 309 Reject.ifNull(avas); 310 this.avas = validateAvas(avas.toArray(new AVA[avas.size()])); 311 } 312 313 // Special constructor for min/max RDN values. 314 private RDN() { 315 this.avas = new AVA[0]; 316 this.stringValue = ""; 317 } 318 319 @Override 320 public int compareTo(final RDN rdn) { 321 // FIXME how about replacing this method body with the following code? 322 // return toNormalizedByteString().compareTo(rdn.toNormalizedByteString()) 323 324 // Identity. 325 if (this == rdn) { 326 return 0; 327 } 328 329 // MAX_VALUE is always greater than any other RDN other than itself. 330 if (this == MAX_VALUE) { 331 return 1; 332 } 333 if (rdn == MAX_VALUE) { 334 return -1; 335 } 336 337 // MIN_VALUE is always less than any other RDN other than itself. 338 if (this == MIN_VALUE) { 339 return -1; 340 } 341 if (rdn == MIN_VALUE) { 342 return 1; 343 } 344 345 // Compare number of AVAs first as this is quick and easy. 346 final int sz1 = avas.length; 347 final int sz2 = rdn.avas.length; 348 if (sz1 != sz2) { 349 return sz1 - sz2 > 0 ? 1 : -1; 350 } 351 352 // Fast path for common case. 353 if (sz1 == 1) { 354 return avas[0].compareTo(rdn.avas[0]); 355 } 356 357 // Need to sort the AVAs before comparing. 358 final AVA[] a1 = new AVA[sz1]; 359 System.arraycopy(avas, 0, a1, 0, sz1); 360 Arrays.sort(a1); 361 362 final AVA[] a2 = new AVA[sz1]; 363 System.arraycopy(rdn.avas, 0, a2, 0, sz1); 364 Arrays.sort(a2); 365 366 for (int i = 0; i < sz1; i++) { 367 final int result = a1[i].compareTo(a2[i]); 368 if (result != 0) { 369 return result; 370 } 371 } 372 373 return 0; 374 } 375 376 @Override 377 public boolean equals(final Object obj) { 378 if (this == obj) { 379 return true; 380 } else if (obj instanceof RDN) { 381 return compareTo((RDN) obj) == 0; 382 } else { 383 return false; 384 } 385 } 386 387 /** 388 * Returns the attribute value contained in this RDN which is associated 389 * with the provided attribute type, or {@code null} if this RDN does not 390 * include such an attribute value. 391 * 392 * @param attributeType 393 * The attribute type. 394 * @return The attribute value. 395 */ 396 public ByteString getAttributeValue(final AttributeType attributeType) { 397 for (final AVA ava : avas) { 398 if (ava.getAttributeType().equals(attributeType)) { 399 return ava.getAttributeValue(); 400 } 401 } 402 return null; 403 } 404 405 /** 406 * Returns the first AVA contained in this RDN. 407 * 408 * @return The first AVA contained in this RDN. 409 */ 410 public AVA getFirstAVA() { 411 return avas[0]; 412 } 413 414 @Override 415 public int hashCode() { 416 // Avoid an algorithm that requires the AVAs to be sorted. 417 int hash = 0; 418 for (final AVA ava : avas) { 419 hash += ava.hashCode(); 420 } 421 return hash; 422 } 423 424 /** 425 * Returns {@code true} if this RDN contains more than one AVA. 426 * 427 * @return {@code true} if this RDN contains more than one AVA, otherwise 428 * {@code false}. 429 */ 430 public boolean isMultiValued() { 431 return avas.length > 1; 432 } 433 434 /** 435 * Indicates whether this RDN includes the specified attribute type. 436 * 437 * @param attributeType The attribute type for which to make the determination. 438 * @return {@code true} if the RDN includes the specified attribute type, 439 * or {@code false} if not. 440 */ 441 public boolean hasAttributeType(AttributeType attributeType) { 442 for (AVA ava : avas) { 443 if (ava.getAttributeType().equals(attributeType)) { 444 return true; 445 } 446 } 447 return false; 448 } 449 450 /** 451 * Returns an iterator of the AVAs contained in this RDN. The AVAs will be 452 * returned in the user provided order. 453 * <p> 454 * Attempts to remove AVAs using an iterator's {@code remove()} method are 455 * not permitted and will result in an {@code UnsupportedOperationException} 456 * being thrown. 457 * 458 * @return An iterator of the AVAs contained in this RDN. 459 */ 460 @Override 461 public Iterator<AVA> iterator() { 462 return Iterators.arrayIterator(avas); 463 } 464 465 /** 466 * Returns the number of AVAs in this RDN. 467 * 468 * @return The number of AVAs in this RDN. 469 */ 470 public int size() { 471 return avas.length; 472 } 473 474 /** 475 * Returns the RFC 4514 string representation of this RDN. 476 * 477 * @return The RFC 4514 string representation of this RDN. 478 * @see <a href="http://tools.ietf.org/html/rfc4514">RFC 4514 - Lightweight 479 * Directory Access Protocol (LDAP): String Representation of 480 * Distinguished Names </a> 481 */ 482 @Override 483 public String toString() { 484 // We don't care about potential race conditions here. 485 if (stringValue == null) { 486 final StringBuilder builder = new StringBuilder(); 487 avas[0].toString(builder); 488 for (int i = 1; i < avas.length; i++) { 489 builder.append(AVA_CHAR_SEPARATOR); 490 avas[i].toString(builder); 491 } 492 stringValue = builder.toString(); 493 } 494 return stringValue; 495 } 496 497 /** 498 * Returns the normalized byte string representation of this RDN. 499 * <p> 500 * The representation is not a valid RDN. 501 * 502 * @param builder 503 * The builder to use to construct the normalized byte string. 504 * @return The normalized byte string representation. 505 * @see DN#toNormalizedByteString() 506 */ 507 ByteStringBuilder toNormalizedByteString(final ByteStringBuilder builder) { 508 switch (size()) { 509 case 0: 510 if (this == MIN_VALUE) { 511 builder.appendByte(NORMALIZED_RDN_SEPARATOR); 512 } else { // can only be MAX_VALUE 513 builder.appendByte(NORMALIZED_AVA_SEPARATOR); 514 } 515 break; 516 case 1: 517 builder.appendByte(NORMALIZED_RDN_SEPARATOR); 518 getFirstAVA().toNormalizedByteString(builder); 519 break; 520 default: 521 builder.appendByte(NORMALIZED_RDN_SEPARATOR); 522 Iterator<AVA> it = getSortedAvas(); 523 it.next().toNormalizedByteString(builder); 524 while (it.hasNext()) { 525 builder.appendByte(NORMALIZED_AVA_SEPARATOR); 526 it.next().toNormalizedByteString(builder); 527 } 528 break; 529 } 530 return builder; 531 } 532 533 /** 534 * Retrieves a normalized string representation of this RDN. 535 * <p> 536 * This representation is safe to use in an URL or in a file name. 537 * However, it is not a valid RDN and can't be reverted to a valid RDN. 538 * 539 * @return The normalized string representation of this RDN. 540 * @see DN#toNormalizedUrlSafeString() 541 */ 542 StringBuilder toNormalizedUrlSafeString(final StringBuilder builder) { 543 switch (size()) { 544 case 0: 545 // since MIN_VALUE and MAX_VALUE are only used for sorting DNs, 546 // it is strange to call toNormalizedUrlSafeString() on one of them 547 if (this == MIN_VALUE) { 548 builder.append(RDN_CHAR_SEPARATOR); 549 } else { // can only be MAX_VALUE 550 builder.append(AVA_CHAR_SEPARATOR); 551 } 552 break; 553 case 1: 554 getFirstAVA().toNormalizedUrlSafe(builder); 555 break; 556 default: 557 Iterator<AVA> it = getSortedAvas(); 558 it.next().toNormalizedUrlSafe(builder); 559 while (it.hasNext()) { 560 builder.append(AVA_CHAR_SEPARATOR); 561 it.next().toNormalizedUrlSafe(builder); 562 } 563 break; 564 } 565 return builder; 566 } 567 568 private Iterator<AVA> getSortedAvas() { 569 TreeSet<AVA> sortedAvas = new TreeSet<>(); 570 Collections.addAll(sortedAvas, avas); 571 return sortedAvas.iterator(); 572 } 573}