001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.microservice.resources;
014
015import java.io.*;
016import java.nio.charset.*;
017import java.text.*;
018import java.util.*;
019import java.util.regex.*;
020
021/**
022 * Utility class for reading log files.
023 *
024 * <p>
025 * Provides the capability of returning splices of log files based on dates and filtering based on thread and logger
026 * names.
027 */
028public final class LogParser implements Iterable<LogParser.Entry>, Iterator<LogParser.Entry>, Closeable {
029   private BufferedReader br;
030   LogEntryFormatter formatter;
031   Date start, end;
032   Set<String> loggerFilter, severityFilter;
033   String threadFilter;
034   private Entry next;
035
036   /**
037    * Constructor.
038    *
039    * @param formatter The log entry formatter.
040    * @param f The log file.
041    * @param start Don't return rows before this date.  If <jk>null</jk>, start from the beginning of the file.
042    * @param end Don't return rows after this date.  If <jk>null</jk>, go to the end of the file.
043    * @param thread Only return log entries with this thread name.
044    * @param loggers Only return log entries produced by these loggers (simple class names).
045    * @param severity Only return log entries with the specified severity.
046    * @throws IOException Thrown by underlying stream.
047    */
048   public LogParser(LogEntryFormatter formatter, File f, Date start, Date end, String thread, String[] loggers, String[] severity) throws IOException {
049      br = new BufferedReader(new InputStreamReader(new FileInputStream(f), Charset.defaultCharset()));
050      this.formatter = formatter;
051      this.start = start;
052      this.end = end;
053      this.threadFilter = thread;
054      if (loggers != null)
055         this.loggerFilter = new LinkedHashSet<>(Arrays.asList(loggers));
056      if (severity != null)
057         this.severityFilter = new LinkedHashSet<>(Arrays.asList(severity));
058
059      // Find the first line.
060      String line;
061      while (next == null && (line = br.readLine()) != null) {
062         Entry e = new Entry(line);
063         if (e.matches())
064            next = e;
065      }
066   }
067
068   @Override /* Iterator */
069   public boolean hasNext() {
070      return next != null;
071   }
072
073   @Override /* Iterator */
074   public Entry next() {
075      Entry current = next;
076      Entry prev = next;
077      try {
078         next = null;
079         String line = null;
080         while (next == null && (line = br.readLine()) != null) {
081            Entry e = new Entry(line);
082            if (e.isRecord) {
083               if (e.matches())
084                  next = e;
085               prev = null;
086            } else {
087               if (prev != null)
088                  prev.addText(e.line);
089            }
090         }
091      } catch (IOException e) {
092         throw new RuntimeException(e);
093      }
094      return current;
095   }
096
097   @Override /* Iterator */
098   public void remove() {
099      throw new NoSuchMethodError();
100   }
101
102   @Override /* Iterable */
103   public Iterator<Entry> iterator() {
104      return this;
105   }
106
107   @Override /* Closeable */
108   public void close() throws IOException {
109      br.close();
110   }
111
112   /**
113    * Serializes the contents of the parsed log file to the specified writer and then closes the underlying reader.
114    *
115    * @param w The writer to write the log file to.
116    * @throws IOException Thrown by underlying stream.
117    */
118   public void writeTo(Writer w) throws IOException {
119      try {
120         if (! hasNext())
121            w.append("[EMPTY]");
122         else for (LogParser.Entry le : this)
123            le.append(w);
124      } finally {
125         close();
126      }
127   }
128
129   /**
130    * Represents a single line from the log file.
131    */
132   @SuppressWarnings("javadoc")
133   public final class Entry {
134      public Date date;
135      public String severity, logger;
136      protected String line, text;
137      protected String thread;
138      protected List<String> additionalText;
139      protected boolean isRecord;
140
141      Entry(String line) throws IOException {
142         try {
143            this.line = line;
144            Matcher m = formatter.getLogEntryPattern().matcher(line);
145            if (m.matches()) {
146               isRecord = true;
147               String s = formatter.getField("date", m);
148               if (s != null)
149                  date = formatter.getDateFormat().parse(s);
150               thread = formatter.getField("thread", m);
151               severity = formatter.getField("level", m);
152               logger = formatter.getField("logger", m);
153               text = formatter.getField("msg", m);
154               if (logger != null && logger.indexOf('.') > -1)
155                  logger = logger.substring(logger.lastIndexOf('.')+1);
156            }
157         } catch (ParseException e) {
158            throw new IOException(e);
159         }
160      }
161
162      void addText(String t) {
163         if (additionalText == null)
164            additionalText = new LinkedList<>();
165         additionalText.add(t);
166      }
167
168      public String getText() {
169         if (additionalText == null)
170            return text;
171         int i = text.length();
172         for (String s : additionalText)
173            i += s.length() + 1;
174         StringBuilder sb = new StringBuilder(i);
175         sb.append(text);
176         for (String s : additionalText)
177            sb.append('\n').append(s);
178         return sb.toString();
179      }
180
181      public String getThread() {
182         return thread;
183      }
184
185      public Writer appendHtml(Writer w) throws IOException {
186         w.append(toHtml(line)).append("<br>");
187         if (additionalText != null)
188            for (String t : additionalText)
189               w.append(toHtml(t)).append("<br>");
190         return w;
191      }
192
193      protected Writer append(Writer w) throws IOException {
194         w.append(line).append('\n');
195         if (additionalText != null)
196            for (String t : additionalText)
197               w.append(t).append('\n');
198         return w;
199      }
200
201      boolean matches() {
202         if (! isRecord)
203            return false;
204         if (start != null && date.before(start))
205            return false;
206         if (end != null && date.after(end))
207            return false;
208         if (threadFilter != null && ! threadFilter.equals(thread))
209            return false;
210         if (loggerFilter != null && ! loggerFilter.contains(logger))
211            return false;
212         if (severityFilter != null && ! severityFilter.contains(severity))
213            return false;
214         return true;
215      }
216   }
217
218   static final String toHtml(String s) {
219      if (s.indexOf('<') != -1)
220         return s.replaceAll("<", "&lt;");//$NON-NLS-2$
221      return s;
222   }
223}
224