| /* |
| * 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:"); |
| } |
| } |