/*
* Lilith - a log event viewer.
* Copyright (C) 2007-2014 Joern Huxhorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright 2007-2014 Joern Huxhorn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.huxhorn.lilith.data.logging;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ThrowableInfoParser
{
private static final String NEWLINE = "\n";
private static final String CARRIAGE_RETURN = "\r";
private static final String CLASS_MESSAGE_SEPARATOR = ": ";
private static final Pattern OMITTED_PATTERN = Pattern.compile("^(\t+)\\.\\.\\. (\\d+) more");
private static final Pattern AT_PATTERN = Pattern.compile("^(\t+)at (.+)");
private static final Pattern MESSAGE_MATCHER = Pattern.compile("^(\t*)?("+ThrowableInfo.CAUSED_BY_PREFIX+"|"+ThrowableInfo.SUPPRESSED_PREFIX+")?(.*)");
public static ThrowableInfo parse(String throwableInfoString)
{
if(throwableInfoString == null)
{
return null;
}
return parse(splitLines(throwableInfoString));
}
public static ThrowableInfo parse(List<String> throwableInfoLines)
{
if(throwableInfoLines == null)
{
return null;
}
if(throwableInfoLines.isEmpty())
{
return null;
}
return parse(throwableInfoLines, /*startIndex=*/0, /*indent=*/0).throwableInfo;
}
private static class ThrowableInfoParseResult
{
public ThrowableInfo throwableInfo;
public int endIndex;
private ThrowableInfoParseResult(ThrowableInfo throwableInfo, int endIndex)
{
this.throwableInfo = throwableInfo;
this.endIndex = endIndex;
}
}
private static ThrowableInfoParseResult parse(List<String> throwableInfoLines, int startIndex, int indent)
{
// sorry, future huxi
final int lineCount = throwableInfoLines.size();
String name=null;
StringBuilder message = null;
List<ExtendedStackTraceElement> stackTraceElements = null;
int omittedElements = 0;
ThrowableInfo cause = null;
List<ThrowableInfo> suppressedInfos = null;
int index = startIndex;
for(; index<lineCount; index++)
{
String currentLine = throwableInfoLines.get(index);
Matcher atMatcher = atMatcher(currentLine);
if(atMatcher.matches())
{
String indentString = atMatcher.group(1);
if(indentString.length() != indent + 1)
{
// we reached wrong nesting...
break;
}
String steString = atMatcher.group(2);
ExtendedStackTraceElement este = ExtendedStackTraceElement.parseStackTraceElement(steString);
if(este != null)
{
if(stackTraceElements == null)
{
stackTraceElements = new ArrayList<>();
}
stackTraceElements.add(este);
}
continue;
}
Matcher omittedMatcher = omittedMatcher(currentLine);
if(omittedMatcher.matches())
{
String indentString = omittedMatcher.group(1);
if(indentString.length() != indent + 1)
{
// we reached wrong nesting...
break;
}
omittedElements = Integer.parseInt(omittedMatcher.group(2));
continue;
}
Matcher messageMatcher = messageMatcher(currentLine); // will always match...
if(messageMatcher.matches())
{
String indentString = messageMatcher.group(1);
String type = messageMatcher.group(2); // either CAUSED_BY_PREFIX, SUPPRESSED_PREFIX or null
String remainder = messageMatcher.group(3); // remainder of the String
if(ThrowableInfo.CAUSED_BY_PREFIX.equals(type))
{
if(index != startIndex)
{
if(indentString.length() != indent)
{
// we reached wrong nesting...
break;
}
ThrowableInfoParseResult parsed = parse(throwableInfoLines, index, indent);
index = parsed.endIndex - 1;
if(parsed.throwableInfo != null)
{
cause = parsed.throwableInfo;
}
continue;
}
}
else if(ThrowableInfo.SUPPRESSED_PREFIX.equals(type))
{
if(index != startIndex)
{
if(indentString.length() != indent + 1)
{
// we reached wrong nesting...
break;
}
ThrowableInfoParseResult parsed = parse(throwableInfoLines, index, indent + 1);
index = parsed.endIndex - 1;
if(parsed.throwableInfo != null)
{
if(suppressedInfos == null)
{
suppressedInfos = new ArrayList<>();
}
suppressedInfos.add(parsed.throwableInfo);
}
continue;
}
}
if(message == null)
{
// first line
int colonIndex = remainder.indexOf(CLASS_MESSAGE_SEPARATOR);
if(colonIndex > -1)
{
name = remainder.substring(0, colonIndex);
message = new StringBuilder();
message.append(remainder.substring(colonIndex + CLASS_MESSAGE_SEPARATOR.length()));
}
else
{
name = remainder;
}
}
else
{
message.append(NEWLINE);
if(indentString != null)
{
message.append(indentString);
}
message.append(remainder);
}
}
else
{
System.out.println("What? "+currentLine);
}
}
ThrowableInfo throwableInfo = null;
if
(
name != null ||
message != null ||
stackTraceElements != null ||
omittedElements != 0 ||
cause != null ||
suppressedInfos != null
)
{
// we found *any* info...
throwableInfo = new ThrowableInfo();
throwableInfo.setName(name);
if(message != null)
{
throwableInfo.setMessage(message.toString());
}
if(stackTraceElements != null)
{
throwableInfo.setStackTrace(stackTraceElements.toArray(new ExtendedStackTraceElement[stackTraceElements.size()]));
}
throwableInfo.setOmittedElements(omittedElements);
throwableInfo.setCause(cause);
if(suppressedInfos != null)
{
throwableInfo.setSuppressed(suppressedInfos.toArray(new ThrowableInfo[suppressedInfos.size()]));
}
}
return new ThrowableInfoParseResult(throwableInfo, index);
}
static List<String> splitLines(String input)
{
if(input == null)
{
return null;
}
StringTokenizer tok = new StringTokenizer(input, NEWLINE+CARRIAGE_RETURN, true);
List<String> lines = new ArrayList<>();
boolean foundAnything=false;
boolean hadContent=false;
while(tok.hasMoreTokens())
{
foundAnything = true;
String current = tok.nextToken();
if(NEWLINE.equals(current))
{
if(hadContent)
{
hadContent=false;
}
else
{
// support empty lines
lines.add("");
}
continue;
}
if(CARRIAGE_RETURN.equals(current))
{
// just ignore carriage returns
continue;
}
lines.add(current);
hadContent=true;
}
if(!foundAnything)
{
// an empty string should result in a single empty line.
lines.add("");
}
return lines;
}
/**
* group(1) is mandatory indent (1..n \t characters)
* group(2) is the number of omitted elements.
*
* @param input inputString
* @return atMatcher
*/
static Matcher omittedMatcher(String input)
{
return OMITTED_PATTERN.matcher(input);
}
/**
* group(1) is mandatory indent (1..n \t characters)
* group(2) is remainder, i.e. the StackTraceElement
*
* @param input inputString
* @return atMatcher
*/
static Matcher atMatcher(String input)
{
return AT_PATTERN.matcher(input);
}
/**
* group(1) is optional indent (0..n \t characters)
* group(2) is optional prefix, i.e. either "Caused by: ", "Suppressed: " or null.
* group(3) is the remainder
*
* @param input inputString
* @return messageMatcher
*/
static Matcher messageMatcher(String input)
{
return MESSAGE_MATCHER.matcher(input);
}
}