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 2013-2016 ForgeRock AS.
015 */
016package org.forgerock.openam.ldap;
017
018import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
019
020import com.sun.identity.shared.Constants;
021import com.sun.identity.shared.configuration.SystemPropertiesManager;
022import com.sun.identity.shared.debug.Debug;
023
024import java.security.GeneralSecurityException;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.LinkedHashSet;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033import java.util.StringTokenizer;
034import java.util.concurrent.TimeUnit;
035import javax.naming.InvalidNameException;
036
037import org.forgerock.i18n.LocalizedIllegalArgumentException;
038import org.forgerock.opendj.ldap.Attribute;
039import org.forgerock.opendj.ldap.ByteString;
040import org.forgerock.opendj.ldap.Connection;
041import org.forgerock.opendj.ldap.ConnectionFactory;
042import org.forgerock.opendj.ldap.Connections;
043import org.forgerock.opendj.ldap.DN;
044import org.forgerock.opendj.ldap.Filter;
045import org.forgerock.opendj.ldap.LDAPConnectionFactory;
046import org.forgerock.opendj.ldap.LdapException;
047import org.forgerock.opendj.ldap.LoadBalancerEventListener;
048import org.forgerock.opendj.ldap.RDN;
049import org.forgerock.opendj.ldap.SSLContextBuilder;
050import org.forgerock.opendj.ldap.SearchResultReferenceIOException;
051import org.forgerock.opendj.ldap.SearchScope;
052import org.forgerock.opendj.ldif.ConnectionEntryReader;
053import org.forgerock.util.Options;
054import org.forgerock.util.Reject;
055import org.forgerock.util.time.Duration;
056
057/**
058 * Utility methods to help interaction with the OpenDJ LDAP SDK.
059 * There are two main ways currently to create connection pools/factories:
060 * <ul>
061 *  <li>Providing a set of servers in the format specified in {@link
062 * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}, which will be prioritized based on the
063 * current server's server ID/site ID.</li>
064 *  <li>Providing a set of LDAPURLs, which are already considered as "prioritized".</li>
065 * </ul>
066 * In case the configuration provides the possibility to assign LDAP servers to OpenAM servers/sites, then either you
067 * can prioritize manually (if the logic differs from this implementation) and create the corresponding {@link LDAPURL}
068 * objects, or you can pass in the list to the newPrioritized* methods.
069 *
070 * @supported.all.api
071 */
072public final class LDAPUtils {
073
074    private static final char[] ESCAPED_CHAR = {',', '+', '"', '\\', '<', '>', ';'};
075    private static final String LDAP_SCOPE_BASE = "SCOPE_BASE";
076    private static final String LDAP_SCOPE_ONE = "SCOPE_ONE";
077    private static final String LDAP_SCOPE_SUB = "SCOPE_SUB";
078    private static final Map<String, SearchScope> SCOPES;
079    private static final Debug DEBUG = Debug.getInstance("LDAPUtils");
080    private static final int DEFAULT_HEARTBEAT_TIMEOUT = 3;
081
082    static {
083        Map<String, SearchScope> mappings = new HashMap<String, SearchScope>(3);
084        mappings.put(LDAP_SCOPE_BASE, SearchScope.BASE_OBJECT);
085        mappings.put(LDAP_SCOPE_ONE, SearchScope.SINGLE_LEVEL);
086        mappings.put(LDAP_SCOPE_SUB, SearchScope.WHOLE_SUBTREE);
087        SCOPES = Collections.unmodifiableMap(mappings);
088    }
089
090    private LDAPUtils() {
091    }
092
093    /**
094     * Based on the incoming parameters prioritizes the LDAP server list, then creates a connection pool that is
095     * capable to failover to the servers defined in case there is an error.
096     *
097     * @param servers The set of servers in the format defined in {@link
098     * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}.
099     * @param hostServerId The server ID for this OpenAM server.
100     * @param hostSiteId The site ID for this OpenAM server.
101     * @param username The directory user's DN. May be null if this is an anonymous connection.
102     * @param password The directory user's password.
103     * @param maxSize The max size of the created pool.
104     * @param heartBeatInterval The interval for sending out heartbeat requests.
105     * @param heartBeatTimeUnit The timeunit for the heartbeat interval.
106     * @param ldapOptions Additional LDAP settings used to create the pool.
107     * @return A failover loadbalanced authenticated/anonymous connection pool, which may also send heartbeat requests.
108     */
109    public static ConnectionFactory newPrioritizedFailoverConnectionPool(Set<String> servers,
110            String hostServerId,
111            String hostSiteId,
112            String username,
113            char[] password,
114            int maxSize,
115            int heartBeatInterval,
116            String heartBeatTimeUnit,
117            Options ldapOptions) {
118        return newFailoverConnectionPool(prioritizeServers(servers, hostServerId, hostSiteId),
119                username, password, maxSize, heartBeatInterval, heartBeatTimeUnit, ldapOptions);
120    }
121
122    /**
123     * Creates a new connection pool that is capable to failover to the servers defined in case there is an error.
124     *
125     * @param servers The set of LDAP URLs that will be used to set up the connection factory.
126     * @param username The directory user's DN. May be null if this is an anonymous connection.
127     * @param password The directory user's password.
128     * @param maxSize The max size of the created pool.
129     * @param heartBeatInterval The interval for sending out heartbeat requests.
130     * @param heartBeatTimeUnit The timeunit for the heartbeat interval.
131     * @param ldapOptions Additional LDAP settings used to create the pool
132     * @return A failover loadbalanced authenticated/anonymous connection pool, which may also send heartbeat requests.
133     */
134    public static ConnectionFactory newFailoverConnectionPool(Set<LDAPURL> servers,
135            String username,
136            char[] password,
137            int maxSize,
138            int heartBeatInterval,
139            String heartBeatTimeUnit,
140            Options ldapOptions) {
141        List<ConnectionFactory> factories = new ArrayList<ConnectionFactory>(servers.size());
142        for (LDAPURL ldapurl : servers) {
143            ConnectionFactory cf = Connections.newFixedConnectionPool(
144                    newConnectionFactory(ldapurl, username, password, heartBeatInterval, heartBeatTimeUnit,
145                    ldapOptions), maxSize);
146            factories.add(cf);
147        }
148
149        return loadBalanceFactories(factories);
150    }
151
152    /**
153     * Based on the incoming parameters prioritizes the LDAP server list, then creates a connection factory that is
154     * capable to failover to the servers defined in case there is an error.
155     *
156     * @param servers The set of servers in the format defined in {@link
157     * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}.
158     * @param hostServerId The server ID for this OpenAM server.
159     * @param hostSiteId The site ID for this OpenAM server.
160     * @param username The directory user's DN. May be null if this is an anonymous connection.
161     * @param password The directory user's password.
162     * @param heartBeatInterval The interval for sending out heartbeat requests.
163     * @param heartBeatTimeUnit The timeunit for the heartbeat interval.
164     * @param options Additional LDAP settings used to create the connection factory.
165     * @return A failover loadbalanced authenticated/anonymous connection factory, which may also send heartbeat
166     * requests.
167     */
168    public static ConnectionFactory newPrioritizedFailoverConnectionFactory(Set<String> servers,
169            String hostServerId,
170            String hostSiteId,
171            String username,
172            char[] password,
173            int heartBeatInterval,
174            String heartBeatTimeUnit,
175            Options options) {
176        return newFailoverConnectionFactory(prioritizeServers(servers, hostServerId, hostSiteId),
177                username, password, heartBeatInterval, heartBeatTimeUnit, options);
178    }
179
180    /**
181     * Creates a new connection factory that is capable to failover to the servers defined in case there is an error.
182     *
183     * @param servers The set of LDAP URLs that will be used to set up the connection factory.
184     * @param username The directory user's DN. May be null if this is an anonymous connection.
185     * @param password The directory user's password.
186     * @param heartBeatInterval The interval for sending out heartbeat requests.
187     * @param heartBeatTimeUnit The timeunit for the heartbeat interval.
188     * @param ldapOptions Additional LDAP settings used to create the connection factory.
189     * @return A failover loadbalanced authenticated/anonymous connection factory, which may also send heartbeat
190     * requests.
191     */
192    public static ConnectionFactory newFailoverConnectionFactory(Set<LDAPURL> servers,
193            String username,
194            char[] password,
195            int heartBeatInterval,
196            String heartBeatTimeUnit,
197            Options ldapOptions) {
198        List<ConnectionFactory> factories = new ArrayList<ConnectionFactory>(servers.size());
199        for (LDAPURL ldapurl : servers) {
200            factories.add(newConnectionFactory(ldapurl, username, password, heartBeatInterval, heartBeatTimeUnit,
201                    ldapOptions));
202        }
203        return loadBalanceFactories(factories);
204    }
205
206    /**
207     * Creates a new connection factory based on the provided parameters.
208     *
209     * @param ldapurl The address of the LDAP server.
210     * @param username The directory user's DN. May be null if this is an anonymous connection.
211     * @param password The directory user's password.
212     * @param heartBeatInterval The interval for sending out heartbeat requests.
213     * @param heartBeatTimeUnit The timeunit for the heartbeat interval.
214     * @param ldapOptions Additional LDAP settings used to create the connection factory.
215     * @return An authenticated/anonymous connection factory, which may also send heartbeat requests.
216     */
217    private static ConnectionFactory newConnectionFactory(LDAPURL ldapurl,
218            String username,
219            char[] password,
220            int heartBeatInterval,
221            String heartBeatTimeUnit,
222            Options ldapOptions) {
223        Boolean ssl = ldapurl.isSSL();
224        int heartBeatTimeout =
225                SystemPropertiesManager.getAsInt(Constants.LDAP_HEARTBEAT_TIMEOUT, DEFAULT_HEARTBEAT_TIMEOUT);
226
227        if (ssl != null && ssl.booleanValue()) {
228            try {
229                //Creating a defensive copy of ldapOptions to handle the case when a mixture of SSL/non-SSL connections
230                //needs to be established.
231                ldapOptions = Options.copyOf(ldapOptions).set(LDAPConnectionFactory.SSL_CONTEXT,
232                        new SSLContextBuilder().getSSLContext());
233            } catch (GeneralSecurityException gse) {
234                DEBUG.error("An error occurred while creating SSLContext", gse);
235            }
236        }
237
238        // Enable heartbeat
239        if (heartBeatInterval > 0 && heartBeatTimeUnit != null) {
240            TimeUnit unit = TimeUnit.valueOf(heartBeatTimeUnit.toUpperCase());
241            ldapOptions = ldapOptions
242                    .set(HEARTBEAT_ENABLED, true)
243                    .set(HEARTBEAT_INTERVAL, new Duration(unit.toSeconds(heartBeatInterval), TimeUnit.SECONDS))
244                    .set(HEARTBEAT_TIMEOUT, new Duration(unit.toSeconds(heartBeatTimeout), TimeUnit.SECONDS));
245        }
246
247        // Enable Authenticated connection
248        if (username != null) {
249            ldapOptions = ldapOptions.set(AUTHN_BIND_REQUEST, LDAPRequests.newSimpleBindRequest(username, password));
250        }
251
252        return new LDAPConnectionFactory(ldapurl.getHost(), ldapurl.getPort(), ldapOptions);
253    }
254
255    private static ConnectionFactory loadBalanceFactories(List<ConnectionFactory> factories) {
256        return Connections.newFailoverLoadBalancer(factories, Options.defaultOptions());
257    }
258
259    /**
260     * Prioritizes the incoming LDAP servers based on their assigned servers/sites.
261     * The format of the server list can be either one of the followings:
262     * <ul>
263     *  <li><code>host:port</code> - The LDAP server has no preferred
264     * server/site</li>
265     *  <li><code>host:port|serverid</code> - The LDAP server should be mainly
266     * used by an OpenAM instance with the same serverid</li>
267     *  <li><code>host:port|serverid|siteid</code> - The LDAP server should be
268     * mainly used by an OpenAM instance with the same serverid or with the same
269     * siteid</li>
270     * </ul>
271     * The resulting priority list will have the following order:
272     * <ul>
273     *  <li>servers that are linked with this server</li>
274     *  <li>servers that are linked with the current site</li>
275     *  <li>any other server that did not match in the same order as they were defined</li>
276     * </ul>
277     *
278     * @param servers The Set of servers that needs to be prioritized in the previously described format.
279     * @param hostServerId This server's ID.
280     * @param hostSiteId This server's site ID.
281     * @return The prioritized Set of LDAP URLs that can be used to create connection factories.
282     */
283    public static Set<LDAPURL> prioritizeServers(Set<String> servers, String hostServerId, String hostSiteId) {
284        Set<LDAPURL> ldapServers = new LinkedHashSet<LDAPURL>(servers.size());
285        Set<LDAPURL> serverDefined = new LinkedHashSet<LDAPURL>(servers.size());
286        Set<LDAPURL> siteDefined = new LinkedHashSet<LDAPURL>(servers.size());
287        Set<LDAPURL> nonMatchingServers = new LinkedHashSet<LDAPURL>(servers.size());
288        for (String server : servers) {
289            StringTokenizer tokenizer = new StringTokenizer(server, "|");
290            String ldapUrl = tokenizer.nextToken();
291            String assignedServerId = "";
292            String assignedSiteId = "";
293
294            if (tokenizer.hasMoreTokens()) {
295                assignedServerId = tokenizer.nextToken();
296            }
297            if (tokenizer.hasMoreTokens()) {
298                assignedSiteId = tokenizer.nextToken();
299            }
300            if (!assignedServerId.isEmpty() && assignedServerId.equals(hostServerId)) {
301                serverDefined.add(LDAPURL.valueOf(ldapUrl));
302            } else if (!assignedSiteId.isEmpty() && assignedSiteId.equals(hostSiteId)) {
303                siteDefined.add(LDAPURL.valueOf(ldapUrl));
304            } else {
305                nonMatchingServers.add(LDAPURL.valueOf(ldapUrl));
306            }
307        }
308        //Let's add them in the order of priority to the ldapServers set, this way the most appropriate servers should
309        //be at the beginning of the list and towards the end of the list are the possibly most remote servers.
310        ldapServers.addAll(serverDefined);
311        ldapServers.addAll(siteDefined);
312        ldapServers.addAll(nonMatchingServers);
313        return ldapServers;
314    }
315
316    /**
317     * Converts string representation of scope (as defined in the configuration) to the corresponding
318     * {@link SearchScope} object.
319     *
320     * @param scope the string representation of the scope.
321     * @param defaultScope in case the coversion fail this default scope should be returned.
322     * @return the corresponding {@link SearchScope} object.
323     */
324    public static SearchScope getSearchScope(String scope, SearchScope defaultScope) {
325        SearchScope searchScope = SCOPES.get(scope);
326        return searchScope == null ? defaultScope : searchScope;
327    }
328
329    /**
330     * Parses the incoming filter, and in case of failure falls back to the default filter.
331     *
332     * @param filter The filter that needs to be parsed.
333     * @param defaultFilter If the parsing fails, this will be returned.
334     * @return The parsed Filter object, or the default Filter, if the parse failed.
335     */
336    public static Filter parseFilter(String filter, Filter defaultFilter) {
337        try {
338            return filter == null ? defaultFilter : Filter.valueOf(filter);
339        } catch (LocalizedIllegalArgumentException liae) {
340            DEBUG.error("Unable to construct Filter from " + filter + " -> " + liae.getMessage()
341                    + "\nFalling back to " + defaultFilter.toString());
342        }
343        return defaultFilter;
344    }
345
346    /**
347     * Returns the RDN without the attribute name from the passed in {@link DN} object, for example:
348     * <code>uid=demo,ou=people,dc=example,dc=com</code> will return <code>demo</code>.
349     *
350     * @param dn The DN that we need the name of.
351     * @return The RDN of the DN without the attribute name.
352     */
353    public static String getName(DN dn) {
354        return dn.rdn().getFirstAVA().getAttributeValue().toString();
355    }
356
357    /**
358     * Converts the Attribute to an attribute name, 2-dimensional byte array map and adds it to the map passed in.
359     * The first dimension of the byte array separates the different values, the second dimension holds the actual
360     * value.
361     *
362     * @param attribute The attribute that needs to be converted.
363     * @param map The map where the converted attribute is added to.
364     */
365    public static void addAttributeToMapAsByteArray(Attribute attribute, Map<String, byte[][]> map) {
366        byte[][] values = new byte[attribute.size()][];
367        int counter = 0;
368        for (ByteString byteString : attribute) {
369            byte[] bytes = byteString.toByteArray();
370            values[counter++] = bytes;
371        }
372        map.put(attribute.getAttributeDescriptionAsString(), values);
373    }
374
375    /**
376     * Converts the Attribute to an attribute name, set of String values map and adds it to the map passed in.
377     *
378     * @param attribute The attribute that needs to be converted.
379     * @param map The map where the converted attribute is added to.
380     */
381    public static void addAttributeToMapAsString(Attribute attribute, Map<String, Set<String>> map) {
382        map.put(attribute.getAttributeDescriptionAsString(), getAttributeValuesAsStringSet(attribute));
383    }
384
385    /**
386     * Converts all the attribute values to a String Set.
387     *
388     * @param attribute the attribute to be converted.
389     * @return A Set of String representations of the Attribute values.
390     */
391    public static Set<String> getAttributeValuesAsStringSet(Attribute attribute) {
392        Set<String> values = new HashSet<String>(attribute.size());
393        for (ByteString byteString : attribute) {
394            values.add(byteString.toString());
395        }
396        return values;
397    }
398
399    /**
400     * Converts the incoming set of URLs to {@link LDAPURL} instances and returns them as a set. The iteration order
401     * of the originally passed in Set is retained.
402     *
403     * @param servers The LDAP server URLs that needs to be converted to {@link LDAPURL} instances.
404     * @return A set of LDAPURLs corresponding to the passed in URLs.
405     */
406    public static Set<LDAPURL> convertToLDAPURLs(Set<String> servers) {
407        if (servers == null) {
408            return new LinkedHashSet<LDAPURL>(0);
409        } else {
410            Set<LDAPURL> ret = new LinkedHashSet<LDAPURL>(servers.size());
411            for (String server : servers) {
412                ret.add(LDAPURL.valueOf(server));
413            }
414            return ret;
415        }
416    }
417
418    /**
419     * When provided a DN, returns the value part of the first RDN.
420     * @param dn A DN.
421     * @return The value part of the first RDN.
422     * @throws IllegalArgumentException When the DN's RDN is multivalued, or when the DN is not a valid name.
423     */
424    public static String rdnValueFromDn(String dn) {
425        return rdnValueFromDn(DN.valueOf(dn));
426    }
427
428    /**
429     * When provided a DN, returns the value part of the first RDN.
430     * @param dn A DN.
431     * @return The value part of the first RDN.
432     * @throws IllegalArgumentException When the DN's RDN is multivalued.
433     */
434    public static String rdnValueFromDn(DN dn) {
435        if (dn.size() > 0) {
436            return rdnValue(dn.rdn());
437        }
438        return "";
439    }
440
441    /**
442     * When provided an RDN, returns the value part.
443     * @param rdn An RDN.
444     * @return The value part.
445     * @throws IllegalArgumentException When the RDN is multivalued.
446     */
447    public static String rdnValue(RDN rdn) {
448        Reject.ifTrue(rdn.isMultiValued(), "Multivalued RDNs not supported");
449        return rdn.getFirstAVA().getAttributeValue().toString();
450    }
451
452    /**
453     * When provided a DN, returns the attribute type name of the first RDN.
454     * @param dn A DN.
455     * @return The attribute type name of the first RDN.
456     * @throws IllegalArgumentException When the DN's RDN is multivalued.
457     */
458    public static String rdnTypeFromDn(String dn) {
459        return rdnTypeFromDn(DN.valueOf(dn));
460    }
461
462    /**
463     * When provided a DN, returns the attribute type name of the first RDN.
464     * @param dn A DN.
465     * @return The attribute type name of the first RDN.
466     * @throws IllegalArgumentException When the DN's RDN is multivalued.
467     */
468    public static String rdnTypeFromDn(DN dn) {
469        if (dn.size() > 0) {
470            return rdnType(dn.rdn());
471        }
472        return "";
473    }
474
475    /**
476     * When provided an RDN, returns the attribute type name.
477     * @param rdn An RDN.
478     * @return The attribute type name.
479     * @throws IllegalArgumentException When the RDN is multivalued.
480     */
481    public static String rdnType(RDN rdn) {
482        Reject.ifTrue(rdn.size() != 1, "Multivalued RDNs not supported");
483        return rdn.getFirstAVA().getAttributeType().getNameOrOID();
484    }
485
486    /**
487     * Returns a set of all the non-root DNs from the collection that are not equal to the {@code compare} parameter.
488     * @param compare The DN to compare against.
489     * @param dns THe DNs to compare.
490     * @return A {@code Set} of non identical DNs.
491     * @throws InvalidNameException If an error occurs.
492     */
493    public static Set<String> collectNonIdenticalValues(DN compare, Set<String> dns) throws InvalidNameException {
494        Set<String> results = new HashSet<>();
495        for (String dnString : dns) {
496            DN dn = DN.valueOf(dnString);
497            if (dn.size() > 0 && compare.compareTo(dn) != 0) {
498                results.add(rdnValueFromDn(dn));
499            }
500        }
501        return results;
502    }
503
504    /**
505     * Gets the DB name.
506     *
507     * @param suffix The suffix.
508     * @param ld The connection.
509     * @return The name of the DB.
510     */
511    public static String getDBName(String suffix, Connection ld) {
512        String filter = "cn=" + suffix;
513
514        try {
515            ConnectionEntryReader results = ld.search(LDAPRequests.newSearchRequest("cn=mapping tree,cn=config",
516                    SearchScope.WHOLE_SUBTREE, filter));
517            while (results.hasNext()) {
518                Attribute dbName = results.readEntry().getAttribute("nsslapd-backend");
519                if (dbName != null) {
520                    return dbName.firstValueAsString();
521                }
522            }
523        } catch (LdapException e) {
524            // If not S1DS, then cn=mapping tree wouldn't exist.
525            // Hence return userRoot as DBNAME.
526        } catch (SearchResultReferenceIOException e) {
527            DEBUG.error("LDAPUtils.getDBName: Did not expect to get a reference", e);
528        }
529        return "userRoot";
530    }
531
532    /**
533     * Tests whether the supplied string is a DN, and is not the root DN.
534     * @param candidateDN The possible DN.
535     * @return {@code true} if the string is a DN.
536     */
537    public static boolean isDN(String candidateDN) {
538        return newDN(candidateDN).size() > 0;
539    }
540
541    /**
542     * Escapes characters that should be escaped.
543     *
544     * @param str The string to escape.
545     * @return The escaped string.
546     */
547    public static String escapeValue(String str) {
548        StringBuilder retbuf = new StringBuilder();
549        for (int i = 0; i < str.length(); i++) {
550            char currentChar = str.charAt(i);
551            if (shouldEscapeCharacter(currentChar)) {
552                retbuf.append('\\');
553            }
554            retbuf.append(currentChar);
555        }
556        return retbuf.toString();
557    }
558
559    private static boolean shouldEscapeCharacter(char c) {
560        for (char escaped : ESCAPED_CHAR) {
561            if (c == escaped) {
562                return true;
563            }
564        }
565        return false;
566    }
567
568    /**
569     * Normalizes the DN.
570     *
571     * @param dn The DN to normalize.
572     * @return The normalized DN.
573     */
574    public static String normalizeDN(String dn) {
575        return newDN(dn).toString().toLowerCase();
576    }
577
578    /**
579     * Creates a DN from the specified DN string.
580     *
581     * @param orgName The DN string.
582     * @return A DN.
583     */
584    public static DN newDN(String orgName) {
585        if (orgName == null || orgName.startsWith("/") || !orgName.contains("=")) {
586            return DN.rootDN();
587        } else {
588            return DN.valueOf(orgName);
589        }
590    }
591
592    /**
593     * Converts a DN String to a RFC format and lowers case.
594     *
595     * @param dn
596     *            the DN String to be formated
597     * @return a lowercase RFC fromat DN String
598     */
599    public static String formatToRFC(String dn) {
600        return DN.valueOf(dn).toString().toLowerCase();
601    }
602
603    /**
604     * Determines if the DN's are equal.
605     *
606     * @param dn1 The first DN.
607     * @param dn2 The second DN.
608     * @return {@code true} if the DN's are equal.
609     */
610    public static boolean dnEquals(String dn1, String dn2) {
611        DN dnObj1 = DN.valueOf(dn1);
612        DN dnObj2 = DN.valueOf(dn2);
613        return dnObj1.equals(dnObj2);
614    }
615
616    private static class LoggingLBEventListener implements LoadBalancerEventListener {
617
618        public void handleConnectionFactoryOffline(ConnectionFactory factory, LdapException error) {
619            DEBUG.error("Connection factory became offline: " + factory, error);
620        }
621
622        public void handleConnectionFactoryOnline(ConnectionFactory factory) {
623            DEBUG.error("Connection factory became online: " + factory);
624        }
625    }
626
627    /**
628     * Creates a ConnectionFactory from the host string and associated details. The host string can be any of the
629     * following:
630     * <ul>
631     *     <li>A plain hostname/IP address</li>
632     *     <li>A hostname and port, in the format <code>[host]:[port]</code></li>
633     *     <li>A space-separated list of hostnames in priority order, e.g. <code>host1 host2 host3</code></li>
634     *     <li>
635     *         A space-separated list of hostnames with port numbers in priority order, e.g.
636     *         <code>host1:389 host2:50389</code>
637     *     </li>
638     * </ul>
639     * If a list of hosts is given, a load balanced {@code ConnectionFactory} is returned. All factories are
640     * pre-authenticated using the supplied credentials.
641     * @param host The host/host-port string.
642     * @param defaultPort The port number to use for hosts that do not specify a port in the string.
643     * @param authDN The DN to bind with.
644     * @param authPasswd The password to bind with.
645     * @param options Any additional options.
646     * @return A connection factory.
647     */
648    public static ConnectionFactory createFailoverConnectionFactory(String host, int defaultPort,
649            String authDN, String authPasswd, Options options) {
650        StringTokenizer st = new StringTokenizer(host);
651        String[] hostList = new String[st.countTokens()];
652        int[] portList = new int[st.countTokens()];
653        int hostCount = 0;
654        while (st.hasMoreTokens()) {
655            String s = st.nextToken();
656            int colon = s.indexOf(':');
657            if (colon > 0) {
658                hostList[hostCount] = s.substring(0, colon);
659                portList[hostCount] = Integer.parseInt(s.substring(colon + 1));
660            } else {
661                hostList[hostCount] = s;
662                portList[hostCount] = defaultPort;
663            }
664            hostCount++;
665        }
666
667        if (hostCount > 1) {
668            List<ConnectionFactory> factories = new ArrayList<>();
669            for (int i = 0; i < hostCount; i++) {
670                factories.add(createSingleHostConnectionFactory(hostList[i], portList[i], authDN, authPasswd, options));
671            }
672            return Connections.newFailoverLoadBalancer(factories, options);
673        } else {
674            return createSingleHostConnectionFactory(hostList[0], portList[0], authDN, authPasswd, options);
675        }
676    }
677
678    private static ConnectionFactory createSingleHostConnectionFactory(String host, int port,
679            String authDN, String authPasswd, Options options) {
680        options = options.set(AUTHN_BIND_REQUEST, LDAPRequests.newSimpleBindRequest(authDN, authPasswd.toCharArray()));
681        return new LDAPConnectionFactory(host, port, options);
682    }
683}