blob: 99becdf5b6d5643f9baeb5c90d172d279f706cb3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.codehaus.groovy.runtime;
import groovy.lang.Closure;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
/**
* Originally was grails.utils.GrailsUtils, removed some grails specific stuff.
* Utility methods removing internal lines from stack traces
*
* @author Graeme Rocher
* @since 1.5
*/
public class StackTraceUtils {
public static final String STACK_LOG_NAME = "StackTrace";
private static final Logger STACK_LOG;
// set log to consume traces by default, end user can override later
static {
outer:
do {
Enumeration existingLogs = LogManager.getLogManager().getLoggerNames();
while (existingLogs.hasMoreElements()) {
if (STACK_LOG_NAME.equals(existingLogs.nextElement())) {
STACK_LOG = Logger.getLogger(STACK_LOG_NAME);
break outer;
}
}
STACK_LOG = Logger.getLogger(STACK_LOG_NAME);
STACK_LOG.setUseParentHandlers(false);
} while (false);
}
private static final String[] GROOVY_PACKAGES =
System.getProperty("groovy.sanitized.stacktraces",
"groovy.," +
"org.codehaus.groovy.," +
"java.," +
"javax.," +
"sun.," +
"gjdk.groovy.,"
).split("(\\s|,)+");
private static final List<Closure> tests = new ArrayList<Closure>();
/**
* Adds a groovy.lang.Closure to test whether the stack trace
* element should be added or not.
* <p>
* The groovy.lang.Closure will be given the class name as parameter.
* the return value decides if the element will be added or not.
* <ul>
* <li><b>true</b> - trace element will be added to the trace
* <li><b>false</b> - trace element will <b>not</b> be added to the trace
* <li><b>null</b> - continue with next test
* </ul>
* Groovy truth will be used to determine true and false, null is excluded from
* defaulting to false here. If all tests have been executed and all of them skipped, then
* the groovy standard filtering will take place.
*
* @param test the testing groovy.lang.Closure
*/
public static void addClassTest(Closure test) {
tests.add(test);
}
/**
* Remove all apparently groovy-internal trace entries from the exception instance
* <p>
* This modifies the original instance and returns it, it does not clone
*
* @param t the Throwable whose stack trace we want to sanitize
* @return The original Throwable but with a sanitized stack trace
*/
public static Throwable sanitize(Throwable t) {
// Note that this getBoolean access may well be synced...
if (!Boolean.getBoolean("groovy.full.stacktrace")) {
StackTraceElement[] trace = t.getStackTrace();
List<StackTraceElement> newTrace = new ArrayList<StackTraceElement>();
for (StackTraceElement stackTraceElement : trace) {
if (isApplicationClass(stackTraceElement.getClassName())) {
newTrace.add(stackTraceElement);
}
}
// We don't want to lose anything, so log it
STACK_LOG.log(Level.WARNING, "Sanitizing stacktrace:", t);
StackTraceElement[] clean = new StackTraceElement[newTrace.size()];
newTrace.toArray(clean);
t.setStackTrace(clean);
}
return t;
}
public static void printSanitizedStackTrace(Throwable t, PrintWriter p) {
t = StackTraceUtils.sanitize(t);
StackTraceElement[] trace = t.getStackTrace();
for (StackTraceElement stackTraceElement : trace) {
p.println("at " + stackTraceElement.getClassName()
+ "(" + stackTraceElement.getMethodName()
+ ":" + stackTraceElement.getLineNumber() + ")");
}
}
public static void printSanitizedStackTrace(Throwable t) {
printSanitizedStackTrace(t, new PrintWriter(System.err));
}
public static boolean isApplicationClass(String className) {
for (Closure test : tests) {
Object result = test.call(className);
if (result != null) {
return DefaultTypeTransformation.castToBoolean(result);
}
}
for (String groovyPackage : GROOVY_PACKAGES) {
if (className.startsWith(groovyPackage)) {
return false;
}
}
return true;
}
/**
* Extracts the root cause of the exception, no matter how nested it is
*
* @param t a Throwable
* @return The deepest cause of the exception that can be found
*/
public static Throwable extractRootCause(Throwable t) {
Throwable result = t;
while (result.getCause() != null) {
result = result.getCause();
}
return result;
}
/**
* Get the root cause of an exception and sanitize it for display to the user
* <p>
* This will MODIFY the stacktrace of the root cause exception object and return it
*
* @param t a throwable
* @return The root cause exception instance, with its stace trace modified to filter out groovy runtime classes
*/
public static Throwable sanitizeRootCause(Throwable t) {
return StackTraceUtils.sanitize(StackTraceUtils.extractRootCause(t));
}
/**
* Sanitize the exception and ALL nested causes
* <p>
* This will MODIFY the stacktrace of the exception instance and all its causes irreversibly
*
* @param t a throwable
* @return The root cause exception instances, with stack trace modified to filter out groovy runtime classes
*/
public static Throwable deepSanitize(Throwable t) {
Throwable current = t;
while (current.getCause() != null) {
current = StackTraceUtils.sanitize(current.getCause());
}
return StackTraceUtils.sanitize(t);
}
}