001/*
002 * Copyright 2018-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2018-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2018-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.unboundidds.logs;
037
038
039
040import java.io.BufferedReader;
041import java.io.Closeable;
042import java.io.File;
043import java.io.FileReader;
044import java.io.InputStream;
045import java.io.IOException;
046import java.io.InputStreamReader;
047import java.io.Reader;
048import java.util.ArrayList;
049import java.util.List;
050
051import com.unboundid.ldif.LDIFAddChangeRecord;
052import com.unboundid.ldif.LDIFChangeRecord;
053import com.unboundid.ldif.LDIFDeleteChangeRecord;
054import com.unboundid.ldif.LDIFModifyChangeRecord;
055import com.unboundid.ldif.LDIFModifyDNChangeRecord;
056import com.unboundid.ldif.LDIFReader;
057import com.unboundid.util.Debug;
058import com.unboundid.util.NotMutable;
059import com.unboundid.util.NotNull;
060import com.unboundid.util.Nullable;
061import com.unboundid.util.StaticUtils;
062import com.unboundid.util.ThreadSafety;
063import com.unboundid.util.ThreadSafetyLevel;
064
065import static com.unboundid.ldap.sdk.unboundidds.logs.LogMessages.*;
066
067
068
069/**
070 * This class provides a mechanism for reading messages from a Directory Server
071 * audit log.
072 * <BR>
073 * <BLOCKQUOTE>
074 *   <B>NOTE:</B>  This class, and other classes within the
075 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
076 *   supported for use against Ping Identity, UnboundID, and
077 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
078 *   for proprietary functionality or for external specifications that are not
079 *   considered stable or mature enough to be guaranteed to work in an
080 *   interoperable way with other types of LDAP servers.
081 * </BLOCKQUOTE>
082 */
083@NotMutable()
084@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
085public final class AuditLogReader
086       implements Closeable
087{
088  // The reader used to read the contents of the log file.
089  @NotNull private final BufferedReader reader;
090
091
092
093  /**
094   * Creates a new audit log reader that will read messages from the specified
095   * log file.
096   *
097   * @param  path  The path of the log file to read.
098   *
099   * @throws  IOException  If a problem occurs while opening the file for
100   *                       reading.
101   */
102  public AuditLogReader(@NotNull final String path)
103         throws IOException
104  {
105    reader = new BufferedReader(new FileReader(path));
106  }
107
108
109
110  /**
111   * Creates a new audit log reader that will read messages from the specified
112   * log file.
113   *
114   * @param  file  The log file to read.
115   *
116   * @throws  IOException  If a problem occurs while opening the file for
117   *                       reading.
118   */
119  public AuditLogReader(@NotNull final File file)
120         throws IOException
121  {
122    reader = new BufferedReader(new FileReader(file));
123  }
124
125
126
127  /**
128   * Creates a new audit log reader that will read messages using the provided
129   * {@code Reader} object.
130   *
131   * @param  reader  The reader to use to read log messages.
132   */
133  public AuditLogReader(@NotNull final Reader reader)
134  {
135    if (reader instanceof BufferedReader)
136    {
137      this.reader = (BufferedReader) reader;
138    }
139    else
140    {
141      this.reader = new BufferedReader(reader);
142    }
143  }
144
145
146
147  /**
148   * Creates a new audit log reader that will read messages from the provided
149   * input stream.
150   *
151   * @param  inputStream  The input stream from which to read log messages.
152   */
153  public AuditLogReader(@NotNull final InputStream inputStream)
154  {
155    reader = new BufferedReader(new InputStreamReader(inputStream));
156  }
157
158
159
160  /**
161   * Reads the next audit log message from the log file.
162   *
163   * @return  The audit log message read from the log file, or {@code null} if
164   *          there are no more messages to be read.
165   *
166   * @throws  IOException  If an error occurs while trying to read from the
167   *                       file.
168   *
169   * @throws  AuditLogException  If an error occurs while trying to parse the
170   *                             log message.
171   */
172  @Nullable()
173  public AuditLogMessage read()
174         throws IOException, AuditLogException
175  {
176    // Read a list of lines until we find the end of the file or a blank line
177    // after a series of non-blank lines.
178    final List<String> fullMessageLines = new ArrayList<>(20);
179    final List<String> nonCommentLines = new ArrayList<>(20);
180    while (true)
181    {
182      final String line = reader.readLine();
183      if (line == null)
184      {
185        // We hit the end of the audit log file.  We obviously can't read any
186        // more.
187        break;
188      }
189
190      if (line.isEmpty())
191      {
192        if (nonCommentLines.isEmpty())
193        {
194          // This means that we encountered consecutive blank lines, or blank
195          // lines with only comments between them.  This is okay.  We'll just
196          // clear the list of full message lines and keep reading.
197          fullMessageLines.clear();
198          continue;
199        }
200        else
201        {
202          // We found a blank line after some non-blank lines that included at
203          // least one non-comment line.  Break out of the loop and process what
204          // we read as an audit log message.
205          break;
206        }
207      }
208      else
209      {
210        // We read a non-empty line.  Add it to the list of full message lines,
211        // and if it's not a comment, then add it to the list of non-comment
212        // lines.
213        fullMessageLines.add(line);
214        if (! line.startsWith("#"))
215        {
216          nonCommentLines.add(line);
217        }
218      }
219    }
220
221
222    // If we've gotten here and the list of non-comment lines is empty, then
223    // that must mean that we hit the end of the audit log without finding any
224    // more messages.  In that case, return null to indicate that we've hit the
225    // end of the file.
226    if (nonCommentLines.isEmpty())
227    {
228      return null;
229    }
230
231
232    // Try to parse the set of non-comment lines as an LDIF change record.  If
233    // that fails, then throw a log exception.
234    final LDIFChangeRecord changeRecord;
235    try
236    {
237      final String[] ldifLines =
238           StaticUtils.toArray(nonCommentLines, String.class);
239      changeRecord = LDIFReader.decodeChangeRecord(ldifLines);
240    }
241    catch (final Exception e)
242    {
243      Debug.debugException(e);
244
245      final String concatenatedLogLines = StaticUtils.concatenateStrings(
246           "[ ", "\"", ", ", "\"", " ]", fullMessageLines);
247      throw new AuditLogException(fullMessageLines,
248           ERR_AUDIT_LOG_READER_CANNOT_PARSE_CHANGE_RECORD.get(
249                concatenatedLogLines, StaticUtils.getExceptionMessage(e)),
250           e);
251    }
252
253
254    // Create the appropriate type of audit log message based on the change
255    // record.
256    if (changeRecord instanceof LDIFAddChangeRecord)
257    {
258      return new AddAuditLogMessage(fullMessageLines,
259           (LDIFAddChangeRecord) changeRecord);
260    }
261    else if (changeRecord instanceof LDIFDeleteChangeRecord)
262    {
263      return new DeleteAuditLogMessage(fullMessageLines,
264           (LDIFDeleteChangeRecord) changeRecord);
265    }
266    else if (changeRecord instanceof LDIFModifyChangeRecord)
267    {
268      return new ModifyAuditLogMessage(fullMessageLines,
269           (LDIFModifyChangeRecord) changeRecord);
270    }
271    else if (changeRecord instanceof LDIFModifyDNChangeRecord)
272    {
273      return new ModifyDNAuditLogMessage(fullMessageLines,
274           (LDIFModifyDNChangeRecord) changeRecord);
275    }
276    else
277    {
278      // This should never happen.
279      final String concatenatedLogLines = StaticUtils.concatenateStrings(
280           "[ ", "\"", ", ", "\"", " ]", fullMessageLines);
281      throw new AuditLogException(fullMessageLines,
282           ERR_AUDIT_LOG_READER_UNSUPPORTED_CHANGE_RECORD.get(
283                concatenatedLogLines, changeRecord.getChangeType().getName()));
284    }
285  }
286
287
288
289  /**
290   * Closes this error log reader.
291   *
292   * @throws  IOException  If a problem occurs while closing the reader.
293   */
294  @Override()
295  public void close()
296         throws IOException
297  {
298    reader.close();
299  }
300}