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