001/*
002 * Copyright 2022-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2022-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) 2022-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.logs.v2.text;
037
038
039
040import java.text.SimpleDateFormat;
041import java.util.ArrayList;
042import java.util.Collections;
043import java.util.Date;
044import java.util.LinkedHashMap;
045import java.util.LinkedHashSet;
046import java.util.List;
047import java.util.Map;
048import java.util.Set;
049import java.util.StringTokenizer;
050
051import com.unboundid.ldap.sdk.unboundidds.logs.LogException;
052import com.unboundid.ldap.sdk.unboundidds.logs.v2.LogField;
053import com.unboundid.ldap.sdk.unboundidds.logs.v2.LogMessage;
054import com.unboundid.util.ByteStringBuffer;
055import com.unboundid.util.Debug;
056import com.unboundid.util.NotExtensible;
057import com.unboundid.util.NotMutable;
058import com.unboundid.util.NotNull;
059import com.unboundid.util.Nullable;
060import com.unboundid.util.StaticUtils;
061import com.unboundid.util.ThreadSafety;
062import com.unboundid.util.ThreadSafetyLevel;
063
064import static com.unboundid.ldap.sdk.unboundidds.logs.v2.text.TextLogMessages.*;
065
066
067
068/**
069 * This class provides a data structure that holds information about a
070 * text-formatted log message in the name=value format used by the Ping
071 * Identity Directory Server and related server products.
072 * <BR>
073 * <BLOCKQUOTE>
074 *   <B>NOTE:</B>  This class, and other classes within the
075 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
076 *   supported for use against Ping Identity, UnboundID, and
077 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
078 *   for proprietary functionality or for external specifications that are not
079 *   considered stable or mature enough to be guaranteed to work in an
080 *   interoperable way with other types of LDAP servers.
081 * </BLOCKQUOTE>
082 */
083@NotExtensible()
084@NotMutable()
085@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_THREADSAFE)
086public class TextFormattedLogMessage
087       implements LogMessage
088{
089  /**
090   * A predefined string that will be used if a field exists in a log message
091   * with just a value but no field name.
092   */
093  @NotNull protected static final String NO_FIELD_NAME = "";
094
095
096
097  /**
098   * The format string that will be used for log message timestamps
099   * with seconds-level precision enabled.
100   */
101  @NotNull static final String TIMESTAMP_FORMAT_SECOND =
102          "'['dd/MMM/yyyy:HH:mm:ss Z']'";
103
104
105
106  /**
107   * The format string that will be used for log message timestamps
108   * with seconds-level precision enabled.
109   */
110  @NotNull static final String TIMESTAMP_FORMAT_MILLISECOND =
111          "'['dd/MMM/yyyy:HH:mm:ss.SSS Z']'";
112
113
114
115  /**
116   * A set of thread-local date formatters that will be used for timestamp with
117   * millisecond-level precision.
118   */
119  @NotNull private static final ThreadLocal<SimpleDateFormat>
120       MILLISECOND_DATE_FORMATTERS = new ThreadLocal<>();
121
122
123
124  /**
125   * A set of thread-local date formatters that will be used for timestamp with
126   * second-level precision.
127   */
128  @NotNull private static final ThreadLocal<SimpleDateFormat>
129       SECOND_DATE_FORMATTERS = new ThreadLocal<>();
130
131
132
133  /**
134   * The serial version UID for this serializable class.
135   */
136  private static final long serialVersionUID = -8953179308642786675L;
137
138
139
140  // The timestamp value for this log message.
141  private final long timestampValue;
142
143  // A map of the fields in this log message.
144  @NotNull private final Map<String,List<String>> logFields;
145
146  // The string representation of this log message.
147  @NotNull private final String logMessageString;
148
149
150
151  /**
152   * Creates a new text-formatted log message from the provided parsed message.
153   *
154   * @param  message  The message to use to create this log message.  It must
155   *                  not be {@code null}.
156   */
157  protected TextFormattedLogMessage(
158                 @NotNull final TextFormattedLogMessage message)
159  {
160    timestampValue = message.timestampValue;
161    logFields = message.logFields;
162    logMessageString = message.logMessageString;
163  }
164
165
166
167  /**
168   * Creates a new text-formatted log message from the provided string.
169   *
170   * @param  logMessageString  The string representation of this log message.
171   *                           It must not be {@code null}.
172   *
173   * @throws  LogException  If the provided string cannot be parsed as a valid
174   *                        text-formatted log message.
175   */
176  public TextFormattedLogMessage(@NotNull final String logMessageString)
177         throws LogException
178  {
179    this.logMessageString = logMessageString;
180
181
182    // The first element of the log message should be the timestamp, and it
183    // should be enclosed in square brackets.
184    final int closeBracketPos = logMessageString.indexOf(']');
185    if ((closeBracketPos <= 0) || (! logMessageString.startsWith("[")))
186    {
187      throw new LogException(logMessageString,
188           ERR_TEXT_LOG_MESSAGE_MISSING_TIMESTAMP.get(logMessageString));
189    }
190
191    final String timestampString =
192         logMessageString.substring(0, (closeBracketPos+1));
193    try
194    {
195      final SimpleDateFormat dateFormat =
196           getDateFormat(timestampString.indexOf('.') > 0);
197      final Date timestampDate = dateFormat.parse(timestampString);
198      timestampValue = timestampDate.getTime();
199    }
200    catch (final Exception e)
201    {
202      Debug.debugException(e);
203      throw new LogException(logMessageString,
204           ERR_TEXT_LOG_MESSAGE_MISSING_TIMESTAMP.get(logMessageString),
205           e);
206    }
207
208
209    // The remainder of the message should be the set of fields.
210    logFields = parseFields(logMessageString, (closeBracketPos + 1));
211  }
212
213
214
215  /**
216   * Retrieves a date formatter instance that should be used for parsing
217   * timestamp values.
218   *
219   * @param  millisecondPrecision  Indicates whether to retrieve a formatter for
220   *                               parsing timestamps with millisecond precision
221   *                               (if {@code true}) or second precision (if
222   *                               {@code false}).
223   *
224   * @return  The date formatter instance.
225   */
226  @NotNull()
227  private static SimpleDateFormat getDateFormat(
228               final boolean millisecondPrecision)
229  {
230    if (millisecondPrecision)
231    {
232      SimpleDateFormat dateFormat = MILLISECOND_DATE_FORMATTERS.get();
233      if (dateFormat == null)
234      {
235        dateFormat = new SimpleDateFormat(TIMESTAMP_FORMAT_MILLISECOND);
236        dateFormat.setLenient(false);
237        MILLISECOND_DATE_FORMATTERS.set(dateFormat);
238      }
239
240      return dateFormat;
241    }
242    else
243    {
244      SimpleDateFormat dateFormat = SECOND_DATE_FORMATTERS.get();
245      if (dateFormat == null)
246      {
247        dateFormat = new SimpleDateFormat(TIMESTAMP_FORMAT_SECOND);
248        dateFormat.setLenient(false);
249        SECOND_DATE_FORMATTERS.set(dateFormat);
250      }
251
252      return dateFormat;
253    }
254  }
255
256
257
258  /**
259   * Parses the set of log fields from the provided message string.
260   *
261   * @param  s         The complete message string being parsed.
262   * @param  startPos  The position at which to start parsing.
263   *
264   * @return  The map containing the fields read from the message string.
265   *
266   * @throws  LogException  If a problem occurs while processing the message.
267   */
268  @NotNull()
269  private static Map<String,List<String>> parseFields(@NotNull final String s,
270                                                      final int startPos)
271          throws LogException
272  {
273    final Map<String,List<String>> fieldMap = new LinkedHashMap<>();
274
275    boolean inQuotes = false;
276    final StringBuilder buffer = new StringBuilder();
277    for (int p=startPos; p < s.length(); p++)
278    {
279      final char c = s.charAt(p);
280      if ((c == ' ') && (! inQuotes))
281      {
282        if (buffer.length() > 0)
283        {
284          processField(s, buffer.toString(), fieldMap);
285          buffer.setLength(0);
286        }
287      }
288      else if (c == '"')
289      {
290        inQuotes = (! inQuotes);
291      }
292      else
293      {
294        buffer.append(c);
295      }
296    }
297
298    if (buffer.length() > 0)
299    {
300      processField(s, buffer.toString(), fieldMap);
301    }
302
303    return Collections.unmodifiableMap(fieldMap);
304  }
305
306
307
308  /**
309   * Processes the provided log field and adds it to the given map.
310   *
311   * @param  logMessageString  The complete log message string being parsed.
312   * @param  fieldString       The string representation of the field being
313   *                           parsed.
314   * @param  fieldMap          The map into which the parsed field should be
315   *                           added.
316   *
317   * @throws  LogException  If a problem occurs while processing the token.
318   */
319  private static void processField(@NotNull final String logMessageString,
320               @NotNull final String fieldString,
321               @NotNull final Map<String,List<String>> fieldMap)
322          throws LogException
323  {
324    // The field name will be the portion of the string before the equal sign.
325    // If there's no equal sign, then use the empty string as the field name.
326    final String fieldName;
327    final String fieldValue;
328    final int equalPos = fieldString.indexOf('=');
329    if (equalPos < 0)
330    {
331      fieldName = NO_FIELD_NAME;
332      fieldValue = processValue(logMessageString, fieldString);
333    }
334    else
335    {
336      fieldName = fieldString.substring(0, equalPos);
337      fieldValue =
338           processValue(logMessageString, fieldString.substring(equalPos+1));
339    }
340
341    // We'll use an immutable list for the field values.  This shouldn't hurt
342    // performance because fields with multiple values should be very rare.
343    final List<String> values = fieldMap.get(fieldName);
344    if (values == null)
345    {
346      fieldMap.put(fieldName, Collections.singletonList(fieldValue));
347    }
348    else
349    {
350      final List<String> updatedValues = new ArrayList<>(values.size() + 1);
351      updatedValues.addAll(values);
352      updatedValues.add(fieldValue);
353      fieldMap.put(fieldName, Collections.unmodifiableList(updatedValues));
354    }
355  }
356
357
358
359  /**
360   * Performs any processing needed on the provided value to obtain the original
361   * text.  This may include removing surrounding quotes and/or un-escaping any
362   * special characters.
363   *
364   * @param  logMessageString  The complete log message string being parsed.
365   * @param  valueString       The value being processed.
366   *
367   * @return  The processed version of the provided value.
368   *
369   * @throws  LogException  If a problem occurs while processing the value.
370   */
371  @NotNull()
372  private static String processValue(@NotNull final String logMessageString,
373                                     @NotNull final String valueString)
374          throws LogException
375  {
376    final ByteStringBuffer b = new ByteStringBuffer();
377
378    for (int i=0; i < valueString.length(); i++)
379    {
380      final char c = valueString.charAt(i);
381      if (c == '"')
382      {
383        // This should only happen at the beginning or end of the string, in
384        // which case it should be stripped out so we don't need to do anything.
385      }
386      else if (c == '#')
387      {
388        // Every octothorpe should be followed by exactly two hex digits, which
389        // represent a byte of a UTF-8 character.
390        if (i > (valueString.length() - 3))
391        {
392          throw new LogException(logMessageString,
393               ERR_TEXT_LOG_MESSAGE_INVALID_ESCAPED_CHARACTER.get(valueString,
394                    logMessageString));
395        }
396
397        byte rawByte = 0x00;
398        for (int j=0; j < 2; j++)
399        {
400          rawByte <<= 4;
401          switch (valueString.charAt(++i))
402          {
403            case '0':
404              break;
405            case '1':
406              rawByte |= 0x01;
407              break;
408            case '2':
409              rawByte |= 0x02;
410              break;
411            case '3':
412              rawByte |= 0x03;
413              break;
414            case '4':
415              rawByte |= 0x04;
416              break;
417            case '5':
418              rawByte |= 0x05;
419              break;
420            case '6':
421              rawByte |= 0x06;
422              break;
423            case '7':
424              rawByte |= 0x07;
425              break;
426            case '8':
427              rawByte |= 0x08;
428              break;
429            case '9':
430              rawByte |= 0x09;
431              break;
432            case 'a':
433            case 'A':
434              rawByte |= 0x0A;
435              break;
436            case 'b':
437            case 'B':
438              rawByte |= 0x0B;
439              break;
440            case 'c':
441            case 'C':
442              rawByte |= 0x0C;
443              break;
444            case 'd':
445            case 'D':
446              rawByte |= 0x0D;
447              break;
448            case 'e':
449            case 'E':
450              rawByte |= 0x0E;
451              break;
452            case 'f':
453            case 'F':
454              rawByte |= 0x0F;
455              break;
456            default:
457              throw new LogException(logMessageString,
458                   ERR_TEXT_LOG_MESSAGE_INVALID_ESCAPED_CHARACTER.get(
459                        valueString, logMessageString));
460          }
461        }
462
463        b.append(rawByte);
464      }
465      else
466      {
467        b.append(c);
468      }
469    }
470
471    return b.toString();
472  }
473
474
475
476  /**
477   * {@inheritDoc}
478   */
479  @Override()
480  @NotNull()
481  public final Date getTimestamp()
482  {
483    return new Date(timestampValue);
484  }
485
486
487
488  /**
489   * {@inheritDoc}
490   */
491  @Override()
492  @NotNull()
493  public final Map<String,List<String>> getFields()
494  {
495    return logFields;
496  }
497
498
499
500  /**
501   * {@inheritDoc}
502   */
503  @Override()
504  @Nullable()
505  public final Boolean getBoolean(@NotNull final LogField logField)
506         throws LogException
507  {
508    final String valueString = getString(logField);
509    if (valueString == null)
510    {
511      return null;
512    }
513
514    if (valueString.equalsIgnoreCase("true"))
515    {
516      return Boolean.TRUE;
517    }
518    else if (valueString.equalsIgnoreCase("false"))
519    {
520      return Boolean.FALSE;
521    }
522    else
523    {
524      throw new LogException(logMessageString,
525           ERR_TEXT_LOG_MESSAGE_VALUE_NOT_BOOLEAN.get(logField.getFieldName(),
526                logMessageString));
527    }
528  }
529
530
531
532  /**
533   * Retrieves the Boolean value of the specified field.
534   *
535   * @param  logField  The field for which to retrieve the Boolean value.
536   *
537   * @return  The Boolean value of the specified field, or {@code null} if the
538   *          field does not exist in the log message or cannot be parsed as a
539   *          Boolean.
540   */
541  @Nullable()
542  final Boolean getBooleanNoThrow(@NotNull final LogField logField)
543  {
544    try
545    {
546      return getBoolean(logField);
547    }
548    catch (final LogException e)
549    {
550      Debug.debugException(e);
551      return null;
552    }
553  }
554
555
556
557  /**
558   * {@inheritDoc}
559   */
560  @Override()
561  @Nullable()
562  public final Date getGeneralizedTime(@NotNull final LogField logField)
563         throws LogException
564  {
565    final String valueString = getString(logField);
566    if (valueString == null)
567    {
568      return null;
569    }
570
571    try
572    {
573      return StaticUtils.decodeGeneralizedTime(valueString);
574    }
575    catch (final Exception e)
576    {
577      Debug.debugException(e);
578      throw new LogException(logMessageString,
579           ERR_TEXT_LOG_MESSAGE_VALUE_NOT_GENERALIZED_TIME.get(
580                logField.getFieldName(), logMessageString),
581           e);
582    }
583  }
584
585
586
587  /**
588   * Retrieves the generalized time value of the specified field.
589   *
590   * @param  logField  The field for which to retrieve the generalized time
591   *                   value.
592   *
593   * @return  The generalized time value of the specified field, or {@code null}
594   *          if the field does not exist in the log message or cannot be parsed
595   *          as a timestamp in the generalized time format.
596   */
597  @Nullable()
598  final Date getGeneralizedTimeNoThrow(@NotNull final LogField logField)
599  {
600    try
601    {
602      return getGeneralizedTime(logField);
603    }
604    catch (final LogException e)
605    {
606      Debug.debugException(e);
607      return null;
608    }
609  }
610
611
612
613  /**
614   * {@inheritDoc}
615   */
616  @Override()
617  @Nullable()
618  public final Double getDouble(@NotNull final LogField logField)
619         throws LogException
620  {
621    final String valueString = getString(logField);
622    if (valueString == null)
623    {
624      return null;
625    }
626
627    try
628    {
629      return Double.parseDouble(valueString);
630    }
631    catch (final Exception e)
632    {
633      Debug.debugException(e);
634      throw new LogException(logMessageString,
635           ERR_TEXT_LOG_MESSAGE_VALUE_NOT_FLOATING_POINT.get(
636                logField.getFieldName(), logMessageString),
637           e);
638    }
639  }
640
641
642
643  /**
644   * Retrieves the floating-point value of the specified field.
645   *
646   * @param  logField  The field for which to retrieve the floating-point value.
647   *
648   * @return  The floating-point value of the specified field, or {@code null}
649   *          if the field does not exist in the log message or cannot be parsed
650   *          as a Double.
651   */
652  @Nullable()
653  final Double getDoubleNoThrow(@NotNull final LogField logField)
654  {
655    try
656    {
657      return getDouble(logField);
658    }
659    catch (final LogException e)
660    {
661      Debug.debugException(e);
662      return null;
663    }
664  }
665
666
667
668  /**
669   * {@inheritDoc}
670   */
671  @Override()
672  @Nullable()
673  public final Integer getInteger(@NotNull final LogField logField)
674         throws LogException
675  {
676    final String valueString = getString(logField);
677    if (valueString == null)
678    {
679      return null;
680    }
681
682    try
683    {
684      return Integer.parseInt(valueString);
685    }
686    catch (final Exception e)
687    {
688      Debug.debugException(e);
689      throw new LogException(logMessageString,
690           ERR_TEXT_LOG_MESSAGE_VALUE_NOT_INTEGER.get(
691                logField.getFieldName(), logMessageString),
692           e);
693    }
694  }
695
696
697
698  /**
699   * Retrieves the integer value of the specified field.
700   *
701   * @param  logField  The field for which to retrieve the integer value.
702   *
703   * @return  The integer value of the specified field, or {@code null} if the
704   *          field does not exist in the log message or cannot be parsed as an
705   *          {@code Integer}.
706   */
707  @Nullable()
708  final Integer getIntegerNoThrow(@NotNull final LogField logField)
709  {
710    try
711    {
712      return getInteger(logField);
713    }
714    catch (final LogException e)
715    {
716      Debug.debugException(e);
717      return null;
718    }
719  }
720
721
722
723  /**
724   * {@inheritDoc}
725   */
726  @Override()
727  @Nullable()
728  public final Long getLong(@NotNull final LogField logField)
729         throws LogException
730  {
731    final String valueString = getString(logField);
732    if (valueString == null)
733    {
734      return null;
735    }
736
737    try
738    {
739      return Long.parseLong(valueString);
740    }
741    catch (final Exception e)
742    {
743      Debug.debugException(e);
744      throw new LogException(logMessageString,
745           ERR_TEXT_LOG_MESSAGE_VALUE_NOT_INTEGER.get(
746                logField.getFieldName(), logMessageString),
747           e);
748    }
749  }
750
751
752
753  /**
754   * Retrieves the integer value of the specified field.
755   *
756   * @param  logField  The field for which to retrieve the integer value.
757   *
758   * @return  The integer value of the specified field, or {@code null} if the
759   *          field does not exist in the log message or cannot be parsed as a
760   *          {@code Long}.
761   */
762  @Nullable()
763  final Long getLongNoThrow(@NotNull final LogField logField)
764  {
765    try
766    {
767      return getLong(logField);
768    }
769    catch (final LogException e)
770    {
771      Debug.debugException(e);
772      return null;
773    }
774  }
775
776
777
778  /**
779   * {@inheritDoc}
780   */
781  @Override()
782  @Nullable()
783  public final Date getRFC3339Timestamp(@NotNull final LogField logField)
784         throws LogException
785  {
786    final String valueString = getString(logField);
787    if (valueString == null)
788    {
789      return null;
790    }
791
792    try
793    {
794      return StaticUtils.decodeRFC3339Time(valueString);
795    }
796    catch (final Exception e)
797    {
798      Debug.debugException(e);
799      throw new LogException(logMessageString,
800           ERR_TEXT_LOG_MESSAGE_VALUE_NOT_RFC_3339_TIMESTAMP.get(
801                logField.getFieldName(), logMessageString),
802           e);
803    }
804  }
805
806
807
808  /**
809   * Retrieves the RFC 3339 timestamp value of the specified field.
810   *
811   * @param  logField  The field for which to retrieve the RFC 3339 timestamp
812   *                   value.
813   *
814   * @return  The RFC 3339 timestamp value of the specified field, or
815   *          {@code null} if the field does not exist in the log message or
816   *          cannot be parsed as a timestamp in the RFC 3339 format.
817   */
818  @Nullable()
819  final Date getRFC3339TimestampNoThrow(@NotNull final LogField logField)
820  {
821    try
822    {
823      return getRFC3339Timestamp(logField);
824    }
825    catch (final LogException e)
826    {
827      Debug.debugException(e);
828      return null;
829    }
830  }
831
832
833
834  /**
835   * {@inheritDoc}
836   */
837  @Override()
838  @Nullable()
839  public final String getString(@NotNull final LogField logField)
840  {
841    final List<String> values = logFields.get(logField.getFieldName());
842    if ((values == null) || values.isEmpty())
843    {
844      return null;
845    }
846
847    return values.get(0);
848  }
849
850
851
852  /**
853   * Retrieves a list of the strings contained in a comma-delimited string held
854   * in the specified field.
855   *
856   * @param  logField  The field containing the comma-delimited list of strings.
857   *
858   * @return  A list of the strings contained in the comma-delimited string
859   *          field, or an empty list if the field was not present or the list
860   *          was empty.
861   */
862  @NotNull()
863  final List<String> getCommaDelimitedStringList(
864             @NotNull final LogField logField)
865  {
866    final String stringValue = getString(logField);
867    if ((stringValue == null) || stringValue.isEmpty())
868    {
869      return Collections.emptyList();
870    }
871    else
872    {
873      final List<String> valueList = new ArrayList<>();
874      final StringTokenizer tokenizer = new StringTokenizer(stringValue, ",");
875      while  (tokenizer.hasMoreTokens())
876      {
877        valueList.add(tokenizer.nextToken().trim());
878      }
879
880      return Collections.unmodifiableList(valueList);
881    }
882  }
883
884
885
886  /**
887   * Retrieves a set of the strings contained in a comma-delimited string held
888   * in the specified field.
889   *
890   * @param  logField  The field containing the comma-delimited list of strings.
891   *
892   * @return  A set of the strings contained in the comma-delimited string
893   *          field, or an empty set if the field was not present or the list
894   *          was empty.
895   */
896  @NotNull()
897  final Set<String> getCommaDelimitedStringSet(
898             @NotNull final LogField logField)
899  {
900    final String stringValue = getString(logField);
901    if ((stringValue == null) || stringValue.isEmpty())
902    {
903      return Collections.emptySet();
904    }
905    else
906    {
907      final Set<String> valueSet = new LinkedHashSet<>();
908      final StringTokenizer tokenizer = new StringTokenizer(stringValue, ",");
909      while  (tokenizer.hasMoreTokens())
910      {
911        valueSet.add(tokenizer.nextToken().trim());
912      }
913
914      return Collections.unmodifiableSet(valueSet);
915    }
916  }
917
918
919
920  /**
921   * {@inheritDoc}
922   */
923  @Override()
924  @NotNull()
925  public final String toString()
926  {
927    return logMessageString;
928  }
929}