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