001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2009-2010 Sun Microsystems, Inc.
025 *      Portions copyright 2011-2015 ForgeRock AS.
026 */
027
028package org.forgerock.opendj.ldap;
029
030import static com.forgerock.opendj.ldap.CoreMessages.ERR_RDN_TYPE_NOT_FOUND;
031
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Collection;
035import java.util.Iterator;
036import java.util.List;
037import java.util.TreeSet;
038
039import org.forgerock.i18n.LocalizableMessage;
040import org.forgerock.i18n.LocalizedIllegalArgumentException;
041import org.forgerock.opendj.ldap.schema.AttributeType;
042import org.forgerock.opendj.ldap.schema.Schema;
043import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
044
045import com.forgerock.opendj.util.Iterators;
046import com.forgerock.opendj.util.SubstringReader;
047import org.forgerock.util.Reject;
048
049/**
050 * A relative distinguished name (RDN) as defined in RFC 4512 section 2.3 is the
051 * name of an entry relative to its immediate superior. An RDN is composed of an
052 * unordered set of one or more attribute value assertions (AVA) consisting of
053 * an attribute description with zero options and an attribute value. These AVAs
054 * are chosen to match attribute values (each a distinguished value) of the
055 * entry.
056 * <p>
057 * An entry's relative distinguished name must be unique among all immediate
058 * subordinates of the entry's immediate superior (i.e. all siblings).
059 * <p>
060 * The following are examples of string representations of RDNs:
061 *
062 * <pre>
063 * uid=12345
064 * ou=Engineering
065 * cn=Kurt Zeilenga+L=Redwood Shores
066 * </pre>
067 *
068 * The last is an example of a multi-valued RDN; that is, an RDN composed of
069 * multiple AVAs.
070 *
071 * @see <a href="http://tools.ietf.org/html/rfc4512#section-2.3">RFC 4512 -
072 *      Lightweight Directory Access Protocol (LDAP): Directory Information
073 *      Models </a>
074 */
075public final class RDN implements Iterable<AVA>, Comparable<RDN> {
076
077    /** Separator for AVAs. */
078    private static final char AVA_CHAR_SEPARATOR = '+';
079
080    /**
081     * A constant holding a special RDN having zero AVAs and which always
082     * compares greater than any other RDN other than itself.
083     */
084    private static final RDN MAX_VALUE = new RDN(new AVA[0], "");
085
086    /**
087     * Returns a constant containing a special RDN which is greater than any
088     * other RDN other than itself. This RDN may be used in order to perform
089     * range queries on DN keyed collections such as {@code SortedSet}s and
090     * {@code SortedMap}s. For example, the following code can be used to
091     * construct a range whose contents is a sub-tree of entries:
092     *
093     * <pre>
094     * SortedMap<DN, Entry> entries = ...;
095     * DN baseDN = ...;
096     *
097     * // Returns a map containing the baseDN and all of its subordinates.
098     * SortedMap<DN,Entry> subtree = entries.subMap(baseDN, baseDN.child(RDN.maxValue));
099     * </pre>
100     *
101     * @return A constant containing a special RDN which is greater than any
102     *         other RDN other than itself.
103     */
104    public static RDN maxValue() {
105        return MAX_VALUE;
106    }
107
108    /**
109     * Parses the provided LDAP string representation of an RDN using the
110     * default schema.
111     *
112     * @param rdn
113     *            The LDAP string representation of a RDN.
114     * @return The parsed RDN.
115     * @throws LocalizedIllegalArgumentException
116     *             If {@code rdn} is not a valid LDAP string representation of a
117     *             RDN.
118     * @throws NullPointerException
119     *             If {@code rdn} was {@code null}.
120     */
121    public static RDN valueOf(final String rdn) {
122        return valueOf(rdn, Schema.getDefaultSchema());
123    }
124
125    /**
126     * Parses the provided LDAP string representation of a RDN using the
127     * provided schema.
128     *
129     * @param rdn
130     *            The LDAP string representation of a RDN.
131     * @param schema
132     *            The schema to use when parsing the RDN.
133     * @return The parsed RDN.
134     * @throws LocalizedIllegalArgumentException
135     *             If {@code rdn} is not a valid LDAP string representation of a
136     *             RDN.
137     * @throws NullPointerException
138     *             If {@code rdn} or {@code schema} was {@code null}.
139     */
140    public static RDN valueOf(final String rdn, final Schema schema) {
141        final SubstringReader reader = new SubstringReader(rdn);
142        try {
143            return decode(rdn, reader, schema);
144        } catch (final UnknownSchemaElementException e) {
145            final LocalizableMessage message =
146                    ERR_RDN_TYPE_NOT_FOUND.get(rdn, e.getMessageObject());
147            throw new LocalizedIllegalArgumentException(message);
148        }
149    }
150
151    // FIXME: ensure that the decoded RDN does not contain multiple AVAs
152    // with the same type.
153    static RDN decode(final String rdnString, final SubstringReader reader, final Schema schema) {
154        final AVA firstAVA = AVA.decode(reader, schema);
155
156        // Skip over any spaces that might be after the attribute value.
157        reader.skipWhitespaces();
158
159        reader.mark();
160        if (reader.remaining() > 0 && reader.read() == '+') {
161            final List<AVA> avas = new ArrayList<>();
162            avas.add(firstAVA);
163
164            do {
165                avas.add(AVA.decode(reader, schema));
166
167                // Skip over any spaces that might be after the attribute value.
168                reader.skipWhitespaces();
169
170                reader.mark();
171            } while (reader.remaining() > 0 && reader.read() == '+');
172
173            reader.reset();
174            return new RDN(avas.toArray(new AVA[avas.size()]), null);
175        } else {
176            reader.reset();
177            return new RDN(new AVA[] { firstAVA }, null);
178        }
179    }
180
181    /** In original order. */
182    private final AVA[] avas;
183
184    /**
185     * We need to store the original string value if provided in order to
186     * preserve the original whitespace.
187     */
188    private String stringValue;
189
190    /**
191     * Creates a new RDN using the provided attribute type and value.
192     * <p>
193     * If {@code attributeValue} is not an instance of {@code ByteString} then
194     * it will be converted using the {@link ByteString#valueOf(Object)} method.
195     *
196     * @param attributeType
197     *            The attribute type.
198     * @param attributeValue
199     *            The attribute value.
200     * @throws NullPointerException
201     *             If {@code attributeType} or {@code attributeValue} was
202     *             {@code null}.
203     */
204    public RDN(final AttributeType attributeType, final Object attributeValue) {
205        this.avas = new AVA[] { new AVA(attributeType, attributeValue) };
206    }
207
208    /**
209     * Creates a new RDN using the provided attribute type and value decoded
210     * using the default schema.
211     * <p>
212     * If {@code attributeValue} is not an instance of {@code ByteString} then
213     * it will be converted using the {@link ByteString#valueOf(Object)} method.
214     *
215     * @param attributeType
216     *            The attribute type.
217     * @param attributeValue
218     *            The attribute value.
219     * @throws UnknownSchemaElementException
220     *             If {@code attributeType} was not found in the default schema.
221     * @throws NullPointerException
222     *             If {@code attributeType} or {@code attributeValue} was
223     *             {@code null}.
224     */
225    public RDN(final String attributeType, final Object attributeValue) {
226        this.avas = new AVA[] { new AVA(attributeType, attributeValue) };
227    }
228
229    /**
230     * Creates a new RDN using the provided AVAs.
231     *
232     * @param avas
233     *            The attribute-value assertions used to build this RDN.
234     * @throws NullPointerException
235     *             If {@code avas} is {@code null} or contains a null ava.
236     */
237    public RDN(final AVA... avas) {
238        this(avas, null);
239    }
240
241    /**
242     * Creates a new RDN using the provided AVAs.
243     *
244     * @param avas
245     *            The attribute-value assertions used to build this RDN.
246     * @throws NullPointerException
247     *             If {@code ava} is {@code null} or contains null ava.
248     */
249    public RDN(Collection<AVA> avas) {
250        this(avas.toArray(new AVA[avas.size()]), null);
251    }
252
253    private RDN(final AVA[] avas, final String stringValue) {
254        Reject.ifNull(avas);
255        this.avas = avas;
256        this.stringValue = stringValue;
257    }
258
259    /** {@inheritDoc} */
260    @Override
261    public int compareTo(final RDN rdn) {
262        // Identity.
263        if (this == rdn) {
264            return 0;
265        }
266
267        // MAX_VALUE is always greater than any other RDN other than itself.
268        if (this == MAX_VALUE) {
269            return 1;
270        }
271
272        if (rdn == MAX_VALUE) {
273            return -1;
274        }
275
276        // Compare number of AVAs first as this is quick and easy.
277        final int sz1 = avas.length;
278        final int sz2 = rdn.avas.length;
279        if (sz1 != sz2) {
280            return sz1 - sz2 > 0 ? 1 : -1;
281        }
282
283        // Fast path for common case.
284        if (sz1 == 1) {
285            return avas[0].compareTo(rdn.avas[0]);
286        }
287
288        // Need to sort the AVAs before comparing.
289        final AVA[] a1 = new AVA[sz1];
290        System.arraycopy(avas, 0, a1, 0, sz1);
291        Arrays.sort(a1);
292
293        final AVA[] a2 = new AVA[sz1];
294        System.arraycopy(rdn.avas, 0, a2, 0, sz1);
295        Arrays.sort(a2);
296
297        for (int i = 0; i < sz1; i++) {
298            final int result = a1[i].compareTo(a2[i]);
299            if (result != 0) {
300                return result;
301            }
302        }
303
304        return 0;
305    }
306
307    /** {@inheritDoc} */
308    @Override
309    public boolean equals(final Object obj) {
310        if (this == obj) {
311            return true;
312        } else if (obj instanceof RDN) {
313            return compareTo((RDN) obj) == 0;
314        } else {
315            return false;
316        }
317    }
318
319    /**
320     * Returns the attribute value contained in this RDN which is associated
321     * with the provided attribute type, or {@code null} if this RDN does not
322     * include such an attribute value.
323     *
324     * @param attributeType
325     *            The attribute type.
326     * @return The attribute value.
327     */
328    public ByteString getAttributeValue(final AttributeType attributeType) {
329        for (final AVA ava : avas) {
330            if (ava.getAttributeType().equals(attributeType)) {
331                return ava.getAttributeValue();
332            }
333        }
334        return null;
335    }
336
337    /**
338     * Returns the first AVA contained in this RDN.
339     *
340     * @return The first AVA contained in this RDN.
341     */
342    public AVA getFirstAVA() {
343        return avas[0];
344    }
345
346    /** {@inheritDoc} */
347    @Override
348    public int hashCode() {
349        // Avoid an algorithm that requires the AVAs to be sorted.
350        int hash = 0;
351        for (final AVA ava : avas) {
352            hash += ava.hashCode();
353        }
354        return hash;
355    }
356
357    /**
358     * Returns {@code true} if this RDN contains more than one AVA.
359     *
360     * @return {@code true} if this RDN contains more than one AVA, otherwise
361     *         {@code false}.
362     */
363    public boolean isMultiValued() {
364        return avas.length > 1;
365    }
366
367    /**
368     * Returns an iterator of the AVAs contained in this RDN. The AVAs will be
369     * returned in the user provided order.
370     * <p>
371     * Attempts to remove AVAs using an iterator's {@code remove()} method are
372     * not permitted and will result in an {@code UnsupportedOperationException}
373     * being thrown.
374     *
375     * @return An iterator of the AVAs contained in this RDN.
376     */
377    @Override
378    public Iterator<AVA> iterator() {
379        return Iterators.arrayIterator(avas);
380    }
381
382    /**
383     * Returns the number of AVAs in this RDN.
384     *
385     * @return The number of AVAs in this RDN.
386     */
387    public int size() {
388        return avas.length;
389    }
390
391    /**
392     * Returns the RFC 4514 string representation of this RDN.
393     *
394     * @return The RFC 4514 string representation of this RDN.
395     * @see <a href="http://tools.ietf.org/html/rfc4514">RFC 4514 - Lightweight
396     *      Directory Access Protocol (LDAP): String Representation of
397     *      Distinguished Names </a>
398     */
399    @Override
400    public String toString() {
401        // We don't care about potential race conditions here.
402        if (stringValue == null) {
403            final StringBuilder builder = new StringBuilder();
404            avas[0].toString(builder);
405            for (int i = 1; i < avas.length; i++) {
406                builder.append('+');
407                avas[i].toString(builder);
408            }
409            stringValue = builder.toString();
410        }
411        return stringValue;
412    }
413
414    StringBuilder toString(final StringBuilder builder) {
415        return builder.append(this);
416    }
417
418    /**
419     * Returns the normalized byte string representation of this RDN.
420     * <p>
421     * The representation is not a valid RDN.
422     *
423     * @param builder
424     *            The builder to use to construct the normalized byte string.
425     * @return The normalized byte string representation.
426     * @see DN#toNormalizedByteString()
427     */
428    ByteStringBuilder toNormalizedByteString(final ByteStringBuilder builder) {
429        switch (size()) {
430        case 0:
431            // Handle RDN.maxValue().
432            builder.append(DN.NORMALIZED_AVA_SEPARATOR);
433            break;
434        case 1:
435            getFirstAVA().toNormalizedByteString(builder);
436            break;
437        default:
438            Iterator<AVA> it = getSortedAvas();
439            it.next().toNormalizedByteString(builder);
440            while (it.hasNext()) {
441                builder.append(DN.NORMALIZED_AVA_SEPARATOR);
442                it.next().toNormalizedByteString(builder);
443            }
444            break;
445        }
446        return builder;
447    }
448
449    /**
450     * Retrieves a normalized string representation of this RDN.
451     * <p>
452     * This representation is safe to use in an URL or in a file name.
453     * However, it is not a valid RDN and can't be reverted to a valid RDN.
454     *
455     * @return The normalized string representation of this RDN.
456     * @see DN#toNormalizedUrlSafeString()
457     */
458    StringBuilder toNormalizedUrlSafeString(final StringBuilder builder) {
459        switch (size()) {
460        case 0:
461            // Handle RDN.maxValue().
462            builder.append(RDN.AVA_CHAR_SEPARATOR);
463            break;
464        case 1:
465            getFirstAVA().toNormalizedUrlSafe(builder);
466            break;
467        default:
468            Iterator<AVA> it = getSortedAvas();
469            it.next().toNormalizedUrlSafe(builder);
470            while (it.hasNext()) {
471                builder.append(RDN.AVA_CHAR_SEPARATOR);
472                it.next().toNormalizedUrlSafe(builder);
473            }
474            break;
475        }
476        return builder;
477    }
478
479    private Iterator<AVA> getSortedAvas() {
480        TreeSet<AVA> sortedAvas = new TreeSet<>();
481        for (AVA ava : avas) {
482            sortedAvas.add(ava);
483        }
484        return sortedAvas.iterator();
485    }
486}