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