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}