001/* 002 * Copyright 2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 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) 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.unboundidds; 037 038 039 040import java.io.Serializable; 041import java.util.Collection; 042import java.util.Collections; 043import java.util.LinkedHashMap; 044import java.util.Iterator; 045import java.util.Map; 046 047import com.unboundid.ldap.sdk.BindResult; 048import com.unboundid.ldap.sdk.DereferencePolicy; 049import com.unboundid.ldap.sdk.DisconnectType; 050import com.unboundid.ldap.sdk.Filter; 051import com.unboundid.ldap.sdk.LDAPConnection; 052import com.unboundid.ldap.sdk.LDAPConnectionPoolHealthCheck; 053import com.unboundid.ldap.sdk.LDAPException; 054import com.unboundid.ldap.sdk.ResultCode; 055import com.unboundid.ldap.sdk.SearchRequest; 056import com.unboundid.ldap.sdk.SearchResultEntry; 057import com.unboundid.ldap.sdk.SearchScope; 058import com.unboundid.util.Debug; 059import com.unboundid.util.NotMutable; 060import com.unboundid.util.NotNull; 061import com.unboundid.util.Nullable; 062import com.unboundid.util.StaticUtils; 063import com.unboundid.util.ThreadSafety; 064import com.unboundid.util.ThreadSafetyLevel; 065 066import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*; 067 068 069 070/** 071 * This class provides an LDAP connection pool health check implementation that 072 * will attempt to retrieve the general monitor entry from a Ping Identity 073 * Directory Server instance to determine if it has any degraded and/or 074 * unavailable alert types. If a server considers itself to be degraded or 075 * unavailable, then it may be considered unsuitable for use in a connection 076 * pool. 077 * <BR> 078 * <BLOCKQUOTE> 079 * <B>NOTE:</B> This class, and other classes within the 080 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 081 * supported for use against Ping Identity, UnboundID, and 082 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 083 * for proprietary functionality or for external specifications that are not 084 * considered stable or mature enough to be guaranteed to work in an 085 * interoperable way with other types of LDAP servers. 086 * </BLOCKQUOTE> 087 */ 088@NotMutable() 089@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 090public final class ActiveAlertsLDAPConnectionPoolHealthCheck 091 extends LDAPConnectionPoolHealthCheck 092 implements Serializable 093{ 094 /** 095 * The default maximum response time value in milliseconds, which is set to 096 * 5,000 milliseconds or 5 seconds. 097 */ 098 private static final long DEFAULT_MAX_RESPONSE_TIME_MILLIS = 5_000L; 099 100 101 102 /** 103 * The name of the attribute in the general monitor entry that holds the list 104 * of active degraded alert types. 105 */ 106 @NotNull() 107 private static final String DEGRADED_ALERT_TYPE_ATTRIBUTE_NAME = 108 "degraded-alert-type"; 109 110 111 112 /** 113 * The DN of the general monitor entry that will be examined. 114 */ 115 @NotNull() 116 private static final String GENERAL_MONITOR_ENTRY_DN = "cn=monitor"; 117 118 119 120 /** 121 * The name of the attribute in the general monitor entry that holds the list 122 * of active unavailable alert types. 123 */ 124 @NotNull() 125 private static final String UNAVAILABLE_ALERT_TYPE_ATTRIBUTE_NAME = 126 "unavailable-alert-type"; 127 128 129 130 /** 131 * The serial version UID for this serializable class. 132 */ 133 private static final long serialVersionUID = -8889308187890719816L; 134 135 136 137 // Indicates whether to ignore all degraded alert types. 138 private final boolean ignoreAllDegradedAlertTypes; 139 140 // Indicates whether to invoke the test after a connection has been 141 // authenticated. 142 private final boolean invokeAfterAuthentication; 143 144 // Indicates whether to invoke the test during background health checks. 145 private final boolean invokeForBackgroundChecks; 146 147 // Indicates whether to invoke the test when checking out a connection. 148 private final boolean invokeOnCheckout; 149 150 // Indicates whether to invoke the test when creating a new connection. 151 private final boolean invokeOnCreate; 152 153 // Indicates whether to invoke the test whenever an exception is encountered 154 // when using the connection. 155 private final boolean invokeOnException; 156 157 // Indicates whether to invoke the test when releasing a connection. 158 private final boolean invokeOnRelease; 159 160 // The maximum response time value in milliseconds. 161 private final long maxResponseTimeMillis; 162 163 // A set of degraded alert types that should not cause the health check to 164 // fail. 165 @NotNull private final Map<String,String> ignoredDegradedAlertTypes; 166 167 // A set of unavailable alert types that should not cause the health check to 168 // fail. 169 @NotNull private final Map<String,String> ignoredUnavailableAlertTypes; 170 171 // The search request that will be used to retrieve the monitor entry. 172 @NotNull private final SearchRequest searchRequest; 173 174 175 176 /** 177 * Creates a new instance of this LDAP connection pool health check with the 178 * provided information. 179 * 180 * @param invokeOnCreate 181 * Indicates whether to test for the existence of the target 182 * entry whenever a new connection is created for use in the 183 * pool. Note that this check will be performed immediately 184 * after the connection has been established and before any 185 * attempt has been made to authenticate that connection. 186 * @param invokeAfterAuthentication 187 * Indicates whether to test for the existence of the target 188 * entry immediately after a connection has been authenticated. 189 * This includes immediately after a newly-created connection 190 * has been authenticated, after a call to the connection pool's 191 * {@code bindAndRevertAuthentication} method, and after a call 192 * to the connection pool's 193 * {@code releaseAndReAuthenticateConnection} method. Note that 194 * even if this is {@code true}, the health check will only be 195 * performed if the provided bind result indicates that the bind 196 * was successful. 197 * @param invokeOnCheckout 198 * Indicates whether to test for the existence of the target 199 * entry immediately before a connection is checked out of the 200 * pool. 201 * @param invokeOnRelease 202 * Indicates whether to test for the existence of the target 203 * entry immediately after a connection has been released back 204 * to the pool. 205 * @param invokeForBackgroundChecks 206 * Indicates whether to test for the existence of the target 207 * entry during periodic background health checks. 208 * @param invokeOnException 209 * Indicates whether to test for the existence of the target 210 * entry if an exception is encountered when using the 211 * connection. 212 * @param maxResponseTimeMillis 213 * The maximum length of time, in milliseconds, to wait for the 214 * monitor entry to be retrieved. If the monitor entry cannot be 215 * retrieved within this length of time, the health check will 216 * fail. If the provided value is less than or equal to zero, 217 * then a default timeout of 5,000 milliseconds (5 seconds) will 218 * be used. 219 * @param ignoreAllDegradedAlertTypes 220 * Indicates whether to ignore all degraded alert types. If this 221 * is {@code true}, then the presence of degraded alert types 222 * will not cause the health check to fail. 223 * @param ignoredDegradedAlertTypes 224 * An optional set of the names of degraded alert types that 225 * should be ignored so that they will not cause the health 226 * check to fail. This may be {@code null} or empty if no 227 * specific degraded alert types should be ignored. 228 * @param ignoredUnavailableAlertTypes 229 * An optional set of the names of unavailable alert types that 230 * should be ignored so that they will not cause the health 231 * check to fail. This may be {@code null} or empty if no 232 * specific unavailable alert types should be ignored. 233 */ 234 public ActiveAlertsLDAPConnectionPoolHealthCheck( 235 final boolean invokeOnCreate, 236 final boolean invokeAfterAuthentication, 237 final boolean invokeOnCheckout, 238 final boolean invokeOnRelease, 239 final boolean invokeForBackgroundChecks, 240 final boolean invokeOnException, 241 final long maxResponseTimeMillis, 242 final boolean ignoreAllDegradedAlertTypes, 243 @Nullable final Collection<String> ignoredDegradedAlertTypes, 244 @Nullable final Collection<String> ignoredUnavailableAlertTypes) 245 { 246 this.invokeOnCreate = invokeOnCreate; 247 this.invokeAfterAuthentication = invokeAfterAuthentication; 248 this.invokeOnCheckout = invokeOnCheckout; 249 this.invokeOnRelease = invokeOnRelease; 250 this.invokeForBackgroundChecks = invokeForBackgroundChecks; 251 this.invokeOnException = invokeOnException; 252 this.ignoreAllDegradedAlertTypes = ignoreAllDegradedAlertTypes; 253 254 this.ignoredDegradedAlertTypes = 255 getIgnoredAlertTypes(ignoredDegradedAlertTypes); 256 this.ignoredUnavailableAlertTypes = 257 getIgnoredAlertTypes(ignoredUnavailableAlertTypes); 258 259 if (maxResponseTimeMillis > 0L) 260 { 261 this.maxResponseTimeMillis = maxResponseTimeMillis; 262 } 263 else 264 { 265 this.maxResponseTimeMillis = DEFAULT_MAX_RESPONSE_TIME_MILLIS; 266 } 267 268 int timeLimitSeconds = (int) (this.maxResponseTimeMillis / 1_000L); 269 if ((this.maxResponseTimeMillis % 1_000L) != 0L) 270 { 271 timeLimitSeconds++; 272 } 273 274 searchRequest = new SearchRequest(GENERAL_MONITOR_ENTRY_DN, 275 SearchScope.BASE, DereferencePolicy.NEVER, 1, timeLimitSeconds, false, 276 Filter.createANDFilter(), 277 DEGRADED_ALERT_TYPE_ATTRIBUTE_NAME, 278 UNAVAILABLE_ALERT_TYPE_ATTRIBUTE_NAME); 279 searchRequest.setResponseTimeoutMillis(this.maxResponseTimeMillis); 280 } 281 282 283 284 /** 285 * Retrieves a map containing the names of the provided alert types (if any). 286 * The keys of the map will be the values in a form that is suitable for 287 * efficient comparison (in all lowercase, with underscores converted to 288 * dashes), while the corresponding values will be the names as they were 289 * originally 290 * 291 * @param alertTypes The collection of alert type names to use. It may 292 * be {@code null} or empty if no ignored alert types 293 * should be used. 294 * 295 * @return A map containing the names of the provided alert types in a form 296 * that is efficient for comparison, or an empty map if the provided 297 * collection is {@code null} or empty. 298 */ 299 @NotNull() 300 private static Map<String,String> getIgnoredAlertTypes( 301 @Nullable final Collection<String> alertTypes) 302 { 303 if ((alertTypes == null) || alertTypes.isEmpty()) 304 { 305 return Collections.emptyMap(); 306 } 307 308 final Map<String,String> alertTypeMap = 309 new LinkedHashMap<>(StaticUtils.computeMapCapacity(alertTypes.size())); 310 for (final String alertType : alertTypes) 311 { 312 alertTypeMap.put(formatAlertTypeForComparison(alertType), alertType); 313 } 314 315 return Collections.unmodifiableMap(alertTypeMap); 316 } 317 318 319 320 /** 321 * Retrieves the provided alert type name in a format this is suited for 322 * efficient comparison. Tbe name will be converted to lowercase, and any 323 * underscores will be converted to dashes. 324 * 325 * @param name The name to be converted. It must not be {@code null}. 326 * 327 * @return A version of the name that is suitable for efficient comparison. 328 */ 329 @NotNull() 330 private static String formatAlertTypeForComparison(@NotNull final String name) 331 { 332 return StaticUtils.toLowerCase(name).replace('_', '-'); 333 } 334 335 336 337 /** 338 * {@inheritDoc} 339 */ 340 @Override() 341 public void ensureNewConnectionValid(@NotNull final LDAPConnection connection) 342 throws LDAPException 343 { 344 if (invokeOnCreate) 345 { 346 checkActiveAlertTypes(connection); 347 } 348 } 349 350 351 352 /** 353 * {@inheritDoc} 354 */ 355 @Override() 356 public void ensureConnectionValidAfterAuthentication( 357 @NotNull final LDAPConnection connection, 358 @NotNull final BindResult bindResult) 359 throws LDAPException 360 { 361 if (invokeAfterAuthentication && 362 (bindResult.getResultCode() == ResultCode.SUCCESS)) 363 { 364 checkActiveAlertTypes(connection); 365 } 366 } 367 368 369 370 /** 371 * {@inheritDoc} 372 */ 373 @Override() 374 public void ensureConnectionValidForCheckout( 375 @NotNull final LDAPConnection connection) 376 throws LDAPException 377 { 378 if (invokeOnCheckout) 379 { 380 checkActiveAlertTypes(connection); 381 } 382 } 383 384 385 386 /** 387 * {@inheritDoc} 388 */ 389 @Override() 390 public void ensureConnectionValidForRelease( 391 @NotNull final LDAPConnection connection) 392 throws LDAPException 393 { 394 if (invokeOnRelease) 395 { 396 checkActiveAlertTypes(connection); 397 } 398 } 399 400 401 402 /** 403 * {@inheritDoc} 404 */ 405 @Override() 406 public void ensureConnectionValidForContinuedUse( 407 @NotNull final LDAPConnection connection) 408 throws LDAPException 409 { 410 if (invokeForBackgroundChecks) 411 { 412 checkActiveAlertTypes(connection); 413 } 414 } 415 416 417 418 /** 419 * {@inheritDoc} 420 */ 421 @Override() 422 public void ensureConnectionValidAfterException( 423 @NotNull final LDAPConnection connection, 424 @NotNull final LDAPException exception) 425 throws LDAPException 426 { 427 if (invokeOnException && 428 (! ResultCode.isConnectionUsable(exception.getResultCode()))) 429 { 430 checkActiveAlertTypes(connection); 431 } 432 } 433 434 435 436 /** 437 * Indicates whether this health check will check for active alerts whenever 438 * a new connection is created. 439 * 440 * @return {@code true} if this health check will check for active alerts 441 * whenever a new connection is created, or {@code false} if not. 442 */ 443 public boolean invokeOnCreate() 444 { 445 return invokeOnCreate; 446 } 447 448 449 450 /** 451 * Indicates whether this health check will check for active alerts after a 452 * connection has been authenticated, including after authenticating a 453 * newly-created connection, as well as after calls to the connection pool's 454 * {@code bindAndRevertAuthentication} and 455 * {@code releaseAndReAuthenticateConnection} methods. 456 * 457 * @return {@code true} if this health check will check for active alerts 458 * whenever a connection has been authenticated, or {@code false} if 459 * not. 460 */ 461 public boolean invokeAfterAuthentication() 462 { 463 return invokeAfterAuthentication; 464 } 465 466 467 468 /** 469 * Indicates whether this health check will check for active alerts whenever a 470 * connection is to be checked out for use. 471 * 472 * @return {@code true} if this health check will check for active alerts 473 * whenever a connection is to be checked out, or {@code false} if 474 * not. 475 */ 476 public boolean invokeOnCheckout() 477 { 478 return invokeOnCheckout; 479 } 480 481 482 483 /** 484 * Indicates whether this health check will check for active alerts whenever a 485 * connection is to be released back to the pool. 486 * 487 * @return {@code true} if this health check will check for active alerts 488 * whenever a connection is to be released, or {@code false} if not. 489 */ 490 public boolean invokeOnRelease() 491 { 492 return invokeOnRelease; 493 } 494 495 496 497 /** 498 * Indicates whether this health check will check for active alerts during 499 * periodic background health checks. 500 * 501 * @return {@code true} if this health check will check for active alerts 502 * during periodic background health checks, or {@code false} if not. 503 */ 504 public boolean invokeForBackgroundChecks() 505 { 506 return invokeForBackgroundChecks; 507 } 508 509 510 511 /** 512 * Indicates whether this health check will check for active alerts if an 513 * exception is caught while processing an operation on a connection. 514 * 515 * @return {@code true} if this health check will check for active alerts 516 * whenever an exception is caught, or {@code false} if not. 517 */ 518 public boolean invokeOnException() 519 { 520 return invokeOnException; 521 } 522 523 524 525 /** 526 * Retrieves the maximum length of time in milliseconds that this health 527 * check should wait for the target monitor entry to be returned. 528 * 529 * @return The maximum length of time in milliseconds that this health check 530 * should wait for the target monitor entry to be returned. 531 */ 532 public long getMaxResponseTimeMillis() 533 { 534 return maxResponseTimeMillis; 535 } 536 537 538 539 /** 540 * Indicates whether to ignore all degraded alert types. 541 * 542 * @return {@code true} if all degraded alert types should be ignored, and 543 * the presence of active degraded alerts will not cause the health 544 * check to fail, or {@code false} if degraded alert types will be 545 * considered significant unless they are explicitly included in the 546 * value returned by {@link #getIgnoredDegradedAlertTypes()}. 547 */ 548 public boolean ignoreAllDegradedAlertTypes() 549 { 550 return ignoreAllDegradedAlertTypes; 551 } 552 553 554 555 /** 556 * A collection of alert type names that will be ignored when evaluating the 557 * set of degraded alert types. This will only be used if 558 * {@link #ignoreAllDegradedAlertTypes()} returns {@code false}. 559 * 560 * @return A collection of alert type names that will be ignored when 561 * evaluating the set of degraded alert types, or an empty collection 562 * if all degraded alert types should be considered significant. 563 */ 564 @NotNull() 565 public Collection<String> getIgnoredDegradedAlertTypes() 566 { 567 return ignoredDegradedAlertTypes.values(); 568 } 569 570 571 572 /** 573 * A collection of alert type names that will be ignored when evaluating the 574 * set of unavailable alert types. 575 * 576 * @return A collection of alert type names that will be ignored when 577 * evaluating the set of unavailable alert types, or an empty 578 * collection if all unavailable alert types should be considered 579 * significant. 580 */ 581 @NotNull() 582 public Collection<String> getIgnoredUnavailableAlertTypes() 583 { 584 return ignoredUnavailableAlertTypes.values(); 585 } 586 587 588 589 /** 590 * Retrieves the general monitor entry and examines it to identify any 591 * active degraded or unavailable alert types. If any are found, the health 592 * check will determine whether they should be ignored, and if not, then an 593 * exception will be thrown. 594 * 595 * @param conn The connection to be checked. 596 * 597 * @throws LDAPException If a problem occurs while trying to retrieve the 598 * target monitor entry, if it cannot be retrieved in 599 * an acceptable length of time, or if the server 600 * reports that it has active degraded or unavailable 601 * alert types that should not be ignored. 602 */ 603 private void checkActiveAlertTypes(@NotNull final LDAPConnection conn) 604 throws LDAPException 605 { 606 final SearchResultEntry monitorEntry; 607 try 608 { 609 monitorEntry = conn.searchForEntry(searchRequest.duplicate()); 610 } 611 catch (final LDAPException e) 612 { 613 Debug.debugException(e); 614 615 final String message = 616 ERR_ACTIVE_ALERTS_HEALTH_CHECK_ERROR_GETTING_MONITOR_ENTRY.get( 617 GENERAL_MONITOR_ENTRY_DN, conn.getHostPort(), 618 StaticUtils.getExceptionMessage(e)); 619 conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, message, 620 e); 621 throw new LDAPException(e.getResultCode(), message, e); 622 } 623 624 625 if (monitorEntry == null) 626 { 627 final String message = 628 ERR_ACTIVE_ALERTS_HEALTH_CHECK_NO_MONITOR_ENTRY.get( 629 GENERAL_MONITOR_ENTRY_DN, conn.getHostPort()); 630 conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, message, 631 null); 632 throw new LDAPException(ResultCode.NO_RESULTS_RETURNED, message); 633 } 634 635 636 final String[] unavailableAlertTypes = monitorEntry.getAttributeValues( 637 UNAVAILABLE_ALERT_TYPE_ATTRIBUTE_NAME); 638 if (unavailableAlertTypes != null) 639 { 640 for (final String alertType : unavailableAlertTypes) 641 { 642 if (! ignoredUnavailableAlertTypes.containsKey( 643 formatAlertTypeForComparison(alertType))) 644 { 645 final String message = 646 ERR_ACTIVE_ALERTS_HEALTH_CHECK_UNAVAILABLE_ALERT.get( 647 GENERAL_MONITOR_ENTRY_DN, conn.getHostPort(), alertType); 648 conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, 649 message, null); 650 throw new LDAPException(ResultCode.UNAVAILABLE, message); 651 } 652 } 653 } 654 655 656 if (! ignoreAllDegradedAlertTypes) 657 { 658 final String[] degradedAlertTypes = monitorEntry.getAttributeValues( 659 DEGRADED_ALERT_TYPE_ATTRIBUTE_NAME); 660 if (degradedAlertTypes != null) 661 { 662 for (final String alertType : degradedAlertTypes) 663 { 664 if (! ignoredDegradedAlertTypes.containsKey( 665 formatAlertTypeForComparison(alertType))) 666 { 667 final String message = 668 ERR_ACTIVE_ALERTS_HEALTH_CHECK_DEGRADED_ALERT.get( 669 GENERAL_MONITOR_ENTRY_DN, conn.getHostPort(), alertType); 670 conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, 671 message, null); 672 throw new LDAPException(ResultCode.UNAVAILABLE, message); 673 } 674 } 675 } 676 } 677 } 678 679 680 681 /** 682 * {@inheritDoc} 683 */ 684 @Override() 685 public void toString(@NotNull final StringBuilder buffer) 686 { 687 buffer.append("ActiveAlertsLDAPConnectionPoolHealthCheck(invokeOnCreate="); 688 buffer.append(invokeOnCreate); 689 buffer.append(", invokeAfterAuthentication="); 690 buffer.append(invokeAfterAuthentication); 691 buffer.append(", invokeOnCheckout="); 692 buffer.append(invokeOnCheckout); 693 buffer.append(", invokeOnRelease="); 694 buffer.append(invokeOnRelease); 695 buffer.append(", invokeForBackgroundChecks="); 696 buffer.append(invokeForBackgroundChecks); 697 buffer.append(", invokeOnException="); 698 buffer.append(invokeOnException); 699 buffer.append(", maxResponseTimeMillis="); 700 buffer.append(maxResponseTimeMillis); 701 buffer.append(", ignoreAllDegradedAlertTypes="); 702 buffer.append(ignoreAllDegradedAlertTypes); 703 704 buffer.append(", ignoredDegradedAlertTypes="); 705 appendAlertTypes(buffer, ignoredDegradedAlertTypes.values()); 706 707 buffer.append(", ignoredUnavailableAlertTypes="); 708 appendAlertTypes(buffer, ignoredUnavailableAlertTypes.values()); 709 710 buffer.append(')'); 711 } 712 713 714 715 /** 716 * Appends a list of the provided alert type names to the given buffer. 717 * 718 * @param buffer The buffer to which the names should be appended. It must 719 * not be {@code null}. 720 * @param names The names of the alert types to append to the buffer. It 721 * must not be {@code null}, but may be empty. 722 */ 723 private static void appendAlertTypes(@NotNull final StringBuilder buffer, 724 @NotNull final Collection<String> names) 725 { 726 buffer.append("{ "); 727 728 final Iterator<String> iterator = names.iterator(); 729 while (iterator.hasNext()) 730 { 731 buffer.append(iterator.next()); 732 733 if (iterator.hasNext()) 734 { 735 buffer.append(','); 736 } 737 738 buffer.append(' '); 739 } 740 741 buffer.append('}'); 742 } 743}