/* * 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 static com.sun.opends.sdk.util.StaticUtils.*; import java.util.*; import org.opends.sdk.schema.*; import com.sun.opends.sdk.util.*; /** * 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 { /** * An attribute value assertion (AVA) as defined in RFC 4512 section * 2.3 consists of an attribute description with zero options and an * attribute value. */ public static final class AVA implements Comparable { private final AttributeType attributeType; private final ByteString attributeValue; /** * Creates a new attribute value assertion (AVA) using the provided * attribute type and value. * * @param attributeType * The attribute type. * @param attributeValue * The attribute value. * @throws NullPointerException * If {@code attributeType} or {@code attributeValue} was * {@code null}. */ public AVA(AttributeType attributeType, ByteString attributeValue) throws NullPointerException { Validator.ensureNotNull(attributeType, attributeValue); this.attributeType = attributeType; this.attributeValue = attributeValue; } /** * Creates a new attribute value assertion (AVA) 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 AVA(String attributeType, Object attributeValue) throws UnknownSchemaElementException, NullPointerException { Validator.ensureNotNull(attributeType, attributeValue); this.attributeType = Schema.getDefaultSchema().getAttributeType( attributeType); this.attributeValue = ByteString.valueOf(attributeValue); } /** * {@inheritDoc} */ public int compareTo(AVA ava) { int result = attributeType.compareTo(ava.attributeType); if (result == 0) { final ByteString nv1 = getNormalizeValue(); final ByteString nv2 = ava.getNormalizeValue(); result = nv1.compareTo(nv2); } return result; } /** * {@inheritDoc} */ public boolean equals(Object obj) { if (this == obj) { return true; } else if (obj instanceof AVA) { return compareTo((AVA) obj) == 0; } else { return false; } } /** * Returns the attribute type associated with this AVA. * * @return The attribute type associated with this AVA. */ public AttributeType getAttributeType() { return attributeType; } /** * Returns the attribute value associated with this AVA. * * @return The attribute value associated with this AVA. */ public ByteString getAttributeValue() { return attributeValue; } /** * {@inheritDoc} */ public int hashCode() { return attributeType.hashCode() * 31 + getNormalizeValue().hashCode(); } /** * {@inheritDoc} */ public String toString() { final StringBuilder builder = new StringBuilder(); return toString(builder).toString(); } private ByteString getNormalizeValue() { final MatchingRule matchingRule = attributeType .getEqualityMatchingRule(); if (matchingRule != null) { try { return matchingRule.normalizeAttributeValue(attributeValue); } catch (final DecodeException de) { // Ignore - we'll drop back to the user provided value. } } return attributeValue; } private StringBuilder toNormalizedString(StringBuilder builder) { return toString(builder, true); } private StringBuilder toString(StringBuilder builder) { return toString(builder, false); } private StringBuilder toString(StringBuilder builder, boolean normalize) { final ByteString value = normalize ? getNormalizeValue() : attributeValue; if (!attributeType.getNames().iterator().hasNext()) { builder.append(attributeType.getOID()); builder.append("=#"); StaticUtils.toHex(value, builder); } else { final String name = attributeType.getNameOrOID(); if (normalize) { // Normalizing. StaticUtils.toLowerCase(name, builder); } else { builder.append(name); } builder.append("="); final Syntax syntax = attributeType.getSyntax(); if (!syntax.isHumanReadable()) { builder.append("#"); StaticUtils.toHex(value, builder); } else { final String str = value.toString(); char c; for (int si = 0; si < str.length(); si++) { c = str.charAt(si); if (c == ' ' || c == '#' || c == '"' || c == '+' || c == ',' || c == ';' || c == '<' || c == '=' || c == '>' || c == '\\' || c == '\u0000') { builder.append('\\'); } builder.append(c); } } } return builder; } } private static final char[] SPECIAL_CHARS = new char[] { '\"', '+', ',', ';', '<', '>', ' ', '#', '=', '\\' }; private static final char[] DELIMITER_CHARS = new char[] { '+', ',', ';' }; private static final char[] DQUOTE_CHAR = new char[] { '\"' }; private static final Comparator ATV_COMPARATOR = new Comparator() { public int compare(AVA o1, AVA o2) { return o1.getAttributeType().compareTo(o2.getAttributeType()); } }; /** * 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(String rdn) throws LocalizedIllegalArgumentException { 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(String rdn, Schema schema) throws LocalizedIllegalArgumentException { 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); } } private static AVA readAttributeTypeAndValue(SubstringReader reader, Schema schema) throws LocalizedIllegalArgumentException, UnknownSchemaElementException { // Skip over any spaces at the beginning. reader.skipWhitespaces(); final AttributeType attribute = readDNAttributeName(reader, schema); // Make sure that we're not at the end of the DN string because // that would be invalid. if (reader.remaining() == 0) { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME .get(reader.getString(), attribute.getNameOrOID()); throw new LocalizedIllegalArgumentException(message); } // The next character must be an equal sign. If it is not, then // that's an error. char c; if ((c = reader.read()) != '=') { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_NO_EQUAL.get(reader .getString(), attribute.getNameOrOID(), c); throw new LocalizedIllegalArgumentException(message); } // Skip over any spaces after the equal sign. reader.skipWhitespaces(); // Parse the value for this RDN component. final ByteString value = readDNAttributeValue(reader); return new AVA(attribute, value); } private static AttributeType readDNAttributeName( SubstringReader reader, Schema schema) throws LocalizedIllegalArgumentException, UnknownSchemaElementException { int length = 1; reader.mark(); // The next character must be either numeric (for an OID) or // alphabetic (for // an attribute description). char c = reader.read(); if (isDigit(c)) { boolean lastWasPeriod = false; do { if (c == '.') { if (lastWasPeriod) { final LocalizableMessage message = ERR_ATTR_SYNTAX_OID_CONSECUTIVE_PERIODS .get(reader.getString(), reader.pos() - 1); throw new LocalizedIllegalArgumentException(message); } else { lastWasPeriod = true; } } else if (!isDigit(c)) { // This must have been an illegal character. final LocalizableMessage message = ERR_ATTR_SYNTAX_OID_ILLEGAL_CHARACTER .get(reader.getString(), reader.pos() - 1); throw new LocalizedIllegalArgumentException(message); } else { lastWasPeriod = false; } length++; } while ((c = reader.read()) != '='); } if (isAlpha(c)) { // This must be an attribute description. In this case, we will // only // accept alphabetic characters, numeric digits, and the hyphen. while ((c = reader.read()) != '=') { if (length == 0 && !isAlpha(c)) { // This is an illegal character. final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR .get(reader.getString(), c, reader.pos() - 1); throw new LocalizedIllegalArgumentException(message); } if (!isAlpha(c) && !isDigit(c) && c != '-') { // This is an illegal character. final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR .get(reader.getString(), c, reader.pos() - 1); throw new LocalizedIllegalArgumentException(message); } length++; } } else { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get( reader.getString(), c, reader.pos() - 1); throw new LocalizedIllegalArgumentException(message); } reader.reset(); // Return the position of the first non-space character after the // token. return schema.getAttributeType(reader.read(length)); } private static ByteString readDNAttributeValue(SubstringReader reader) throws LocalizedIllegalArgumentException { // All leading spaces have already been stripped so we can start // reading the value. However, it may be empty so check for that. if (reader.remaining() == 0) { return ByteString.empty(); } reader.mark(); // Look at the first character. If it is an octothorpe (#), then // that means that the value should be a hex string. char c = reader.read(); int length = 0; if (c == '#') { // The first two characters must be hex characters. reader.mark(); if (reader.remaining() < 2) { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT .get(reader.getString()); throw new LocalizedIllegalArgumentException(message); } for (int i = 0; i < 2; i++) { c = reader.read(); if (isHexDigit(c)) { length++; } else { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT .get(reader.getString(), c); throw new LocalizedIllegalArgumentException(message); } } // The rest of the value must be a multiple of two hex // characters. The end of the value may be designated by the // end of the DN, a comma or semicolon, or a space. while (reader.remaining() > 0) { c = reader.read(); if (isHexDigit(c)) { length++; if (reader.remaining() > 0) { c = reader.read(); if (isHexDigit(c)) { length++; } else { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT .get(reader.getString(), c); throw new LocalizedIllegalArgumentException(message); } } else { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT .get(reader.getString()); throw new LocalizedIllegalArgumentException(message); } } else if ((c == ' ') || (c == ',') || (c == ';')) { // This denotes the end of the value. break; } else { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT .get(reader.getString(), c); throw new LocalizedIllegalArgumentException(message); } } // At this point, we should have a valid hex string. Convert it // to a byte array and set that as the value of the provided // octet string. try { reader.reset(); return ByteString .wrap(hexStringToByteArray(reader.read(length))); } catch (final Exception e) { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE .get(reader.getString(), String.valueOf(e)); throw new LocalizedIllegalArgumentException(message); } } // If the first character is a quotation mark, then the value // should continue until the corresponding closing quotation mark. else if (c == '"') { try { return StaticUtils.evaluateEscapes(reader, DQUOTE_CHAR, false); } catch (final DecodeException e) { throw new LocalizedIllegalArgumentException(e .getMessageObject()); } } // Otherwise, use general parsing to find the end of the value. else { reader.reset(); ByteString bytes; try { bytes = StaticUtils.evaluateEscapes(reader, SPECIAL_CHARS, DELIMITER_CHARS, true); } catch (final DecodeException e) { throw new LocalizedIllegalArgumentException(e .getMessageObject()); } if (bytes.length() == 0) { // We don't allow an empty attribute value. final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR .get(reader.getString(), reader.pos()); throw new LocalizedIllegalArgumentException(message); } return bytes; } } // FIXME: ensure that the decoded RDN does not contain multiple AVAs // with the same type. static RDN decode(String rdnString, SubstringReader reader, Schema schema) throws LocalizedIllegalArgumentException, UnknownSchemaElementException { final AVA firstAVA = readAttributeTypeAndValue(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(readAttributeTypeAndValue(reader, schema)); // Skip over any spaces that might be after the attribute value. reader.skipWhitespaces(); reader.mark(); } while (reader.read() == '+'); reader.reset(); return new RDN(avas.toArray(new AVA[avas.size()]), rdnString); } else { reader.reset(); return new RDN(new AVA[] { firstAVA }, rdnString); } } // 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. * * @param attributeType * The attribute type. * @param attributeValue * The attribute value. * @throws NullPointerException * If {@code attributeType} or {@code attributeValue} was * {@code null}. */ public RDN(AttributeType attributeType, ByteString attributeValue) throws NullPointerException { 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(String attributeType, Object attributeValue) throws UnknownSchemaElementException, NullPointerException { this.avas = new AVA[] { new AVA(attributeType, attributeValue) }; } private RDN(AVA[] avas, String stringValue) { this.avas = avas; this.stringValue = stringValue; } /** * {@inheritDoc} */ public int compareTo(RDN rdn) { final int sz1 = avas.length; final int sz2 = rdn.avas.length; if (sz1 != sz2) { return sz1 - sz2; } 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, ATV_COMPARATOR); final AVA[] a2 = new AVA[sz1]; System.arraycopy(rdn.avas, 0, a2, 0, sz1); Arrays.sort(a2, ATV_COMPARATOR); for (int i = 0; i < sz1; i++) { final int result = a1[i].compareTo(a2[i]); if (result != 0) { return result; } } return 0; } /** * {@inheritDoc} */ public boolean equals(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(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} */ public int hashCode() { // Avoid an algorithm that requires the AVAs to be sorted. int hash = 0; for (int i = 0; i < avas.length; i++) { hash += avas[i].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 */ 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 toNormalizedString(StringBuilder builder) { final int sz = avas.length; if (sz == 1) { return avas[0].toNormalizedString(builder); } else { // Need to sort the AVAs before comparing. final AVA[] a = new AVA[sz]; System.arraycopy(avas, 0, a, 0, sz); Arrays.sort(a, ATV_COMPARATOR); a[0].toString(builder); for (int i = 1; i < sz; i++) { builder.append(','); a[i].toNormalizedString(builder); } return builder; } } StringBuilder toString(StringBuilder builder) { return builder.append(toString()); } }