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 java.util.Iterator;
021import java.util.LinkedHashMap;
022import java.util.LinkedList;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.NoSuchElementException;
026import java.util.UUID;
027
028import org.forgerock.i18n.LocalizedIllegalArgumentException;
029import org.forgerock.opendj.ldap.schema.CoreSchema;
030import org.forgerock.opendj.ldap.schema.Schema;
031import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
032import org.forgerock.util.Pair;
033import org.forgerock.util.Reject;
034
035import com.forgerock.opendj.util.SubstringReader;
036
037import static com.forgerock.opendj.ldap.CoreMessages.*;
038
039/**
040 * A distinguished name (DN) as defined in RFC 4512 section 2.3 is the
041 * concatenation of its relative distinguished name (RDN) and its immediate
042 * superior's DN. A DN unambiguously refers to an entry in the Directory.
043 * <p>
044 * The following are examples of string representations of DNs:
045 *
046 * <pre>
047 * UID=nobody@example.com,DC=example,DC=com CN=John
048 * Smith,OU=Sales,O=ACME Limited,L=Moab,ST=Utah,C=US
049 * </pre>
050 *
051 * @see <a href="http://tools.ietf.org/html/rfc4512#section-2.3">RFC 4512 -
052 *      Lightweight Directory Access Protocol (LDAP): Directory Information
053 *      Models </a>
054 */
055public final class DN implements Iterable<RDN>, Comparable<DN> {
056    static final byte NORMALIZED_RDN_SEPARATOR = 0x00;
057    static final byte NORMALIZED_AVA_SEPARATOR = 0x01;
058    static final byte NORMALIZED_ESC_BYTE = 0x02;
059
060    static final char RDN_CHAR_SEPARATOR = ',';
061    static final char AVA_CHAR_SEPARATOR = '+';
062
063    private static final DN ROOT_DN = new DN(CoreSchema.getInstance(), null, null);
064
065    /**
066     * This is the size of the per-thread per-schema DN cache. We should
067     * be conservative here in case there are many threads. We will only
068     * cache parent DNs, so there's no need for it to be big.
069     */
070    private static final int DN_CACHE_SIZE = 32;
071
072    private static final ThreadLocal<Map<String, DN>> CACHE = new ThreadLocal<Map<String, DN>>() {
073        @SuppressWarnings("serial")
074        @Override
075        protected Map<String, DN> initialValue() {
076            return new LinkedHashMap<String, DN>(DN_CACHE_SIZE, 0.75f, true) {
077                @Override
078                protected boolean removeEldestEntry(Entry<String, DN> eldest) {
079                    return size() > DN_CACHE_SIZE;
080                }
081            };
082        }
083    };
084
085    /**
086     * Returns the LDAP string representation of the provided DN attribute value
087     * in a form suitable for substitution directly into a DN string. This
088     * method may be useful in cases where a DN is to be constructed from a DN
089     * template using {@code String#format(String, Object...)}. The following
090     * example illustrates two approaches to constructing a DN:
091     *
092     * <pre>
093     * // This may contain user input.
094     * String attributeValue = ...;
095     *
096     * // Using the equality filter constructor:
097     * DN dn = DN.valueOf("ou=people,dc=example,dc=com").child("uid", attributeValue);
098     *
099     * // Using a String template:
100     * String dnTemplate = "uid=%s,ou=people,dc=example,dc=com";
101     * String dnString = String.format(dnTemplate,
102     *                                 DN.escapeAttributeValue(attributeValue));
103     * DN dn = DN.valueOf(dnString);
104     * </pre>
105     *
106     * <b>Note:</b> attribute values do not and should not be escaped before
107     * passing them to constructors like {@link #child(String, Object)}.
108     * Escaping is only required when creating DN strings.
109     *
110     * @param attributeValue
111     *            The attribute value.
112     * @return The LDAP string representation of the provided filter assertion
113     *         value in a form suitable for substitution directly into a filter
114     *         string.
115     */
116    public static String escapeAttributeValue(final Object attributeValue) {
117        Reject.ifNull(attributeValue);
118        final String s = String.valueOf(attributeValue);
119        final StringBuilder builder = new StringBuilder(s.length());
120        AVA.escapeAttributeValue(s, builder);
121        return builder.toString();
122    }
123
124    /**
125     * Creates a new DN using the provided DN template and unescaped attribute
126     * values using the default schema. This method first escapes each of the
127     * attribute values and then substitutes them into the template using
128     * {@link String#format(String, Object...)}. Finally, the formatted string
129     * is parsed as an LDAP DN using {@link #valueOf(String)}.
130     * <p>
131     * This method may be useful in cases where the structure of a DN is not
132     * known at compile time, for example, it may be obtained from a
133     * configuration file. Example usage:
134     *
135     * <pre>
136     * String template = &quot;uid=%s,ou=people,dc=example,dc=com&quot;;
137     * DN dn = DN.format(template, &quot;bjensen&quot;);
138     * </pre>
139     *
140     * @param template
141     *            The DN template.
142     * @param attributeValues
143     *            The attribute values to be substituted into the template.
144     * @return The formatted template parsed as a {@code DN}.
145     * @throws LocalizedIllegalArgumentException
146     *             If the formatted template is not a valid LDAP string
147     *             representation of a DN.
148     * @see #escapeAttributeValue(Object)
149     */
150    public static DN format(final String template, final Object... attributeValues) {
151        return format(template, Schema.getDefaultSchema(), attributeValues);
152    }
153
154    /**
155     * Creates a new DN using the provided DN template and unescaped attribute
156     * values using the provided schema. This method first escapes each of the
157     * attribute values and then substitutes them into the template using
158     * {@link String#format(String, Object...)}. Finally, the formatted string
159     * is parsed as an LDAP DN using {@link #valueOf(String)}.
160     * <p>
161     * This method may be useful in cases where the structure of a DN is not
162     * known at compile time, for example, it may be obtained from a
163     * configuration file. Example usage:
164     *
165     * <pre>
166     * String template = &quot;uid=%s,ou=people,dc=example,dc=com&quot;;
167     * DN dn = DN.format(template, &quot;bjensen&quot;);
168     * </pre>
169     *
170     * @param template
171     *            The DN template.
172     * @param schema
173     *            The schema to use when parsing the DN.
174     * @param attributeValues
175     *            The attribute values to be substituted into the template.
176     * @return The formatted template parsed as a {@code DN}.
177     * @throws LocalizedIllegalArgumentException
178     *             If the formatted template is not a valid LDAP string
179     *             representation of a DN.
180     * @see #escapeAttributeValue(Object)
181     */
182    public static DN format(final String template, final Schema schema,
183            final Object... attributeValues) {
184        final String[] attributeValueStrings = new String[attributeValues.length];
185        for (int i = 0; i < attributeValues.length; i++) {
186            attributeValueStrings[i] = escapeAttributeValue(attributeValues[i]);
187        }
188        final String dnString = String.format(template, (Object[]) attributeValueStrings);
189        return valueOf(dnString, schema);
190    }
191
192    /**
193     * Returns the Root DN. The Root DN does not contain and RDN components and
194     * is superior to all other DNs.
195     *
196     * @return The Root DN.
197     */
198    public static DN rootDN() {
199        return ROOT_DN;
200    }
201
202    /**
203     * Parses the provided LDAP string representation of a DN using the default schema.
204     *
205     * @param dn
206     *            The LDAP string representation of a DN.
207     * @return The parsed DN.
208     * @throws LocalizedIllegalArgumentException
209     *             If {@code dn} is not a valid LDAP string representation of a DN.
210     * @throws NullPointerException
211     *             If {@code dn} was {@code null}.
212     * @see #format(String, Object...)
213     */
214    public static DN valueOf(final String dn) {
215        return valueOf(dn, Schema.getDefaultSchema());
216    }
217
218    /**
219     * Parses the provided LDAP string representation of a DN using the provided schema.
220     *
221     * @param dn
222     *            The LDAP string representation of a DN.
223     * @param schema
224     *            The schema to use when parsing the DN.
225     * @return The parsed DN.
226     * @throws LocalizedIllegalArgumentException
227     *             If {@code dn} is not a valid LDAP string representation of a DN.
228     * @throws NullPointerException
229     *             If {@code dn} or {@code schema} was {@code null}.
230     * @see #format(String, Schema, Object...)
231     */
232    public static DN valueOf(final String dn, final Schema schema) {
233        Reject.ifNull(dn, schema);
234        if (dn.length() == 0) {
235            return ROOT_DN;
236        }
237
238        // First check if DN is already cached.
239        final Map<String, DN> cache = CACHE.get();
240        final DN cachedDN = cache.get(dn);
241        if (cachedDN != null && cachedDN.schema == schema) {
242            return cachedDN;
243        }
244
245        // Not in cache so decode.
246        return decode(new SubstringReader(dn), schema, cache);
247    }
248
249    /**
250     * Parses the provided LDAP string representation of a DN using the default schema.
251     *
252     * @param dn
253     *            The LDAP byte string representation of a DN.
254     * @return The parsed DN.
255     * @throws LocalizedIllegalArgumentException
256     *             If {@code dn} is not a valid LDAP byte string representation of a DN.
257     * @throws NullPointerException
258     *             If {@code dn} was {@code null}.
259     */
260    public static DN valueOf(ByteString dn) {
261        return DN.valueOf(dn.toString());
262    }
263
264    /** Decodes a DN using the provided reader and schema. */
265    private static DN decode(final SubstringReader reader, final Schema schema, final Map<String, DN> cache) {
266        reader.skipWhitespaces();
267        if (reader.remaining() == 0) {
268            return ROOT_DN;
269        }
270
271        final RDN rdn;
272        try {
273            rdn = RDN.decode(reader, schema);
274        } catch (final UnknownSchemaElementException e) {
275            throw new LocalizedIllegalArgumentException(
276                    ERR_DN_TYPE_NOT_FOUND.get(reader.getString(), e.getMessageObject()));
277        }
278
279        LinkedList<Pair<Integer, RDN>> parentRDNs = null;
280        DN parent = null;
281        while (reader.remaining() > 0 && reader.read() == ',') {
282            reader.skipWhitespaces();
283            if (reader.remaining() == 0) {
284                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME.get(reader.getString()));
285            }
286            reader.mark();
287            final String parentString = reader.read(reader.remaining());
288            parent = cache.get(parentString);
289            if (parent != null) {
290                break;
291            }
292            reader.reset();
293            if (parentRDNs == null) {
294                parentRDNs = new LinkedList<>();
295            }
296            parentRDNs.add(Pair.of(reader.pos(), RDN.decode(reader, schema)));
297        }
298        if (parent == null) {
299            parent = ROOT_DN;
300        }
301
302        if (parentRDNs != null) {
303            Iterator<Pair<Integer, RDN>> iter = parentRDNs.descendingIterator();
304            int parentsLeft = parentRDNs.size();
305            while (iter.hasNext()) {
306                Pair<Integer, RDN> parentRDN = iter.next();
307                parent = new DN(schema, parent, parentRDN.getSecond());
308                if (parentsLeft-- < DN_CACHE_SIZE) {
309                    cache.put(reader.getString().substring(parentRDN.getFirst()), parent);
310                }
311            }
312        }
313        return new DN(schema, parent, rdn);
314    }
315
316    private final RDN rdn;
317    private DN parent;
318    private final int size;
319    private int hashCode = -1;
320
321    /**
322     * The normalized byte string representation of this DN, which is not
323     * a valid DN and is not reversible to a valid DN.
324     */
325    private ByteString normalizedDN;
326
327    /**
328     * The RFC 4514 string representation of this DN. A value of {@code null}
329     * indicates that the value needs to be computed lazily.
330     */
331    private String stringValue;
332
333    /** The schema used to create this DN. */
334    private final Schema schema;
335
336    /** Private constructor. */
337    private DN(final Schema schema, final DN parent, final RDN rdn) {
338        this(schema, parent, rdn, parent != null ? parent.size + 1 : 0);
339    }
340
341    /** Private constructor. */
342    private DN(final Schema schema, final DN parent, final RDN rdn, final int size) {
343        this.schema = schema;
344        this.parent = parent;
345        this.rdn = rdn;
346        this.size = size;
347        this.stringValue = rdn == null ? "" : null;
348    }
349
350    /**
351     * Returns a DN which is subordinate to this DN and having the additional
352     * RDN components contained in the provided DN.
353     *
354     * @param dn
355     *            The DN containing the RDN components to be added to this DN.
356     * @return The subordinate DN.
357     * @throws NullPointerException
358     *             If {@code dn} was {@code null}.
359     */
360    public DN child(final DN dn) {
361        Reject.ifNull(dn);
362
363        if (dn.isRootDN()) {
364            return this;
365        } else if (isRootDN()) {
366            return dn;
367        } else {
368            final RDN[] rdns = new RDN[dn.size()];
369            int i = rdns.length;
370            for (DN next = dn; next.rdn != null; next = next.parent) {
371                rdns[--i] = next.rdn;
372            }
373            DN newDN = this;
374            for (i = 0; i < rdns.length; i++) {
375                newDN = new DN(this.schema, newDN, rdns[i]);
376            }
377            return newDN;
378        }
379    }
380
381    /**
382     * Returns a DN which is an immediate child of this DN and having the
383     * specified RDN.
384     * <p>
385     * <b>Note:</b> the child DN whose RDN is {@link RDN#maxValue()} compares
386     * greater than all other possible child DNs, and may be used to construct
387     * range queries against DN keyed sorted collections such as
388     * {@code SortedSet} and {@code SortedMap}.
389     *
390     * @param rdn
391     *            The RDN for the child DN.
392     * @return The child DN.
393     * @throws NullPointerException
394     *             If {@code rdn} was {@code null}.
395     * @see RDN#maxValue()
396     */
397    public DN child(final RDN rdn) {
398        Reject.ifNull(rdn);
399        return new DN(this.schema, this, rdn);
400    }
401
402    /**
403     * Returns a DN which is subordinate to this DN and having the additional
404     * RDN components contained in the provided DN decoded using the default
405     * schema.
406     *
407     * @param dn
408     *            The DN containing the RDN components to be added to this DN.
409     * @return The subordinate DN.
410     * @throws LocalizedIllegalArgumentException
411     *             If {@code dn} is not a valid LDAP string representation of a
412     *             DN.
413     * @throws NullPointerException
414     *             If {@code dn} was {@code null}.
415     */
416    public DN child(final String dn) {
417        Reject.ifNull(dn);
418        return child(valueOf(dn));
419    }
420
421    /**
422     * Returns a DN which is an immediate child of this DN and with an RDN
423     * having the provided attribute type and value decoded using the default
424     * schema.
425     * <p>
426     * If {@code attributeValue} is not an instance of {@code ByteString} then
427     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
428     *
429     * @param attributeType
430     *            The attribute type.
431     * @param attributeValue
432     *            The attribute value.
433     * @return The child DN.
434     * @throws UnknownSchemaElementException
435     *             If {@code attributeType} was not found in the default schema.
436     * @throws NullPointerException
437     *             If {@code attributeType} or {@code attributeValue} was
438     *             {@code null}.
439     */
440    public DN child(final String attributeType, final Object attributeValue) {
441        return child(new RDN(attributeType, attributeValue));
442    }
443
444    @Override
445    public int compareTo(final DN dn) {
446        return toNormalizedByteString().compareTo(dn.toNormalizedByteString());
447    }
448
449    @Override
450    public boolean equals(final Object obj) {
451        if (this == obj) {
452            return true;
453        }
454        if (obj instanceof DN) {
455            DN otherDN = (DN) obj;
456            return toNormalizedByteString().equals(otherDN.toNormalizedByteString());
457        }
458        return false;
459    }
460
461    @Override
462    public int hashCode() {
463        if (hashCode == -1) {
464            hashCode = toNormalizedByteString().hashCode();
465        }
466        return hashCode;
467    }
468
469    /**
470     * Returns {@code true} if this DN is an immediate child of the provided DN.
471     *
472     * @param dn
473     *            The potential parent DN.
474     * @return {@code true} if this DN is the immediate child of the provided
475     *         DN, otherwise {@code false}.
476     * @throws NullPointerException
477     *             If {@code dn} was {@code null}.
478     */
479    public boolean isChildOf(final DN dn) {
480        // If this is the Root DN then parent will be null but this is ok.
481        return dn.equals(parent);
482    }
483
484    /**
485     * Returns {@code true} if this DN is an immediate child of the provided DN
486     * decoded using the default schema.
487     *
488     * @param dn
489     *            The potential parent DN.
490     * @return {@code true} if this DN is the immediate child of the provided
491     *         DN, otherwise {@code false}.
492     * @throws LocalizedIllegalArgumentException
493     *             If {@code dn} is not a valid LDAP string representation of a
494     *             DN.
495     * @throws NullPointerException
496     *             If {@code dn} was {@code null}.
497     */
498    public boolean isChildOf(final String dn) {
499        // If this is the Root DN then parent will be null but this is ok.
500        return isChildOf(valueOf(dn));
501    }
502
503    /**
504     * Returns {@code true} if this DN matches the provided base DN and search
505     * scope.
506     *
507     * @param dn
508     *            The base DN.
509     * @param scope
510     *            The search scope.
511     * @return {@code true} if this DN matches the provided base DN and search
512     *         scope, otherwise {@code false}.
513     * @throws NullPointerException
514     *             If {@code dn} or {@code scope} was {@code null}.
515     */
516    public boolean isInScopeOf(DN dn, SearchScope scope) {
517        switch (scope.asEnum()) {
518        case BASE_OBJECT:
519            // The base DN must equal this DN.
520            return equals(dn);
521        case SINGLE_LEVEL:
522            // The parent DN must equal the base DN.
523            return isChildOf(dn);
524        case SUBORDINATES:
525            // This DN must be a descendant of the provided base DN, but
526            // not equal to it.
527            return isSubordinateOrEqualTo(dn) && !equals(dn);
528        case WHOLE_SUBTREE:
529            // This DN must be a descendant of the provided base DN.
530            return isSubordinateOrEqualTo(dn);
531        default:
532            // This is a scope that we don't recognize.
533            return false;
534        }
535    }
536
537    /**
538     * Returns {@code true} if this DN matches the provided base DN and search
539     * scope.
540     *
541     * @param dn
542     *            The base DN.
543     * @param scope
544     *            The search scope.
545     * @return {@code true} if this DN matches the provided base DN and search
546     *         scope, otherwise {@code false}.
547     * @throws LocalizedIllegalArgumentException
548     *             If {@code dn} is not a valid LDAP string representation of a
549     *             DN.
550     * @throws NullPointerException
551     *             If {@code dn} or {@code scope} was {@code null}.
552     */
553    public boolean isInScopeOf(String dn, SearchScope scope) {
554        return isInScopeOf(valueOf(dn), scope);
555    }
556
557    /**
558     * Returns {@code true} if this DN is the immediate parent of the provided
559     * DN.
560     *
561     * @param dn
562     *            The potential child DN.
563     * @return {@code true} if this DN is the immediate parent of the provided
564     *         DN, otherwise {@code false}.
565     * @throws NullPointerException
566     *             If {@code dn} was {@code null}.
567     */
568    public boolean isParentOf(final DN dn) {
569        // If dn is the Root DN then parent will be null but this is ok.
570        return equals(dn.parent);
571    }
572
573    /**
574     * Returns {@code true} if this DN is the immediate parent of the provided
575     * DN.
576     *
577     * @param dn
578     *            The potential child DN.
579     * @return {@code true} if this DN is the immediate parent of the provided
580     *         DN, otherwise {@code false}.
581     * @throws LocalizedIllegalArgumentException
582     *             If {@code dn} is not a valid LDAP string representation of a
583     *             DN.
584     * @throws NullPointerException
585     *             If {@code dn} was {@code null}.
586     */
587    public boolean isParentOf(final String dn) {
588        // If dn is the Root DN then parent will be null but this is ok.
589        return isParentOf(valueOf(dn));
590    }
591
592    /**
593     * Returns {@code true} if this DN is the Root DN.
594     *
595     * @return {@code true} if this DN is the Root DN, otherwise {@code false}.
596     */
597    public boolean isRootDN() {
598        return size == 0;
599    }
600
601    /**
602     * Returns {@code true} if this DN is subordinate to or equal to the
603     * provided DN.
604     *
605     * @param dn
606     *            The potential child DN.
607     * @return {@code true} if this DN is subordinate to or equal to the
608     *         provided DN, otherwise {@code false}.
609     * @throws NullPointerException
610     *             If {@code dn} was {@code null}.
611     */
612    public boolean isSubordinateOrEqualTo(final DN dn) {
613        if (size < dn.size) {
614            return false;
615        } else if (size == dn.size) {
616            return equals(dn);
617        } else {
618            // dn is a potential superior of this.
619            return parent(size - dn.size).equals(dn);
620        }
621    }
622
623    /**
624     * Returns {@code true} if this DN is subordinate to or equal to the
625     * provided DN.
626     *
627     * @param dn
628     *            The potential child DN.
629     * @return {@code true} if this DN is subordinate to or equal to the
630     *         provided DN, otherwise {@code false}.
631     * @throws LocalizedIllegalArgumentException
632     *             If {@code dn} is not a valid LDAP string representation of a
633     *             DN.
634     * @throws NullPointerException
635     *             If {@code dn} was {@code null}.
636     */
637    public boolean isSubordinateOrEqualTo(final String dn) {
638        return isSubordinateOrEqualTo(valueOf(dn));
639    }
640
641    /**
642     * Returns {@code true} if this DN is superior to or equal to the provided
643     * DN.
644     *
645     * @param dn
646     *            The potential child DN.
647     * @return {@code true} if this DN is superior to or equal to the provided
648     *         DN, otherwise {@code false}.
649     * @throws NullPointerException
650     *             If {@code dn} was {@code null}.
651     */
652    public boolean isSuperiorOrEqualTo(final DN dn) {
653        if (size > dn.size) {
654            return false;
655        } else if (size == dn.size) {
656            return equals(dn);
657        } else {
658            // dn is a potential subordinate of this.
659            return dn.parent(dn.size - size).equals(this);
660        }
661    }
662
663    /**
664     * Returns {@code true} if this DN is superior to or equal to the provided
665     * DN.
666     *
667     * @param dn
668     *            The potential child DN.
669     * @return {@code true} if this DN is superior to or equal to the provided
670     *         DN, otherwise {@code false}.
671     * @throws LocalizedIllegalArgumentException
672     *             If {@code dn} is not a valid LDAP string representation of a
673     *             DN.
674     * @throws NullPointerException
675     *             If {@code dn} was {@code null}.
676     */
677    public boolean isSuperiorOrEqualTo(final String dn) {
678        return isSuperiorOrEqualTo(valueOf(dn));
679    }
680
681    /**
682     * Returns an iterator of the RDNs contained in this DN. The RDNs will be
683     * returned in the order starting with this DN's RDN, followed by the RDN of
684     * the parent DN, and so on.
685     * <p>
686     * Attempts to remove RDNs using an iterator's {@code remove()} method are
687     * not permitted and will result in an {@code UnsupportedOperationException}
688     * being thrown.
689     *
690     * @return An iterator of the RDNs contained in this DN.
691     */
692    @Override
693    public Iterator<RDN> iterator() {
694        return new Iterator<RDN>() {
695            private DN dn = DN.this;
696
697            @Override
698            public boolean hasNext() {
699                return dn.rdn != null;
700            }
701
702            @Override
703            public RDN next() {
704                if (dn.rdn == null) {
705                    throw new NoSuchElementException();
706                }
707
708                final RDN rdn = dn.rdn;
709                dn = dn.parent;
710                return rdn;
711            }
712
713            @Override
714            public void remove() {
715                throw new UnsupportedOperationException();
716            }
717        };
718    }
719
720    /**
721     * Returns the DN whose content is the specified number of RDNs from this
722     * DN. The following equivalences hold:
723     *
724     * <pre>
725     * dn.localName(0).isRootDN();
726     * dn.localName(1).equals(rootDN.child(dn.rdn()));
727     * dn.localName(dn.size()).equals(dn);
728     * </pre>
729     *
730     * Said otherwise, a new DN is built using {@code index} RDNs,
731     * retained in the same order, starting from the left.
732     *
733     * @param index
734     *            The number of RDNs to be included in the local name.
735     * @return The DN whose content is the specified number of RDNs from this
736     *         DN.
737     * @throws IllegalArgumentException
738     *             If {@code index} is less than zero.
739     * @see #parent(int) for the reverse operation (starting from the right)
740     */
741    public DN localName(final int index) {
742        Reject.ifFalse(index >= 0, "index less than zero");
743
744        if (index == 0) {
745            return ROOT_DN;
746        } else if (index >= size) {
747            return this;
748        } else {
749            final DN localName = new DN(this.schema, null, rdn, index);
750            DN nextLocalName = localName;
751            DN lastDN = parent;
752            for (int i = index - 1; i > 0; i--) {
753                nextLocalName.parent = new DN(this.schema, null, lastDN.rdn, i);
754                nextLocalName = nextLocalName.parent;
755                lastDN = lastDN.parent;
756            }
757            nextLocalName.parent = ROOT_DN;
758            return localName;
759        }
760    }
761
762    /**
763     * Returns the DN which is the immediate parent of this DN, or {@code null}
764     * if this DN is the Root DN.
765     * <p>
766     * This method is equivalent to:
767     *
768     * <pre>
769     * parent(1);
770     * </pre>
771     *
772     * @return The DN which is the immediate parent of this DN, or {@code null}
773     *         if this DN is the Root DN.
774     */
775    public DN parent() {
776        return parent;
777    }
778
779    /**
780     * Returns the DN which is equal to this DN with the specified number of
781     * RDNs removed. Note that if {@code index} is zero then this DN will be
782     * returned (identity).
783     *
784     * Said otherwise, a new DN is built using {@code index} RDNs,
785     * retained in the same order, starting from the right.
786     *
787     * @param index
788     *            The number of RDNs to be removed.
789     * @return The DN which is equal to this DN with the specified number of
790     *         RDNs removed, or {@code null} if the parent of the Root DN is
791     *         reached.
792     * @throws IllegalArgumentException
793     *             If {@code index} is less than zero.
794     * @see #localName(int) for the reverse operation (starting from the left)
795     */
796    public DN parent(final int index) {
797        // We allow size + 1 so that we can return null as the parent of the Root DN.
798        Reject.ifTrue(index < 0, "index less than zero");
799
800        if (index > size) {
801            return null;
802        }
803        DN parentDN = this;
804        for (int i = 0; parentDN != null && i < index; i++) {
805            parentDN = parentDN.parent;
806        }
807        return parentDN;
808    }
809
810    /**
811     * Returns the RDN of this DN, or {@code null} if this DN is the Root DN.
812     * <p>
813     * This is the equivalent of calling {@code dn.rdn(0)}.
814     *
815     * @return The RDN of this DN, or {@code null} if this DN is the Root DN.
816     */
817    public RDN rdn() {
818        return rdn;
819    }
820
821    /**
822     * Returns the RDN at the specified index for this DN,
823     * or {@code null} if no such RDN exists.
824     * <p>
825     * Here is an example usage:
826     * <pre>
827     * DN.valueOf("ou=people,dc=example,dc=com").rdn(0) =&gt; "ou=people"
828     * DN.valueOf("ou=people,dc=example,dc=com").rdn(1) =&gt; "dc=example"
829     * DN.valueOf("ou=people,dc=example,dc=com").rdn(2) =&gt; "dc=com"
830     * DN.valueOf("ou=people,dc=example,dc=com").rdn(3) =&gt; null
831     * </pre>
832     *
833     * @param index
834     *            The index of the requested RDN.
835     * @return The RDN at the specified index, or {@code null} if no such RDN exists.
836     * @throws IllegalArgumentException
837     *             If {@code index} is less than zero.
838     */
839    public RDN rdn(int index) {
840        DN parentDN = parent(index);
841        return parentDN != null ? parentDN.rdn : null;
842    }
843
844    /**
845     * Returns a copy of this DN whose parent DN, {@code fromDN}, has been
846     * renamed to the new parent DN, {@code toDN}. If this DN is not subordinate
847     * or equal to {@code fromDN} then this DN is returned (i.e. the DN is not
848     * renamed).
849     *
850     * @param fromDN
851     *            The old parent DN.
852     * @param toDN
853     *            The new parent DN.
854     * @return The renamed DN, or this DN if no renaming was performed.
855     * @throws NullPointerException
856     *             If {@code fromDN} or {@code toDN} was {@code null}.
857     */
858    public DN rename(final DN fromDN, final DN toDN) {
859        Reject.ifNull(fromDN, toDN);
860
861        if (!isSubordinateOrEqualTo(fromDN)) {
862            return this;
863        } else if (equals(fromDN)) {
864            return toDN;
865        } else {
866            return toDN.child(localName(size - fromDN.size));
867        }
868    }
869
870    /**
871     * Returns the number of RDN components in this DN.
872     *
873     * @return The number of RDN components in this DN.
874     */
875    public int size() {
876        return size;
877    }
878
879    /**
880     * Returns the RFC 4514 string representation of this DN.
881     *
882     * @return The RFC 4514 string representation of this DN.
883     * @see <a href="http://tools.ietf.org/html/rfc4514">RFC 4514 - Lightweight
884     *      Directory Access Protocol (LDAP): String Representation of
885     *      Distinguished Names </a>
886     */
887    @Override
888    public String toString() {
889        // We don't care about potential race conditions here.
890        if (stringValue == null) {
891            final StringBuilder builder = new StringBuilder();
892            builder.append(rdn);
893            for (DN dn = parent; dn.rdn != null; dn = dn.parent) {
894                builder.append(RDN_CHAR_SEPARATOR);
895                if (dn.stringValue != null) {
896                    builder.append(dn.stringValue);
897                    break;
898                }
899                builder.append(dn.rdn);
900            }
901            stringValue = builder.toString();
902        }
903        return stringValue;
904    }
905
906    /**
907     * Retrieves a normalized byte string representation of this DN.
908     * <p>
909     * This representation is suitable for equality and comparisons, and
910     * for providing a natural hierarchical ordering.
911     * However, it is not a valid DN and can't be reverted to a valid DN.
912     * You should consider using a {@code CompactDn} as an alternative.
913     *
914     * @return The normalized string representation of this DN.
915     */
916    public ByteString toNormalizedByteString() {
917        if (normalizedDN == null) {
918            if (rdn == null) {
919                normalizedDN = ByteString.empty();
920            } else {
921                final ByteStringBuilder builder = new ByteStringBuilder(size * 8);
922                if (parent.normalizedDN != null) {
923                    builder.appendBytes(parent.normalizedDN);
924                } else {
925                    for (int i = size() - 1; i > 0; i--) {
926                        parent(i).rdn().toNormalizedByteString(builder);
927                    }
928                }
929                rdn.toNormalizedByteString(builder);
930                normalizedDN = builder.toByteString();
931            }
932        }
933        return normalizedDN;
934    }
935
936    /**
937     * Retrieves a normalized string representation of this DN.
938     * <p>
939     * This representation is safe to use in an URL or in a file name.
940     * However, it is not a valid DN and can't be reverted to a valid DN.
941     *
942     * @return The normalized string representation of this DN.
943     */
944    public String toNormalizedUrlSafeString() {
945        if (rdn() == null) {
946            return "";
947        }
948
949        // This code differs from toNormalizedByteString(),
950        // because we do not care about ordering comparisons.
951        // (so we do not append the RDN_SEPARATOR first)
952        final StringBuilder builder = new StringBuilder();
953        int i = size() - 1;
954        parent(i).rdn().toNormalizedUrlSafeString(builder);
955        for (i--; i >= 0; i--) {
956            final RDN rdn = parent(i).rdn();
957            // Only add a separator if the RDN is not RDN.maxValue() or RDN.minValue().
958            if (rdn.size() != 0) {
959                builder.append(RDN_CHAR_SEPARATOR);
960            }
961            rdn.toNormalizedUrlSafeString(builder);
962        }
963        return builder.toString();
964    }
965
966    /**
967     * Returns a UUID whose content is based on the normalized content of this DN.
968     * Two equivalent DNs subject to the same schema will always yield the same UUID.
969     *
970     * @return the UUID representing this DN
971     */
972    public UUID toUUID() {
973        ByteString normDN = toNormalizedByteString();
974        if (!normDN.isEmpty()) {
975            // remove leading RDN separator
976            normDN = normDN.subSequence(1, normDN.length());
977        }
978        return UUID.nameUUIDFromBytes(normDN.toByteArray());
979    }
980
981}