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