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}