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}