changeset:   372:7fdfd927094f
branch:      5.2.1-I20090930_0116
tag:         tip
parent:      369:77d2e1a9c939
user:        Bob Browning <bob.browning@pa.press.net>
date:        Tue Feb 09 13:55:06 2010 +0000
summary:     Modified LDAP implementation to handle retrieval of urls from DNS SRV record

diff -r 77d2e1a9c939 -r 7fdfd927094f nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/LDAPDirectory.java
--- a/nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/LDAPDirectory.java	Mon Jan 25 17:45:50 2010 +0100
+++ b/nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/LDAPDirectory.java	Tue Feb 09 13:55:06 2010 +0000
@@ -51,7 +51,6 @@
 
 /**
  * @author ogrisel
- *
  */
 public class LDAPDirectory extends AbstractDirectory {
 
@@ -130,18 +129,26 @@
         props.put(Context.INITIAL_CONTEXT_FACTORY,
                 "com.sun.jndi.ldap.LdapCtxFactory");
 
+        /*
+         * Get inital connection URLs, dynamic URLs may cause the list to be
+         * updated when creating the session
+         */
         String ldapUrls = serverConfig.getLdapUrls();
         props.put(Context.PROVIDER_URL, ldapUrls);
         props.put(Context.REFERRAL, "follow");
 
         /*
-        * NOTE: Currently this directory service does not use SSL, however if
-        * it should then for SSL connections this value must not be set as this
-        * causes the connection to fail
-        */
+         * SSL Connections do not work with connection timeout property
+         */
         if (serverConfig.getConnectionTimeout() > -1) {
-            props.put("com.sun.jndi.ldap.connect.timeout",
-            Integer.toString(serverConfig.getConnectionTimeout()));
+            if (!serverConfig.useSsl()) {
+                props.put("com.sun.jndi.ldap.connect.timeout",
+                        Integer.toString(serverConfig.getConnectionTimeout()));
+            } else {
+                if (log.isDebugEnabled()) {
+                    log.debug("SSL connections do not operate correctly when used with the connection timeout parameter, disabling");
+                }
+            }
         }
 
         String bindDn = serverConfig.getBindDn();
@@ -170,7 +177,7 @@
 
     /**
      * Search controls that only fetch attributes defined by the schema
-     *
+     * 
      * @return common search controls to use for all LDAP search queries
      * @throws DirectoryException
      */
@@ -223,6 +230,15 @@
 
     protected DirContext createContext() throws DirectoryException {
         try {
+            /*
+             * Dynamic server list requires re-computation on each access
+             */
+            String serverName = config.getServerName();
+            LDAPServerDescriptor serverConfig = factory.getServer(serverName);
+            if (serverConfig.isDynamicServerList()) {
+                String ldapUrls = serverConfig.getLdapUrls();
+                contextProperties.put(Context.PROVIDER_URL, ldapUrls);
+            }
             return new InitialDirContext(contextProperties);
         } catch (NamingException e) {
             log.error(e);
diff -r 77d2e1a9c939 -r 7fdfd927094f nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/LDAPServerDescriptor.java
--- a/nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/LDAPServerDescriptor.java	Mon Jan 25 17:45:50 2010 +0100
+++ b/nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/LDAPServerDescriptor.java	Tue Feb 09 13:55:06 2010 +0000
@@ -19,16 +19,38 @@
 
 package org.nuxeo.ecm.directory.ldap;
 
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.naming.NamingException;
+
 import org.apache.commons.lang.StringUtils;
 import org.nuxeo.common.xmap.annotation.XNode;
 import org.nuxeo.common.xmap.annotation.XNodeList;
 import org.nuxeo.common.xmap.annotation.XObject;
 import org.nuxeo.ecm.directory.DirectoryException;
+import org.nuxeo.ecm.directory.ldap.internal.DNSService;
+import org.nuxeo.ecm.directory.ldap.internal.DNSServiceEntry;
+import org.nuxeo.ecm.directory.ldap.internal.DNSServiceImpl;
+import org.nuxeo.ecm.directory.ldap.internal.HashCodeUtil;
 
+import com.sun.jndi.ldap.LdapURL;
 
 @XObject(value = "server")
 public class LDAPServerDescriptor {
 
+    private static final String LDAPS_SCHEME = "ldaps"; //$NON-NLS-1$
+
+    private static final String LDAP_SCHEME = "ldap"; //$NON-NLS-1$
+
+    private static final Pattern LDAP_SRV_PATTERN = Pattern.compile(
+            "(?:dc=(\\w+),)*(?:dc=(\\w+))", Pattern.CASE_INSENSITIVE);
+
     @XNode("@name")
     public String name;
 
@@ -42,6 +64,18 @@
     @XNode("poolingEnabled")
     public boolean poolingEnabled = true;
 
+    private LinkedHashSet<LdapEntry> ldapEntries;
+
+    private boolean isDynamicServerList = false;
+
+    private boolean useSsl = false;
+
+    private DNSService dnsUtils = DNSServiceImpl.getInstance();
+
+    public boolean isDynamicServerList() {
+        return isDynamicServerList;
+    }
+
     public String getName() {
         return name;
     }
@@ -77,18 +111,178 @@
     }
 
     public String getLdapUrls() {
-        return ldapUrls;
+        if (ldapUrls != null) {
+            return ldapUrls;
+        }
+
+        // Leverage JNDI support for clustered servers by concatinating
+        // all the provided URLs for failover
+        StringBuilder calculatedLdapUrls = new StringBuilder();
+        for (LdapEntry entry : ldapEntries) {
+            calculatedLdapUrls.append(entry);
+            calculatedLdapUrls.append(' ');
+        }
+
+        /*
+         * If the configuration does not contain any domain entries then cache
+         * the urls, domain entries should always be re-queried however as the
+         * LDAP server list should change dynamically
+         */
+        if (!isDynamicServerList) {
+            return ldapUrls = calculatedLdapUrls.toString().trim();
+        }
+        return calculatedLdapUrls.toString().trim();
     }
 
-    @XNodeList(value = "ldapUrl", componentType = String.class, type = String[].class)
-    public void setLdapUrls(String[] ldapUrls) throws DirectoryException {
+    @XNodeList(value = "ldapUrl", componentType = LDAPUrlDescriptor.class, type = LDAPUrlDescriptor[].class)
+    public void setLdapUrls(LDAPUrlDescriptor[] ldapUrls)
+            throws DirectoryException {
         if (ldapUrls == null) {
             throw new DirectoryException(
                     "At least one <ldapUrl/> server declaration is required");
         }
-        // Leverage JNDI support for clustered servers by concatinating
-        // all the provided URLs for failover
-        this.ldapUrls = StringUtils.join(ldapUrls, " ");
+
+        ldapEntries = new LinkedHashSet<LdapEntry>();
+
+        Set<LDAPUrlDescriptor> processed = new HashSet<LDAPUrlDescriptor>();
+
+        List<String> urls = new ArrayList<String>(ldapUrls.length);
+        for (LDAPUrlDescriptor url : ldapUrls) {
+            LdapURL ldapUrl;
+            try {
+                /*
+                 * Empty string translates to ldap://localhost:389 through JNDI
+                 */
+                if (StringUtils.isEmpty(url.getValue())) {
+                    urls.add(url.getValue());
+                    ldapEntries.add(new LdapEntryDescriptor(url));
+                    continue;
+                }
+
+                /*
+                 * Parse the URI to make sure it is valid
+                 */
+                ldapUrl = new LdapURL(url.getValue());
+                if (!processed.add(url)) {
+                    continue;
+                }
+            } catch (NamingException e) {
+                throw new DirectoryException(e);
+            }
+
+            useSsl = useSsl || ldapUrl.useSsl();
+
+            /*
+             * RFC-2255 - The "ldap" prefix indicates an entry or entries
+             * residing in the LDAP server running on the given hostname at the
+             * given portnumber. The default LDAP port is TCP port 389. If no
+             * hostport is given, the client must have some apriori knowledge of
+             * an appropriate LDAP server to contact.
+             */
+            if (ldapUrl.getHost() == null) {
+                /*
+                 * RFC-2782 - Check to see if an LDAP SRV record is defined in
+                 * the DNS server
+                 */
+                String domain = convertDNtoFQDN(ldapUrl.getDN());
+                if (domain != null) {
+                    /*
+                     * Dynamic URL - retrieve from SRV record
+                     */
+                    List<String> discoveredUrls = discoverLdapServers(domain,
+                            ldapUrl.useSsl(), url.getSrvPrefix());
+
+                    /*
+                     * Discovered URLs could be empty, lets check at the end
+                     * though
+                     */
+                    urls.addAll(discoveredUrls);
+
+                    /*
+                     * Store entries in an ordered set and remember that we were
+                     * dynamic
+                     */
+                    ldapEntries.add(new LdapEntryDomain(url, domain,
+                            ldapUrl.useSsl()));
+                    isDynamicServerList = true;
+                } else {
+                    throw new DirectoryException(
+                            "Invalid LDAP SRV reference, this should be of the form ldap:///dc=example,dc=org");
+                }
+            } else {
+                /*
+                 * Static URL - store the value
+                 */
+                urls.add(url.getValue());
+
+                /*
+                 * Store entries in an ordered set
+                 */
+                ldapEntries.add(new LdapEntryDescriptor(url));
+            }
+        }
+
+        /*
+         * Oops no valid URLs to connect to :(
+         */
+        if (urls.isEmpty()) {
+            throw new DirectoryException(
+                    "No valid server urls returned from DNS query");
+        }
+    }
+
+    /**
+     * Whether this server descriptor defines a secure ldap connection
+     */
+    public boolean useSsl() {
+        return useSsl;
+    }
+
+    /**
+     * Retrieve server URLs from DNS SRV record
+     * 
+     * @param domain The domain to query
+     * @param useSsl Whether the connection to this domain should be secure
+     * @return List of servers or empty list
+     */
+    private List<String> discoverLdapServers(String domain, boolean useSsl,
+            String srvPrefix) {
+        List<String> result = new ArrayList<String>();
+        List<DNSServiceEntry> servers = getDnsUtils().resolveLDAPDomainServers(
+                domain, srvPrefix);
+
+        for (DNSServiceEntry serviceEntry : servers) {
+            /*
+             * Rebuild the URL
+             */
+            StringBuilder realUrl = (useSsl) ? new StringBuilder(LDAPS_SCHEME
+                    + "://") : new StringBuilder(LDAP_SCHEME + "://"); //$NON-NLS-1$ //$NON-NLS-2$
+            realUrl.append(serviceEntry);
+            result.add(realUrl.toString());
+        }
+        return result;
+    }
+
+    /**
+     * Convert domain from the ldap form dc=nuxeo,dc=org to the DNS domain name
+     * form nuxeo.org
+     * 
+     * @param path
+     * @return
+     */
+    private String convertDNtoFQDN(String path) {
+        Matcher matcher = LDAP_SRV_PATTERN.matcher(path);
+        if (matcher.find()) {
+            StringBuilder sb = new StringBuilder();
+            for (int i = 1; i <= matcher.groupCount(); i++) {
+                sb.append(matcher.group(i));
+                if (i < matcher.groupCount()) {
+                    sb.append('.');
+                }
+            }
+            return sb.toString();
+        }
+        return null;
     }
 
     public boolean isPoolingEnabled() {
@@ -102,4 +296,105 @@
     public void setConnectionTimeout(int connectionTimeout) {
         this.connectionTimeout = connectionTimeout;
     }
+
+    protected DNSService getDnsUtils() {
+        return dnsUtils;
+    }
+
+    /**
+     * Common internal interface for Ldap entries
+     * 
+     * @author Bob Browning
+     */
+    private interface LdapEntry {
+        public abstract String getUrl();
+    }
+
+    /**
+     * Server URL implementation of {@link LdapEntry}
+     * 
+     * @author Bob Browning
+     */
+    private class LdapEntryDescriptor implements LdapEntry {
+        protected LDAPUrlDescriptor url;
+
+        public LdapEntryDescriptor(LDAPUrlDescriptor descriptor) {
+            this.url = descriptor;
+        }
+
+        @Override
+        public String toString() {
+            return getUrl();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof LdapEntryDescriptor) {
+                return url.equals(obj);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return url.hashCode();
+        }
+
+        public String getUrl() {
+            return url.getValue();
+        }
+
+    }
+
+    /**
+     * Domain implmentation of {@link LdapEntry} using DNS SRV record
+     * 
+     * @author Bob Browning
+     */
+    private final class LdapEntryDomain extends LdapEntryDescriptor {
+
+        private String domain;
+
+        private boolean useSsl;
+
+        private int fHashCode = 0;
+
+        public LdapEntryDomain(LDAPUrlDescriptor descriptor,
+                final String domain, boolean useSsl) {
+            super(descriptor);
+            this.domain = domain;
+            this.useSsl = useSsl;
+        }
+
+        @Override
+        public int hashCode() {
+            /*
+             * Object is immutable so store the hash value
+             */
+            if (fHashCode == 0) {
+                fHashCode = HashCodeUtil.SEED;
+                fHashCode = HashCodeUtil.hash(fHashCode, domain);
+                fHashCode = HashCodeUtil.hash(fHashCode, useSsl);
+            }
+            return fHashCode;
+        }
+
+        @Override
+        public String getUrl() {
+            List<DNSServiceEntry> servers = getDnsUtils().resolveLDAPDomainServers(
+                    domain, url.getSrvPrefix());
+
+            StringBuilder result = new StringBuilder();
+            for (DNSServiceEntry serviceEntry : servers) {
+                /*
+                 * Rebuild the URL
+                 */
+                result.append(useSsl ? LDAPS_SCHEME + "://" : LDAP_SCHEME + "://"); //$NON-NLS-1$ //$NON-NLS-2$
+                result.append(serviceEntry);
+                result.append(' ');
+            }
+            return result.toString().trim();
+        }
+    }
+
 }
diff -r 77d2e1a9c939 -r 7fdfd927094f nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/LDAPUrlDescriptor.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/LDAPUrlDescriptor.java	Tue Feb 09 13:55:06 2010 +0000
@@ -0,0 +1,48 @@
+package org.nuxeo.ecm.directory.ldap;
+
+import org.nuxeo.common.xmap.annotation.XNode;
+import org.nuxeo.common.xmap.annotation.XObject;
+import org.nuxeo.ecm.directory.ldap.internal.DNSService;
+import org.nuxeo.ecm.directory.ldap.internal.HashCodeUtil;
+
+@XObject
+public class LDAPUrlDescriptor {
+    @XNode(value = "@srvPrefix")
+    private String srvPrefix = DNSService.LDAP_SERVICE_PREFIX;
+
+    @XNode
+    private String value;
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof LDAPUrlDescriptor) {
+            return value.equals(((LDAPUrlDescriptor) obj).value)
+                    && srvPrefix.equals(((LDAPUrlDescriptor) obj).srvPrefix);
+        }
+        return false;
+    }
+
+    public String getSrvPrefix() {
+        return srvPrefix;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public int hashCode() {        
+        int result = HashCodeUtil.SEED;
+        result = HashCodeUtil.hash(result, value);
+        result = HashCodeUtil.hash(result, srvPrefix);
+        return result;
+    }
+
+    public void setSrvPrefix(String srvPrefix) {
+        this.srvPrefix = srvPrefix;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+}
diff -r 77d2e1a9c939 -r 7fdfd927094f nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/internal/Cache.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/internal/Cache.java	Tue Feb 09 13:55:06 2010 +0000
@@ -0,0 +1,884 @@
+package org.nuxeo.ecm.directory.ldap.internal;
+
+import java.util.AbstractCollection;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A specialized Map that is size-limited (using an LRU algorithm) and has an
+ * optional expiration time for cache items. The Map is thread-safe.
+ * <p>
+ * The algorithm for cache is as follows: a HashMap is maintained for fast
+ * object lookup. Two linked lists are maintained: one keeps objects in the
+ * order they are accessed from cache, the other keeps objects in the order they
+ * were originally added to cache. When objects are added to cache, they are
+ * first wrapped by a CacheObject which maintains the following pieces of
+ * information:
+ * <ul>
+ * <li>A pointer to the node in the linked list that maintains accessed order
+ * for the object. Keeping a reference to the node lets us avoid linear scans of
+ * the linked list.
+ * <li>A pointer to the node in the linked list that maintains the age of the
+ * object in cache. Keeping a reference to the node lets us avoid linear scans
+ * of the linked list.
+ * </ul>
+ * <p/>
+ * To get an object from cache, a hash lookup is performed to get a reference to
+ * the CacheObject that wraps the real object we are looking for. The object is
+ * subsequently moved to the front of the accessed linked list and any necessary
+ * cache cleanups are performed. Cache deletion and expiration is performed as
+ * needed.
+ * 
+ * @author Matt Tucker
+ */
+class Cache<K, V> implements Map<K, V> {
+
+    /**
+     * The map the keys and values are stored in.
+     */
+    protected Map<K, CacheObject<V>> map;
+
+    /**
+     * Linked list to maintain order that cache objects are accessed in, most
+     * used to least used.
+     */
+    protected LinkedList lastAccessedList;
+
+    /**
+     * Linked list to maintain time that cache objects were initially added to
+     * the cache, most recently added to oldest added.
+     */
+    protected LinkedList ageList;
+
+    /**
+     * Maximum number of items the cache will hold.
+     */
+    protected int maxCacheSize;
+
+    /**
+     * Maximum length of time objects can exist in cache before expiring.
+     */
+    protected long maxLifetime;
+
+    /**
+     * Maintain the number of cache hits and misses. A cache hit occurs every
+     * time the get method is called and the cache contains the requested
+     * object. A cache miss represents the opposite occurence.
+     * <p>
+     * Keeping track of cache hits and misses lets one measure how efficient the
+     * cache is; the higher the percentage of hits, the more efficient.
+     */
+    protected long cacheHits, cacheMisses = 0L;
+
+    /**
+     * Create a new cache and specify the maximum size of for the cache in
+     * bytes, and the maximum lifetime of objects.
+     * 
+     * @param maxSize the maximum number of objects the cache will hold. -1
+     *            means the cache has no max size.
+     * @param maxLifetime the maximum amount of time (in ms) objects can exist
+     *            in cache before being deleted. -1 means objects never expire.
+     */
+    public Cache(int maxSize, long maxLifetime) {
+        if (maxSize == 0) {
+            throw new IllegalArgumentException("Max cache size cannot be 0.");
+        }
+        this.maxCacheSize = maxSize;
+        this.maxLifetime = maxLifetime;
+
+        // Our primary data structure is a hash map. The default capacity of 11
+        // is too small in almost all cases, so we set it bigger.
+        map = new HashMap<K, CacheObject<V>>(103);
+
+        lastAccessedList = new LinkedList();
+        ageList = new LinkedList();
+    }
+
+    public synchronized V put(K key, V value) {
+        V oldValue = null;
+        // Delete an old entry if it exists.
+        if (map.containsKey(key)) {
+            oldValue = remove(key, true);
+        }
+
+        CacheObject<V> cacheObject = new CacheObject<V>(value);
+        map.put(key, cacheObject);
+        // Make an entry into the cache order list.
+        // Store the cache order list entry so that we can get back to it
+        // during later lookups.
+        cacheObject.lastAccessedListNode = lastAccessedList.addFirst(key);
+        // Add the object to the age list
+        LinkedListNode ageNode = ageList.addFirst(key);
+        ageNode.timestamp = System.currentTimeMillis();
+        cacheObject.ageListNode = ageNode;
+
+        // If cache is too full, remove least used cache entries until it is not
+        // too full.
+        cullCache();
+
+        return oldValue;
+    }
+
+    public synchronized V get(Object key) {
+        // First, clear all entries that have been in cache longer than the
+        // maximum defined age.
+        deleteExpiredEntries();
+
+        CacheObject<V> cacheObject = map.get(key);
+        if (cacheObject == null) {
+            // The object didn't exist in cache, so increment cache misses.
+            cacheMisses++;
+            return null;
+        }
+        // Remove the object from it's current place in the cache order list,
+        // and re-insert it at the front of the list.
+        cacheObject.lastAccessedListNode.remove();
+        lastAccessedList.addFirst(cacheObject.lastAccessedListNode);
+
+        // The object exists in cache, so increment cache hits. Also, increment
+        // the object's read count.
+        cacheHits++;
+        cacheObject.readCount++;
+
+        return cacheObject.object;
+    }
+
+    public synchronized V remove(Object key) {
+        return remove(key, false);
+    }
+
+    /*
+     * Remove operation with a flag so we can tell coherence if the remove was
+     * caused by cache internal processing such as eviction or loading
+     */
+    public synchronized V remove(Object key, boolean internal) {
+        // noinspection SuspiciousMethodCalls
+        CacheObject<V> cacheObject = map.remove(key);
+        // If the object is not in cache, stop trying to remove it.
+        if (cacheObject == null) {
+            return null;
+        }
+        // Remove from the cache order list
+        cacheObject.lastAccessedListNode.remove();
+        cacheObject.ageListNode.remove();
+        // Remove references to linked list nodes
+        cacheObject.ageListNode = null;
+        cacheObject.lastAccessedListNode = null;
+
+        return cacheObject.object;
+    }
+
+    public synchronized void clear() {
+        Object[] keys = map.keySet().toArray();
+        for (Object key : keys) {
+            remove(key);
+        }
+
+        // Now, reset all containers.
+        map.clear();
+        lastAccessedList.clear();
+        ageList.clear();
+
+        cacheHits = 0;
+        cacheMisses = 0;
+    }
+
+    public synchronized int size() {
+        // First, clear all entries that have been in cache longer than the
+        // maximum defined age.
+        deleteExpiredEntries();
+
+        return map.size();
+    }
+
+    public synchronized boolean isEmpty() {
+        // First, clear all entries that have been in cache longer than the
+        // maximum defined age.
+        deleteExpiredEntries();
+
+        return map.isEmpty();
+    }
+
+    public synchronized Collection<V> values() {
+        // First, clear all entries that have been in cache longer than the
+        // maximum defined age.
+        deleteExpiredEntries();
+
+        return Collections.unmodifiableCollection(new AbstractCollection<V>() {
+            Collection<CacheObject<V>> values = map.values();
+
+            public Iterator<V> iterator() {
+                return new Iterator<V>() {
+                    Iterator<CacheObject<V>> it = values.iterator();
+
+                    public boolean hasNext() {
+                        return it.hasNext();
+                    }
+
+                    public V next() {
+                        return it.next().object;
+                    }
+
+                    public void remove() {
+                        it.remove();
+                    }
+                };
+            }
+
+            public int size() {
+                return values.size();
+            }
+        });
+    }
+
+    public synchronized boolean containsKey(Object key) {
+        // First, clear all entries that have been in cache longer than the
+        // maximum defined age.
+        deleteExpiredEntries();
+
+        return map.containsKey(key);
+    }
+
+    public void putAll(Map<? extends K, ? extends V> map) {
+        for (Entry<? extends K, ? extends V> entry : map.entrySet()) {
+            V value = entry.getValue();
+            // If the map is another DefaultCache instance than the
+            // entry values will be CacheObject instances that need
+            // to be converted to the normal object form.
+            if (value instanceof CacheObject) {
+                // noinspection unchecked
+                value = ((CacheObject<V>) value).object;
+            }
+            put(entry.getKey(), value);
+        }
+    }
+
+    public synchronized boolean containsValue(Object value) {
+        // First, clear all entries that have been in cache longer than the
+        // maximum defined age.
+        deleteExpiredEntries();
+
+        // noinspection unchecked
+        CacheObject<V> cacheObject = new CacheObject<V>((V) value);
+
+        return map.containsValue(cacheObject);
+    }
+
+    public synchronized Set<Map.Entry<K, V>> entrySet() {
+        // Warning -- this method returns CacheObject instances and not Objects
+        // in the same form they were put into cache.
+
+        // First, clear all entries that have been in cache longer than the
+        // maximum defined age.
+        deleteExpiredEntries();
+
+        return new AbstractSet<Map.Entry<K, V>>() {
+            private final Set<Map.Entry<K, CacheObject<V>>> set = map.entrySet();
+
+            public Iterator<Entry<K, V>> iterator() {
+                return new Iterator<Entry<K, V>>() {
+                    private final Iterator<Entry<K, CacheObject<V>>> it = set.iterator();
+
+                    public boolean hasNext() {
+                        return it.hasNext();
+                    }
+
+                    public Entry<K, V> next() {
+                        Map.Entry<K, CacheObject<V>> entry = it.next();
+                        return new AbstractMapEntry<K, V>(entry.getKey(),
+                                entry.getValue().object) {
+                            @Override
+                            public V setValue(V value) {
+                                throw new UnsupportedOperationException(
+                                        "Cannot set");
+                            }
+                        };
+                    }
+
+                    public void remove() {
+                        it.remove();
+                    }
+                };
+
+            }
+
+            public int size() {
+                return set.size();
+            }
+        };
+    }
+
+    public synchronized Set<K> keySet() {
+        // First, clear all entries that have been in cache longer than the
+        // maximum defined age.
+        deleteExpiredEntries();
+
+        return Collections.unmodifiableSet(map.keySet());
+    }
+
+    public long getCacheHits() {
+        return cacheHits;
+    }
+
+    public long getCacheMisses() {
+        return cacheMisses;
+    }
+
+    public int getMaxCacheSize() {
+        return maxCacheSize;
+    }
+
+    public synchronized void setMaxCacheSize(int maxCacheSize) {
+        this.maxCacheSize = maxCacheSize;
+        // It's possible that the new max size is smaller than our current cache
+        // size. If so, we need to delete infrequently used items.
+        cullCache();
+    }
+
+    public long getMaxLifetime() {
+        return maxLifetime;
+    }
+
+    public void setMaxLifetime(long maxLifetime) {
+        this.maxLifetime = maxLifetime;
+    }
+
+    /**
+     * Clears all entries out of cache where the entries are older than the
+     * maximum defined age.
+     */
+    protected synchronized void deleteExpiredEntries() {
+        // Check if expiration is turned on.
+        if (maxLifetime <= 0) {
+            return;
+        }
+
+        // Remove all old entries. To do this, we remove objects from the end
+        // of the linked list until they are no longer too old. We get to avoid
+        // any hash lookups or looking at any more objects than is strictly
+        // neccessary.
+        LinkedListNode node = ageList.getLast();
+        // If there are no entries in the age list, return.
+        if (node == null) {
+            return;
+        }
+
+        // Determine the expireTime, which is the moment in time that elements
+        // should expire from cache. Then, we can do an easy check to see
+        // if the expire time is greater than the expire time.
+        long expireTime = System.currentTimeMillis() - maxLifetime;
+
+        while (expireTime > node.timestamp) {
+            if (remove(node.object, true) == null) {
+                System.err.println("Error attempting to remove("
+                        + node.object.toString()
+                        + ") - cacheObject not found in cache!");
+                // remove from the ageList
+                node.remove();
+            }
+
+            // Get the next node.
+            node = ageList.getLast();
+            // If there are no more entries in the age list, return.
+            if (node == null) {
+                return;
+            }
+        }
+    }
+
+    /**
+     * Removes the least recently used elements if the cache size is greater
+     * than or equal to the maximum allowed size until the cache is at least 10%
+     * empty.
+     */
+    protected synchronized void cullCache() {
+        // Check if a max cache size is defined.
+        if (maxCacheSize < 0) {
+            return;
+        }
+
+        // See if the cache is too big. If so, clean out cache until it's 10%
+        // free.
+        if (map.size() > maxCacheSize) {
+            // First, delete any old entries to see how much memory that frees.
+            deleteExpiredEntries();
+            // Next, delete the least recently used elements until 10% of the
+            // cache
+            // has been freed.
+            int desiredSize = (int) (maxCacheSize * .90);
+            for (int i = map.size(); i > desiredSize; i--) {
+                // Get the key and invoke the remove method on it.
+                if (remove(lastAccessedList.getLast().object, true) == null) {
+                    System.err.println("Error attempting to cullCache with remove("
+                            + lastAccessedList.getLast().object.toString()
+                            + ") - " + "cacheObject not found in cache!");
+                    lastAccessedList.getLast().remove();
+                }
+            }
+        }
+    }
+
+    /**
+     * Wrapper for all objects put into cache. It's primary purpose is to
+     * maintain references to the linked lists that maintain the creation time
+     * of the object and the ordering of the most used objects. This class is
+     * optimized for speed rather than strictly correct encapsulation.
+     */
+    private static class CacheObject<V> {
+
+        /**
+         * Underlying object wrapped by the CacheObject.
+         */
+        public V object;
+
+        /**
+         * A reference to the node in the cache order list. We keep the
+         * reference here to avoid linear scans of the list. Every time the
+         * object is accessed, the node is removed from its current spot in the
+         * list and moved to the front.
+         */
+        public LinkedListNode lastAccessedListNode;
+
+        /**
+         * A reference to the node in the age order list. We keep the reference
+         * here to avoid linear scans of the list. The reference is used if the
+         * object has to be deleted from the list.
+         */
+        public LinkedListNode ageListNode;
+
+        /**
+         * A count of the number of times the object has been read from cache.
+         */
+        public int readCount = 0;
+
+        /**
+         * Creates a new cache object wrapper.
+         * 
+         * @param object the underlying Object to wrap.
+         */
+        public CacheObject(V object) {
+            this.object = object;
+        }
+
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof CacheObject)) {
+                return false;
+            }
+
+            final CacheObject cacheObject = (CacheObject) o;
+
+            return object.equals(cacheObject.object);
+
+        }
+
+        public int hashCode() {
+            return object.hashCode();
+        }
+    }
+
+    /**
+     * Simple LinkedList implementation. The main feature is that list nodes are
+     * public, which allows very fast delete operations when one has a reference
+     * to the node that is to be deleted.
+     * <p>
+     */
+    private static class LinkedList {
+
+        /**
+         * The root of the list keeps a reference to both the first and last
+         * elements of the list.
+         */
+        private LinkedListNode head = new LinkedListNode("head", null, null);
+
+        /**
+         * Creates a new linked list.
+         */
+        public LinkedList() {
+            head.next = head.previous = head;
+        }
+
+        /**
+         * Returns the first linked list node in the list.
+         * 
+         * @return the first element of the list.
+         */
+        public LinkedListNode getFirst() {
+            LinkedListNode node = head.next;
+            if (node == head) {
+                return null;
+            }
+            return node;
+        }
+
+        /**
+         * Returns the last linked list node in the list.
+         * 
+         * @return the last element of the list.
+         */
+        public LinkedListNode getLast() {
+            LinkedListNode node = head.previous;
+            if (node == head) {
+                return null;
+            }
+            return node;
+        }
+
+        /**
+         * Adds a node to the beginning of the list.
+         * 
+         * @param node the node to add to the beginning of the list.
+         * @return the node
+         */
+        public LinkedListNode addFirst(LinkedListNode node) {
+            node.next = head.next;
+            node.previous = head;
+            node.previous.next = node;
+            node.next.previous = node;
+            return node;
+        }
+
+        /**
+         * Adds an object to the beginning of the list by automatically creating
+         * a a new node and adding it to the beginning of the list.
+         * 
+         * @param object the object to add to the beginning of the list.
+         * @return the node created to wrap the object.
+         */
+        public LinkedListNode addFirst(Object object) {
+            LinkedListNode node = new LinkedListNode(object, head.next, head);
+            node.previous.next = node;
+            node.next.previous = node;
+            return node;
+        }
+
+        /**
+         * Adds an object to the end of the list by automatically creating a a
+         * new node and adding it to the end of the list.
+         * 
+         * @param object the object to add to the end of the list.
+         * @return the node created to wrap the object.
+         */
+        public LinkedListNode addLast(Object object) {
+            LinkedListNode node = new LinkedListNode(object, head,
+                    head.previous);
+            node.previous.next = node;
+            node.next.previous = node;
+            return node;
+        }
+
+        /**
+         * Erases all elements in the list and re-initializes it.
+         */
+        public void clear() {
+            // Remove all references in the list.
+            LinkedListNode node = getLast();
+            while (node != null) {
+                node.remove();
+                node = getLast();
+            }
+
+            // Re-initialize.
+            head.next = head.previous = head;
+        }
+
+        /**
+         * Returns a String representation of the linked list with a comma
+         * delimited list of all the elements in the list.
+         * 
+         * @return a String representation of the LinkedList.
+         */
+        public String toString() {
+            LinkedListNode node = head.next;
+            StringBuilder buf = new StringBuilder();
+            while (node != head) {
+                buf.append(node.toString()).append(", ");
+                node = node.next;
+            }
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Doubly linked node in a LinkedList. Most LinkedList implementations keep
+     * the equivalent of this class private. We make it public so that
+     * references to each node in the list can be maintained externally.
+     * Exposing this class lets us make remove operations very fast. Remove is
+     * built into this class and only requires two reference reassignments. If
+     * remove existed in the main LinkedList class, a linear scan would have to
+     * be performed to find the correct node to delete. The linked list
+     * implementation was specifically written for the Jive cache system. While
+     * it can be used as a general purpose linked list, for most applications,
+     * it is more suitable to use the linked list that is part of the Java
+     * Collections package.
+     */
+    private static class LinkedListNode {
+
+        public LinkedListNode previous;
+
+        public LinkedListNode next;
+
+        public Object object;
+
+        /**
+         * This class is further customized for the Jive cache system. It
+         * maintains a timestamp of when a Cacheable object was first added to
+         * cache. Timestamps are stored as long values and represent the number
+         * of milliseconds passed since January 1, 1970 00:00:00.000 GMT.
+         * <p>
+         * The creation timestamp is used in the case that the cache has a
+         * maximum lifetime set. In that case, when [current time] - [creation
+         * time] > [max lifetime], the object will be deleted from cache.
+         */
+        public long timestamp;
+
+        /**
+         * Constructs a new linked list node.
+         * 
+         * @param object the Object that the node represents.
+         * @param next a reference to the next LinkedListNode in the list.
+         * @param previous a reference to the previous LinkedListNode in the
+         *            list.
+         */
+        public LinkedListNode(Object object, LinkedListNode next,
+                LinkedListNode previous) {
+            this.object = object;
+            this.next = next;
+            this.previous = previous;
+        }
+
+        /**
+         * Removes this node from the linked list that it is a part of.
+         */
+        public void remove() {
+            previous.next = next;
+            next.previous = previous;
+        }
+
+        /**
+         * Returns a String representation of the linked list node by calling
+         * the toString method of the node's object.
+         * 
+         * @return a String representation of the LinkedListNode.
+         */
+        public String toString() {
+            return object.toString();
+        }
+    }
+}
+
+// GenericsNote: Converted.
+/*
+ * Copyright 2003-2004 The Apache Software Foundation 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.
+ */
+
+/**
+ * Abstract Pair class to assist with creating correct Map Entry
+ * implementations.
+ * 
+ * @author James Strachan
+ * @author Michael A. Smith
+ * @author Neil O'Toole
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $
+ * @since Commons Collections 3.0
+ */
+abstract class AbstractMapEntry<K, V> extends AbstractKeyValue<K, V> implements
+        Map.Entry<K, V> {
+
+    /**
+     * Constructs a new entry with the given key and given value.
+     * 
+     * @param key the key for the entry, may be null
+     * @param value the value for the entry, may be null
+     */
+    protected AbstractMapEntry(K key, V value) {
+        super(key, value);
+    }
+
+    // Map.Entry interface
+    // -------------------------------------------------------------------------
+    /**
+     * Sets the value stored in this Map Entry.
+     * <p/>
+     * This Map Entry is not connected to a Map, so only the local data is
+     * changed.
+     * 
+     * @param value the new value
+     * @return the previous value
+     */
+    public V setValue(V value) {
+        V answer = this.value;
+        this.value = value;
+        return answer;
+    }
+
+    /**
+     * Compares this Map Entry with another Map Entry.
+     * <p/>
+     * Implemented per API documentation of
+     * {@link java.util.Map.Entry#equals(Object)}
+     * 
+     * @param obj the object to compare to
+     * @return true if equal key and value
+     */
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj instanceof Map.Entry == false) {
+            return false;
+        }
+        Map.Entry other = (Map.Entry) obj;
+        return (getKey() == null ? other.getKey() == null : getKey().equals(
+                other.getKey()))
+                && (getValue() == null ? other.getValue() == null
+                        : getValue().equals(other.getValue()));
+    }
+
+    /**
+     * Gets a hashCode compatible with the equals method.
+     * <p/>
+     * Implemented per API documentation of
+     * {@link java.util.Map.Entry#hashCode()}
+     * 
+     * @return a suitable hash code
+     */
+    public int hashCode() {
+        return (getKey() == null ? 0 : getKey().hashCode())
+                ^ (getValue() == null ? 0 : getValue().hashCode());
+    }
+
+}
+
+// GenericsNote: Converted.
+/*
+ * Copyright 2003-2004 The Apache Software Foundation 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.
+ */
+
+/**
+ * Abstract pair class to assist with creating KeyValue and MapEntry
+ * implementations.
+ * 
+ * @author James Strachan
+ * @author Michael A. Smith
+ * @author Neil O'Toole
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $
+ * @since Commons Collections 3.0
+ */
+abstract class AbstractKeyValue<K, V> implements KeyValue<K, V> {
+
+    /**
+     * The key
+     */
+    protected K key;
+
+    /**
+     * The value
+     */
+    protected V value;
+
+    /**
+     * Constructs a new pair with the specified key and given value.
+     * 
+     * @param key the key for the entry, may be null
+     * @param value the value for the entry, may be null
+     */
+    protected AbstractKeyValue(K key, V value) {
+        super();
+        this.key = key;
+        this.value = value;
+    }
+
+    /**
+     * Gets the key from the pair.
+     * 
+     * @return the key
+     */
+    public K getKey() {
+        return key;
+    }
+
+    /**
+     * Gets the value from the pair.
+     * 
+     * @return the value
+     */
+    public V getValue() {
+        return value;
+    }
+
+    /**
+     * Gets a debugging String view of the pair.
+     * 
+     * @return a String view of the entry
+     */
+    public String toString() {
+        return new StringBuilder().append(getKey()).append('=').append(
+                getValue()).toString();
+    }
+
+}
+
+// GenericsNote: Converted.
+/*
+ * Copyright 2003-2004 The Apache Software Foundation 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.
+ */
+
+/**
+ * Defines a simple key value pair.
+ * <p/>
+ * A Map Entry has considerable additional semantics over and above a simple
+ * key-value pair. This interface defines the minimum key value, with just the
+ * two get methods.
+ * 
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:19 $
+ * @since Commons Collections 3.0
+ */
+interface KeyValue<K, V> {
+
+    /**
+     * Gets the key from the pair.
+     * 
+     * @return the key
+     */
+    K getKey();
+
+    /**
+     * Gets the value from the pair.
+     * 
+     * @return the value
+     */
+    V getValue();
+
+}
\ No newline at end of file
diff -r 77d2e1a9c939 -r 7fdfd927094f nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/internal/DNSService.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/internal/DNSService.java	Tue Feb 09 13:55:06 2010 +0000
@@ -0,0 +1,41 @@
+package org.nuxeo.ecm.directory.ldap.internal;
+
+import java.util.List;
+
+
+public interface DNSService {
+
+    /**
+     * DNS Cache Expiry property
+     */
+    public static final String DNS_CACHE_EXPIRY = "org.nuxeo.ecm.directory.ldap.dns.cache.expiry"; //$NON-NLS-1$
+
+    /**
+     * Prefix to locate LDAP service on DNS Server<br/>
+     * <br/>
+     * <b>service</b>: _ldap<br/>
+     * <b>protocol</b>: _tcp
+     */
+    public static final String LDAP_SERVICE_PREFIX = "_ldap._tcp"; //$NON-NLS-1$
+
+    /**
+     * Returns a list of AD servers for the specified domain by performing an
+     * SRV DNS lookup
+     * 
+     * @param domain
+     * @return
+     */
+    public abstract List<DNSServiceEntry> resolveLDAPDomainServers(
+            final String domain);
+
+    /**
+     * Returns a list of AD servers for the specified domain by performing an
+     * SRV DNS lookup
+     * 
+     * @param domain
+     * @return
+     */
+    public abstract List<DNSServiceEntry> resolveLDAPDomainServers(
+            final String domain, final String prefix);
+
+}
\ No newline at end of file
diff -r 77d2e1a9c939 -r 7fdfd927094f nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/internal/DNSServiceEntry.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/internal/DNSServiceEntry.java	Tue Feb 09 13:55:06 2010 +0000
@@ -0,0 +1,106 @@
+package org.nuxeo.ecm.directory.ldap.internal;
+
+
+/**
+ * Encapsulates a hostname and port.
+ */
+public class DNSServiceEntry implements Comparable<DNSServiceEntry> {
+
+    private String host;
+
+    private int port;
+
+    private int priority;
+
+    private int weight;
+
+    private int fHashCode = 0;
+
+    DNSServiceEntry(String host, int port, int priority, int weight) {
+        this.host = host;
+        this.port = port;
+        this.priority = priority;
+        this.weight = weight;
+    }
+
+    /**
+     * Get the priority of this DNS entry, descending priority
+     * 0(highest)..Integer.MAX_VALUE(lowest)
+     */
+    public int getPriority() {
+        return priority;
+    }
+
+    /**
+     * Get the weight of this DNS entry to compare entries with equal
+     * priority, ascending weight 0(lowest)..Integer.MAX_VALUE(highest)
+     */
+    public int getWeight() {
+        return weight;
+    }
+
+    /**
+     * Returns the hostname.
+     */
+    public String getHost() {
+        return host;
+    }
+
+    /**
+     * Returns the port.
+     */
+    public int getPort() {
+        return port;
+    }
+
+    /**
+     * Return the hostname in the form (hostname:port)
+     */
+    @Override
+    public String toString() {
+        return host + ":" + port; //$NON-NLS-1$
+    }
+
+    /**
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof DNSServiceEntry)) {
+            return false;
+        }
+
+        final DNSServiceEntry address = (DNSServiceEntry) o;
+
+        if (!host.equals(address.host)) {
+            return false;
+        }
+        return port == address.port;
+    }
+
+    /**
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        if (fHashCode == 0) {
+            fHashCode = HashCodeUtil.SEED;
+            fHashCode = HashCodeUtil.hash(fHashCode, host);
+            fHashCode = HashCodeUtil.hash(fHashCode, port);
+        }
+        return fHashCode;
+    }
+
+    /**
+     * @see java.lang.Comparable#compareTo(java.lang.Object)
+     */
+    public int compareTo(DNSServiceEntry o) {
+        if (o.priority == priority) {
+            return o.weight - weight;
+        }
+        return priority - o.priority;
+    }
+}
\ No newline at end of file
diff -r 77d2e1a9c939 -r 7fdfd927094f nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/internal/DNSServiceImpl.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/internal/DNSServiceImpl.java	Tue Feb 09 13:55:06 2010 +0000
@@ -0,0 +1,159 @@
+package org.nuxeo.ecm.directory.ldap.internal;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+
+/**
+ * Utility class to perform DNS lookups for services.
+ */
+public class DNSServiceImpl implements DNSService {
+
+    private static DNSService instance;
+
+    private static final String SRV_RECORD = "SRV"; //$NON-NLS-1$
+
+    /**
+     * Create a cache to hold the 100 most recently accessed DNS lookups for a
+     * period of 10 minutes.
+     */
+    private static Map<String, List<DNSServiceEntry>> cache;
+
+    private static DirContext context;
+
+    public static final synchronized DNSService getInstance() {
+        if (instance == null) {
+            instance = new DNSServiceImpl();
+        }
+        return instance;
+    }
+
+    private DNSServiceImpl() {
+        /*
+         * The expiry of the cache in minutes
+         */
+        int cacheExpiry = 10;
+        try {
+            cacheExpiry = Integer.parseInt(System.getProperty(DNS_CACHE_EXPIRY,
+                    "10")); //$NON-NLS-1$
+        } catch (NumberFormatException e) {
+        }
+        cache = new Cache<String, List<DNSServiceEntry>>(100,
+                1000 * 60 * cacheExpiry);
+
+        try {
+            Properties env = new Properties();
+            env.put(
+                    "java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); //$NON-NLS-1$ //$NON-NLS-2$
+            context = new InitialDirContext(env);
+        } catch (Exception e) {
+            // Ignore.
+        }
+    }
+
+    /**
+     * Returns the host name and port that a server providing the specified
+     * service can be reached at. A DNS lookup for a SRV record in the form
+     * "_service.example.com" is attempted.
+     * <p>
+     * As an example, a lookup for "example.com" for the service _gc._tcp may
+     * return "dc01.example.com:3268".
+     * 
+     * @param service the service.
+     * @param domain the domain.
+     * @return a List of DNSServiceEntrys, which encompasses the hostname and
+     *         port that the server can be reached at for the specified domain.
+     */
+    private List<DNSServiceEntry> resolveDnsServiceRecord(final String service,
+            final String domain) {
+        List<DNSServiceEntry> addresses = new ArrayList<DNSServiceEntry>();
+
+        if (context == null) {
+            return addresses;
+        }
+
+        final String key = service + "." + domain; //$NON-NLS-1$
+        /*
+         * Return item from cache if it exists.
+         */
+        if (cache.containsKey(key)) {
+            addresses = (List<DNSServiceEntry>) cache.get(key);
+            if (addresses != null) {
+                return addresses;
+            }
+        }
+
+        try {
+            Attributes dnsLookup = context.getAttributes(
+                    service + "." + domain, new String[] { SRV_RECORD }); //$NON-NLS-1$
+
+            Attribute attribute = dnsLookup.get(SRV_RECORD);
+            for (int i = 0; i < attribute.size(); i++) {
+                /*
+                 * Get the current resource record
+                 */
+                String entry = (String) attribute.get(i);
+
+                String[] records = entry.split(" "); //$NON-NLS-1$
+                String host = records[records.length - 1];
+                int port = Integer.parseInt(records[records.length - 2]);
+                int weight = Integer.parseInt(records[records.length - 3]);
+                int priority = Integer.parseInt(records[records.length - 4]);
+
+                /*
+                 * possible to get TTL?
+                 */
+
+                /*
+                 * Host entries in DNS should end with a "."
+                 */
+                if (host.endsWith(".")) { //$NON-NLS-1$
+                    host = host.substring(0, host.length() - 1);
+                }
+
+                addresses.add(new DNSServiceEntry(host, port, priority, weight));
+            }
+        } catch (Exception e) {
+            // Ignore.
+        }
+
+        /*
+         * Sort the addresses by DNS priority and weight settings
+         */
+        Collections.sort(addresses);
+
+        /*
+         * Add item to cache.
+         */
+        cache.put(key, addresses);
+        return addresses;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see
+     * org.nuxeo.ecm.directory.ldap.internal.DNSService#resolveLDAPDomainServers
+     * (java.lang.String)
+     */
+    public List<DNSServiceEntry> resolveLDAPDomainServers(final String domain) {
+        return resolveDnsServiceRecord(LDAP_SERVICE_PREFIX, domain);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see
+     * org.nuxeo.ecm.directory.ldap.internal.DNSService#resolveLDAPDomainServers
+     * (java.lang.String, java.lang.String)
+     */
+    public List<DNSServiceEntry> resolveLDAPDomainServers(final String domain,
+            final String prefix) {
+        return resolveDnsServiceRecord(prefix, domain);
+    }
+}
\ No newline at end of file
diff -r 77d2e1a9c939 -r 7fdfd927094f nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/internal/HashCodeUtil.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuxeo-platform-directory-ldap/src/main/java/org/nuxeo/ecm/directory/ldap/internal/HashCodeUtil.java	Tue Feb 09 13:55:06 2010 +0000
@@ -0,0 +1,111 @@
+package org.nuxeo.ecm.directory.ldap.internal;
+
+import java.lang.reflect.Array;
+
+/**
+ * Collected methods which allow easy implementation of <code>hashCode</code>.
+ * Example use case:
+ * 
+ * <pre>
+ * public int hashCode() {
+ *     int result = HashCodeUtil.SEED;
+ *     //collect the contributions of various fields
+ *     result = HashCodeUtil.hash(result, fPrimitive);
+ *     result = HashCodeUtil.hash(result, fObject);
+ *     result = HashCodeUtil.hash(result, fArray);
+ *     return result;
+ * }
+ * </pre>
+ * 
+ * The following utility class allows simple construction of an effective
+ * hashCode method. It is based on the recommendations of Effective Java, by
+ * Joshua Bloch.
+ */
+public final class HashCodeUtil {
+
+    /**
+     * An initial value for a <code>hashCode</code>, to which is added
+     * contributions from fields. Using a non-zero value decreases collisons of
+     * <code>hashCode</code> values.
+     */
+    public static final int SEED = 23;
+
+    /**
+     * booleans.
+     */
+    public static int hash(int aSeed, boolean aBoolean) {
+        return firstTerm(aSeed) + (aBoolean ? 1 : 0);
+    }
+
+    /**
+     * chars.
+     */
+    public static int hash(int aSeed, char aChar) {
+        return firstTerm(aSeed) + (int) aChar;
+    }
+
+    /**
+     * ints.
+     */
+    public static int hash(int aSeed, int aInt) {
+        /*
+         * Implementation Note Note that byte and short are handled by this
+         * method, through implicit conversion.
+         */
+        return firstTerm(aSeed) + aInt;
+    }
+
+    /**
+     * longs.
+     */
+    public static int hash(int aSeed, long aLong) {
+        return firstTerm(aSeed) + (int) (aLong ^ (aLong >>> 32));
+    }
+
+    /**
+     * floats.
+     */
+    public static int hash(int aSeed, float aFloat) {
+        return hash(aSeed, Float.floatToIntBits(aFloat));
+    }
+
+    /**
+     * doubles.
+     */
+    public static int hash(int aSeed, double aDouble) {
+        return hash(aSeed, Double.doubleToLongBits(aDouble));
+    }
+
+    /**
+     * <code>aObject</code> is a possibly-null object field, and possibly an
+     * array. If <code>aObject</code> is an array, then each element may be a
+     * primitive or a possibly-null object.
+     */
+    public static int hash(int aSeed, Object aObject) {
+        int result = aSeed;
+        if (aObject == null) {
+            result = hash(result, 0);
+        } else if (!isArray(aObject)) {
+            result = hash(result, aObject.hashCode());
+        } else {
+            int length = Array.getLength(aObject);
+            for (int idx = 0; idx < length; ++idx) {
+                Object item = Array.get(aObject, idx);
+                // recursive call!
+                result = hash(result, item);
+            }
+        }
+        return result;
+    }
+
+    // / PRIVATE ///
+    private static final int fODD_PRIME_NUMBER = 37;
+
+    private static int firstTerm(int aSeed) {
+        return fODD_PRIME_NUMBER * aSeed;
+    }
+
+    private static boolean isArray(Object aObject) {
+        return aObject.getClass().isArray();
+    }
+}
\ No newline at end of file
diff -r 77d2e1a9c939 -r 7fdfd927094f nuxeo-platform-directory-ldap/src/test/java/org/nuxeo/ecm/directory/ldap/internal/LDAPServerDescriptorDNSTestCase.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuxeo-platform-directory-ldap/src/test/java/org/nuxeo/ecm/directory/ldap/internal/LDAPServerDescriptorDNSTestCase.java	Tue Feb 09 13:55:06 2010 +0000
@@ -0,0 +1,145 @@
+package org.nuxeo.ecm.directory.ldap.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.nuxeo.ecm.directory.ldap.LDAPServerDescriptor;
+import org.nuxeo.ecm.directory.ldap.LDAPUrlDescriptor;
+
+import com.sun.jndi.ldap.LdapURL;
+
+/**
+ * Test case to ensure LDAPServerDescriptor correctly handles entries returned
+ * from DNSService implementation
+ * 
+ * @author Bob Browning
+ */
+public class LDAPServerDescriptorDNSTestCase extends TestCase {
+
+    private static final Log log = LogFactory.getLog(LDAPServerDescriptorDNSTestCase.class);
+
+    private final class MockDNSService implements DNSService {
+
+        public List<DNSServiceEntry> resolveLDAPDomainServers(String domain,
+                String prefix) {
+            List<DNSServiceEntry> entries = new ArrayList<DNSServiceEntry>();
+            if (prefix.equals("_gc._tcp")) { //$NON-NLS-1$
+                entries.add(new DNSServiceEntry("localhost", 3268, 0, 100)); //$NON-NLS-1$
+            } else if (prefix.equals("_ldap._tcp")) { //$NON-NLS-1$
+                entries.add(new DNSServiceEntry("localhost", 389, 0, 100)); //$NON-NLS-1$
+            }
+            return entries;
+        }
+
+        public List<DNSServiceEntry> resolveLDAPDomainServers(String domain) {
+            List<DNSServiceEntry> entries = new ArrayList<DNSServiceEntry>();
+            entries.add(new DNSServiceEntry("localhost", 389, 0, 100)); //$NON-NLS-1$
+            return entries;
+        }
+
+    }
+
+    private final class MockLDAPServerDescriptor extends LDAPServerDescriptor {
+        /*
+         * Mock DNS Service
+         */
+        private DNSService service = new MockDNSService();
+
+        @Override
+        protected DNSService getDnsUtils() {
+            return service;
+        };
+
+    }
+
+    private MockDNSService dns;
+
+    @Override
+    protected void setUp() throws Exception {
+        dns = new MockDNSService();
+    }
+
+    private final String domain = "ldap:///dc=nuxeo,dc=org"; //$NON-NLS-1$
+
+    /**
+     * Unit test the set/get LdapUrl methods
+     * 
+     * @throws Exception
+     */
+    public void testLdapServerDnsParsing() throws Exception {
+        List<DNSServiceEntry> actual = dns.resolveLDAPDomainServers("nuxeo.org"); //$NON-NLS-1$
+        if (log.isDebugEnabled()) {
+            log.debug(actual);
+        }
+
+        /*
+         * Convert our discovered server list into URIs
+         */
+        LDAPUrlDescriptor[] uris = new LDAPUrlDescriptor[actual.size()];
+        int i = 0;
+        for (DNSServiceEntry serviceEntry : actual) {
+            LDAPUrlDescriptor u = new LDAPUrlDescriptor();
+            u.setValue("ldap://" + serviceEntry.toString());
+            uris[i++] = u;
+        }
+
+        LDAPServerDescriptor d = new MockLDAPServerDescriptor();
+
+        d.setLdapUrls(uris);
+        String testA = d.getLdapUrls();
+        if (log.isDebugEnabled()) {
+            log.debug(testA);
+        }
+        assertEquals("ldap://localhost:389", testA);
+
+        d = new MockLDAPServerDescriptor();
+
+        /*
+         * _ldap._tcp test
+         */
+        LDAPUrlDescriptor u = new LDAPUrlDescriptor();
+        u.setValue(domain);
+
+        /*
+         * _gc._tcp test
+         */
+        LDAPUrlDescriptor u2 = new LDAPUrlDescriptor();
+        u2.setSrvPrefix("_gc._tcp");
+        u2.setValue(domain);
+
+        d.setLdapUrls(new LDAPUrlDescriptor[] { u, u2 });
+
+        for (int j = 0; j < 100; j++) {
+            String urls = d.getLdapUrls();
+            if (log.isDebugEnabled()) {
+                log.debug(urls);
+            }
+            assertEquals("ldap://localhost:389 ldap://localhost:3268", urls);
+        }
+
+        d.setLdapUrls(new LDAPUrlDescriptor[] { u });
+
+        /*
+         * Assert that when run using empty hostname in URI we get the same
+         * result as passing in the list of servers
+         */
+        assertEquals(testA, d.getLdapUrls());
+    }
+
+    /**
+     * Ensure LdapURL correctly determines SSL support of server URL
+     * 
+     * @throws Exception
+     */
+    public void testLdapUrlSslSupport() throws Exception {
+        LdapURL url = new LdapURL("ldap:///dc=example,dc=com");
+        assertFalse(url.useSsl());
+
+        url = new LdapURL("ldaps:///dc=example,dc=com");
+        assertTrue(url.useSsl());
+    }
+}

