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