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