001/*
002 * Copyright 2023-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2023-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) 2023-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.io.Closeable;
041import java.util.ArrayList;
042import java.util.Iterator;
043import java.util.List;
044import java.util.Map;
045import java.util.concurrent.ConcurrentHashMap;
046import java.util.concurrent.atomic.AtomicBoolean;
047import javax.net.ssl.SSLSocketFactory;
048
049import com.unboundid.util.NotMutable;
050import com.unboundid.util.NotNull;
051import com.unboundid.util.Nullable;
052import com.unboundid.util.StaticUtils;
053import com.unboundid.util.ThreadSafety;
054import com.unboundid.util.ThreadSafetyLevel;
055
056import static com.unboundid.ldap.sdk.LDAPMessages.*;
057
058
059
060/**
061 * This class provides an implementation of a reusable referral connector that
062 * maintains pools of connections to each of the servers accessed in the course
063 * of following referrals.  Connections may be reused across multiple
064 * referrals.  Note that it is important to close the connector when it is no
065 * longer needed, as that will ensure that all of the connection pools that it
066 * maintains will be closed.
067 * <BR><BR>
068 * <H2>Example</H2>
069 * The following example demonstrates the process for establishing an LDAP
070 * connection that will use this connector for following any referrals that are
071 * encountered during processing:
072 * <PRE>
073 *   PooledReferralConnectorProperties properties =
074 *        new PooledReferralConnectorProperties();
075 *
076 *   PooledReferralConnector referralConnector =
077 *        new PooledReferralConnector(properties);
078 *
079 *   LDAPConnectionOptions options = new LDAPConnectionOptions();
080 *   options.setFollowReferrals(true);
081 *   options.setReferralConnector(referralConnector);
082 *
083 *   try (LDAPConnection conn = new LDAPConnection(socketFactory, options,
084 *             serverAddress, serverPort)
085 *   {
086 *     // Use the connection to perform whatever processing is needed that might
087 *     // involve receiving referrals.
088 *   }
089 *   finally
090 *   {
091 *     referralConnector.close();
092 *   }
093 * </PRE>
094 */
095@NotMutable()
096@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
097public final class PooledReferralConnector
098       implements ReusableReferralConnector, Closeable
099{
100  // Indicates whether a request has been made to close the connector.
101  @NotNull private final AtomicBoolean closeRequested;
102
103  // The bind request to use to authenticate to pooled connections, as an
104  // alternative to the bind request used to authenticate connections on which
105  // referrals were received.
106  @Nullable private final BindRequest bindRequest;
107
108  // Indicates whether to retry operations on a newlye established connection
109  // if the initial attempt fails in a way that suggests that the pooled
110  // connection may not be valid.
111  private final boolean retryFailedOperationsDueToInvalidConnections;
112
113  // The initial number of connections to establish when creating a connection
114  // pool.
115  private final int initialConnectionsPerPool;
116
117  // The maximum number of connections to maintain in each of the connection
118  // pools.
119  private final int maximumConnectionsPerPool;
120
121  // The connection options to use when establishing new connections, as an
122  // alternative to the connection options used for a connection on which a
123  // referral was received.
124  @Nullable private final LDAPConnectionOptions connectionOptions;
125
126  // A health check to use for the connection pools.
127  @Nullable private final LDAPConnectionPoolHealthCheck healthCheck;
128
129  // The interval that the background thread should use when checking for
130  // cleanup operations.
131  private final long backgroundThreadCheckIntervalMillis;
132
133  // The health check interval in milliseconds to use for the connection pools.
134  private final long healthCheckIntervalMillis;
135
136  // The maximum length of time in milliseconds that any individual pooled
137  // connection should be allowed to remain established.
138  private final long maximumConnectionAgeMillis;
139
140  // The maximum length of time in milliseconds that any connection pool should
141  // be allowed to remain active.
142  private final long maximumPoolAgeMillis;
143
144  // The maximum length of time in milliseconds that should be allowed to pass
145  // since a connection pool was last used to follow a referral before it is
146  // discarded.
147  private final long maximumPoolIdleDurationMillis;
148
149  // The map of connection pools that have been created for this referral
150  // connector, indexed by the address and port of the target server.
151  @NotNull private final Map<String,List<ReferralConnectionPool>>
152       poolsByHostPort;
153
154  // The background thread that will monitor the set of pools to determine
155  // whether any of them should be destroyed.
156  @Nullable private final PooledReferralConnectorBackgroundThread
157       backgroundThread;
158
159  // The security type to use when establishing connections in response to
160  // referral URLs with a scheme of "ldap".
161  @NotNull private final PooledReferralConnectorLDAPURLSecurityType
162       ldapURLSecurityType;
163
164  // The SSL socket factory to use when performing TLS negotiation, as an
165  // alternative to the socket factory from the associated connection.
166  @Nullable private final SSLSocketFactory sslSocketFactory;
167
168
169
170  /**
171   * Creates a new pooled referral connector with a default set of properties.
172   */
173  public PooledReferralConnector()
174  {
175    this(new PooledReferralConnectorProperties());
176  }
177
178
179
180  /**
181   * Creates a new pooled referral connector with the provided set of
182   * properties.
183   *
184   * @param  properties  The properties to use for the pooled referral
185   *                     connector.  It must not be {@code null}.
186   */
187  public PooledReferralConnector(
188              @NotNull final PooledReferralConnectorProperties properties)
189  {
190    bindRequest = properties.getBindRequest();
191    retryFailedOperationsDueToInvalidConnections =
192         properties.retryFailedOperationsDueToInvalidConnections();
193    initialConnectionsPerPool = properties.getInitialConnectionsPerPool();
194    maximumConnectionsPerPool = properties.getMaximumConnectionsPerPool();
195    connectionOptions = properties.getConnectionOptions();
196    healthCheck = properties.getHealthCheck();
197    backgroundThreadCheckIntervalMillis =
198         properties.getBackgroundThreadCheckIntervalMillis();
199    healthCheckIntervalMillis = properties.getHealthCheckIntervalMillis();
200    maximumConnectionAgeMillis = properties.getMaximumConnectionAgeMillis();
201    maximumPoolAgeMillis = properties.getMaximumPoolAgeMillis();
202    maximumPoolIdleDurationMillis =
203         properties.getMaximumPoolIdleDurationMillis();
204    ldapURLSecurityType = properties.getLDAPURLSecurityType();
205    sslSocketFactory = properties.getSSLSocketFactory();
206
207    closeRequested = new AtomicBoolean(false);
208    poolsByHostPort = new ConcurrentHashMap<>();
209
210    if ((maximumPoolAgeMillis > 0L) || (maximumPoolIdleDurationMillis > 0L))
211    {
212      backgroundThread = new PooledReferralConnectorBackgroundThread(this);
213      backgroundThread.start();
214    }
215    else
216    {
217      backgroundThread = null;
218    }
219  }
220
221
222
223  /**
224   * Retrieves the initial number of connections to establish when creating a
225   * new connection pool for the purpose of following referrals.  By default,
226   * only a single connection will be established.
227   *
228   * @return  The initial number of connections to establish when creating a
229   *          new connection pool for the purpose of following referrals.
230   */
231  public int getInitialConnectionsPerPool()
232  {
233    return initialConnectionsPerPool;
234  }
235
236
237
238  /**
239   * Retrieves the maximum number of idle connections that the server should
240   * maintain in each connection pool used for following referrals.  By default,
241   * a maximum of ten connections will be retained.
242   *
243   * @return  The maximum number of idle connections that the server should
244   *          maintain in each connection pool used for following referrals.
245   */
246  public int getMaximumConnectionsPerPool()
247  {
248    return maximumConnectionsPerPool;
249  }
250
251
252
253  /**
254   * Indicates whether the connection pools should be configured to
255   * automatically retry an operation on a newly established connection if the
256   * initial attempt fails in a manner that suggests that the connection may no
257   * longer be valid.  By default, operations that fail in that manner will
258   * automatically be retried.
259   *
260   * @return  {@code true} if connection pools should be configured to
261   *          automatically retry an operation on a newly established connection
262   *          if the initial attempt fails in a manner that suggests the
263   *          connection may no longer be valid, or {@code false} if not.
264   */
265  public boolean retryFailedOperationsDueToInvalidConnections()
266  {
267    return retryFailedOperationsDueToInvalidConnections;
268  }
269
270
271
272  /**
273   * Retrieves the maximum length of time in milliseconds that each pooled
274   * connection may remain established.  If a pooled connection is established
275   * for longer than this duration, it will be closed and re-established.  By
276   * default, pooled connections will be allowed to remain established for up
277   * to 30 minutes.  A value of zero indicates that pooled connections will be
278   * allowed to remain established indefinitely (or at least until it is
279   * determined to be invalid or the pool is closed).
280   *
281   * @return  The maximum length of time in milliseconds that each pooled
282   *          connection may remain established.
283   */
284  public long getMaximumConnectionAgeMillis()
285  {
286    return maximumConnectionAgeMillis;
287  }
288
289
290
291  /**
292   * Retrieves the maximum length of time in milliseconds that a connection pool
293   * created for the purpose of following referrals should be retained,
294   * regardless of how often it is used.  If it has been longer than this length
295   * of time since a referral connection pool was created, it will be
296   * automatically closed, and a new pool will be created if another applicable
297   * referral is received.  A value of zero, which is the default, indicates
298   * that connection pools should not be automatically closed based on the
299   * length of time since they were created.
300   *
301   * @return  The maximum length of time in milliseconds that a referral
302   *          connection pool should be retained, or zero if connection pools
303   *          should not be automatically closed based on the length of time
304   *          since they were created.
305   */
306  public long getMaximumPoolAgeMillis()
307  {
308    return maximumPoolAgeMillis;
309  }
310
311
312
313  /**
314   * Retrieves the maximum length of time in milliseconds that a connection pool
315   * created for the purpose of following referrals should be retained after its
316   * most recent use.  By default, referral connection pools will be
317   * automatically discarded if they have remained unused for over one hour.  A
318   * value of zero indicates that pools may remain in use indefinitely,
319   * regardless of how long it has been since they were last used.
320   *
321   * @return  The maximum length of time in milliseconds that a connection pool
322   *          created for the purpose of following referrals should be retained
323   *          after its most recent use, or zero if referral connection pools
324   *          should not be discarded regardless of how long it has been since
325   *          they were last used.
326   */
327  public long getMaximumPoolIdleDurationMillis()
328  {
329    return maximumPoolIdleDurationMillis;
330  }
331
332
333
334  /**
335   * Retrieves the health check that should be used to determine whether pooled
336   * connections are still valid.  By default, no special health checking will
337   * be performed for pooled connections (aside from checking them against
338   * the maximum connection age).
339   *
340   * @return  The health check that should be used to determine whether pooled
341   *          connections are still valid, or {@code null} if no special
342   *          health checking should be performed.
343   */
344  @Nullable()
345  public LDAPConnectionPoolHealthCheck getHealthCheck()
346  {
347    return healthCheck;
348  }
349
350
351
352  /**
353   * Retrieves the length of time in milliseconds between background health
354   * checks performed against pooled connections.  By default, background health
355   * checks will be performed every sixty seconds.
356   *
357   * @return  The length of time in milliseconds between background health
358   *          checks performed against pooled connections.
359   */
360  public long getHealthCheckIntervalMillis()
361  {
362    return healthCheckIntervalMillis;
363  }
364
365
366
367  /**
368   * Retrieves the bind request that should be used to authenticate pooled
369   * connections, if defined.  By default, pooled connections will be
370   * authenticated with the same bind request that was used to authenticate
371   * the connection on which the referral was received (with separate pools used
372   * for referrals received on connections authenticated as different users).
373   *
374   * @return  The bind request that should be used to authenticate pooled
375   *          connections, or {@code null} if pooled connections should be
376   *          authenticated with the same bind request that was used to
377   *          authenticate the connection on which the referral was received.
378   */
379  @Nullable()
380  public BindRequest getBindRequest()
381  {
382    return bindRequest;
383  }
384
385
386
387  /**
388   * Retrieves the set of options that will be used when establishing new pooled
389   * connections for the purpose of following referrals.  By default, new
390   * connections will use the same set of options as the connection on which a
391   * referral was received.
392   *
393   * @return  The set of options that will be used when establishing new
394   *          pooled connections for the purpose of following referrals, or
395   *          {@code null} if new connections will use the same set of options
396   *          as the connection on which a referral was received.
397   */
398  @Nullable()
399  public LDAPConnectionOptions getConnectionOptions()
400  {
401    return connectionOptions;
402  }
403
404
405
406  /**
407   * Indicates the type of communication security that the referral connector
408   * should use when creating connections for referral URLs with a scheme of
409   * "ldap".  Although the connector will always use LDAPS for connections
410   * created from referral URLs with a scheme of "ldaps", the determination of
411   * which security type to use for referral URLs with a scheme of "ldap" is
412   * more complicated because the official LDAP URL specification lists "ldap"
413   * as the only allowed scheme type.  See the class-level and value-level
414   * documentation in the {@link PooledReferralConnectorLDAPURLSecurityType}
415   * enum for more information.  By default, the
416   * {@code CONDITIONALLY_USE_LDAP_AND_CONDITIONALLY_USE_START_TLS} security
417   * type will be used.
418   *
419   * @return  The type of communication security that the referral connector
420   *          should use when creating connections for referral URLs with a
421   *          scheme of "ldap".
422   */
423  @NotNull()
424  public PooledReferralConnectorLDAPURLSecurityType getLDAPURLSecurityType()
425  {
426    return ldapURLSecurityType;
427  }
428
429
430
431  /**
432   * Retrieves the SSL socket factory that will be used when performing TLS
433   * negotiation on any new connections created for the purpose of following
434   * referrals.  By default, new pooled connections will use the same socket
435   * factory as the connection on which a referral was received.
436   *
437   * @return  The SSL socket factory that will be used when performing TLS
438   *          negotiation on any new connections created for the purpose of
439   *          following referrals, or {@code null} if new pooled connections
440   *          will use the same socket factory as the connection on which a
441   *          referral was received.
442   */
443  @Nullable()
444  public SSLSocketFactory getSSLSocketFactory()
445  {
446    return sslSocketFactory;
447  }
448
449
450
451  /**
452   * Retrieves the interval duration in milliseconds that the
453   * {@link PooledReferralConnectorBackgroundThread} should use when sleeping
454   * between checks to determine if any of the established referral connection
455   * pools should be closed.  This is only intended for internal use.  By
456   * default, the interval duration will be 10 seconds (10,000 milliseconds).
457   *
458   * @return  The interval duration in milliseconds that the
459   *          {@code PooledReferralConnectorBackgroundThread} should use when
460   *          sleeping between checks.
461   */
462  long getBackgroundThreadCheckIntervalMillis()
463  {
464    return backgroundThreadCheckIntervalMillis;
465  }
466
467
468
469  /**
470   * Closes and discards all connection pools that are associated with this
471   * connector.  The connector will be unusable after it is closed.
472   */
473  public void close()
474  {
475    if (backgroundThread != null)
476    {
477      backgroundThread.shutDown();
478    }
479
480    synchronized (poolsByHostPort)
481    {
482      closeRequested.set(true);
483
484      final Iterator<Map.Entry<String,List<ReferralConnectionPool>>> iterator =
485           poolsByHostPort.entrySet().iterator();
486      while (iterator.hasNext())
487      {
488        final Map.Entry<String,List<ReferralConnectionPool>> e =
489             iterator.next();
490        iterator.remove();
491
492        for (final ReferralConnectionPool pool : e.getValue())
493        {
494          pool.close();
495        }
496      }
497    }
498  }
499
500
501
502  /**
503   * Retrieves the map of connection pools that have been created for the
504   * purpose of following referrals.  This is for internal use only, and the
505   * caller must synchronize on the returned map for any access to it.
506   *
507   * @return  The map of connection pools that have been created for the
508   *          purpose of following referrals.
509   */
510  @NotNull()
511  Map<String,List<ReferralConnectionPool>> getPoolsByHostPort()
512  {
513    return poolsByHostPort;
514  }
515
516
517
518  /**
519   * {@inheritDoc}
520   */
521  @Override()
522  @NotNull()
523  public LDAPConnectionPool getReferralInterface(
524              @NotNull final LDAPURL referralURL,
525              @NotNull final LDAPConnection connection)
526         throws LDAPException
527  {
528    final String hostPort = StaticUtils.toLowerCase(referralURL.getHost()) +
529         ":" + referralURL.getPort();
530
531    synchronized (poolsByHostPort)
532    {
533      if (closeRequested.get())
534      {
535        throw new LDAPException(ResultCode.UNAVAILABLE,
536             ERR_POOLED_REFERRAL_CONNECTOR_CLOSED.get(
537                  String.valueOf(referralURL)));
538      }
539
540      List<ReferralConnectionPool> pools = poolsByHostPort.get(hostPort);
541      if (pools == null)
542      {
543        pools = new ArrayList<>();
544        poolsByHostPort.put(hostPort, pools);
545      }
546
547      for (final ReferralConnectionPool pool : pools)
548      {
549        if (pool.isApplicableToReferral(referralURL, connection))
550        {
551          return pool.getConnectionPool();
552        }
553      }
554
555      final ReferralConnectionPool newPool =
556           new ReferralConnectionPool(referralURL, connection, this);
557      pools.add(newPool);
558      return newPool.getConnectionPool();
559    }
560  }
561
562
563
564  /**
565   * {@inheritDoc}
566   */
567  @Override()
568  @NotNull()
569  public LDAPConnection getReferralConnection(
570              @NotNull final LDAPURL referralURL,
571              @NotNull final LDAPConnection connection)
572         throws LDAPException
573  {
574    final LDAPConnectionPool connectionPool =
575         getReferralInterface(referralURL, connection);
576    return connectionPool.getConnection();
577  }
578}