blob: da2413eccb62f7fd1cc5305b72278471f6e35486 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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 com.android.tools.idea.logcat;
import com.android.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.intellij.openapi.util.text.StringUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* When printing out exceptions, Java collapses frames that match those of the enclosing exception, and just says "... N more".
* This class receives parses a sequence of lines from logcat, and maintains a knowledge of the current stack trace.
* If it ever sees the pattern "... N more", it then tries to see if that can be fully expanded with the correct frames
* from the enclosing exception. The logcat view then folds these frames back and displays "...N more", except now users can
* unfold it to view the full trace.
*
* @see <a href="http://docs.oracle.com/javase/7/docs/api/java/lang/Throwable.html#printStackTrace%28%29">Description in
* Throwable.printStackTrace</a>
*/
public class StackTraceExpander {
private final String myContinuationPrefix;
private final String myStackTracePrefix;
private final String myExpandedStackTracePrefix;
private final String myCauseLinePrefix;
public StackTraceExpander(String continuationLinePrefix, String stackTraceLinePrefix, String expandedStackTracePrefix,
String stackTraceCauseLinePrefix) {
myContinuationPrefix = continuationLinePrefix;
myStackTracePrefix = stackTraceLinePrefix;
myExpandedStackTracePrefix = expandedStackTracePrefix;
myCauseLinePrefix = stackTraceCauseLinePrefix;
}
/** Regex to match a stack trace line. E.g.: "at com.foo.Class.method(FileName.extension:10)" */
private static final Pattern EXCEPTION_LINE_PATTERN = Pattern.compile("^at .*(.*)$");
/** Regex to match an the excluded frames line i.e. line of form "... N more" */
private static final Pattern ELIDED_LINE_PATTERN = Pattern.compile("^... (\\d+) more$");
private final List<String> myPreviousStack = new ArrayList<String>();
private final List<String> myCurrentStack = new ArrayList<String>();
public String expand(String line) {
line = line.trim();
// are we in the middle of a stack trace?
boolean isInTrace = !myCurrentStack.isEmpty() || !myPreviousStack.isEmpty();
// is this a stack frame line?
boolean isStackTrace = isStackFrame(line);
// most lines are not related to stack traces, quit early in such cases
if (!isStackTrace && !isInTrace) {
return myContinuationPrefix + line;
}
// if it is a stack frame, then just add to current stack
if (isStackTrace) {
myCurrentStack.add(line);
return myStackTracePrefix + line;
}
// Now we know that this is not a stack trace line, but we are in the middle
// of parsing a stack trace, so it is one of: "Caused By:", "...N more", or the end of the trace
// if it is a "Caused by:" line, then we move the stack we've seen till now to be the
// outer stack
if (isCauseLine(line)) {
myPreviousStack.clear();
for (String s : myCurrentStack) {
myPreviousStack.add(s);
}
myCurrentStack.clear();
return myCauseLinePrefix + line;
}
// if it is the "...N more", we replace that line with the last N frames from the outer stack
int elidedFrameCount = getElidedFrameCount(line);
if (elidedFrameCount > 0) {
if (elidedFrameCount <= myPreviousStack.size()) {
StringBuilder sb = new StringBuilder();
for (int i = myPreviousStack.size() - elidedFrameCount; i < myPreviousStack.size(); i++) {
String frame = myPreviousStack.get(i);
sb.append(myExpandedStackTracePrefix);
sb.append(frame);
myCurrentStack.add(frame);
if (i != myPreviousStack.size() - 1) {
sb.append('\n');
}
}
return sb.toString();
} else {
// something went wrong: we don't actually have the required number of frames in the outer stack
// in this case, we don't expand the frames
return myStackTracePrefix + line;
}
}
// otherwise we've reached the end of a stack trace that we don't need to retain anymore
myCurrentStack.clear();
myPreviousStack.clear();
return myContinuationPrefix + line;
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
static boolean isStackFrame(String line) {
return EXCEPTION_LINE_PATTERN.matcher(line).matches();
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
static int getElidedFrameCount(String line) {
Matcher matcher = ELIDED_LINE_PATTERN.matcher(line);
return matcher.matches() ? StringUtil.parseInt(matcher.group(1), -1) : -1;
}
private static boolean isCauseLine(String line) {
return line.startsWith("Caused by:");
}
}