/* * 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 org.forgerock.opendj.ldap.CoreMessages.ERR_RDN_TYPE_NOT_FOUND; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizedIllegalArgumentException; import org.forgerock.opendj.ldap.schema.AttributeType; import org.forgerock.opendj.ldap.schema.Schema; import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException; import com.forgerock.opendj.util.Iterators; import com.forgerock.opendj.util.SubstringReader; /** * A relative distinguished name (RDN) as defined in RFC 4512 section 2.3 is the * name of an entry relative to its immediate superior. An RDN is composed of an * unordered set of one or more attribute value assertions (AVA) consisting of * an attribute description with zero options and an attribute value. These AVAs * are chosen to match attribute values (each a distinguished value) of the * entry. *

* An entry's relative distinguished name must be unique among all immediate * subordinates of the entry's immediate superior (i.e. all siblings). *

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

 * uid=12345
 * ou=Engineering
 * cn=Kurt Zeilenga+L=Redwood Shores
 * 
* * The last is an example of a multi-valued RDN; that is, an RDN composed of * multiple AVAs. * * @see RFC 4512 - * Lightweight Directory Access Protocol (LDAP): Directory Information * Models */ public final class RDN implements Iterable, Comparable { // A constant holding a special RDN having zero AVAs and which always // compares greater than any other RDN other than itself. private static final RDN MAX_VALUE = new RDN(new AVA[0], ""); /** * Returns a constant containing a special RDN which is greater than any * other RDN other than itself. This RDN may be used in order to perform * range queries on DN keyed collections such as {@code SortedSet}s and * {@code SortedMap}s. For example, the following code can be used to * construct a range whose contents is a sub-tree of entries: * *
     * SortedMap entries = ...;
     * DN baseDN = ...;
     *
     * // Returns a map containing the baseDN and all of its subordinates.
     * SortedMap subtree = entries.subMap(baseDN, baseDN.child(RDN.maxValue));
     * 
* * @return A constant containing a special RDN which is greater than any * other RDN other than itself. */ public static RDN maxValue() { return MAX_VALUE; } /** * Parses the provided LDAP string representation of an RDN using the * default schema. * * @param rdn * The LDAP string representation of a RDN. * @return The parsed RDN. * @throws LocalizedIllegalArgumentException * If {@code rdn} is not a valid LDAP string representation of a * RDN. * @throws NullPointerException * If {@code rdn} was {@code null}. */ public static RDN valueOf(final String rdn) { return valueOf(rdn, Schema.getDefaultSchema()); } /** * Parses the provided LDAP string representation of a RDN using the * provided schema. * * @param rdn * The LDAP string representation of a RDN. * @param schema * The schema to use when parsing the RDN. * @return The parsed RDN. * @throws LocalizedIllegalArgumentException * If {@code rdn} is not a valid LDAP string representation of a * RDN. * @throws NullPointerException * If {@code rdn} or {@code schema} was {@code null}. */ public static RDN valueOf(final String rdn, final Schema schema) { final SubstringReader reader = new SubstringReader(rdn); try { return decode(rdn, reader, schema); } catch (final UnknownSchemaElementException e) { final LocalizableMessage message = ERR_RDN_TYPE_NOT_FOUND.get(rdn, e.getMessageObject()); throw new LocalizedIllegalArgumentException(message); } } // FIXME: ensure that the decoded RDN does not contain multiple AVAs // with the same type. static RDN decode(final String rdnString, final SubstringReader reader, final Schema schema) { final AVA firstAVA = AVA.decode(reader, schema); // Skip over any spaces that might be after the attribute value. reader.skipWhitespaces(); reader.mark(); if (reader.remaining() > 0 && reader.read() == '+') { final List avas = new ArrayList(); avas.add(firstAVA); do { avas.add(AVA.decode(reader, schema)); // Skip over any spaces that might be after the attribute value. reader.skipWhitespaces(); reader.mark(); } while (reader.remaining() > 0 && reader.read() == '+'); reader.reset(); return new RDN(avas.toArray(new AVA[avas.size()]), null); } else { reader.reset(); return new RDN(new AVA[] { firstAVA }, null); } } // In original order. private final AVA[] avas; // We need to store the original string value if provided in order to // preserve the original whitespace. private String stringValue; /** * Creates a new RDN using the provided attribute type and value. *

* 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. * @throws NullPointerException * If {@code attributeType} or {@code attributeValue} was * {@code null}. */ public RDN(final AttributeType attributeType, final Object attributeValue) { this.avas = new AVA[] { new AVA(attributeType, attributeValue) }; } /** * Creates a new RDN using 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. * @throws UnknownSchemaElementException * If {@code attributeType} was not found in the default schema. * @throws NullPointerException * If {@code attributeType} or {@code attributeValue} was * {@code null}. */ public RDN(final String attributeType, final Object attributeValue) { this.avas = new AVA[] { new AVA(attributeType, attributeValue) }; } private RDN(final AVA[] avas, final String stringValue) { this.avas = avas; this.stringValue = stringValue; } /** * {@inheritDoc} */ public int compareTo(final RDN rdn) { // Identity. if (this == rdn) { return 0; } // MAX_VALUE is always greater than any other RDN other than itself. if (this == MAX_VALUE) { return 1; } if (rdn == MAX_VALUE) { return -1; } // Compare number of AVAs first as this is quick and easy. final int sz1 = avas.length; final int sz2 = rdn.avas.length; if (sz1 != sz2) { return sz1 - sz2 > 0 ? 1 : -1; } // Fast path for common case. if (sz1 == 1) { return avas[0].compareTo(rdn.avas[0]); } // Need to sort the AVAs before comparing. final AVA[] a1 = new AVA[sz1]; System.arraycopy(avas, 0, a1, 0, sz1); Arrays.sort(a1); final AVA[] a2 = new AVA[sz1]; System.arraycopy(rdn.avas, 0, a2, 0, sz1); Arrays.sort(a2); for (int i = 0; i < sz1; i++) { final int result = a1[i].compareTo(a2[i]); if (result != 0) { return result; } } return 0; } /** * {@inheritDoc} */ @Override public boolean equals(final Object obj) { if (this == obj) { return true; } else if (obj instanceof RDN) { return compareTo((RDN) obj) == 0; } else { return false; } } /** * Returns the attribute value contained in this RDN which is associated * with the provided attribute type, or {@code null} if this RDN does not * include such an attribute value. * * @param attributeType * The attribute type. * @return The attribute value. */ public ByteString getAttributeValue(final AttributeType attributeType) { for (final AVA ava : avas) { if (ava.getAttributeType().equals(attributeType)) { return ava.getAttributeValue(); } } return null; } /** * Returns the first AVA contained in this RDN. * * @return The first AVA contained in this RDN. */ public AVA getFirstAVA() { return avas[0]; } /** * {@inheritDoc} */ @Override public int hashCode() { // Avoid an algorithm that requires the AVAs to be sorted. int hash = 0; for (final AVA ava : avas) { hash += ava.hashCode(); } return hash; } /** * Returns {@code true} if this RDN contains more than one AVA. * * @return {@code true} if this RDN contains more than one AVA, otherwise * {@code false}. */ public boolean isMultiValued() { return avas.length > 1; } /** * Returns an iterator of the AVAs contained in this RDN. The AVAs will be * returned in the user provided order. *

* Attempts to remove AVAs using an iterator's {@code remove()} method are * not permitted and will result in an {@code UnsupportedOperationException} * being thrown. * * @return An iterator of the AVAs contained in this RDN. */ public Iterator iterator() { return Iterators.arrayIterator(avas); } /** * Returns the number of AVAs in this RDN. * * @return The number of AVAs in this RDN. */ public int size() { return avas.length; } /** * Returns the RFC 4514 string representation of this RDN. * * @return The RFC 4514 string representation of this RDN. * @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(); avas[0].toString(builder); for (int i = 1; i < avas.length; i++) { builder.append('+'); avas[i].toString(builder); } stringValue = builder.toString(); } return stringValue; } StringBuilder toString(final StringBuilder builder) { return builder.append(toString()); } }