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}