001/* 002 * Copyright 2018-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2018-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) 2018-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; 037 038 039 040import java.io.ByteArrayInputStream; 041import java.io.Serializable; 042import java.text.ParseException; 043import java.text.SimpleDateFormat; 044import java.util.ArrayList; 045import java.util.Collections; 046import java.util.Date; 047import java.util.LinkedHashMap; 048import java.util.List; 049import java.util.Map; 050import java.util.StringTokenizer; 051import java.util.regex.Pattern; 052 053import com.unboundid.ldap.sdk.ChangeType; 054import com.unboundid.ldap.sdk.Entry; 055import com.unboundid.ldap.sdk.ReadOnlyEntry; 056import com.unboundid.ldap.sdk.persist.PersistUtils; 057import com.unboundid.ldap.sdk.unboundidds.controls. 058 IntermediateClientRequestControl; 059import com.unboundid.ldap.sdk.unboundidds.controls. 060 IntermediateClientRequestValue; 061import com.unboundid.ldap.sdk.unboundidds.controls. 062 OperationPurposeRequestControl; 063import com.unboundid.ldif.LDIFChangeRecord; 064import com.unboundid.ldif.LDIFReader; 065import com.unboundid.util.ByteStringBuffer; 066import com.unboundid.util.Debug; 067import com.unboundid.util.NotExtensible; 068import com.unboundid.util.NotNull; 069import com.unboundid.util.Nullable; 070import com.unboundid.util.StaticUtils; 071import com.unboundid.util.ThreadSafety; 072import com.unboundid.util.ThreadSafetyLevel; 073import com.unboundid.util.json.JSONObject; 074import com.unboundid.util.json.JSONObjectReader; 075 076import static com.unboundid.ldap.sdk.unboundidds.logs.LogMessages.*; 077 078 079 080/** 081 * This class provides a data structure that holds information about a log 082 * message that may appear in the Directory Server audit log. 083 * <BR> 084 * <BLOCKQUOTE> 085 * <B>NOTE:</B> This class, and other classes within the 086 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 087 * supported for use against Ping Identity, UnboundID, and 088 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 089 * for proprietary functionality or for external specifications that are not 090 * considered stable or mature enough to be guaranteed to work in an 091 * interoperable way with other types of LDAP servers. 092 * </BLOCKQUOTE> 093 */ 094@NotExtensible() 095@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_THREADSAFE) 096public abstract class AuditLogMessage 097 implements Serializable 098{ 099 /** 100 * A regular expression that can be used to determine if a line looks like an 101 * audit log message header. 102 */ 103 @NotNull private static final Pattern STARTS_WITH_TIMESTAMP_PATTERN = 104 Pattern.compile( 105 "^# " + // Starts with an octothorpe and a space. 106 "\\d\\d" + // Two digits for the day of the month. 107 "\\/" + // A slash to separate the day from the month. 108 "\\w\\w\\w" + // Three characters for the month. 109 "\\/" + // A slash to separate the month from the year. 110 "\\d\\d\\d\\d" + // Four digits for the year. 111 ":" + // A colon to separate the year from the hour. 112 "\\d\\d" + // Two digits for the hour. 113 ":" + // A colon to separate the hour from the minute. 114 "\\d\\d" + // Two digits for the minute. 115 ":" + // A colon to separate the minute from the second. 116 "\\d\\d" + // Two digits for the second. 117 ".*$"); // The rest of the line. 118 119 120 121 /** 122 * The format string that will be used for log message timestamps 123 * with second-level precision enabled. 124 */ 125 @NotNull private static final String TIMESTAMP_SEC_FORMAT = 126 "dd/MMM/yyyy:HH:mm:ss Z"; 127 128 129 130 /** 131 * The format string that will be used for log message timestamps 132 * with second-level precision enabled. 133 */ 134 @NotNull private static final String TIMESTAMP_MS_FORMAT = 135 "dd/MMM/yyyy:HH:mm:ss.SSS Z"; 136 137 138 139 /** 140 * A set of thread-local date formatters that can be used to parse timestamps 141 * with second-level precision. 142 */ 143 @NotNull private static final ThreadLocal<SimpleDateFormat> 144 TIMESTAMP_SEC_FORMAT_PARSERS = new ThreadLocal<>(); 145 146 147 148 /** 149 * A set of thread-local date formatters that can be used to parse timestamps 150 * with millisecond-level precision. 151 */ 152 @NotNull private static final ThreadLocal<SimpleDateFormat> 153 TIMESTAMP_MS_FORMAT_PARSERS = new ThreadLocal<>(); 154 155 156 157 /** 158 * The serial version UID for this serializable class. 159 */ 160 private static final long serialVersionUID = 1817887018590767411L; 161 162 163 164 // Indicates whether the associated operation was processed using a worker 165 // thread from the administrative thread pool. 166 @Nullable private final Boolean usingAdminSessionWorkerThread; 167 168 // The timestamp for this audit log message. 169 @NotNull private final Date timestamp; 170 171 // The intermediate client request control for this audit log message. 172 @Nullable private final IntermediateClientRequestControl 173 intermediateClientRequestControl; 174 175 // The lines that comprise the complete audit log message. 176 @NotNull private final List<String> logMessageLines; 177 178 // The request control OIDs for this audit log message. 179 @Nullable private final List<String> requestControlOIDs; 180 181 // The connection ID for this audit log message. 182 @Nullable private final Long connectionID; 183 184 // The operation ID for this audit log message. 185 @Nullable private final Long operationID; 186 187 // The thread ID for this audit log message. 188 @Nullable private final Long threadID; 189 190 // The connection ID for the operation that triggered this audit log message. 191 @Nullable private final Long triggeredByConnectionID; 192 193 // The operation ID for the operation that triggered this audit log message. 194 @Nullable private final Long triggeredByOperationID; 195 196 // The map of named fields contained in this audit log message. 197 @NotNull private final Map<String, String> namedValues; 198 199 // The operation purpose request control for this audit log message. 200 @Nullable private final OperationPurposeRequestControl 201 operationPurposeRequestControl; 202 203 // The DN of the alternate authorization identity for this audit log message. 204 @Nullable private final String alternateAuthorizationDN; 205 206 // The line that comprises the header for this log message, including the 207 // opening comment sequence. 208 @NotNull private final String commentedHeaderLine; 209 210 // The server instance name for this audit log message. 211 @Nullable private final String instanceName; 212 213 // The origin for this audit log message. 214 @Nullable private final String origin; 215 216 // The replication change ID for the audit log message. 217 @Nullable private final String replicationChangeID; 218 219 // The requester DN for this audit log message. 220 @Nullable private final String requesterDN; 221 222 // The requester IP address for this audit log message. 223 @Nullable private final String requesterIP; 224 225 // The product name for this audit log message. 226 @Nullable private final String productName; 227 228 // The startup ID for this audit log message. 229 @Nullable private final String startupID; 230 231 // The transaction ID for this audit log message. 232 @Nullable private final String transactionID; 233 234 // The line that comprises the header for this log message, without the 235 // opening comment sequence. 236 @NotNull private final String uncommentedHeaderLine; 237 238 239 240 /** 241 * Creates a new audit log message from the provided set of lines. 242 * 243 * @param logMessageLines The lines that comprise the log message. It must 244 * not be {@code null} or empty, and it must not 245 * contain any blank lines, although it may contain 246 * comments. In fact, it must contain at least one 247 * comment line that appears before any non-comment 248 * lines (but possibly after other comment lines) 249 * that serves as the message header. 250 * 251 * @throws AuditLogException If a problem is encountered while processing 252 * the provided list of log message lines. 253 */ 254 protected AuditLogMessage(@NotNull final List<String> logMessageLines) 255 throws AuditLogException 256 { 257 if (logMessageLines == null) 258 { 259 throw new AuditLogException(Collections.<String>emptyList(), 260 ERR_AUDIT_LOG_MESSAGE_LIST_NULL.get()); 261 } 262 263 if (logMessageLines.isEmpty()) 264 { 265 throw new AuditLogException(Collections.<String>emptyList(), 266 ERR_AUDIT_LOG_MESSAGE_LIST_EMPTY.get()); 267 } 268 269 for (final String line : logMessageLines) 270 { 271 if ((line == null) || line.isEmpty()) 272 { 273 throw new AuditLogException(logMessageLines, 274 ERR_AUDIT_LOG_MESSAGE_LIST_CONTAINS_EMPTY_LINE.get()); 275 } 276 } 277 278 this.logMessageLines = Collections.unmodifiableList( 279 new ArrayList<>(logMessageLines)); 280 281 282 // Iterate through the message lines until we find the commented header line 283 // (which is good) or until we find a non-comment line (which is bad because 284 // it means there is no header and we can't handle that). 285 String headerLine = null; 286 for (final String line : logMessageLines) 287 { 288 if (STARTS_WITH_TIMESTAMP_PATTERN.matcher(line).matches()) 289 { 290 headerLine = line; 291 break; 292 } 293 } 294 295 if (headerLine == null) 296 { 297 throw new AuditLogException(logMessageLines, 298 ERR_AUDIT_LOG_MESSAGE_LIST_DOES_NOT_START_WITH_COMMENT.get()); 299 } 300 301 commentedHeaderLine = headerLine; 302 uncommentedHeaderLine = commentedHeaderLine.substring(2); 303 304 final LinkedHashMap<String,String> nameValuePairs = 305 new LinkedHashMap<>(StaticUtils.computeMapCapacity(10)); 306 timestamp = parseHeaderLine(logMessageLines, uncommentedHeaderLine, 307 nameValuePairs); 308 namedValues = Collections.unmodifiableMap(nameValuePairs); 309 310 connectionID = getNamedValueAsLong("conn", namedValues); 311 operationID = getNamedValueAsLong("op", namedValues); 312 threadID = getNamedValueAsLong("threadID", namedValues); 313 triggeredByConnectionID = 314 getNamedValueAsLong("triggeredByConn", namedValues); 315 triggeredByOperationID = getNamedValueAsLong("triggeredByOp", namedValues); 316 alternateAuthorizationDN = namedValues.get("authzDN"); 317 instanceName = namedValues.get("instanceName"); 318 origin = namedValues.get("origin"); 319 replicationChangeID = namedValues.get("replicationChangeID"); 320 requesterDN = namedValues.get("requesterDN"); 321 requesterIP = namedValues.get("clientIP"); 322 productName = namedValues.get("productName"); 323 startupID = namedValues.get("startupID"); 324 transactionID = namedValues.get("txnID"); 325 usingAdminSessionWorkerThread = 326 getNamedValueAsBoolean("usingAdminSessionWorkerThread", namedValues); 327 operationPurposeRequestControl = 328 decodeOperationPurposeRequestControl(namedValues); 329 intermediateClientRequestControl = 330 decodeIntermediateClientRequestControl(namedValues); 331 332 final String oidsString = namedValues.get("requestControlOIDs"); 333 if (oidsString == null) 334 { 335 requestControlOIDs = null; 336 } 337 else 338 { 339 final ArrayList<String> oidList = new ArrayList<>(10); 340 final StringTokenizer tokenizer = new StringTokenizer(oidsString, ","); 341 while (tokenizer.hasMoreTokens()) 342 { 343 oidList.add(tokenizer.nextToken()); 344 } 345 requestControlOIDs = Collections.unmodifiableList(oidList); 346 } 347 } 348 349 350 351 /** 352 * Parses the provided header line for this audit log message. 353 * 354 * @param logMessageLines The lines that comprise the log message. It 355 * must not be {@code null} or empty. 356 * @param uncommentedHeaderLine The uncommented representation of the header 357 * line. It must not be {@code null}. 358 * @param nameValuePairs A map into which the parsed name-value pairs 359 * may be placed. It must not be {@code null} 360 * and must be updatable. 361 * 362 * @return The date parsed from the header line. The name-value pairs parsed 363 * from the header line will be added to the {@code nameValuePairs} 364 * map. 365 * 366 * @throws AuditLogException If the line cannot be parsed as a valid header. 367 */ 368 @NotNull() 369 private static Date parseHeaderLine( 370 @NotNull final List<String> logMessageLines, 371 @NotNull final String uncommentedHeaderLine, 372 @NotNull final Map<String,String> nameValuePairs) 373 throws AuditLogException 374 { 375 final byte[] uncommentedHeaderBytes = 376 StaticUtils.getBytes(uncommentedHeaderLine); 377 378 final ByteStringBuffer buffer = 379 new ByteStringBuffer(uncommentedHeaderBytes.length); 380 381 final ByteArrayInputStream inputStream = 382 new ByteArrayInputStream(uncommentedHeaderBytes); 383 final Date timestamp = readTimestamp(logMessageLines, inputStream, buffer); 384 while (true) 385 { 386 if (! readNameValuePair(logMessageLines, inputStream, nameValuePairs, 387 buffer)) 388 { 389 break; 390 } 391 } 392 393 return timestamp; 394 } 395 396 397 398 /** 399 * Reads the timestamp from the provided input stream and parses it using one 400 * of the expected formats. 401 * 402 * @param logMessageLines The lines that comprise the log message. It must 403 * not be {@code null} or empty. 404 * @param inputStream The input stream from which to read the timestamp. 405 * It must not be {@code null}. 406 * @param buffer A buffer that may be used to hold temporary data 407 * for reading. It must not be {@code null} and it 408 * must be empty. 409 * 410 * @return The parsed timestamp. 411 * 412 * @throws AuditLogException If the provided string cannot be parsed as a 413 * timestamp. 414 */ 415 @NotNull() 416 private static Date readTimestamp( 417 @NotNull final List<String> logMessageLines, 418 @NotNull final ByteArrayInputStream inputStream, 419 @NotNull final ByteStringBuffer buffer) 420 throws AuditLogException 421 { 422 while (true) 423 { 424 final int intRead = inputStream.read(); 425 if ((intRead < 0) || (intRead == ';')) 426 { 427 break; 428 } 429 430 buffer.append((byte) (intRead & 0xFF)); 431 } 432 433 SimpleDateFormat parser; 434 final String timestampString = buffer.toString().trim(); 435 if (timestampString.length() == 30) 436 { 437 parser = TIMESTAMP_MS_FORMAT_PARSERS.get(); 438 if (parser == null) 439 { 440 parser = new SimpleDateFormat(TIMESTAMP_MS_FORMAT); 441 parser.setLenient(false); 442 TIMESTAMP_MS_FORMAT_PARSERS.set(parser); 443 } 444 } 445 else if (timestampString.length() == 26) 446 { 447 parser = TIMESTAMP_SEC_FORMAT_PARSERS.get(); 448 if (parser == null) 449 { 450 parser = new SimpleDateFormat(TIMESTAMP_SEC_FORMAT); 451 parser.setLenient(false); 452 TIMESTAMP_SEC_FORMAT_PARSERS.set(parser); 453 } 454 } 455 else 456 { 457 throw new AuditLogException(logMessageLines, 458 ERR_AUDIT_LOG_MESSAGE_HEADER_MALFORMED_TIMESTAMP.get()); 459 } 460 461 try 462 { 463 return parser.parse(timestampString); 464 } 465 catch (final ParseException e) 466 { 467 Debug.debugException(e); 468 throw new AuditLogException(logMessageLines, 469 ERR_AUDIT_LOG_MESSAGE_HEADER_MALFORMED_TIMESTAMP.get(), e); 470 } 471 } 472 473 474 475 /** 476 * Reads a name-value pair from the provided buffer. 477 * 478 * @param logMessageLines The lines that comprise the log message. It must 479 * not be {@code null} or empty. 480 * @param inputStream The input stream from which to read the name-value 481 * pair. It must not be {@code null}. 482 * @param nameValuePairs A map to which the name-value pair should be 483 * added. 484 * @param buffer A buffer that may be used to hold temporary data 485 * for reading. It must not be {@code null}, but may 486 * not be empty and should be cleared before use. 487 * 488 * @return {@code true} if a name-value pair was read, or {@code false} if 489 * the end of the input stream was read without reading any more 490 * data. 491 * 492 * @throws AuditLogException If a problem is encountered while trying to 493 * read the name-value pair. 494 */ 495 private static boolean readNameValuePair( 496 @NotNull final List<String> logMessageLines, 497 @NotNull final ByteArrayInputStream inputStream, 498 @NotNull final Map<String,String> nameValuePairs, 499 @NotNull final ByteStringBuffer buffer) 500 throws AuditLogException 501 { 502 // Read the property name. It will be followed by an equal sign to separate 503 // the name from the value. 504 buffer.clear(); 505 while (true) 506 { 507 final int intRead = inputStream.read(); 508 if (intRead < 0) 509 { 510 // We've hit the end of the input stream. This is okay if we haven't 511 // yet read any data. 512 if (buffer.isEmpty()) 513 { 514 return false; 515 } 516 else 517 { 518 throw new AuditLogException(logMessageLines, 519 ERR_AUDIT_LOG_MESSAGE_HEADER_ENDS_WITH_PROPERTY_NAME.get( 520 buffer.toString())); 521 } 522 } 523 else if (intRead == '=') 524 { 525 break; 526 } 527 else if (intRead != ' ') 528 { 529 buffer.append((byte) (intRead & 0xFF)); 530 } 531 } 532 533 final String name = buffer.toString(); 534 if (name.isEmpty()) 535 { 536 throw new AuditLogException(logMessageLines, 537 ERR_AUDIT_LOG_MESSAGE_HEADER_EMPTY_PROPERTY_NAME.get()); 538 } 539 540 541 // Read the property value. Start by peeking at the next byte in the 542 // input stream. If it's a space, then skip it and loop back to the next 543 // byte. If it's an opening curly brace ({), then read the value as a JSON 544 // object followed by a semicolon. If it's a double quote ("), then read 545 // the value as a quoted string followed by a semicolon. If it's anything 546 // else, then read the value as an unquoted string followed by a semicolon. 547 final String valueString; 548 while (true) 549 { 550 inputStream.mark(1); 551 final int intRead = inputStream.read(); 552 if (intRead < 0) 553 { 554 // We hit the end of the input stream after the equal sign. This is 555 // fine. We'll just use an empty value. 556 valueString = ""; 557 break; 558 } 559 else if (intRead == ' ') 560 { 561 continue; 562 } 563 else if (intRead == '{') 564 { 565 inputStream.reset(); 566 final JSONObject jsonObject = 567 readJSONObject(logMessageLines, name, inputStream); 568 valueString = jsonObject.toString(); 569 break; 570 } 571 else if (intRead == '"') 572 { 573 valueString = 574 readString(logMessageLines, name, true, inputStream, buffer); 575 break; 576 } 577 else if (intRead == ';') 578 { 579 valueString = ""; 580 break; 581 } 582 else 583 { 584 inputStream.reset(); 585 valueString = 586 readString(logMessageLines, name, false, inputStream, buffer); 587 break; 588 } 589 } 590 591 nameValuePairs.put(name, valueString); 592 return true; 593 } 594 595 596 597 /** 598 * Reads a JSON object from the provided input stream. 599 * 600 * @param logMessageLines The lines that comprise the log message. It must 601 * not be {@code null} or empty. 602 * @param propertyName The name of the property whose value is expected 603 * to be a JSON object. It must not be {@code null}. 604 * @param inputStream The input stream from which to read the JSON 605 * object. It must not be {@code null}. 606 * 607 * @return The JSON object that was read. 608 * 609 * @throws AuditLogException If a problem is encountered while trying to 610 * read the JSON object. 611 */ 612 @NotNull() 613 private static JSONObject readJSONObject( 614 @NotNull final List<String> logMessageLines, 615 @NotNull final String propertyName, 616 @NotNull final ByteArrayInputStream inputStream) 617 throws AuditLogException 618 { 619 final JSONObject jsonObject; 620 try 621 { 622 final JSONObjectReader reader = new JSONObjectReader(inputStream, false); 623 jsonObject = reader.readObject(); 624 } 625 catch (final Exception e) 626 { 627 Debug.debugException(e); 628 throw new AuditLogException(logMessageLines, 629 ERR_AUDIT_LOG_MESSAGE_ERROR_READING_JSON_OBJECT.get(propertyName, 630 StaticUtils.getExceptionMessage(e)), 631 e); 632 } 633 634 readSpacesAndSemicolon(logMessageLines, propertyName, inputStream); 635 return jsonObject; 636 } 637 638 639 640 /** 641 * Reads a string from the provided input stream. It may optionally be 642 * treated as a quoted string, in which everything read up to an unescaped 643 * quote will be treated as part of the string, or an unquoted string, in 644 * which the first space or semicolon encountered will signal the end of the 645 * string. Any character prefixed by a backslash will be added to the string 646 * as-is (for example, a backslash followed by a quotation mark will cause the 647 * quotation mark to be part of the string rather than signalling the end of 648 * the quoted string). Any octothorpe (#) character must be followed by two 649 * hexadecimal digits that signify a single raw byte to add to the value. 650 * 651 * @param logMessageLines The lines that comprise the log message. It must 652 * not be {@code null} or empty. 653 * @param propertyName The name of the property with which the string 654 * value is associated. It must not be {@code null}. 655 * @param isQuoted Indicates whether to read a quoted string or an 656 * unquoted string. In the case of a a quoted 657 * string, the opening quote must have already been 658 * read. 659 * @param inputStream The input stream from which to read the string 660 * value. It must not be {@code null}. 661 * @param buffer A buffer that may be used while reading the 662 * string. It must not be {@code null}, but may not 663 * be empty and should be cleared before use. 664 * 665 * @return The string that was read. 666 * 667 * @throws AuditLogException If a problem is encountered while trying to 668 * read the string. 669 */ 670 @NotNull() 671 private static String readString(@NotNull final List<String> logMessageLines, 672 @NotNull final String propertyName, 673 final boolean isQuoted, 674 @NotNull final ByteArrayInputStream inputStream, 675 @NotNull final ByteStringBuffer buffer) 676 throws AuditLogException 677 { 678 buffer.clear(); 679 680stringLoop: 681 while (true) 682 { 683 inputStream.mark(1); 684 final int intRead = inputStream.read(); 685 if (intRead < 0) 686 { 687 if (isQuoted) 688 { 689 throw new AuditLogException(logMessageLines, 690 ERR_AUDIT_LOG_MESSAGE_END_BEFORE_CLOSING_QUOTE.get( 691 propertyName)); 692 } 693 else 694 { 695 return buffer.toString(); 696 } 697 } 698 699 switch (intRead) 700 { 701 case '\\': 702 final int literalCharacter = inputStream.read(); 703 if (literalCharacter < 0) 704 { 705 throw new AuditLogException(logMessageLines, 706 ERR_AUDIT_LOG_MESSAGE_END_BEFORE_ESCAPED.get(propertyName)); 707 } 708 else 709 { 710 buffer.append((byte) (literalCharacter & 0xFF)); 711 } 712 break; 713 714 case '#': 715 int hexByte = 716 readHexDigit(logMessageLines, propertyName, inputStream); 717 hexByte = (hexByte << 4) | 718 readHexDigit(logMessageLines, propertyName, inputStream); 719 buffer.append((byte) (hexByte & 0xFF)); 720 break; 721 722 case '"': 723 if (isQuoted) 724 { 725 break stringLoop; 726 } 727 728 buffer.append('"'); 729 break; 730 731 case ' ': 732 if (! isQuoted) 733 { 734 break stringLoop; 735 } 736 737 buffer.append(' '); 738 break; 739 740 case ';': 741 if (! isQuoted) 742 { 743 inputStream.reset(); 744 break stringLoop; 745 } 746 747 buffer.append(';'); 748 break; 749 750 default: 751 buffer.append((byte) (intRead & 0xFF)); 752 break; 753 } 754 } 755 756 readSpacesAndSemicolon(logMessageLines, propertyName, inputStream); 757 return buffer.toString(); 758 } 759 760 761 762 /** 763 * Reads a single hexadecimal digit from the provided input stream and returns 764 * its integer value. 765 * 766 * @param logMessageLines The lines that comprise the log message. It must 767 * not be {@code null} or empty. 768 * @param propertyName The name of the property with which the string 769 * value is associated. It must not be {@code null}. 770 * @param inputStream The input stream from which to read the string 771 * value. It must not be {@code null}. 772 * 773 * @return The integer value of the hexadecimal digit that was read. 774 * 775 * @throws AuditLogException If the end of the input stream was reached 776 * before the byte could be read, or if the byte 777 * that was read did not represent a hexadecimal 778 * digit. 779 */ 780 private static int readHexDigit(@NotNull final List<String> logMessageLines, 781 @NotNull final String propertyName, 782 @NotNull final ByteArrayInputStream inputStream) 783 throws AuditLogException 784 { 785 final int byteRead = inputStream.read(); 786 if (byteRead < 0) 787 { 788 throw new AuditLogException(logMessageLines, 789 ERR_AUDIT_LOG_MESSAGE_END_BEFORE_HEX.get(propertyName)); 790 } 791 792 switch (byteRead) 793 { 794 case '0': 795 return 0; 796 case '1': 797 return 1; 798 case '2': 799 return 2; 800 case '3': 801 return 3; 802 case '4': 803 return 4; 804 case '5': 805 return 5; 806 case '6': 807 return 6; 808 case '7': 809 return 7; 810 case '8': 811 return 8; 812 case '9': 813 return 9; 814 case 'a': 815 case 'A': 816 return 10; 817 case 'b': 818 case 'B': 819 return 11; 820 case 'c': 821 case 'C': 822 return 12; 823 case 'd': 824 case 'D': 825 return 13; 826 case 'e': 827 case 'E': 828 return 14; 829 case 'f': 830 case 'F': 831 return 15; 832 default: 833 throw new AuditLogException(logMessageLines, 834 ERR_AUDIT_LOG_MESSAGE_INVALID_HEX_DIGIT.get(propertyName)); 835 } 836 } 837 838 839 840 /** 841 * Reads zero or more spaces and the following semicolon from the provided 842 * input stream. It is also acceptable to encounter the end of the stream. 843 * 844 * @param logMessageLines The lines that comprise the log message. It must 845 * not be {@code null} or empty. 846 * @param propertyName The name of the property that was just read. It 847 * must not be {@code null}. 848 * @param inputStream The input stream from which to read the spaces and 849 * semicolon. It must not be {@code null}. 850 * 851 * @throws AuditLogException If any byte is encountered that is not a space 852 * or a semicolon. 853 */ 854 private static void readSpacesAndSemicolon( 855 @NotNull final List<String> logMessageLines, 856 @NotNull final String propertyName, 857 @NotNull final ByteArrayInputStream inputStream) 858 throws AuditLogException 859 { 860 while (true) 861 { 862 final int intRead = inputStream.read(); 863 if ((intRead < 0) || (intRead == ';')) 864 { 865 return; 866 } 867 else if (intRead != ' ') 868 { 869 throw new AuditLogException(logMessageLines, 870 ERR_AUDIT_LOG_MESSAGE_UNEXPECTED_CHAR_AFTER_PROPERTY.get( 871 String.valueOf((char) intRead), propertyName)); 872 } 873 } 874 } 875 876 877 878 /** 879 * Retrieves the value of the header property with the given name as a 880 * {@code Boolean} object. 881 * 882 * @param name The name of the property to retrieve. It must not 883 * be {@code null}, and it will be treated in a 884 * case-sensitive manner. 885 * @param nameValuePairs The map containing the header properties as 886 * name-value pairs. It must not be {@code null}. 887 * 888 * @return The value of the specified property as a {@code Boolean}, or 889 * {@code null} if the property is not defined or if it cannot be 890 * parsed as a {@code Boolean}. 891 */ 892 @Nullable() 893 protected static Boolean getNamedValueAsBoolean(@NotNull final String name, 894 @NotNull final Map<String,String> nameValuePairs) 895 { 896 final String valueString = nameValuePairs.get(name); 897 if (valueString == null) 898 { 899 return null; 900 } 901 902 final String lowerValueString = StaticUtils.toLowerCase(valueString); 903 if (lowerValueString.equals("true") || 904 lowerValueString.equals("t") || 905 lowerValueString.equals("yes") || 906 lowerValueString.equals("y") || 907 lowerValueString.equals("on") || 908 lowerValueString.equals("1")) 909 { 910 return Boolean.TRUE; 911 } 912 else if (lowerValueString.equals("false") || 913 lowerValueString.equals("f") || 914 lowerValueString.equals("no") || 915 lowerValueString.equals("n") || 916 lowerValueString.equals("off") || 917 lowerValueString.equals("0")) 918 { 919 return Boolean.FALSE; 920 } 921 else 922 { 923 return null; 924 } 925 } 926 927 928 929 /** 930 * Retrieves the value of the header property with the given name as a 931 * {@code Long} object. 932 * 933 * @param name The name of the property to retrieve. It must not 934 * be {@code null}, and it will be treated in a 935 * case-sensitive manner. 936 * @param nameValuePairs The map containing the header properties as 937 * name-value pairs. It must not be {@code null}. 938 * 939 * @return The value of the specified property as a {@code Long}, or 940 * {@code null} if the property is not defined or if it cannot be 941 * parsed as a {@code Long}. 942 */ 943 @Nullable() 944 protected static Long getNamedValueAsLong(@NotNull final String name, 945 @NotNull final Map<String,String> nameValuePairs) 946 { 947 final String valueString = nameValuePairs.get(name); 948 if (valueString == null) 949 { 950 return null; 951 } 952 953 try 954 { 955 return Long.parseLong(valueString); 956 } 957 catch (final Exception e) 958 { 959 Debug.debugException(e); 960 return null; 961 } 962 } 963 964 965 966 /** 967 * Decodes an entry (or list of attributes) from the commented header 968 * contained in the log message lines. 969 * 970 * @param header The header line that appears before the encoded 971 * entry. 972 * @param logMessageLines The lines that comprise the audit log message. 973 * @param entryDN The DN to use for the entry that is read. It 974 * should be {@code null} if the commented entry 975 * includes a DN, and non-{@code null} if the 976 * commented entry does not include a DN. 977 * 978 * @return The entry that was decoded from the commented header, or 979 * {@code null} if it is not included in the header or if it cannot 980 * be decoded. If the commented entry does not include a DN, then 981 * the DN of the entry returned will be the null DN. 982 */ 983 @Nullable() 984 protected static ReadOnlyEntry decodeCommentedEntry( 985 @NotNull final String header, 986 @NotNull final List<String> logMessageLines, 987 @Nullable final String entryDN) 988 { 989 List<String> ldifLines = null; 990 StringBuilder invalidLDAPNameReason = null; 991 for (final String line : logMessageLines) 992 { 993 final String uncommentedLine; 994 if (line.startsWith("# ")) 995 { 996 uncommentedLine = line.substring(2); 997 } 998 else 999 { 1000 break; 1001 } 1002 1003 if (ldifLines == null) 1004 { 1005 if (uncommentedLine.equalsIgnoreCase(header)) 1006 { 1007 ldifLines = new ArrayList<>(logMessageLines.size()); 1008 if (entryDN != null) 1009 { 1010 ldifLines.add("dn: " + entryDN); 1011 } 1012 } 1013 } 1014 else 1015 { 1016 final int colonPos = uncommentedLine.indexOf(':'); 1017 if (colonPos <= 0) 1018 { 1019 break; 1020 } 1021 1022 if (invalidLDAPNameReason == null) 1023 { 1024 invalidLDAPNameReason = new StringBuilder(); 1025 } 1026 1027 final String potentialAttributeName = 1028 uncommentedLine.substring(0, colonPos); 1029 if (PersistUtils.isValidLDAPName(potentialAttributeName, 1030 invalidLDAPNameReason)) 1031 { 1032 ldifLines.add(uncommentedLine); 1033 } 1034 else 1035 { 1036 break; 1037 } 1038 } 1039 } 1040 1041 if (ldifLines == null) 1042 { 1043 return null; 1044 } 1045 1046 try 1047 { 1048 final String[] ldifLineArray = ldifLines.toArray(StaticUtils.NO_STRINGS); 1049 final Entry ldifEntry = LDIFReader.decodeEntry(ldifLineArray); 1050 return new ReadOnlyEntry(ldifEntry); 1051 } 1052 catch (final Exception e) 1053 { 1054 Debug.debugException(e); 1055 return null; 1056 } 1057 } 1058 1059 1060 1061 /** 1062 * Decodes the operation purpose request control, if any, from the provided 1063 * set of name-value pairs. 1064 * 1065 * @param nameValuePairs The map containing the header properties as 1066 * name-value pairs. It must not be {@code null}. 1067 * 1068 * @return The operation purpose request control retrieved and decoded from 1069 * the provided set of name-value pairs, or {@code null} if no 1070 * valid operation purpose request control was included. 1071 */ 1072 @Nullable() 1073 private static OperationPurposeRequestControl 1074 decodeOperationPurposeRequestControl( 1075 @NotNull final Map<String,String> nameValuePairs) 1076 { 1077 final String valueString = nameValuePairs.get("operationPurpose"); 1078 if (valueString == null) 1079 { 1080 return null; 1081 } 1082 1083 try 1084 { 1085 final JSONObject o = new JSONObject(valueString); 1086 1087 final String applicationName = o.getFieldAsString("applicationName"); 1088 final String applicationVersion = 1089 o.getFieldAsString("applicationVersion"); 1090 final String codeLocation = o.getFieldAsString("codeLocation"); 1091 final String requestPurpose = o.getFieldAsString("requestPurpose"); 1092 1093 return new OperationPurposeRequestControl(false, applicationName, 1094 applicationVersion, codeLocation, requestPurpose); 1095 } 1096 catch (final Exception e) 1097 { 1098 Debug.debugException(e); 1099 return null; 1100 } 1101 } 1102 1103 1104 1105 /** 1106 * Decodes the intermediate client request control, if any, from the provided 1107 * set of name-value pairs. 1108 * 1109 * @param nameValuePairs The map containing the header properties as 1110 * name-value pairs. It must not be {@code null}. 1111 * 1112 * @return The intermediate client request control retrieved and decoded from 1113 * the provided set of name-value pairs, or {@code null} if no 1114 * valid operation purpose request control was included. 1115 */ 1116 @Nullable() 1117 private static IntermediateClientRequestControl 1118 decodeIntermediateClientRequestControl( 1119 @NotNull final Map<String,String> nameValuePairs) 1120 { 1121 final String valueString = 1122 nameValuePairs.get("intermediateClientRequestControl"); 1123 if (valueString == null) 1124 { 1125 return null; 1126 } 1127 1128 try 1129 { 1130 final JSONObject o = new JSONObject(valueString); 1131 return new IntermediateClientRequestControl( 1132 decodeIntermediateClientRequestValue(o)); 1133 } 1134 catch (final Exception e) 1135 { 1136 Debug.debugException(e); 1137 return null; 1138 } 1139 } 1140 1141 1142 1143 /** 1144 * decodes the provided JSON object as an intermediate client request control 1145 * value. 1146 * 1147 * @param o The JSON object to be decoded. It must not be {@code null}. 1148 * 1149 * @return The intermediate client request control value decoded from the 1150 * provided JSON object. 1151 */ 1152 @Nullable() 1153 private static IntermediateClientRequestValue 1154 decodeIntermediateClientRequestValue( 1155 @Nullable final JSONObject o) 1156 { 1157 if (o == null) 1158 { 1159 return null; 1160 } 1161 1162 final String clientIdentity = o.getFieldAsString("clientIdentity"); 1163 final String downstreamClientAddress = 1164 o.getFieldAsString("downstreamClientAddress"); 1165 final Boolean downstreamClientSecure = 1166 o.getFieldAsBoolean("downstreamClientSecure"); 1167 final String clientName = o.getFieldAsString("clientName"); 1168 final String clientSessionID = o.getFieldAsString("clientSessionID"); 1169 final String clientRequestID = o.getFieldAsString("clientRequestID"); 1170 final IntermediateClientRequestValue downstreamRequest = 1171 decodeIntermediateClientRequestValue( 1172 o.getFieldAsObject("downstreamRequest")); 1173 1174 return new IntermediateClientRequestValue(downstreamRequest, 1175 downstreamClientAddress, downstreamClientSecure, clientIdentity, 1176 clientName, clientSessionID, clientRequestID); 1177 } 1178 1179 1180 1181 /** 1182 * Retrieves the lines that comprise the complete audit log message. 1183 * 1184 * @return The lines that comprise the complete audit log message. 1185 */ 1186 @NotNull() 1187 public final List<String> getLogMessageLines() 1188 { 1189 return logMessageLines; 1190 } 1191 1192 1193 1194 /** 1195 * Retrieves the line that comprises the header for this log message, 1196 * including the leading octothorpe (#) and space that make it a comment. 1197 * 1198 * @return The line that comprises the header for this log message, including 1199 * the leading octothorpe (#) and space that make it a comment. 1200 */ 1201 @NotNull() 1202 public final String getCommentedHeaderLine() 1203 { 1204 return commentedHeaderLine; 1205 } 1206 1207 1208 1209 /** 1210 * Retrieves the line that comprises the header for this log message, without 1211 * the leading octothorpe (#) and space that make it a comment. 1212 * 1213 * @return The line that comprises the header for this log message, without 1214 * the leading octothorpe (#) and space that make it a comment. 1215 */ 1216 @NotNull() 1217 public final String getUncommentedHeaderLine() 1218 { 1219 return uncommentedHeaderLine; 1220 } 1221 1222 1223 1224 /** 1225 * Retrieves the timestamp for this audit log message. 1226 * 1227 * @return The timestamp for this audit log message. 1228 */ 1229 @NotNull() 1230 public final Date getTimestamp() 1231 { 1232 return timestamp; 1233 } 1234 1235 1236 1237 /** 1238 * Retrieves a map of the name-value pairs contained in the header for this 1239 * log message. 1240 * 1241 * @return A map of the name-value pairs contained in the header for this log 1242 * message. 1243 */ 1244 @NotNull() 1245 public final Map<String,String> getHeaderNamedValues() 1246 { 1247 return namedValues; 1248 } 1249 1250 1251 1252 /** 1253 * Retrieves the server product name for this audit log message, if available. 1254 * 1255 * @return The server product name for this audit log message, or 1256 * {@code null} if it is not available. 1257 */ 1258 @Nullable() 1259 public final String getProductName() 1260 { 1261 return productName; 1262 } 1263 1264 1265 1266 /** 1267 * Retrieves the server instance name for this audit log message, if 1268 * available. 1269 * 1270 * @return The server instance name for this audit log message, or 1271 * {@code null} if it is not available. 1272 */ 1273 @Nullable() 1274 public final String getInstanceName() 1275 { 1276 return instanceName; 1277 } 1278 1279 1280 1281 /** 1282 * Retrieves the unique identifier generated when the server was started, if 1283 * available. 1284 * 1285 * @return The unique identifier generated when the server was started, or 1286 * {@code null} if it is not available. 1287 */ 1288 @Nullable() 1289 public final String getStartupID() 1290 { 1291 return startupID; 1292 } 1293 1294 1295 1296 /** 1297 * Retrieves the identifier for the server thread that processed the change, 1298 * if available. 1299 * 1300 * @return The identifier for the server thread that processed the change, or 1301 * {@code null} if it is not available. 1302 */ 1303 @Nullable() 1304 public final Long getThreadID() 1305 { 1306 return threadID; 1307 } 1308 1309 1310 1311 /** 1312 * Retrieves the DN of the user that requested the change, if available. 1313 * 1314 * @return The DN of the user that requested the change, or {@code null} if 1315 * it is not available. 1316 */ 1317 @Nullable() 1318 public final String getRequesterDN() 1319 { 1320 return requesterDN; 1321 } 1322 1323 1324 1325 /** 1326 * Retrieves the IP address of the client that requested the change, if 1327 * available. 1328 * 1329 * @return The IP address of the client that requested the change, or 1330 * {@code null} if it is not available. 1331 */ 1332 @Nullable() 1333 public final String getRequesterIPAddress() 1334 { 1335 return requesterIP; 1336 } 1337 1338 1339 1340 /** 1341 * Retrieves the connection ID for the connection on which the change was 1342 * requested, if available. 1343 * 1344 * @return The connection ID for the connection on which the change was 1345 * requested, or {@code null} if it is not available. 1346 */ 1347 @Nullable() 1348 public final Long getConnectionID() 1349 { 1350 return connectionID; 1351 } 1352 1353 1354 1355 /** 1356 * Retrieves the connection ID for the connection on which the change was 1357 * requested, if available. 1358 * 1359 * @return The connection ID for the connection on which the change was 1360 * requested, or {@code null} if it is not available. 1361 */ 1362 @Nullable() 1363 public final Long getOperationID() 1364 { 1365 return operationID; 1366 } 1367 1368 1369 1370 /** 1371 * Retrieves the connection ID for the external operation that triggered the 1372 * internal operation with which this audit log message is associated, if 1373 * available. 1374 * 1375 * @return The connection ID for the external operation that triggered the 1376 * internal operation with which this audit log message is 1377 * associated, or {@code null} if it is not available. 1378 */ 1379 @Nullable() 1380 public final Long getTriggeredByConnectionID() 1381 { 1382 return triggeredByConnectionID; 1383 } 1384 1385 1386 1387 /** 1388 * Retrieves the operation ID for the external operation that triggered the 1389 * internal operation with which this audit log message is associated, if 1390 * available. 1391 * 1392 * @return The operation ID for the external operation that triggered the 1393 * internal operation with which this audit log message is 1394 * associated, or {@code null} if it is not available. 1395 */ 1396 @Nullable() 1397 public final Long getTriggeredByOperationID() 1398 { 1399 return triggeredByOperationID; 1400 } 1401 1402 1403 1404 /** 1405 * Retrieves the replication change ID for this audit log message, if 1406 * available. 1407 * 1408 * @return The replication change ID for this audit log message, or 1409 * {@code null} if it is not available. 1410 */ 1411 @Nullable() 1412 public final String getReplicationChangeID() 1413 { 1414 return replicationChangeID; 1415 } 1416 1417 1418 1419 /** 1420 * Retrieves the alternate authorization DN for this audit log message, if 1421 * available. 1422 * 1423 * @return The alternate authorization DN for this audit log message, or 1424 * {@code null} if it is not available. 1425 */ 1426 @Nullable() 1427 public final String getAlternateAuthorizationDN() 1428 { 1429 return alternateAuthorizationDN; 1430 } 1431 1432 1433 1434 /** 1435 * Retrieves the transaction ID for this audit log message, if available. 1436 * 1437 * @return The transaction ID for this audit log message, or {@code null} if 1438 * it is not available. 1439 */ 1440 @Nullable() 1441 public final String getTransactionID() 1442 { 1443 return transactionID; 1444 } 1445 1446 1447 1448 /** 1449 * Retrieves the origin for this audit log message, if available. 1450 * 1451 * @return The origin for this audit log message, or {@code null} if it is 1452 * not available. 1453 */ 1454 @Nullable() 1455 public final String getOrigin() 1456 { 1457 return origin; 1458 } 1459 1460 1461 1462 /** 1463 * Retrieves the value of the flag indicating whether the associated operation 1464 * was processed using an administrative session worker thread, if available. 1465 * 1466 * @return {@code Boolean.TRUE} if it is known that the associated operation 1467 * was processed using an administrative session worker thread, 1468 * {@code Boolean.FALSE} if it is known that the associated operation 1469 * was not processed using an administrative session worker thread, 1470 * or {@code null} if it is not available. 1471 */ 1472 @Nullable() 1473 public final Boolean getUsingAdminSessionWorkerThread() 1474 { 1475 return usingAdminSessionWorkerThread; 1476 } 1477 1478 1479 1480 /** 1481 * Retrieves a list of the OIDs of the request controls included in the 1482 * operation request, if available. 1483 * 1484 * @return A list of the OIDs of the request controls included in the 1485 * operation, an empty list if it is known that there were no request 1486 * controls, or {@code null} if it is not available. 1487 */ 1488 @Nullable() 1489 public final List<String> getRequestControlOIDs() 1490 { 1491 return requestControlOIDs; 1492 } 1493 1494 1495 1496 /** 1497 * Retrieves an operation purpose request control with information about the 1498 * purpose for the associated operation, if available. 1499 * 1500 * @return An operation purpose request control with information about the 1501 * purpose for the associated operation, or {@code null} if it is not 1502 * available. 1503 */ 1504 @Nullable() 1505 public final OperationPurposeRequestControl 1506 getOperationPurposeRequestControl() 1507 { 1508 return operationPurposeRequestControl; 1509 } 1510 1511 1512 1513 /** 1514 * Retrieves an intermediate client request control with information about the 1515 * downstream processing for the associated operation, if available. 1516 * 1517 * @return An intermediate client request control with information about the 1518 * downstream processing for the associated operation, or 1519 * {@code null} if it is not available. 1520 */ 1521 @Nullable() 1522 public final IntermediateClientRequestControl 1523 getIntermediateClientRequestControl() 1524 { 1525 return intermediateClientRequestControl; 1526 } 1527 1528 1529 1530 /** 1531 * Retrieves the DN of the entry targeted by the associated operation. 1532 * 1533 * @return The DN of the entry targeted by the associated operation. 1534 */ 1535 @NotNull() 1536 public abstract String getDN(); 1537 1538 1539 1540 /** 1541 * Retrieves the change type for this audit log message. 1542 * 1543 * @return The change type for this audit log message. 1544 */ 1545 @NotNull() 1546 public abstract ChangeType getChangeType(); 1547 1548 1549 1550 /** 1551 * Retrieves an LDIF change record that encapsulates the change represented by 1552 * this audit log message. 1553 * 1554 * @return An LDIF change record that encapsulates the change represented by 1555 * this audit log message. 1556 */ 1557 @NotNull() 1558 public abstract LDIFChangeRecord getChangeRecord(); 1559 1560 1561 1562 /** 1563 * Indicates whether it is possible to use the 1564 * {@link #getRevertChangeRecords()} method to obtain a list of LDIF change 1565 * records that can be used to revert the changes described by this audit log 1566 * message. 1567 * 1568 * @return {@code true} if it is possible to use the 1569 * {@link #getRevertChangeRecords()} method to obtain a list of LDIF 1570 * change records that can be used to revert the changes described 1571 * by this audit log message, or {@code false} if not. 1572 */ 1573 public abstract boolean isRevertible(); 1574 1575 1576 1577 /** 1578 * Retrieves a list of the change records that can be used to revert the 1579 * changes described by this audit log message. 1580 * 1581 * @return A list of the change records that can be used to revert the 1582 * changes described by this audit log message. 1583 * 1584 * @throws AuditLogException If this audit log message cannot be reverted. 1585 */ 1586 @NotNull() 1587 public abstract List<LDIFChangeRecord> getRevertChangeRecords() 1588 throws AuditLogException; 1589 1590 1591 1592 /** 1593 * Retrieves a single-line string representation of this audit log message. 1594 * It will start with the string returned by 1595 * {@link #getUncommentedHeaderLine()}, but will also contain additional 1596 * name-value pairs that are pertinent to the type of operation that the audit 1597 * log message represents. 1598 * 1599 * @return A string representation of this audit log message. 1600 */ 1601 @Override() 1602 @NotNull() 1603 public final String toString() 1604 { 1605 final StringBuilder buffer = new StringBuilder(); 1606 toString(buffer); 1607 return buffer.toString(); 1608 } 1609 1610 1611 1612 /** 1613 * Appends a single-line string representation of this audit log message to 1614 * the provided buffer. The message will start with the string returned by 1615 * {@link #getUncommentedHeaderLine()}, but will also contain additional 1616 * name-value pairs that are pertinent to the type of operation that the audit 1617 * log message represents. 1618 * 1619 * @param buffer The buffer to which the information should be appended. 1620 */ 1621 public abstract void toString(@NotNull StringBuilder buffer); 1622 1623 1624 1625 /** 1626 * Retrieves a multi-line string representation of this audit log message. It 1627 * will simply be a concatenation of all of the lines that comprise the 1628 * complete log message, with line breaks between them. 1629 * 1630 * @return A multi-line string representation of this audit log message. 1631 */ 1632 @NotNull() 1633 public final String toMultiLineString() 1634 { 1635 return StaticUtils.concatenateStrings(null, null, StaticUtils.EOL, null, 1636 null, logMessageLines); 1637 } 1638}