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
021import org.apache.juneau.collections.*;
022
023/**
024 * Utility class for reading log files.
025 *
026 * <p>
027 * Provides the capability of returning splices of log files based on dates and filtering based on thread and logger
028 * names.
029 */
030public final class LogParser implements Iterable<LogParser.Entry>, Iterator<LogParser.Entry>, Closeable {
031   private BufferedReader br;
032   LogEntryFormatter formatter;
033   Date start, end;
034   Set<String> loggerFilter, severityFilter;
035   String threadFilter;
036   private Entry next;
037
038   /**
039    * Constructor.
040    *
041    * @param formatter The log entry formatter.
042    * @param f The log file.
043    * @param start Don't return rows before this date.  If <jk>null</jk>, start from the beginning of the file.
044    * @param end Don't return rows after this date.  If <jk>null</jk>, go to the end of the file.
045    * @param thread Only return log entries with this thread name.
046    * @param loggers Only return log entries produced by these loggers (simple class names).
047    * @param severity Only return log entries with the specified severity.
048    * @throws IOException Thrown by underlying stream.
049    */
050   public LogParser(LogEntryFormatter formatter, File f, Date start, Date end, String thread, String[] loggers, String[] severity) throws IOException {
051      br = new BufferedReader(new InputStreamReader(new FileInputStream(f), Charset.defaultCharset()));
052      this.formatter = formatter;
053      this.start = start;
054      this.end = end;
055      this.threadFilter = thread;
056      if (loggers != null)
057         this.loggerFilter = ASet.of(loggers);
058      if (severity != null)
059         this.severityFilter = ASet.of(severity);
060
061      // Find the first line.
062      String line;
063      while (next == null && (line = br.readLine()) != null) {
064         Entry e = new Entry(line);
065         if (e.matches())
066            next = e;
067      }
068   }
069
070   @Override /* Iterator */
071   public boolean hasNext() {
072      return next != null;
073   }
074
075   @Override /* Iterator */
076   public Entry next() {
077      Entry current = next;
078      Entry prev = next;
079      try {
080         next = null;
081         String line = null;
082         while (next == null && (line = br.readLine()) != null) {
083            Entry e = new Entry(line);
084            if (e.isRecord) {
085               if (e.matches())
086                  next = e;
087               prev = null;
088            } else {
089               if (prev != null)
090                  prev.addText(e.line);
091            }
092         }
093      } catch (IOException e) {
094         throw new RuntimeException(e);
095      }
096      return current;
097   }
098
099   @Override /* Iterator */
100   public void remove() {
101      throw new NoSuchMethodError();
102   }
103
104   @Override /* Iterable */
105   public Iterator<Entry> iterator() {
106      return this;
107   }
108
109   @Override /* Closeable */
110   public void close() throws IOException {
111      br.close();
112   }
113
114   /**
115    * Serializes the contents of the parsed log file to the specified writer and then closes the underlying reader.
116    *
117    * @param w The writer to write the log file to.
118    * @throws IOException Thrown by underlying stream.
119    */
120   public void writeTo(Writer w) throws IOException {
121      try {
122         if (! hasNext())
123            w.append("[EMPTY]");
124         else for (LogParser.Entry le : this)
125            le.append(w);
126      } finally {
127         close();
128      }
129   }
130
131   /**
132    * Represents a single line from the log file.
133    */
134   @SuppressWarnings("javadoc")
135   public final class Entry {
136      public Date date;
137      public String severity, logger;
138      protected String line, text;
139      protected String thread;
140      protected List<String> additionalText;
141      protected boolean isRecord;
142
143      Entry(String line) throws IOException {
144         try {
145            this.line = line;
146            Matcher m = formatter.getLogEntryPattern().matcher(line);
147            if (m.matches()) {
148               isRecord = true;
149               String s = formatter.getField("date", m);
150               if (s != null)
151                  date = formatter.getDateFormat().parse(s);
152               thread = formatter.getField("thread", m);
153               severity = formatter.getField("level", m);
154               logger = formatter.getField("logger", m);
155               text = formatter.getField("msg", m);
156               if (logger != null && logger.indexOf('.') > -1)
157                  logger = logger.substring(logger.lastIndexOf('.')+1);
158            }
159         } catch (ParseException e) {
160            throw new IOException(e);
161         }
162      }
163
164      void addText(String t) {
165         if (additionalText == null)
166            additionalText = new LinkedList<>();
167         additionalText.add(t);
168      }
169
170      public String getText() {
171         if (additionalText == null)
172            return text;
173         int i = text.length();
174         for (String s : additionalText)
175            i += s.length() + 1;
176         StringBuilder sb = new StringBuilder(i);
177         sb.append(text);
178         for (String s : additionalText)
179            sb.append('\n').append(s);
180         return sb.toString();
181      }
182
183      public String getThread() {
184         return thread;
185      }
186
187      public Writer appendHtml(Writer w) throws IOException {
188         w.append(toHtml(line)).append("<br>");
189         if (additionalText != null)
190            for (String t : additionalText)
191               w.append(toHtml(t)).append("<br>");
192         return w;
193      }
194
195      protected Writer append(Writer w) throws IOException {
196         w.append(line).append('\n');
197         if (additionalText != null)
198            for (String t : additionalText)
199               w.append(t).append('\n');
200         return w;
201      }
202
203      boolean matches() {
204         if (! isRecord)
205            return false;
206         if (start != null && date.before(start))
207            return false;
208         if (end != null && date.after(end))
209            return false;
210         if (threadFilter != null && ! threadFilter.equals(thread))
211            return false;
212         if (loggerFilter != null && ! loggerFilter.contains(logger))
213            return false;
214         if (severityFilter != null && ! severityFilter.contains(severity))
215            return false;
216         return true;
217      }
218   }
219
220   static final String toHtml(String s) {
221      if (s.indexOf('<') != -1)
222         return s.replaceAll("<", "&lt;");//$NON-NLS-2$
223      return s;
224   }
225}
226