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}