org.springframework.ldap.core.DistinguishedName.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.ldap.core.DistinguishedName.java

Source

/*
 * Copyright 2005-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.ldap.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import javax.naming.CompositeName;
import javax.naming.InvalidNameException;
import javax.naming.Name;
import javax.naming.ldap.Rdn;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.ldap.BadLdapGrammarException;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.ldap.support.ListComparator;
import org.springframework.util.Assert;

/**
 * Default implementation of a {@link Name} corresponding to an LDAP path. A
 * Distinguished Name manipulation implementation is included in JDK1.5
 * (LdapName), but not in prior releases.
 * 
 * A <code>DistinguishedName</code> is particularly useful when building or
 * modifying an LDAP path dynamically, as escaping will be taken care of.
 * 
 * A path is split into several names. The {@link Name} interface specifies that
 * the most significant part be in position 0.
 * <p>
 * Example:
 * 
 * <dl>
 * <dt>The path</dt>
 * <dd>uid=adam.skogman, ou=People, ou=EU</dd>
 * <dt>Name[0]</dt>
 * <dd>ou=EU</dd>
 * <dt>Name[1]</dt>
 * <dd>ou=People</dd>
 * <dt>Name[2]</dt>
 * <dd>uid=adam.skogman</dd>
 * </dl>
 * </p>
 * <p>
 * <code>Name</code> instances, and consequently <code>DistinguishedName</code>
 * instances are naturally mutable, which is useful when constructing
 * DistinguishedNames. Example:
 * 
 * <pre>
 * DistinguishedName path = new DistinguishedName(&quot;dc=jayway,dc=se&quot;);
 * path.add(&quot;ou&quot;, &quot;People&quot;);
 * path.add(&quot;uid&quot;, &quot;adam.skogman&quot;);
 * String dn = path.toString();
 * </pre>
 * 
 * will render <code>uid=adam.skogman,ou=People,dc=jayway,dc=se</code>.
 * </p>
 * <p>
 * <b>NOTE:</b> The fact that DistinguishedName instances are mutable needs to
 * be taken into careful account, as this means that they may be modified
 * involuntarily. This means that whenever a <code>DistinguishedName</code>
 * instance is kept for reference (e.g. for identification of a domain entry) or
 * as a constant, you should consider getting an immutable copy of the instance
 * using {@link #immutableDistinguishedName()} or
 * {@link #immutableDistinguishedName(String)}.
 * </p>
 * <p>
 * <b>NB:</b>As of version 1.3 the default toString representation of
 * DistinguishedName now defaults to a compact one, without spaces between the
 * respective RDNs. For backward compatibility, set the
 * {@link #SPACED_DN_FORMAT_PROPERTY} ({@value #SPACED_DN_FORMAT_PROPERTY}) to
 * <code>true</code>.
 * @author Adam Skogman
 * @author Mattias Hellborg Arthursson
 */
public class DistinguishedName implements Name {
    /**
     * System property that will be inspected to determine whether
     * {@link #toString()} will format the DN with spaces after each comma or
     * use a more compact representation, i.e.:
     * <code>uid=adam.skogman, ou=People, dc=jayway, dc=se</code> rather than
     * <code>uid=adam.skogman,ou=People,dc=jayway,dc=se</code>. A value other
     * than null or blank will trigger the spaced format. Default is the compact
     * representation.
     * <p>
     * Valid values are:
     * <ul>
     * <li>blank or null (property not set)</li>
     * <li>any non-blank value</li>
     * </ul>
     * @since 1.3
     * @see #toCompactString()
     */
    public static final String SPACED_DN_FORMAT_PROPERTY = "org.springframework.ldap.core.spacedDnFormat";

    /**
     * System property that will be inspected to determine whether creating a
     * DistinguishedName will convert the keys to <em>lowercase</em>, convert
     * the keys to <em>uppercase</em>, or leave the keys as they were in the
     * original String, ie <em>none</em>. Default is to convert the keys to
     * lowercase.
     * <p>
     * Valid values are:
     * <ul>
     * <li>"lower" or blank or null (property not set)</li>
     * <li>"upper"</li>
     * <li>"none"</li>
     * </ul>
     * @since 1.3.1
     * @see #KEY_CASE_FOLD_LOWER
     * @see #KEY_CASE_FOLD_UPPER
     * @see #KEY_CASE_FOLD_NONE
     */
    public static final String KEY_CASE_FOLD_PROPERTY = "org.springframework.ldap.core.keyCaseFold";

    public static final String KEY_CASE_FOLD_LOWER = "lower";

    public static final String KEY_CASE_FOLD_UPPER = "upper";

    public static final String KEY_CASE_FOLD_NONE = "none";

    private static final Log log = LogFactory.getLog(DistinguishedName.class);

    private static final boolean COMPACT = true;

    private static final boolean NON_COMPACT = false;

    private static final long serialVersionUID = 3514344371999042586L;

    /**
     * An empty, unmodifiable DistinguishedName.
     */
    public static final DistinguishedName EMPTY_PATH = new DistinguishedName(Collections.EMPTY_LIST);

    private List names;

    /**
     * Construct a new DistinguishedName with no components.
     */
    public DistinguishedName() {
        names = new LinkedList();
    }

    /**
     * Construct a new <code>DistinguishedName</code> from a String.
     * 
     * @param path a String corresponding to a (syntactically) valid LDAP path.
     */
    public DistinguishedName(String path) {
        if (StringUtils.isBlank(path)) {
            names = new LinkedList();
        } else {
            parse(path);
        }
    }

    /**
     * Construct a new <code>DistinguishedName</code> from the supplied
     * <code>List</code> of {@link LdapRdn} objects.
     * 
     * @param list the components that this instance will consist of.
     */
    public DistinguishedName(List list) {
        this.names = list;
    }

    /**
     * Construct a new <code>DistinguishedName</code> from the supplied
     * {@link Name}. The parts of the supplied {@link Name} must be
     * syntactically correct {@link LdapRdn}s.
     * 
     * @param name the {@link Name} to construct a new
     * <code>DistinguishedName</code> from.
     */
    public DistinguishedName(Name name) {
        Assert.notNull(name, "name cannot be null");
        if (name instanceof CompositeName) {
            parse(LdapUtils.convertCompositeNameToString((CompositeName) name));
            return;
        }
        names = new LinkedList();
        for (int i = 0; i < name.size(); i++) {
            names.add(new LdapRdn(name.get(i)));
        }
    }

    /**
     * Parse the supplied String and make this instance represent the
     * corresponding distinguished name.
     * 
     * @param path the LDAP path to parse.
     */
    protected void parse(String path) {
        DnParser parser = DefaultDnParserFactory.createDnParser(unmangleCompositeName(path));
        DistinguishedName dn;
        try {
            dn = parser.dn();
        } catch (ParseException e) {
            throw new BadLdapGrammarException("Failed to parse DN", e);
        } catch (TokenMgrError e) {
            throw new BadLdapGrammarException("Failed to parse DN", e);
        }
        this.names = dn.names;
    }

    /**
     * If path is surrounded by quotes, strip them. JNDI considers forward slash
     * ('/') special, but LDAP doesn't. {@link CompositeName#toString()} tends
     * to mangle a {@link Name} with a slash by surrounding it with quotes
     * ('"').
     * 
     * @param path Path to check and possibly strip.
     * @return A String with the possibly stripped path.
     */
    private String unmangleCompositeName(String path) {
        String tempPath;
        // Check if CompositeName has mangled the name with quotes
        if (path.startsWith("\"") && path.endsWith("\"")) {
            tempPath = path.substring(1, path.length() - 1);
        } else {
            tempPath = path;
        }
        return tempPath;
    }

    /**
     * Get the {@link LdapRdn} at a specified position.
     * 
     * @param index the {@link LdapRdn} to retrieve.
     * @return the {@link LdapRdn} at the requested position.
     */
    public LdapRdn getLdapRdn(int index) {
        return (LdapRdn) names.get(index);
    }

    /**
     * Get the {@link LdapRdn} with the specified key. If there are several
     * {@link Rdn}s with the same key, the first one found (in order of
     * significance) will be returned.
     * 
     * @param key Attribute name of the {@link LdapRdn} to retrieve.
     * @return the {@link LdapRdn} with the requested key.
     * @throws IllegalArgumentException if no Rdn matches the given key.
     */
    public LdapRdn getLdapRdn(String key) {
        for (Iterator iter = names.iterator(); iter.hasNext();) {
            LdapRdn rdn = (LdapRdn) iter.next();
            if (StringUtils.equals(rdn.getKey(), key)) {
                return rdn;
            }
        }

        throw new IllegalArgumentException("No Rdn with the requested key: '" + key + "'");
    }

    /**
     * Get the value of the {@link LdapRdnComponent} with the specified key
     * (Attribute value). If there are several Rdns with the same key, the value
     * of the first one found (in order of significance) will be returned.
     * 
     * @param key Attribute name of the {@link LdapRdn} to retrieve.
     * @return the value.
     * @throws IllegalArgumentException if no Rdn matches the given key.
     */
    public String getValue(String key) {
        return getLdapRdn(key).getValue();
    }

    /**
     * Get the name <code>List</code>.
     * 
     * @return the list of {@link LdapRdn}s that this
     * <code>DistinguishedName</code> consists of.
     */
    public List getNames() {
        return names;
    }

    /**
     * Get the String representation of this <code>DistinguishedName</code>.
     * Depending on the setting of property
     * <code>org.springframework.ldap.core.spacedDnFormat</code> a space will be
     * added after each comma, to make the result more readable. Default is
     * compact representation, i.e. without any spaces.
     * 
     * @return a syntactically correct, properly escaped String representation
     * of the <code>DistinguishedName</code>.
     * @see #SPACED_DN_FORMAT_PROPERTY
     */
    public String toString() {
        String spacedFormatting = System.getProperty(SPACED_DN_FORMAT_PROPERTY);
        if (StringUtils.isBlank(spacedFormatting)) {
            return format(COMPACT);
        } else {
            return format(NON_COMPACT);
        }
    }

    /**
     * Get the compact String representation of this
     * <code>DistinguishedName</code>. Add no space after each comma, to make it
     * compact.
     * 
     * @return a syntactically correct, properly escaped String representation
     * of the <code>DistinguishedName</code>.
     */
    public String toCompactString() {
        return format(COMPACT);
    }

    /**
     * Builds a complete LDAP path, ldap encoded, useful as a DN.
     * 
     * Always uses lowercase, always separates with ", " i.e. comma and a space.
     * 
     * @return the LDAP path.
     */
    public String encode() {
        return format(NON_COMPACT);
    }

    private String format(boolean compact) {
        // empty path
        if (names.size() == 0)
            return "";

        StringBuffer buffer = new StringBuffer(256);

        ListIterator i = names.listIterator(names.size());
        while (i.hasPrevious()) {
            LdapRdn rdn = (LdapRdn) i.previous();
            buffer.append(rdn.getLdapEncoded());

            // add comma, except in last iteration
            if (i.hasPrevious()) {
                if (compact) {
                    buffer.append(",");
                } else {
                    buffer.append(", ");

                }
            }
        }

        return buffer.toString();
    }

    /**
     * Builds a complete LDAP path, ldap and url encoded. Separates only with
     * ",".
     * 
     * @return the LDAP path, for use in an url.
     */
    public String toUrl() {
        StringBuffer buffer = new StringBuffer(256);

        for (int i = names.size() - 1; i >= 0; i--) {
            LdapRdn n = (LdapRdn) names.get(i);
            buffer.append(n.encodeUrl());
            if (i > 0) {
                buffer.append(",");
            }
        }
        return buffer.toString();
    }

    /**
     * Determines if this <code>DistinguishedName</code> path contains another
     * path.
     * 
     * @param path the path to check.
     * @return <code>true</code> if the supplied path is conained in this
     * instance, <code>false</code> otherwise.
     */
    public boolean contains(DistinguishedName path) {

        List shortlist = path.getNames();

        // this path must be at least as long
        if (getNames().size() < shortlist.size())
            return false;

        // must have names
        if (shortlist.size() == 0)
            return false;

        Iterator longiter = getNames().iterator();
        Iterator shortiter = shortlist.iterator();

        LdapRdn longname = (LdapRdn) longiter.next();
        LdapRdn shortname = (LdapRdn) shortiter.next();

        // find first match
        while (!longname.equals(shortname) && longiter.hasNext()) {
            longname = (LdapRdn) longiter.next();
        }

        // Done?
        if (!shortiter.hasNext() && longname.equals(shortname))
            return true;
        if (!longiter.hasNext())
            return false;

        // compare
        while (longname.equals(shortname) && longiter.hasNext() && shortiter.hasNext()) {
            longname = (LdapRdn) longiter.next();
            shortname = (LdapRdn) shortiter.next();
        }

        // Done
        if (!shortiter.hasNext() && longname.equals(shortname))
            return true;
        else
            return false;

    }

    /**
     * Add an LDAP path last in this DistinguishedName. E.g.:
     * 
     * <pre>
     * DistinguishedName name1 = new DistinguishedName(&quot;c=SE, dc=jayway, dc=se&quot;);
     * DistinguishedName name2 = new DistinguishedName(&quot;ou=people&quot;);
     * name1.append(name2);
     * </pre>
     * 
     * will result in <code>ou=people, c=SE, dc=jayway, dc=se</code>
     * 
     * @param path the path to append.
     * @return this instance.
     */
    public DistinguishedName append(DistinguishedName path) {
        getNames().addAll(path.getNames());
        return this;
    }

    /**
     * Append a new {@link LdapRdn} using the supplied key and value.
     * 
     * @param key the key of the {@link LdapRdn}.
     * @param value the value of the {@link LdapRdn}.
     * @return this instance.
     */
    public DistinguishedName append(String key, String value) {
        add(key, value);
        return this;
    }

    /**
     * Add an LDAP path first in this DistinguishedName. E.g.:
     * 
     * <pre>
     * DistinguishedName name1 = new DistinguishedName(&quot;ou=people&quot;);
     * DistinguishedName name2 = new DistinguishedName(&quot;c=SE, dc=jayway, dc=se&quot;);
     * name1.prepend(name2);
     * </pre>
     * 
     * will result in <code>ou=people, c=SE, dc=jayway, dc=se</code>
     * 
     * @param path the path to prepend.
     */
    public void prepend(DistinguishedName path) {
        ListIterator i = path.getNames().listIterator(path.getNames().size());
        while (i.hasPrevious()) {
            names.add(0, i.previous());
        }
    }

    /**
     * Remove the first part of this <code>DistinguishedName</code>.
     * 
     * @return the removed entry.
     */
    public LdapRdn removeFirst() {
        return (LdapRdn) names.remove(0);
    }

    /**
     * Remove the supplied path from the beginning of this
     * <code>DistinguishedName</code> if this instance starts with
     * <code>path</code>. Useful for stripping base path suffix from a
     * <code>DistinguishedName</code>.
     * 
     * @param path the path to remove from the beginning of this instance.
     */
    public void removeFirst(Name path) {
        if (path != null && this.startsWith(path)) {
            for (int i = 0; i < path.size(); i++)
                this.removeFirst();
        }
    }

    /**
     * @see java.lang.Object#clone()
     */
    public Object clone() {
        try {
            DistinguishedName result = (DistinguishedName) super.clone();
            result.names = new LinkedList(names);
            return result;
        } catch (CloneNotSupportedException e) {
            log.fatal("CloneNotSupported thrown from superclass - this should not happen");
            throw new RuntimeException("Fatal error in clone", e);
        }
    }

    /**
     * @see java.lang.Object#equals(java.lang.Object)
     */
    public boolean equals(Object obj) {
        // A subclass with identical values should NOT be considered equal.
        // EqualsBuilder in commons-lang cannot handle subclasses correctly.
        if (obj == null || obj.getClass() != this.getClass()) {
            return false;
        }

        DistinguishedName name = (DistinguishedName) obj;

        // compare the lists
        return getNames().equals(name.getNames());
    }

    /**
     * @see java.lang.Object#hashCode()
     */
    public int hashCode() {
        return this.getClass().hashCode() ^ getNames().hashCode();
    }

    /**
     * Compare this instance to another object. Note that the comparison is done
     * in order of significance, so the most significant Rdn is compared first,
     * then the second and so on.
     * 
     * @see javax.naming.Name#compareTo(java.lang.Object)
     */
    public int compareTo(Object obj) {
        DistinguishedName that = (DistinguishedName) obj;
        ListComparator comparator = new ListComparator();
        return comparator.compare(this.names, that.names);
    }

    public int size() {
        return names.size();
    }

    public boolean isEmpty() {
        return names.size() == 0;
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.naming.Name#getAll()
     */
    public Enumeration getAll() {
        LinkedList strings = new LinkedList();
        for (Iterator iter = names.iterator(); iter.hasNext();) {
            LdapRdn rdn = (LdapRdn) iter.next();
            strings.add(rdn.getLdapEncoded());
        }

        return Collections.enumeration(strings);
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.naming.Name#get(int)
     */
    public String get(int index) {
        LdapRdn rdn = (LdapRdn) names.get(index);
        return rdn.getLdapEncoded();
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.naming.Name#getPrefix(int)
     */
    public Name getPrefix(int index) {
        LinkedList newNames = new LinkedList();
        for (int i = 0; i < index; i++) {
            newNames.add(names.get(i));
        }

        return new DistinguishedName(newNames);
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.naming.Name#getSuffix(int)
     */
    public Name getSuffix(int index) {
        if (index > names.size()) {
            throw new ArrayIndexOutOfBoundsException();
        }

        LinkedList newNames = new LinkedList();
        for (int i = index; i < names.size(); i++) {
            newNames.add(names.get(i));
        }

        return new DistinguishedName(newNames);
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.naming.Name#startsWith(javax.naming.Name)
     */
    public boolean startsWith(Name name) {
        if (name.size() == 0) {
            return false;
        }

        DistinguishedName start = null;
        if (name instanceof DistinguishedName) {
            start = (DistinguishedName) name;
        } else {
            return false;
        }

        if (start.size() > this.size()) {
            return false;
        }

        Iterator longiter = names.iterator();
        Iterator shortiter = start.getNames().iterator();

        while (shortiter.hasNext()) {
            Object longname = longiter.next();
            Object shortname = shortiter.next();

            if (!longname.equals(shortname)) {
                return false;
            }
        }

        // All names in shortiter matched.
        return true;
    }

    /**
     * Determines if this <code>DistinguishedName</code> ends with a certian
     * path.
     * 
     * If the argument path is empty (no names in path) this method will return
     * <code>false</code>.
     * 
     * @param name The suffix to check for.
     * 
     */
    public boolean endsWith(Name name) {
        DistinguishedName path = null;
        if (name instanceof DistinguishedName) {
            path = (DistinguishedName) name;
        } else {
            return false;
        }

        List shortlist = path.getNames();

        // this path must be at least as long
        if (getNames().size() < shortlist.size())
            return false;

        // must have names
        if (shortlist.size() == 0)
            return false;

        ListIterator longiter = getNames().listIterator(getNames().size());
        ListIterator shortiter = shortlist.listIterator(shortlist.size());

        while (shortiter.hasPrevious()) {
            LdapRdn longname = (LdapRdn) longiter.previous();
            LdapRdn shortname = (LdapRdn) shortiter.previous();

            if (!longname.equals(shortname))
                return false;
        }

        // if short list ended, all were equal
        return true;

    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.naming.Name#addAll(javax.naming.Name)
     */
    public Name addAll(Name name) throws InvalidNameException {
        return addAll(names.size(), name);
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.naming.Name#addAll(int, javax.naming.Name)
     */
    public Name addAll(int arg0, Name name) throws InvalidNameException {
        DistinguishedName distinguishedName = null;
        try {
            distinguishedName = (DistinguishedName) name;
        } catch (ClassCastException e) {
            throw new InvalidNameException("Invalid name type");
        }

        names.addAll(arg0, distinguishedName.getNames());
        return this;
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.naming.Name#add(java.lang.String)
     */
    public Name add(String string) throws InvalidNameException {
        return add(names.size(), string);
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.naming.Name#add(int, java.lang.String)
     */
    public Name add(int index, String string) throws InvalidNameException {
        try {
            names.add(index, new LdapRdn(string));
        } catch (BadLdapGrammarException e) {
            throw new InvalidNameException("Failed to parse rdn '" + string + "'");
        }
        return this;
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.naming.Name#remove(int)
     */
    public Object remove(int arg0) throws InvalidNameException {
        LdapRdn rdn = (LdapRdn) names.remove(arg0);
        return rdn.getLdapEncoded();
    }

    /**
     * Remove the last part of this <code>DistinguishedName</code>.
     * 
     * @return the removed {@link LdapRdn}.
     */
    public LdapRdn removeLast() {
        return (LdapRdn) names.remove(names.size() - 1);
    }

    /**
     * Add a new {@link LdapRdn} using the supplied key and value.
     * 
     * @param key the key of the {@link LdapRdn}.
     * @param value the value of the {@link LdapRdn}.
     */
    public void add(String key, String value) {
        names.add(new LdapRdn(key, value));
    }

    /**
     * Add the supplied {@link LdapRdn} last in the list of Rdns.
     * 
     * @param rdn the {@link LdapRdn} to add.
     */
    public void add(LdapRdn rdn) {
        names.add(rdn);
    }

    /**
     * Add the supplied {@link LdapRdn} att the specified index.
     * 
     * @param idx the index at which to add the LdapRdn.
     * @param rdn the LdapRdn to add.
     */
    public void add(int idx, LdapRdn rdn) {
        names.add(idx, rdn);
    }

    /**
     * Return an immutable copy of this instance. It will not be possible to add
     * or remove any Rdns to or from the returned instance, and the respective
     * Rdns will also be immutable in turn.
     * 
     * @return a copy of this instance backed by an immutable list.
     * @since 1.2
     */
    public DistinguishedName immutableDistinguishedName() {
        List listWithImmutableRdns = new ArrayList(names.size());
        for (Iterator iterator = names.iterator(); iterator.hasNext();) {
            LdapRdn rdn = (LdapRdn) iterator.next();
            listWithImmutableRdns.add(rdn.immutableLdapRdn());
        }

        return new DistinguishedName(Collections.unmodifiableList(listWithImmutableRdns));
    }

    /**
     * Create an immutable DistinguishedName instance, suitable as a constant.
     * 
     * @param dnString the DN string to parse.
     * @return an immutable DistinguishedName corresponding to the supplied DN
     * string.
     * @since 1.3
     */
    public static final DistinguishedName immutableDistinguishedName(String dnString) {
        return new DistinguishedName(dnString).immutableDistinguishedName();
    }
}