001/*
002 * Copyright 2014-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2014-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2014-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk;
037
038
039
040import java.net.InetAddress;
041import java.net.UnknownHostException;
042import java.util.ArrayList;
043import java.util.Arrays;
044import java.util.Collections;
045import java.util.Hashtable;
046import java.util.List;
047import java.util.Map;
048import java.util.Properties;
049import java.util.StringTokenizer;
050import java.util.concurrent.atomic.AtomicLong;
051import java.util.concurrent.atomic.AtomicReference;
052import javax.naming.Context;
053import javax.naming.NamingEnumeration;
054import javax.naming.directory.Attribute;
055import javax.naming.directory.Attributes;
056import javax.naming.directory.InitialDirContext;
057import javax.net.SocketFactory;
058
059import com.unboundid.util.Debug;
060import com.unboundid.util.NotMutable;
061import com.unboundid.util.NotNull;
062import com.unboundid.util.Nullable;
063import com.unboundid.util.ObjectPair;
064import com.unboundid.util.PropertyManager;
065import com.unboundid.util.StaticUtils;
066import com.unboundid.util.ThreadLocalRandom;
067import com.unboundid.util.ThreadSafety;
068import com.unboundid.util.ThreadSafetyLevel;
069import com.unboundid.util.Validator;
070
071import static com.unboundid.ldap.sdk.LDAPMessages.*;
072
073
074
075/**
076 * This class provides a server set implementation that handles the case in
077 * which a given host name may resolve to multiple IP addresses.  Note that
078 * while a setup like this is typically referred to as "round-robin DNS", this
079 * server set implementation does not strictly require DNS (as names may be
080 * resolved through alternate mechanisms like a hosts file or an alternate name
081 * service), and it does not strictly require round-robin use of those addresses
082 * (as alternate ordering mechanisms, like randomized or failover, may be used).
083 * <BR><BR>
084 * <H2>Example</H2>
085 * The following example demonstrates the process for creating a round-robin DNS
086 * server set for the case in which the hostname "directory.example.com" may be
087 * associated with multiple IP addresses, and the LDAP SDK should attempt to use
088 * them in a round robin manner.
089 * <PRE>
090 *   // Define a number of variables that will be used by the server set.
091 *   String                hostname           = "directory.example.com";
092 *   int                   port               = 389;
093 *   AddressSelectionMode  selectionMode      =
094 *        AddressSelectionMode.ROUND_ROBIN;
095 *   long                  cacheTimeoutMillis = 3600000L; // 1 hour
096 *   String                providerURL        = "dns:"; // Default DNS config.
097 *   SocketFactory         socketFactory      = null; // Default socket factory.
098 *   LDAPConnectionOptions connectionOptions  = null; // Default options.
099 *
100 *   // Create the server set using the settings defined above.
101 *   RoundRobinDNSServerSet serverSet = new RoundRobinDNSServerSet(hostname,
102 *        port, selectionMode, cacheTimeoutMillis, providerURL, socketFactory,
103 *        connectionOptions);
104 *
105 *   // Verify that we can establish a single connection using the server set.
106 *   LDAPConnection connection = serverSet.getConnection();
107 *   RootDSE rootDSEFromConnection = connection.getRootDSE();
108 *   connection.close();
109 *
110 *   // Verify that we can establish a connection pool using the server set.
111 *   SimpleBindRequest bindRequest =
112 *        new SimpleBindRequest("uid=pool.user,dc=example,dc=com", "password");
113 *   LDAPConnectionPool pool =
114 *        new LDAPConnectionPool(serverSet, bindRequest, 10);
115 *   RootDSE rootDSEFromPool = pool.getRootDSE();
116 *   pool.close();
117 * </PRE>
118 */
119@NotMutable()
120@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
121public final class RoundRobinDNSServerSet
122       extends ServerSet
123{
124  /**
125   * The name of a system property that can be used to specify a comma-delimited
126   * list of IP addresses to use if resolution fails.  This is intended
127   * primarily for testing purposes.
128   */
129  @NotNull static final String PROPERTY_DEFAULT_ADDRESSES =
130       RoundRobinDNSServerSet.class.getName() + ".defaultAddresses";
131
132
133
134  /**
135   * An enum that defines the modes that may be used to select the order in
136   * which addresses should be used in attempts to establish connections.
137   */
138  public enum AddressSelectionMode
139  {
140    /**
141     * The address selection mode that will cause addresses to be consistently
142     * attempted in the order they are retrieved from the name service.
143     */
144    FAILOVER,
145
146
147
148    /**
149     * The address selection mode that will cause the order of addresses to be
150     * randomized for each attempt.
151     */
152    RANDOM,
153
154
155
156    /**
157     * The address selection mode that will cause connection attempts to be made
158     * in a round-robin order.
159     */
160    ROUND_ROBIN;
161
162
163
164    /**
165     * Retrieves the address selection mode with the specified name.
166     *
167     * @param  name  The name of the address selection mode to retrieve.  It
168     *              must not be {@code null}.
169     *
170     * @return  The requested address selection mode, or {@code null} if no such
171     *          change mode is defined.
172     */
173    @Nullable()
174    public static AddressSelectionMode forName(@NotNull final String name)
175    {
176      switch (StaticUtils.toLowerCase(name))
177      {
178        case "failover":
179          return FAILOVER;
180        case "random":
181          return RANDOM;
182        case "roundrobin":
183        case "round-robin":
184        case "round_robin":
185          return ROUND_ROBIN;
186        default:
187          return null;
188      }
189    }
190  }
191
192
193
194  // The address selection mode that should be used if the provided hostname
195  // resolves to multiple addresses.
196  @NotNull private final AddressSelectionMode selectionMode;
197
198  // A counter that will be used to handle round-robin ordering.
199  @NotNull private final AtomicLong roundRobinCounter;
200
201  // A reference to an object that combines the resolved addresses with a
202  // timestamp indicating when the value should no longer be trusted.
203  @NotNull private final AtomicReference<ObjectPair<InetAddress[],Long>>
204       resolvedAddressesWithTimeout;
205
206  // The bind request to use to authenticate connections created by this
207  // server set.
208  @Nullable private final BindRequest bindRequest;
209
210  // The properties that will be used to initialize the JNDI context, if any.
211  @Nullable private final Hashtable<String,String> jndiProperties;
212
213  // The port number for the target server.
214  private final int port;
215
216  // The set of connection options to use for new connections.
217  @NotNull private final LDAPConnectionOptions connectionOptions;
218
219  // The maximum length of time, in milliseconds, to cache resolved addresses.
220  private final long cacheTimeoutMillis;
221
222  // The post-connect processor to invoke against connections created by this
223  // server set.
224  @Nullable private final PostConnectProcessor postConnectProcessor;
225
226  // The socket factory to use to establish connections.
227  @NotNull private final SocketFactory socketFactory;
228
229  // The hostname to be resolved.
230  @NotNull private final String hostname;
231
232  // The provider URL to use to resolve names, if any.
233  @Nullable private final String providerURL;
234
235  // The DNS record types that will be used to obtain the IP addresses for the
236  // specified hostname.
237  @NotNull private final String[] dnsRecordTypes;
238
239
240
241  /**
242   * Creates a new round-robin DNS server set with the provided information.
243   *
244   * @param  hostname            The hostname to be resolved to one or more
245   *                             addresses.  It must not be {@code null}.
246   * @param  port                The port to use to connect to the server.  Note
247   *                             that even if the provided hostname resolves to
248   *                             multiple addresses, the same port must be used
249   *                             for all addresses.
250   * @param  selectionMode       The selection mode that should be used if the
251   *                             hostname resolves to multiple addresses.  It
252   *                             must not be {@code null}.
253   * @param  cacheTimeoutMillis  The maximum length of time in milliseconds to
254   *                             cache addresses resolved from the provided
255   *                             hostname.  Caching resolved addresses can
256   *                             result in better performance and can reduce the
257   *                             number of requests to the name service.  A
258   *                             that is less than or equal to zero indicates
259   *                             that no caching should be used.
260   * @param  providerURL         The JNDI provider URL that should be used when
261   *                             communicating with the DNS server.  If this is
262   *                             {@code null}, then the underlying system's
263   *                             name service mechanism will be used (which may
264   *                             make use of other services instead of or in
265   *                             addition to DNS).  If this is non-{@code null},
266   *                             then only DNS will be used to perform the name
267   *                             resolution.  A value of "dns:" indicates that
268   *                             the underlying system's DNS configuration
269   *                             should be used.
270   * @param  socketFactory       The socket factory to use to establish the
271   *                             connections.  It may be {@code null} if the
272   *                             JVM-default socket factory should be used.
273   * @param  connectionOptions   The set of connection options that should be
274   *                             used for the connections.  It may be
275   *                             {@code null} if a default set of connection
276   *                             options should be used.
277   */
278  public RoundRobinDNSServerSet(@NotNull final String hostname, final int port,
279              @NotNull final AddressSelectionMode selectionMode,
280              final long cacheTimeoutMillis,
281              @Nullable final String providerURL,
282              @Nullable final SocketFactory socketFactory,
283              @Nullable final LDAPConnectionOptions connectionOptions)
284  {
285    this(hostname, port, selectionMode, cacheTimeoutMillis, providerURL,
286         null, null, socketFactory, connectionOptions);
287  }
288
289
290
291  /**
292   * Creates a new round-robin DNS server set with the provided information.
293   *
294   * @param  hostname            The hostname to be resolved to one or more
295   *                             addresses.  It must not be {@code null}.
296   * @param  port                The port to use to connect to the server.  Note
297   *                             that even if the provided hostname resolves to
298   *                             multiple addresses, the same port must be used
299   *                             for all addresses.
300   * @param  selectionMode       The selection mode that should be used if the
301   *                             hostname resolves to multiple addresses.  It
302   *                             must not be {@code null}.
303   * @param  cacheTimeoutMillis  The maximum length of time in milliseconds to
304   *                             cache addresses resolved from the provided
305   *                             hostname.  Caching resolved addresses can
306   *                             result in better performance and can reduce the
307   *                             number of requests to the name service.  A
308   *                             that is less than or equal to zero indicates
309   *                             that no caching should be used.
310   * @param  providerURL         The JNDI provider URL that should be used when
311   *                             communicating with the DNS server.If both
312   *                             {@code providerURL} and {@code jndiProperties}
313   *                             are {@code null}, then then JNDI will not be
314   *                             used to interact with DNS and the hostname
315   *                             resolution will be performed via the underlying
316   *                             system's name service mechanism (which may make
317   *                             use of other services instead of or in addition
318   *                             to DNS).  If this is non-{@code null}, then
319   *                             only DNS will be used to perform the name
320   *                             resolution.  A value of "dns:" indicates that
321   *                             the underlying system's DNS configuration
322   *                             should be used.
323   * @param  jndiProperties      A set of JNDI-related properties that should be
324   *                             be used when initializing the context for
325   *                             interacting with the DNS server via JNDI.  If
326   *                             both {@code providerURL} and
327   *                             {@code jndiProperties} are {@code null}, then
328   *                             then JNDI will not be used to interact with
329   *                             DNS and the hostname resolution will be
330   *                             performed via the underlying system's name
331   *                             service mechanism (which may make use of other
332   *                             services instead of or in addition to DNS).  If
333   *                             {@code providerURL} is {@code null} and
334   *                             {@code jndiProperties} is non-{@code null},
335   *                             then the provided properties must specify the
336   *                             URL.
337   * @param  dnsRecordTypes      Specifies the types of DNS records that will be
338   *                             used to obtain the addresses for the specified
339   *                             hostname.  This will only be used if at least
340   *                             one of {@code providerURL} and
341   *                             {@code jndiProperties} is non-{@code null}.  If
342   *                             this is {@code null} or empty, then a default
343   *                             record type of "A" (indicating IPv4 addresses)
344   *                             will be used.
345   * @param  socketFactory       The socket factory to use to establish the
346   *                             connections.  It may be {@code null} if the
347   *                             JVM-default socket factory should be used.
348   * @param  connectionOptions   The set of connection options that should be
349   *                             used for the connections.  It may be
350   *                             {@code null} if a default set of connection
351   *                             options should be used.
352   */
353  public RoundRobinDNSServerSet(@NotNull final String hostname, final int port,
354              @NotNull final AddressSelectionMode selectionMode,
355              final long cacheTimeoutMillis,
356              @Nullable final String providerURL,
357              @Nullable final Properties jndiProperties,
358              @Nullable final String[] dnsRecordTypes,
359              @Nullable final SocketFactory socketFactory,
360              @Nullable final LDAPConnectionOptions connectionOptions)
361  {
362    this(hostname, port, selectionMode, cacheTimeoutMillis, providerURL,
363         jndiProperties, dnsRecordTypes, socketFactory, connectionOptions, null,
364         null);
365  }
366
367
368
369  /**
370   * Creates a new round-robin DNS server set with the provided information.
371   *
372   * @param  hostname              The hostname to be resolved to one or more
373   *                               addresses.  It must not be {@code null}.
374   * @param  port                  The port to use to connect to the server.
375   *                               Note that even if the provided hostname
376   *                               resolves to multiple addresses, the same
377   *                               port must be used for all addresses.
378   * @param  selectionMode         The selection mode that should be used if the
379   *                               hostname resolves to multiple addresses.  It
380   *                               must not be {@code null}.
381   * @param  cacheTimeoutMillis    The maximum length of time in milliseconds to
382   *                               cache addresses resolved from the provided
383   *                               hostname.  Caching resolved addresses can
384   *                               result in better performance and can reduce
385   *                               the number of requests to the name service.
386   *                               A that is less than or equal to zero
387   *                               indicates that no caching should be used.
388   * @param  providerURL           The JNDI provider URL that should be used
389   *                               when communicating with the DNS server.  If
390   *                               both {@code providerURL} and
391   *                               {@code jndiProperties} are {@code null},
392   *                               then then JNDI will not be used to interact
393   *                               with DNS and the hostname resolution will be
394   *                               performed via the underlying system's name
395   *                               service mechanism (which may make use of
396   *                               other services instead of or in addition to
397   *                               DNS).  If this is non-{@code null}, then only
398   *                               DNS will be used to perform the name
399   *                               resolution.  A value of "dns:" indicates that
400   *                               the underlying system's DNS configuration
401   *                               should be used.
402   * @param  jndiProperties        A set of JNDI-related properties that should
403   *                               be used when initializing the context for
404   *                               interacting with the DNS server via JNDI.  If
405   *                               both {@code providerURL} and
406   *                               {@code jndiProperties} are {@code null}, then
407   *                               JNDI will not be used to interact with DNS
408   *                               and the hostname resolution will be
409   *                               performed via the underlying system's name
410   *                               service mechanism (which may make use of
411   *                               other services instead of or in addition to
412   *                               DNS).  If {@code providerURL} is
413   *                               {@code null} and {@code jndiProperties} is
414   *                               non-{@code null}, then the provided
415   *                               properties must specify the URL.
416   * @param  dnsRecordTypes        Specifies the types of DNS records that will
417   *                               be used to obtain the addresses for the
418   *                               specified hostname.  This will only be used
419   *                               if at least one of {@code providerURL} and
420   *                               {@code jndiProperties} is non-{@code null}.
421   *                               If this is {@code null} or empty, then a
422   *                               default record type of "A" (indicating IPv4
423   *                               addresses) will be used.
424   * @param  socketFactory         The socket factory to use to establish the
425   *                               connections.  It may be {@code null} if the
426   *                               JVM-default socket factory should be used.
427   * @param  connectionOptions     The set of connection options that should be
428   *                               used for the connections.  It may be
429   *                               {@code null} if a default set of connection
430   *                               options should be used.
431   * @param  bindRequest           The bind request that should be used to
432   *                               authenticate newly-established connections.
433   *                               It may be {@code null} if this server set
434   *                               should not perform any authentication.
435   * @param  postConnectProcessor  The post-connect processor that should be
436   *                               invoked on newly-established connections.  It
437   *                               may be {@code null} if this server set should
438   *                               not perform any post-connect processing.
439   */
440  public RoundRobinDNSServerSet(@NotNull final String hostname, final int port,
441              @NotNull final AddressSelectionMode selectionMode,
442              final long cacheTimeoutMillis,
443              @Nullable final String providerURL,
444              @Nullable final Properties jndiProperties,
445              @Nullable final String[] dnsRecordTypes,
446              @Nullable final SocketFactory socketFactory,
447              @Nullable final LDAPConnectionOptions connectionOptions,
448              @Nullable final BindRequest bindRequest,
449              @Nullable final PostConnectProcessor postConnectProcessor)
450  {
451    Validator.ensureNotNull(hostname);
452    Validator.ensureTrue((port >= 1) && (port <= 65_535));
453    Validator.ensureNotNull(selectionMode);
454
455    this.hostname = hostname;
456    this.port = port;
457    this.selectionMode = selectionMode;
458    this.providerURL = providerURL;
459    this.bindRequest = bindRequest;
460    this.postConnectProcessor = postConnectProcessor;
461
462    if (jndiProperties == null)
463    {
464      if (providerURL == null)
465      {
466        this.jndiProperties = null;
467      }
468      else
469      {
470        this.jndiProperties = new Hashtable<>(2);
471        this.jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY,
472             "com.sun.jndi.dns.DnsContextFactory");
473        this.jndiProperties.put(Context.PROVIDER_URL, providerURL);
474      }
475    }
476    else
477    {
478      this.jndiProperties = new Hashtable<>(jndiProperties.size()+2);
479      for (final Map.Entry<Object,Object> e : jndiProperties.entrySet())
480      {
481        this.jndiProperties.put(String.valueOf(e.getKey()),
482             String.valueOf(e.getValue()));
483      }
484
485      if (! this.jndiProperties.containsKey(Context.INITIAL_CONTEXT_FACTORY))
486      {
487        this.jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY,
488             "com.sun.jndi.dns.DnsContextFactory");
489      }
490
491      if ((! this.jndiProperties.containsKey(Context.PROVIDER_URL)) &&
492         (providerURL != null))
493      {
494        this.jndiProperties.put(Context.PROVIDER_URL, providerURL);
495      }
496    }
497
498    if (dnsRecordTypes == null)
499    {
500      this.dnsRecordTypes = new String[] { "A" };
501    }
502    else
503    {
504      this.dnsRecordTypes = dnsRecordTypes;
505    }
506
507    if (cacheTimeoutMillis > 0L)
508    {
509      this.cacheTimeoutMillis = cacheTimeoutMillis;
510    }
511    else
512    {
513      this.cacheTimeoutMillis = 0L;
514    }
515
516    if (socketFactory == null)
517    {
518      this.socketFactory = SocketFactory.getDefault();
519    }
520    else
521    {
522      this.socketFactory = socketFactory;
523    }
524
525    if (connectionOptions == null)
526    {
527      this.connectionOptions = new LDAPConnectionOptions();
528    }
529    else
530    {
531      this.connectionOptions = connectionOptions;
532    }
533
534    roundRobinCounter = new AtomicLong(0L);
535    resolvedAddressesWithTimeout = new AtomicReference<>();
536  }
537
538
539
540  /**
541   * Retrieves the hostname to be resolved.
542   *
543   * @return  The hostname to be resolved.
544   */
545  @NotNull()
546  public String getHostname()
547  {
548    return hostname;
549  }
550
551
552
553  /**
554   * Retrieves the port to use to connect to the server.
555   *
556   * @return  The port to use to connect to the server.
557   */
558  public int getPort()
559  {
560    return port;
561  }
562
563
564
565  /**
566   * Retrieves the address selection mode that should be used if the provided
567   * hostname resolves to multiple addresses.
568   *
569   * @return  The address selection
570   */
571  @NotNull()
572  public AddressSelectionMode getAddressSelectionMode()
573  {
574    return selectionMode;
575  }
576
577
578
579  /**
580   * Retrieves the length of time in milliseconds that resolved addresses may be
581   * cached.
582   *
583   * @return  The length of time in milliseconds that resolved addresses may be
584   *          cached, or zero if no caching should be performed.
585   */
586  public long getCacheTimeoutMillis()
587  {
588    return cacheTimeoutMillis;
589  }
590
591
592
593  /**
594   * Retrieves the provider URL that should be used when interacting with DNS to
595   * resolve the hostname to its corresponding addresses.
596   *
597   * @return  The provider URL that should be used when interacting with DNS to
598   *          resolve the hostname to its corresponding addresses, or
599   *          {@code null} if the system's configured naming service should be
600   *          used.
601   */
602  @Nullable()
603  public String getProviderURL()
604  {
605    return providerURL;
606  }
607
608
609
610  /**
611   * Retrieves an unmodifiable map of properties that will be used to initialize
612   * the JNDI context used to interact with DNS.  Note that the map returned
613   * will reflect the actual properties that will be used, and may not exactly
614   * match the properties provided when creating this server set.
615   *
616   * @return  An unmodifiable map of properties that will be used to initialize
617   *          the JNDI context used to interact with DNS, or {@code null} if
618   *          JNDI will nto be used to interact with DNS.
619   */
620  @Nullable()
621  public Map<String,String> getJNDIProperties()
622  {
623    if (jndiProperties == null)
624    {
625      return null;
626    }
627    else
628    {
629      return Collections.unmodifiableMap(jndiProperties);
630    }
631  }
632
633
634
635  /**
636   * Retrieves an array of record types that will be requested if JNDI will be
637   * used to interact with DNS.
638   *
639   * @return  An array of record types that will be requested if JNDI will be
640   *          used to interact with DNS.
641   */
642  @NotNull()
643  public String[] getDNSRecordTypes()
644  {
645    return dnsRecordTypes;
646  }
647
648
649
650  /**
651   * Retrieves the socket factory that will be used to establish connections.
652   * This will not be {@code null}, even if no socket factory was provided when
653   * the server set was created.
654   *
655   * @return  The socket factory that will be used to establish connections.
656   */
657  @NotNull()
658  public SocketFactory getSocketFactory()
659  {
660    return socketFactory;
661  }
662
663
664
665  /**
666   * Retrieves the set of connection options that will be used for underlying
667   * connections.  This will not be {@code null}, even if no connection options
668   * object was provided when the server set was created.
669   *
670   * @return  The set of connection options that will be used for underlying
671   *          connections.
672   */
673  @NotNull()
674  public LDAPConnectionOptions getConnectionOptions()
675  {
676    return connectionOptions;
677  }
678
679
680
681  /**
682   * {@inheritDoc}
683   */
684  @Override()
685  public boolean includesAuthentication()
686  {
687    return (bindRequest != null);
688  }
689
690
691
692  /**
693   * {@inheritDoc}
694   */
695  @Override()
696  public boolean includesPostConnectProcessing()
697  {
698    return (postConnectProcessor != null);
699  }
700
701
702
703  /**
704   * {@inheritDoc}
705   */
706  @Override()
707  @NotNull()
708  public LDAPConnection getConnection()
709         throws LDAPException
710  {
711    return getConnection(null);
712  }
713
714
715
716  /**
717   * {@inheritDoc}
718   */
719  @Override()
720  @NotNull()
721  public synchronized LDAPConnection getConnection(
722              @Nullable final LDAPConnectionPoolHealthCheck healthCheck)
723         throws LDAPException
724  {
725    LDAPException firstException = null;
726
727    final LDAPConnection conn =
728         new LDAPConnection(socketFactory, connectionOptions);
729    for (final InetAddress a : orderAddresses(resolveHostname()))
730    {
731      boolean close = true;
732      try
733      {
734        conn.connect(hostname, a, port,
735             connectionOptions.getConnectTimeoutMillis());
736        doBindPostConnectAndHealthCheckProcessing(conn, bindRequest,
737             postConnectProcessor, healthCheck);
738        close = false;
739        associateConnectionWithThisServerSet(conn);
740        return conn;
741      }
742      catch (final LDAPException le)
743      {
744        Debug.debugException(le);
745        if (firstException == null)
746        {
747          firstException = le;
748        }
749      }
750      finally
751      {
752        if (close)
753        {
754          conn.close();
755        }
756      }
757    }
758
759    throw firstException;
760  }
761
762
763
764  /**
765   * Resolve the hostname to its corresponding addresses.
766   *
767   * @return  The addresses resolved from the hostname.
768   *
769   * @throws  LDAPException  If
770   */
771  @NotNull()
772  InetAddress[] resolveHostname()
773          throws LDAPException
774  {
775    // First, see if we can use the cached addresses.
776    final ObjectPair<InetAddress[],Long> pair =
777         resolvedAddressesWithTimeout.get();
778    if (pair != null)
779    {
780      if (pair.getSecond() >= System.currentTimeMillis())
781      {
782        return pair.getFirst();
783      }
784    }
785
786
787    // Try to resolve the address.
788    InetAddress[] addresses = null;
789    try
790    {
791      if (jndiProperties == null)
792      {
793        addresses = connectionOptions.getNameResolver().getAllByName(hostname);
794      }
795      else
796      {
797        final Attributes attributes;
798        final InitialDirContext context = new InitialDirContext(jndiProperties);
799        try
800        {
801          attributes = context.getAttributes(hostname, dnsRecordTypes);
802        }
803        finally
804        {
805          context.close();
806        }
807
808        if (attributes != null)
809        {
810          final ArrayList<InetAddress> addressList = new ArrayList<>(10);
811          for (final String recordType : dnsRecordTypes)
812          {
813            final Attribute a = attributes.get(recordType);
814            if (a != null)
815            {
816              final NamingEnumeration<?> values = a.getAll();
817              while (values.hasMore())
818              {
819                final Object value = values.next();
820                addressList.add(getInetAddressForIP(String.valueOf(value)));
821              }
822            }
823          }
824
825          if (! addressList.isEmpty())
826          {
827            addresses = new InetAddress[addressList.size()];
828            addressList.toArray(addresses);
829          }
830        }
831      }
832    }
833    catch (final Exception e)
834    {
835      Debug.debugException(e);
836      addresses = getDefaultAddresses();
837    }
838
839
840    // If we were able to resolve the hostname, then cache and return the
841    // resolved addresses.
842    if ((addresses != null) && (addresses.length > 0))
843    {
844      final long timeoutTime;
845      if (cacheTimeoutMillis > 0L)
846      {
847        timeoutTime = System.currentTimeMillis() + cacheTimeoutMillis;
848      }
849      else
850      {
851        timeoutTime = System.currentTimeMillis() - 1L;
852      }
853
854      resolvedAddressesWithTimeout.set(
855           new ObjectPair<>(addresses, timeoutTime));
856      return addresses;
857    }
858
859
860    // If we've gotten here, then we couldn't resolve the hostname.  If we have
861    // cached addresses, then use them even though the timeout has expired
862    // because that's better than nothing.
863    if (pair != null)
864    {
865      return pair.getFirst();
866    }
867
868    throw new LDAPException(ResultCode.CONNECT_ERROR,
869         ERR_ROUND_ROBIN_DNS_SERVER_SET_CANNOT_RESOLVE.get(hostname));
870  }
871
872
873
874  /**
875   * Orders the provided array of InetAddress objects to reflect the order in
876   * which the addresses should be used to try to create a new connection.
877   *
878   * @param  addresses  The array of addresses to be ordered.
879   *
880   * @return  A list containing the ordered addresses.
881   */
882  @NotNull()
883  List<InetAddress> orderAddresses(@NotNull final InetAddress[] addresses)
884  {
885    final ArrayList<InetAddress> l = new ArrayList<>(addresses.length);
886
887    switch (selectionMode)
888    {
889      case RANDOM:
890        l.addAll(Arrays.asList(addresses));
891        Collections.shuffle(l, ThreadLocalRandom.get());
892        break;
893
894      case ROUND_ROBIN:
895        final int index =
896             (int) (roundRobinCounter.getAndIncrement() % addresses.length);
897        for (int i=index; i < addresses.length; i++)
898        {
899          l.add(addresses[i]);
900        }
901        for (int i=0; i < index; i++)
902        {
903          l.add(addresses[i]);
904        }
905        break;
906
907      case FAILOVER:
908      default:
909        // We'll use the addresses in the same order we originally got them.
910        l.addAll(Arrays.asList(addresses));
911        break;
912    }
913
914    return l;
915  }
916
917
918
919  /**
920   * Retrieves a default set of addresses that may be used for testing.
921   *
922   * @return  A default set of addresses that may be used for testing.
923   */
924  @NotNull()
925  InetAddress[] getDefaultAddresses()
926  {
927    final String defaultAddrsStr =
928         PropertyManager.get(PROPERTY_DEFAULT_ADDRESSES);
929    if (defaultAddrsStr == null)
930    {
931      return null;
932    }
933
934    final StringTokenizer tokenizer =
935         new StringTokenizer(defaultAddrsStr, " ,");
936    final InetAddress[] addresses = new InetAddress[tokenizer.countTokens()];
937    for (int i=0; i < addresses.length; i++)
938    {
939      try
940      {
941        addresses[i] = getInetAddressForIP(tokenizer.nextToken());
942      }
943      catch (final Exception e)
944      {
945        Debug.debugException(e);
946        return null;
947      }
948    }
949
950    return addresses;
951  }
952
953
954
955  /**
956   * Retrieves an InetAddress object with the configured hostname and the
957   * provided IP address.
958   *
959   * @param  ipAddress  The string representation of the IP address to use in
960   *                    the returned InetAddress.
961   *
962   * @return  The created InetAddress.
963   *
964   * @throws  UnknownHostException  If the provided string does not represent a
965   *                                valid IPv4 or IPv6 address.
966   */
967  @NotNull()
968  private InetAddress getInetAddressForIP(@NotNull final String ipAddress)
969          throws UnknownHostException
970  {
971    // We want to create an InetAddress that has the provided hostname and the
972    // specified IP address.  To do that, we need to use
973    // InetAddress.getByAddress.  But that requires the IP address to be
974    // specified as a byte array, and the easiest way to convert an IP address
975    // string to a byte array is to use InetAddress.getByName.
976    final InetAddress byName = connectionOptions.getNameResolver().
977         getByName(String.valueOf(ipAddress));
978    return InetAddress.getByAddress(hostname, byName.getAddress());
979  }
980
981
982
983  /**
984   * {@inheritDoc}
985   */
986  @Override()
987  public void toString(@NotNull final StringBuilder buffer)
988  {
989    buffer.append("RoundRobinDNSServerSet(hostname='");
990    buffer.append(hostname);
991    buffer.append("', port=");
992    buffer.append(port);
993    buffer.append(", addressSelectionMode=");
994    buffer.append(selectionMode.name());
995    buffer.append(", cacheTimeoutMillis=");
996    buffer.append(cacheTimeoutMillis);
997
998    if (providerURL != null)
999    {
1000      buffer.append(", providerURL='");
1001      buffer.append(providerURL);
1002      buffer.append('\'');
1003    }
1004
1005    buffer.append(", includesAuthentication=");
1006    buffer.append(bindRequest != null);
1007    buffer.append(", includesPostConnectProcessing=");
1008    buffer.append(postConnectProcessor != null);
1009    buffer.append(')');
1010  }
1011}