View Javadoc

1   package com.github.triceo.splitlog.splitters.exceptions;
2   
3   import java.util.Arrays;
4   import java.util.Collection;
5   import java.util.Collections;
6   import java.util.LinkedList;
7   import java.util.List;
8   import java.util.Queue;
9   
10  /**
11   * A finite automaton for recognizing a Java exception stack trace spanning
12   * multiple lines in a random text.
13   *
14   * Each line has a type. When a line is recognized, a new state is reached. Each
15   * line type can only be followed by lines of specific types, together forming a
16   * transition function. When the next line is not one of these types, that
17   * transition is not in the transition function and the parsing is terminated
18   * with an exception. After the last line has been read, if the automaton is not
19   * in one of the accepting states, the parsing is terminated with an exception.
20   */
21  final class ExceptionParser {
22  
23      /**
24       * Various kinds of states for the parser automaton.
25       */
26      private static enum LineType {
27          CAUSE(true, false), POST_END(false, true), PRE_START(true, false), STACK_TRACE(false, true), STACK_TRACE_END(
28                  false, true), SUB_CAUSE(false, false);
29  
30          private final boolean mayBeFirstLine, mayBeLastLine;
31          private ExceptionLineParser<?> parser;
32  
33          LineType(final boolean mayBeginWith, final boolean mayEndWith) {
34              this.mayBeFirstLine = mayBeginWith;
35              this.mayBeLastLine = mayEndWith;
36          }
37  
38          private ExceptionLineParser<?> determineParser() {
39              switch (this) {
40                  case PRE_START:
41                  case POST_END:
42                      return null;
43                  case CAUSE:
44                      return new CauseParser();
45                  case STACK_TRACE:
46                      return new StackTraceParser();
47                  case STACK_TRACE_END:
48                      return new StackTraceEndParser();
49                  case SUB_CAUSE:
50                      return new SubCauseParser();
51              }
52              throw new IllegalStateException("This can never happen.");
53          }
54  
55          /**
56           * Whether or not this type can be a start state.
57           */
58          public boolean isAcceptableAsFirstLine() {
59              return this.mayBeFirstLine;
60          }
61  
62          /**
63           * Whether or not this type can be an accepting state.
64           */
65          public boolean isAcceptableAsLastLine() {
66              return this.mayBeLastLine;
67          }
68  
69          /**
70           * Parse the line according to this state's parser.
71           *
72           * @param line
73           *            Line to parse.
74           * @return Parsed line.
75           * @throws ExceptionParseException
76           *             If the parser doesn't recognize the line.
77           */
78          public ExceptionLine parse(final String line) throws ExceptionParseException {
79              if ((this == LineType.POST_END) || (this == LineType.PRE_START)) {
80                  throw new IllegalStateException("No need to parse garbage lines.");
81              } else if (this.parser == null) {
82                  this.parser = this.determineParser();
83              }
84              return this.parser.parse(line);
85          }
86      }
87  
88      private static String greatestCommonPrefix(final String a, final String b) {
89          final int minLength = Math.min(a.length(), b.length());
90          for (int i = 0; i < minLength; i++) {
91              if (a.charAt(i) != b.charAt(i)) {
92                  return a.substring(0, i);
93              }
94          }
95          return a.substring(0, minLength);
96      }
97  
98      /**
99       * Identifies and removes the common prefix - the longest beginning
100      * substring that all the lines share.
101      *
102      * @param input
103      *            Lines to be evaluated.
104      * @return The same lines, without the prefix. And stripped of white space
105      *         at the ends.
106      */
107     private static Queue<String> removePrefix(final List<String> input) {
108         if (input.size() < 2) {
109             return new LinkedList<String>(input);
110         }
111         String resultingPrefix = "";
112         final String previousGreatestCommonPrefix = input.get(0).trim();
113         String greatestCommonPrefix = "";
114         for (int i = 1; i < input.size(); i++) {
115             final String previousLine = input.get(i - 1).trim();
116             final String currentLine = input.get(i).trim();
117             greatestCommonPrefix = ExceptionParser.greatestCommonPrefix(previousLine, currentLine);
118             resultingPrefix = ExceptionParser.greatestCommonPrefix(previousGreatestCommonPrefix, greatestCommonPrefix);
119             if (resultingPrefix.length() == 0) {
120                 break;
121             }
122         }
123         final int prefixLength = resultingPrefix.length();
124         final boolean hasPrefix = prefixLength > 0;
125         final Queue<String> result = new LinkedList<String>();
126         for (final String line : input) {
127             final String line2 = line.trim();
128             if (hasPrefix) {
129                 result.add(line2.substring(prefixLength));
130             } else {
131                 result.add(line2);
132             }
133         }
134         return result;
135     }
136 
137     private final Collection<ExceptionLine> parsedLines = new LinkedList<ExceptionLine>();
138 
139     /**
140      * Browsers through a random log and returns first exception stack trace it
141      * could find.
142      *
143      * @param input
144      *            Lines of the log. May begin and end with any garbage, may or
145      *            may not contain exception.
146      * @return Raw exception data.
147      * @throws ExceptionParseException
148      *             If parsing of the log file failed.
149      */
150     public synchronized Collection<ExceptionLine> parse(final Collection<String> input) throws ExceptionParseException {
151         this.parsedLines.clear();
152         final Queue<String> linesFromInput = ExceptionParser.removePrefix(new LinkedList<String>(input));
153         LineType previousLineType = LineType.PRE_START;
154         String currentLine = null;
155         boolean isFirstLine = true;
156         while (!linesFromInput.isEmpty()) {
157             currentLine = linesFromInput.poll();
158             previousLineType = this.parseLine(previousLineType, currentLine);
159             if (isFirstLine) {
160                 if (!previousLineType.isAcceptableAsFirstLine()) {
161                     throw new ExceptionParseException(currentLine, "Invalid line type detected at the beginning: "
162                             + previousLineType);
163                 }
164                 isFirstLine = false;
165             }
166         }
167         if (!previousLineType.isAcceptableAsLastLine()) {
168             throw new ExceptionParseException(currentLine, "Invalid line type detected at the end: " + previousLineType);
169         }
170         return Collections.unmodifiableCollection(new LinkedList<ExceptionLine>(this.parsedLines));
171     }
172 
173     /**
174      * Parse one line in the log.
175      *
176      * @param previousLineType
177      *            Type of the previous line in the log. (The state the automaton
178      *            is currently in.)
179      * @param line
180      *            Line in question.
181      * @return Identified type of this line.
182      * @throws ExceptionParseException
183      *             If parsing of the log file failed.
184      */
185     private LineType parseLine(final LineType previousLineType, final String line) throws ExceptionParseException {
186         switch (previousLineType) {
187             case PRE_START:
188                 return this.parseLine(line, LineType.CAUSE, LineType.PRE_START);
189             case CAUSE:
190             case SUB_CAUSE:
191                 return this.parseLine(line, LineType.STACK_TRACE);
192             case STACK_TRACE:
193                 return this.parseLine(line, LineType.STACK_TRACE, LineType.STACK_TRACE_END, LineType.SUB_CAUSE);
194             case STACK_TRACE_END:
195                 return this.parseLine(line, LineType.SUB_CAUSE, LineType.POST_END);
196             case POST_END:
197                 return this.parseLine(line, LineType.POST_END);
198             default:
199                 throw new IllegalArgumentException("Unsupported line type: " + previousLineType);
200         }
201     }
202 
203     /**
204      * Parse one line in the log, when knowing the types of lines acceptable at
205      * this point in the log.
206      *
207      * @param line
208      *            Line to parse.
209      * @param allowedLineTypes
210      *            Possible line types, in the order of evaluation. (Possible
211      *            transitions.) If evaluation matches for a type, transition
212      *            will be made and any subsequent types will be ignored.
213      * @return Identified type of this line.
214      * @throws ExceptionParseException
215      *             If no allowed types match.
216      */
217     private LineType parseLine(final String line, final LineType... allowedLineTypes) throws ExceptionParseException {
218         for (final LineType possibleType : allowedLineTypes) {
219             if ((possibleType == LineType.POST_END) || (possibleType == LineType.PRE_START)) {
220                 // this is garbage, all is accepted without parsing
221                 return possibleType;
222             }
223             final ExceptionLine parsedLine = possibleType.parse(line);
224             if (parsedLine != null) {
225                 this.parsedLines.add(parsedLine);
226                 return possibleType;
227             }
228         }
229         throw new ExceptionParseException(line, "Line not any of the expected types: "
230                 + Arrays.toString(allowedLineTypes));
231     }
232 
233 }