/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2009-2010 Sun Microsystems, Inc. */ package org.opends.sdk; import static com.sun.opends.sdk.messages.Messages.ERR_DN_TYPE_NOT_FOUND; import java.util.*; import org.opends.sdk.schema.Schema; import org.opends.sdk.schema.UnknownSchemaElementException; import com.sun.opends.sdk.util.SubstringReader; import com.sun.opends.sdk.util.Validator; /** * A distinguished name (DN) as defined in RFC 4512 section 2.3 is the * concatenation of its relative distinguished name (RDN) and its immediate * superior's DN. A DN unambiguously refers to an entry in the Directory. *

* The following are examples of string representations of DNs: * *

 * UID=nobody@example.com,DC=example,DC=com CN=John
 * Smith,OU=Sales,O=ACME Limited,L=Moab,ST=Utah,C=US
 * 
* * @see RFC 4512 - * Lightweight Directory Access Protocol (LDAP): Directory Information * Models */ public final class DN implements Iterable, Comparable { private static final DN ROOT_DN = new DN(null, null, ""); // This is the size of the per-thread per-schema DN cache. We should // be conservative here in case there are many threads. We will only // cache parent DNs, so there's no need for it to be big. private static final int DN_CACHE_SIZE = 32; private static final ThreadLocal>> CACHE = new ThreadLocal>>() { /** * {@inheritDoc} */ @Override protected WeakHashMap> initialValue() { return new WeakHashMap>(); } }; /** * Returns the Root DN. The Root DN does not contain and RDN components and is * superior to all other DNs. * * @return The Root DN. */ public static DN rootDN() { return ROOT_DN; } /** * Parses the provided LDAP string representation of a DN using the default * schema. * * @param dn * The LDAP string representation of a DN. * @return The parsed DN. * @throws LocalizedIllegalArgumentException * If {@code dn} is not a valid LDAP string representation of a DN. * @throws NullPointerException * If {@code dn} was {@code null}. */ public static DN valueOf(final String dn) throws LocalizedIllegalArgumentException, NullPointerException { return valueOf(dn, Schema.getDefaultSchema()); } /** * Parses the provided LDAP string representation of a DN using the provided * schema. * * @param dn * The LDAP string representation of a DN. * @param schema * The schema to use when parsing the DN. * @return The parsed DN. * @throws LocalizedIllegalArgumentException * If {@code dn} is not a valid LDAP string representation of a DN. * @throws NullPointerException * If {@code dn} or {@code schema} was {@code null}. */ public static DN valueOf(final String dn, final Schema schema) throws LocalizedIllegalArgumentException, NullPointerException { Validator.ensureNotNull(dn, schema); if (dn.length() == 0) { return ROOT_DN; } // First check if DN is already cached. final Map cache = getCache(schema); final DN cachedDN = cache.get(dn); if (cachedDN != null) { return cachedDN; } // Not in cache so decode. final SubstringReader reader = new SubstringReader(dn); return decode(dn, reader, schema, cache); } /** * Compares the provided DN values to determine their relative order in a * sorted list. * * @param dn1 * The first DN to be compared. It must not be {@code null}. * @param dn2 * The second DN to be compared. It must not be {@code null}. * @return A negative integer if the first DN should come before the second DN * in a sorted list, a positive integer if the first DN should come * after the second DN in a sorted list, or zero if the two DN values * can be considered equal. */ private static int compareTo(final DN dn1, final DN dn2) { // Quickly check if we are comparing against root dse. if (dn1.isRootDN()) { if (dn2.isRootDN()) { // both are equal. return 0; } else { // dn1 comes before dn2. return -1; } } if (dn2.isRootDN()) { // dn1 comes after dn2. return 1; } int dn1Size = dn1.size - 1; int dn2Size = dn2.size - 1; while (dn1Size >= 0 && dn2Size >= 0) { final DN dn1Parent = dn1.parent(dn1Size--); final DN dn2Parent = dn2.parent(dn2Size--); if (dn1Parent.isRootDN()) { if (dn2Parent.isRootDN()) { break; } return -1; } if (dn2Parent.isRootDN()) { return 1; } final int result = dn1Parent.rdn.compareTo(dn2Parent.rdn); if (result > 0) { return 1; } else if (result < 0) { return -1; } } // What do we have here? if (dn1Size > dn2Size) { return 1; } else if (dn1Size < dn2Size) { return -1; } return 0; } // Decodes a DN using the provided reader and schema. private static DN decode(final String dnString, final SubstringReader reader, final Schema schema, final Map cache) throws LocalizedIllegalArgumentException { reader.skipWhitespaces(); if (reader.remaining() == 0) { return ROOT_DN; } RDN rdn; try { rdn = RDN.decode(null, reader, schema); } catch (final UnknownSchemaElementException e) { final LocalizableMessage message = ERR_DN_TYPE_NOT_FOUND.get(reader .getString(), e.getMessageObject()); throw new LocalizedIllegalArgumentException(message); } DN parent; if (reader.remaining() > 0 && reader.read() == ',') { reader.mark(); final String parentString = reader.read(reader.remaining()); parent = cache.get(parentString); if (parent == null) { reader.reset(); parent = decode(parentString, reader, schema, cache); // Only cache parent DNs since leaf DNs are likely to make the // cache to volatile. cache.put(parentString, parent); } } else { parent = ROOT_DN; } return new DN(rdn, parent, dnString); } @SuppressWarnings("serial") private static Map getCache(final Schema schema) { final WeakHashMap> threadLocalMap = CACHE.get(); Map schemaLocalMap = threadLocalMap.get(schema); if (schemaLocalMap == null) { schemaLocalMap = new LinkedHashMap(DN_CACHE_SIZE, 0.75f, true) { @Override protected boolean removeEldestEntry(final Map.Entry e) { return size() > DN_CACHE_SIZE; } }; threadLocalMap.put(schema, schemaLocalMap); } return schemaLocalMap; } private final RDN rdn; private final DN parent; private final int size; // We need to store the original string value if provided in order to // preserve the original whitespace. private String stringValue; // Private constructor. private DN(final RDN rdn, final DN parent, final String stringValue) { this.rdn = rdn; this.parent = parent; this.stringValue = stringValue; this.size = parent != null ? parent.size + 1 : 0; } /** * Returns a DN which is subordinate to this DN and having the additional RDN * components contained in the provided DN. * * @param dn * The DN containing the RDN components to be added to this DN. * @return The subordinate DN. * @throws NullPointerException * If {@code dn} was {@code null}. */ public DN child(final DN dn) throws NullPointerException { Validator.ensureNotNull(dn); if (dn.isRootDN()) { return this; } else if (isRootDN()) { return dn; } else { final RDN[] rdns = new RDN[dn.size()]; int i = rdns.length; for (DN next = dn; next.rdn != null; next = next.parent) { rdns[--i] = next.rdn; } DN newDN = this; for (i = 0; i < rdns.length; i++) { newDN = new DN(rdns[i], newDN, null); } return newDN; } } /** * Returns a DN which is an immediate child of this DN and having the * specified RDN. * * @param rdn * The RDN for the child DN. * @return The child DN. * @throws NullPointerException * If {@code rdn} was {@code null}. */ public DN child(final RDN rdn) throws NullPointerException { Validator.ensureNotNull(rdn); return new DN(rdn, this, null); } /** * Returns a DN which is subordinate to this DN and having the additional RDN * components contained in the provided DN decoded using the default schema. * * @param dn * The DN containing the RDN components to be added to this DN. * @return The subordinate DN. * @throws LocalizedIllegalArgumentException * If {@code dn} is not a valid LDAP string representation of a DN. * @throws NullPointerException * If {@code dn} was {@code null}. */ public DN child(final String dn) throws LocalizedIllegalArgumentException, NullPointerException { Validator.ensureNotNull(dn); return child(valueOf(dn)); } /** * {@inheritDoc} */ public int compareTo(final DN dn) { return compareTo(this, dn); } /** * {@inheritDoc} */ @Override public boolean equals(final Object obj) { if (this == obj) { return true; } else if (obj instanceof DN) { DN other = (DN)obj; if(size == other.size()) { if(size == 0) { return true; } if(rdn.equals(other.rdn)) { return parent.equals(other.parent); } } } return false; } /** * {@inheritDoc} */ @Override public int hashCode() { if (size == 0) { return 0; } else { return 31 * parent.hashCode() + rdn.hashCode(); } } /** * Returns {@code true} if this DN is an immediate child of the provided DN. * * @param dn * The potential parent DN. * @return {@code true} if this DN is the immediate child of the provided DN, * otherwise {@code false}. * @throws NullPointerException * If {@code dn} was {@code null}. */ public boolean isChildOf(final DN dn) throws NullPointerException { // If this is the Root DN then parent will be null but this is ok. return dn.equals(parent); } /** * Returns {@code true} if this DN is an immediate child of the provided DN * decoded using the default schema. * * @param dn * The potential parent DN. * @return {@code true} if this DN is the immediate child of the provided DN, * otherwise {@code false}. * @throws LocalizedIllegalArgumentException * If {@code dn} is not a valid LDAP string representation of a DN. * @throws NullPointerException * If {@code dn} was {@code null}. */ public boolean isChildOf(final String dn) throws LocalizedIllegalArgumentException, NullPointerException { // If this is the Root DN then parent will be null but this is ok. return isChildOf(valueOf(dn)); } /** * Returns {@code true} if this DN is the immediate parent of the provided DN. * * @param dn * The potential child DN. * @return {@code true} if this DN is the immediate parent of the provided DN, * otherwise {@code false}. * @throws NullPointerException * If {@code dn} was {@code null}. */ public boolean isParentOf(final DN dn) throws NullPointerException { // If dn is the Root DN then parent will be null but this is ok. return equals(dn.parent); } /** * Returns {@code true} if this DN is the immediate parent of the provided DN. * * @param dn * The potential child DN. * @return {@code true} if this DN is the immediate parent of the provided DN, * otherwise {@code false}. * @throws LocalizedIllegalArgumentException * If {@code dn} is not a valid LDAP string representation of a DN. * @throws NullPointerException * If {@code dn} was {@code null}. */ public boolean isParentOf(final String dn) throws LocalizedIllegalArgumentException, NullPointerException { // If dn is the Root DN then parent will be null but this is ok. return isParentOf(valueOf(dn)); } /** * Returns {@code true} if this DN is the Root DN. * * @return {@code true} if this DN is the Root DN, otherwise {@code false}. */ public boolean isRootDN() { return size == 0; } /** * Returns {@code true} if this DN is subordinate to or equal to the provided * DN. * * @param dn * The potential child DN. * @return {@code true} if this DN is subordinate to or equal to the provided * DN, otherwise {@code false}. * @throws NullPointerException * If {@code dn} was {@code null}. */ public boolean isSubordinateOrEqualTo(final DN dn) throws NullPointerException { if (size < dn.size) { return false; } else if (size == dn.size) { return equals(dn); } else { // dn is a potential superior of this. return parent(size - dn.size).equals(dn); } } /** * Returns {@code true} if this DN is subordinate to or equal to the provided * DN. * * @param dn * The potential child DN. * @return {@code true} if this DN is subordinate to or equal to the provided * DN, otherwise {@code false}. * @throws LocalizedIllegalArgumentException * If {@code dn} is not a valid LDAP string representation of a DN. * @throws NullPointerException * If {@code dn} was {@code null}. */ public boolean isSubordinateOrEqualTo(final String dn) throws LocalizedIllegalArgumentException, NullPointerException { return isSubordinateOrEqualTo(valueOf(dn)); } /** * Returns {@code true} if this DN is superior to or equal to the provided DN. * * @param dn * The potential child DN. * @return {@code true} if this DN is superior to or equal to the provided DN, * otherwise {@code false}. * @throws NullPointerException * If {@code dn} was {@code null}. */ public boolean isSuperiorOrEqualTo(final DN dn) throws NullPointerException { if (size > dn.size) { return false; } else if (size == dn.size) { return equals(dn); } else { // dn is a potential subordinate of this. return dn.parent(dn.size - size).equals(this); } } /** * Returns {@code true} if this DN is superior to or equal to the provided DN. * * @param dn * The potential child DN. * @return {@code true} if this DN is superior to or equal to the provided DN, * otherwise {@code false}. * @throws LocalizedIllegalArgumentException * If {@code dn} is not a valid LDAP string representation of a DN. * @throws NullPointerException * If {@code dn} was {@code null}. */ public boolean isSuperiorOrEqualTo(final String dn) throws LocalizedIllegalArgumentException, NullPointerException { return isSuperiorOrEqualTo(valueOf(dn)); } /** * Returns an iterator of the RDNs contained in this DN. The RDNs will be * returned in the order starting with this DN's RDN, followed by the RDN of * the parent DN, and so on. *

* Attempts to remove RDNs using an iterator's {@code remove()} method are not * permitted and will result in an {@code UnsupportedOperationException} being * thrown. * * @return An iterator of the RDNs contained in this DN. */ public Iterator iterator() { return new Iterator() { private DN dn = DN.this; public boolean hasNext() { return dn.rdn != null; } public RDN next() { if (dn.rdn == null) { throw new NoSuchElementException(); } final RDN rdn = dn.rdn; dn = dn.parent; return rdn; } public void remove() { throw new UnsupportedOperationException(); } }; } /** * Returns the DN which is the immediate parent of this DN, or {@code null} if * this DN is the Root DN. *

* This method is equivalent to: * *

   * parent(1);
   * 
* * @return The DN which is the immediate parent of this DN, or {@code null} if * this DN is the Root DN. */ public DN parent() { return parent; } /** * Returns the DN which is equal to this DN with the specified number of RDNs * removed. Note that if {@code index} is zero then this DN will be returned * (identity). * * @param index * The number of RDNs to be removed. * @return The DN which is equal to this DN with the specified number of RDNs * removed, or {@code null} if the parent of the Root DN is reached. * @throws IllegalArgumentException * If {@code index} is less than zero. */ public DN parent(final int index) throws IllegalArgumentException { // We allow size + 1 so that we can return null as the parent of the // Root DN. Validator.ensureTrue(index >= 0, "index less than zero"); DN parentDN = this; for (int i = 0; parentDN != null && i < index; i++) { parentDN = parentDN.parent; } return parentDN; } /** * Returns the RDN of this DN, or {@code null} if this DN is the Root DN. * * @return The RDN of this DN, or {@code null} if this DN is the Root DN. */ public RDN rdn() { return rdn; } /** * Returns the number of RDN components in this DN. * * @return The number of RDN components in this DN. */ public int size() { return size; } /** * Returns the RFC 4514 string representation of this DN. * * @return The RFC 4514 string representation of this DN. * @see RFC 4514 - Lightweight * Directory Access Protocol (LDAP): String Representation of * Distinguished Names */ @Override public String toString() { // We don't care about potential race conditions here. if (stringValue == null) { final StringBuilder builder = new StringBuilder(); rdn.toString(builder); if (!parent.isRootDN()) { builder.append(','); builder.append(parent.toString()); } stringValue = builder.toString(); } return stringValue; } }