/* * 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 legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * 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 legal-notices/CDDLv1_0.txt. * 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. * Portions copyright 2011-2012 ForgeRock AS. */ package org.forgerock.opendj.ldap; import static com.forgerock.opendj.util.StaticUtils.getBytes; import static org.forgerock.opendj.ldap.CoreMessages.ERR_DN_TYPE_NOT_FOUND; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.TreeSet; import java.util.WeakHashMap; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizedIllegalArgumentException; import org.forgerock.opendj.ldap.schema.MatchingRule; import org.forgerock.opendj.ldap.schema.Schema; import org.forgerock.opendj.ldap.schema.Syntax; import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException; import com.forgerock.opendj.util.StaticUtils; import com.forgerock.opendj.util.SubstringReader; import com.forgerock.opendj.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
* // This may contain user input.
* String attributeValue = ...;
*
* // Using the equality filter constructor:
* DN dn = DN.valueOf("ou=people,dc=example,dc=com").child("uid", attributeValue);
*
* // Using a String template:
* String dnTemplate = "uid=%s,ou=people,dc=example,dc=com";
* String dnString = String.format(dnTemplate,
* DN.escapeAttributeValue(attributeValue));
* DN dn = DN.valueOf(dnString);
*
*
* Note: attribute values do not and should not be escaped before
* passing them to constructors like {@link #child(String, Object)}.
* Escaping is only required when creating DN strings.
*
* @param attributeValue
* The attribute value.
* @return The LDAP string representation of the provided filter assertion
* value in a form suitable for substitution directly into a filter
* string.
*/
public static String escapeAttributeValue(final Object attributeValue) {
Validator.ensureNotNull(attributeValue);
final String s = String.valueOf(attributeValue);
final StringBuilder builder = new StringBuilder(s.length());
AVA.escapeAttributeValue(s, builder);
return builder.toString();
}
/**
* Creates a new DN using the provided DN template and unescaped attribute
* values using the default schema. This method first escapes each of the
* attribute values and then substitutes them into the template using
* {@link String#format(String, Object...)}. Finally, the formatted string
* is parsed as an LDAP DN using {@link #valueOf(String)}.
* * This method may be useful in cases where the structure of a DN is not * known at compile time, for example, it may be obtained from a * configuration file. Example usage: * *
* String template = "uid=%s,ou=people,dc=example,dc=com";
* DN dn = DN.format(template, "bjensen");
*
*
* @param template
* The DN template.
* @param attributeValues
* The attribute values to be substituted into the template.
* @return The formatted template parsed as a {@code DN}.
* @throws LocalizedIllegalArgumentException
* If the formatted template is not a valid LDAP string
* representation of a DN.
* @see #escapeAttributeValue(Object)
*/
public static DN format(final String template, final Object... attributeValues) {
return format(template, Schema.getDefaultSchema(), attributeValues);
}
/**
* Creates a new DN using the provided DN template and unescaped attribute
* values using the provided schema. This method first escapes each of the
* attribute values and then substitutes them into the template using
* {@link String#format(String, Object...)}. Finally, the formatted string
* is parsed as an LDAP DN using {@link #valueOf(String)}.
* * This method may be useful in cases where the structure of a DN is not * known at compile time, for example, it may be obtained from a * configuration file. Example usage: * *
* String template = "uid=%s,ou=people,dc=example,dc=com";
* DN dn = DN.format(template, "bjensen");
*
*
* @param template
* The DN template.
* @param schema
* The schema to use when parsing the DN.
* @param attributeValues
* The attribute values to be substituted into the template.
* @return The formatted template parsed as a {@code DN}.
* @throws LocalizedIllegalArgumentException
* If the formatted template is not a valid LDAP string
* representation of a DN.
* @see #escapeAttributeValue(Object)
*/
public static DN format(final String template, final Schema schema,
final Object... attributeValues) {
final String[] attributeValueStrings = new String[attributeValues.length];
for (int i = 0; i < attributeValues.length; i++) {
attributeValueStrings[i] = escapeAttributeValue(attributeValues[i]);
}
final String dnString = String.format(template, (Object[]) attributeValueStrings);
return valueOf(dnString, schema);
}
/**
* 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}.
* @see #format(String, Object...)
*/
public static DN valueOf(final String dn) {
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}.
* @see #format(String, Schema, Object...)
*/
public static DN valueOf(final String dn, final Schema schema) {
Validator.ensureNotNull(dn, schema);
if (dn.length() == 0) {
return ROOT_DN;
}
// First check if DN is already cached.
final Map* Note: the child DN whose RDN is {@link RDN#maxValue()} compares * greater than all other possible child DNs, and may be used to construct * range queries against DN keyed sorted collections such as * {@code SortedSet} and {@code SortedMap}. * * @param rdn * The RDN for the child DN. * @return The child DN. * @throws NullPointerException * If {@code rdn} was {@code null}. * @see RDN#maxValue() */ public DN child(final RDN rdn) { Validator.ensureNotNull(rdn); return new DN(this, rdn, 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) { Validator.ensureNotNull(dn); return child(valueOf(dn)); } /** * Returns a DN which is an immediate child of this DN and with an RDN * having the provided attribute type and value decoded using the default * schema. *
* If {@code attributeValue} is not an instance of {@code ByteString} then * it will be converted using the {@link ByteString#valueOf(Object)} method. * * @param attributeType * The attribute type. * @param attributeValue * The attribute value. * @return The child DN. * @throws UnknownSchemaElementException * If {@code attributeType} was not found in the default schema. * @throws NullPointerException * If {@code attributeType} or {@code attributeValue} was * {@code null}. */ public DN child(final String attributeType, final Object attributeValue) { return child(new RDN(attributeType, attributeValue)); } /** * {@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) { // 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) { // 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 matches the provided base DN and search * scope. * * @param dn * The base DN. * @param scope * The search scope. * @return {@code true} if this DN matches the provided base DN and search * scope, otherwise {@code false}. * @throws NullPointerException * If {@code dn} or {@code scope} was {@code null}. */ public boolean isInScopeOf(DN dn, SearchScope scope) { if (scope == SearchScope.BASE_OBJECT) { // The base DN must equal this DN. return equals(dn); } else if (scope == SearchScope.SINGLE_LEVEL) { // The parent DN must equal the base DN. return isChildOf(dn); } else if (scope == SearchScope.SUBORDINATES) { // This DN must be a descendant of the provided base DN, but // not equal to it. return isSubordinateOrEqualTo(dn) && !equals(dn); } else if (scope == SearchScope.WHOLE_SUBTREE) { // This DN must be a descendant of the provided base DN. return isSubordinateOrEqualTo(dn); } else { // This is a scope that we don't recognize. return false; } } /** * Returns {@code true} if this DN matches the provided base DN and search * scope. * * @param dn * The base DN. * @param scope * The search scope. * @return {@code true} if this DN matches the provided base DN and search * scope, otherwise {@code false}. * @throws LocalizedIllegalArgumentException * If {@code dn} is not a valid LDAP string representation of a * DN. * @throws NullPointerException * If {@code dn} or {@code scope} was {@code null}. */ public boolean isInScopeOf(String dn, SearchScope scope) { return isInScopeOf(valueOf(dn), scope); } /** * 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) { // 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) { // 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) { 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) { 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) { 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) { 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
* This method is equivalent to:
*
*
* dn.localName(0).isRootDN();
* dn.localName(1).equals(rootDN.child(dn.rdn()));
* dn.localName(dn.size()).equals(dn);
*
*
* @param index
* The number of RDNs to be included in the local name.
* @return The DN whose content is the specified number of RDNs from this
* DN.
* @throws IllegalArgumentException
* If {@code index} is less than zero.
*/
public DN localName(final int index) {
Validator.ensureTrue(index >= 0, "index less than zero");
if (index == 0) {
return ROOT_DN;
} else if (index >= size) {
return this;
} else {
final DN localName = new DN(null, rdn, null, index);
DN nextLocalName = localName;
DN lastDN = parent;
for (int i = index - 1; i > 0; i--) {
nextLocalName.parent = new DN(null, lastDN.rdn, null, i);
nextLocalName = nextLocalName.parent;
lastDN = lastDN.parent;
}
nextLocalName.parent = ROOT_DN;
return localName;
}
}
/**
* Returns the DN which is the immediate parent of this DN, or {@code null}
* if this DN is the Root DN.
*
* 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) {
// 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 a copy of this DN whose parent DN, {@code fromDN}, has been
* renamed to the new parent DN, {@code toDN}. If this DN is not subordinate
* or equal to {@code fromDN} then this DN is returned (i.e. the DN is not
* renamed).
*
* @param fromDN
* The old parent DN.
* @param toDN
* The new parent DN.
* @return The renamed DN, or this DN if no renaming was performed.
* @throws NullPointerException
* If {@code fromDN} or {@code toDN} was {@code null}.
*/
public DN rename(final DN fromDN, final DN toDN) {
Validator.ensureNotNull(fromDN, toDN);
if (!isSubordinateOrEqualTo(fromDN)) {
return this;
} else if (equals(fromDN)) {
return toDN;
} else {
return toDN.child(localName(size - fromDN.size));
}
}
/**
* 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;
}
/**
* Returns the normalized string representation of a DN.
*
* @return The normalized string representation of the provided DN.
*/
public String toNormalizedString() {
final StringBuilder builder = new StringBuilder(this.size());
if (rdn() == null) {
return builder.toString();
}
int i = this.size() - 1;
normalizeRDN(builder, parent(i).rdn());
for (i--; i >= 0; i--) {
final RDN rdn = parent(i).rdn();
// Only add a separator if the RDN is not RDN.maxValue().
if (rdn.size() != 0) {
builder.append('\u0000');
}
normalizeRDN(builder, rdn);
}
return builder.toString();
}
/**
* Returns the normalized string representation of a RDN.
*
* @param builder
* The StringBuilder to use to construct the normalized string.
* @param rdn
* The RDN.
* @return The normalized string representation of the provided RDN.
*/
private static StringBuilder normalizeRDN(final StringBuilder builder, final RDN rdn) {
final int sz = rdn.size();
switch (sz) {
case 0:
// Handle RDN.maxValue().
builder.append('\u0001');
break;
case 1:
normalizeAVA(builder, rdn.getFirstAVA());
break;
default:
// Need to sort the AVAs before comparing.
TreeSet