package org.xtest; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.Stack; import java.util.concurrent.Callable; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.nodemodel.ICompositeNode; import org.eclipse.xtext.nodemodel.INode; import org.eclipse.xtext.nodemodel.util.NodeModelUtils; import org.eclipse.xtext.util.Strings; import org.eclipse.xtext.xbase.XBlockExpression; import org.eclipse.xtext.xbase.XExpression; import org.xtest.xTest.impl.BodyImplCustom; import com.google.common.base.CharMatcher; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; /** * Utilities for working with stack traces * * @author Michael Barry */ @SuppressWarnings("restriction") public class XtestUtil { /** * Prefix of packages that should not show up in sanitized stack traces */ private static final ImmutableSet<String> BLACKLIST = ImmutableSet.of("org.eclipse.xtext", "org.xtest", "sun.reflect", "java.lang.reflect"); /** * Prefix of packages that should always show up in sanitized stack traces even if they have a * prefix in {@link #BLACKLIST} */ private static final ImmutableSet<String> WHITELIST = ImmutableSet.of("org.eclipse.xtext.lib", "org.xtest.lib"); /** * Generates a condensed stack trace that omits all internal Xtest methods and the base of the * stack that originally invoked executing the Xtest file. Xtest line-number traces are mixed * into the stack whenever a closure is invoked * * @param startTrace * Base stack trace up to invoking the Xtest interpreter, will be filtered out * @param stackTrace * The raw stack trace * @param callStack * The stack of Xtest expressions * @return The compressed Xtest stack traces */ public static StackTraceElement[] generateXtestStackTrace(StackTraceElement[] startTrace, StackTraceElement[] stackTrace, Stack<XExpression> callStack) { StackTraceElement[] normalize = removeCommonBase(startTrace, stackTrace); StackTraceElement[] withXtestElements = insertXtestElements(normalize, callStack); StackTraceElement[] finalTrace = sanitize(withXtestElements); return finalTrace; } /** * Returns the root cause of the throwable provided * * @param exception * The exception * @return The root cause of that exception */ public static Throwable getRootCause(Throwable exception) { while (exception.getCause() != null) { exception = exception.getCause(); } return exception; } /** * Get the text of an Expression * * @param expression * The expression * @param maxLen * The maximum allowed length of the text * @return The test of the expression */ public static String getText(XExpression expression, int maxLen) { String name; ICompositeNode node = NodeModelUtils.getNode(expression); if (node != null) { String nodeWithoutComments = getNodeWithoutComments(node); name = trimIfNecessary(nodeWithoutComments, maxLen); } else { name = ""; } name = trimIfNecessary(name, maxLen); return name; } /** * Get the text of the first line of an expression * * @param expression * The expression * @param maxLen * The maximum allowed length of the text * @return The test of the first line of the expression */ public static String getTextOfFirstLine(XExpression expression, int maxLen) { while (expression instanceof XBlockExpression && !((XBlockExpression) expression).getExpressions().isEmpty()) { expression = ((XBlockExpression) expression).getExpressions().get(0); } return getText(expression, maxLen); } /** * Run a block of code marking that this is on a new level of the Xtest stack so that * {@link #generateXtestStackTrace(StackTraceElement[], StackTraceElement[], Stack)} will insert * the Xtest stack trace element here * * @param callable * The code to run * @return The result of running that code * @throws Exception * If an error occurs calling the callable */ public static <T> T runOnNewLevelOfXtestStack(Callable<T> callable) throws Exception { return XtestStackMarker.run(callable); } /** * Converts any string to java upper-camel-case format. Non java letter-or-digits are dropped * and used to tokenize. Leading non-java letters are dropped entirely. * * @param input * The input * @return The upper-camel-case output */ public static String toUpperCamel(String input) { String result = ""; if (input != null) { // 1. Trim non-java letters from start of the input input = CharMatcher.JAVA_LETTER.negate().trimLeadingFrom(input); // 2. Split on non-java or digit characters Iterable<String> split = Splitter.on(CharMatcher.JAVA_LETTER_OR_DIGIT.negate()).split( input); // 3. Convert the first character of each token to upper case split = Iterables.transform(split, new Function<String, String>() { @Override public String apply(String input) { return Strings.toFirstUpper(input); } }); // 4. Pack them back together result = Joiner.on("").join(split); } return result; } /** * Removes line breaks to fit onto one line and trims to the {@code maxLen}, inserting ellipsis * if necessary. * * @param string * The string * @param maxLen * The maximum length * @return The original string, trimmed with ellipsis if necessary */ public static String trimIfNecessary(String string, int maxLen) { string = string.replaceAll("\\s+", " ").trim(); if (string.length() >= maxLen) { string = string.substring(0, maxLen - 3) + "..."; } return string; } private static StackTraceElement generateXtestStackTraceElement(XExpression expression) { ICompositeNode node = NodeModelUtils.getNode(expression); String fileName = getBody(expression).getTypeName(); int startLine = node.getStartLine(); return new StackTraceElement(fileName, "\"" + getTextOfFirstLine(expression, 60) + "\"", fileName, startLine); } private static BodyImplCustom getBody(XExpression expression) { EObject object = expression; while (object.eContainer() != null) { object = object.eContainer(); } return (BodyImplCustom) object; } private static String getNodeWithoutComments(ICompositeNode node) { INode rootNode = node.getRootNode(); if (rootNode != null) { int offset = node.getOffset(); int length = node.getLength(); return rootNode.getText().substring(offset, offset + length); } return null; } private static StackTraceElement[] insertXtestElements(StackTraceElement[] stackTrace, Stack<XExpression> callStack) { List<StackTraceElement> list = Lists.newArrayList(stackTrace); // Stack iterator starts at base, want to start at top and work down List<XExpression> reverse = Lists.reverse(callStack); Queue<XExpression> queue = Lists.newLinkedList(reverse); // Search for Xtest stack markers for (int i = 0; i < list.size(); i++) { StackTraceElement element = list.get(i); if (queue.isEmpty()) { break; } if (element.getClassName().equals(XtestStackMarker.class.getName())) { StackTraceElement newElement = generateXtestStackTraceElement(queue.poll()); list.add(i++, newElement); } } // add the rest... for (XExpression expression : queue) { list.add(generateXtestStackTraceElement(expression)); } return list.toArray(new StackTraceElement[list.size()]); } private static Predicate<String> isStartOf(final String string) { return new Predicate<String>() { @Override public boolean apply(String input) { return string.startsWith(input); } }; } private static StackTraceElement[] removeCommonBase(StackTraceElement[] startTrace, StackTraceElement[] stackTrace) { ArrayList<StackTraceElement> result = Lists.newArrayList(stackTrace); List<StackTraceElement> start = Lists.reverse(Lists.newArrayList(startTrace)); List<StackTraceElement> stack = Lists.reverse(result); // While beginnings are the same, remove them while (!stack.isEmpty() && !start.isEmpty() && stack.get(0).equals(start.get(0))) { stack.remove(0); start.remove(0); } return result.toArray(new StackTraceElement[result.size()]); } private static StackTraceElement[] sanitize(StackTraceElement[] stackTrace) { List<StackTraceElement> traceList = Lists.newArrayList(stackTrace); List<StackTraceElement> toRemove = Lists.newArrayList(); for (StackTraceElement element : traceList) { String className = element.getClassName(); Predicate<String> pred = isStartOf(className); if (!Iterables.any(WHITELIST, pred) && Iterables.any(BLACKLIST, pred)) { toRemove.add(element); } } traceList.removeAll(toRemove); return traceList.toArray(new StackTraceElement[traceList.size()]); } private static class XtestStackMarker { public static <T> T run(Callable<T> callable) throws Exception { return callable.call(); } } }