/*
* 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 Sun Microsystems, Inc.
*/
package org.opends.sdk;
import static com.sun.opends.sdk.messages.Messages.*;
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(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(String dn, Schema schema)
throws LocalizedIllegalArgumentException
{
Validator.ensureNotNull(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);
}
// Decodes a DN using the provided reader and schema.
private static DN decode(String dnString, SubstringReader reader,
Schema schema, 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(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(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 String normalizedStringValue = null;
// Private constructor.
private DN(RDN rdn, DN parent, 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(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(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(String dn) throws LocalizedIllegalArgumentException,
NullPointerException
{
Validator.ensureNotNull(dn);
return child(valueOf(dn));
}
/**
* {@inheritDoc}
*/
public int compareTo(DN dn)
{
final String s1 = toNormalizedString();
final String s2 = dn.toNormalizedString();
return s1.compareTo(s2);
}
/**
* {@inheritDoc}
*/
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
else if (obj instanceof DN)
{
final String s1 = toNormalizedString();
final String s2 = ((DN) obj).toNormalizedString();
return s1.equals(s2);
}
else
{
return false;
}
}
/**
* {@inheritDoc}
*/
public int hashCode()
{
final String s = toNormalizedString();
return s.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(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(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(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(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(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(dn.size - 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(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(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(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(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 normalized string representation of this DN.
*
* @return The normalized string representation of this DN.
*/
public String toNormalizedString()
{
if (normalizedStringValue == null)
{
final StringBuilder builder = new StringBuilder();
if (!parent.isRootDN())
{
builder.append(parent.toNormalizedString());
builder.append(',');
}
rdn.toNormalizedString(builder);
normalizedStringValue = builder.toString();
}
return normalizedStringValue;
}
/**
* 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
*/
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;
}
}