/*
* Copyright 2008-2017 the original author or authors.
*
* 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 griffon.core;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static griffon.util.GriffonNameUtils.getShortName;
import static java.util.Arrays.asList;
/**
* Catches and sanitizes all uncaught exceptions.
*
* @author Danno Ferrin
* @author Andres Almiray
*/
public class GriffonExceptionHandler implements ExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(GriffonExceptionHandler.class);
private static final String[] CONFIG_OPTIONS = {
GRIFFON_FULL_STACKTRACE,
GRIFFON_EXCEPTION_OUTPUT
};
private static final String[] GRIFFON_PACKAGES =
System.getProperty("griffon.sanitized.stacktraces",
"org.codehaus.groovy.," +
"org.codehaus.griffon.," +
"groovy.," +
"java.," +
"javax.," +
"sun.," +
"com.sun.,"
).split("(\\s|,)+");
private static final List<CallableWithArgs<Boolean>> TESTS = new ArrayList<>();
private static final String SANITIZED_STACKTRACE_MSG = "Stacktrace was sanitized. Set System property '" + GRIFFON_FULL_STACKTRACE + "' to 'true' for full report.";
public static void addClassTest(CallableWithArgs<Boolean> test) {
TESTS.add(test);
}
private GriffonApplication application;
public GriffonExceptionHandler() {
Thread.setDefaultUncaughtExceptionHandler(this);
}
// @Inject
public GriffonExceptionHandler(GriffonApplication application) {
this.application = application;
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Nullable
protected GriffonApplication getApplication() {
return application;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
handle(e);
}
@Override
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public void handle(Throwable throwable) {
try {
sanitize(throwable);
printStacktrace(throwable);
logError("Uncaught Exception.", throwable);
if (application != null) {
application.getEventRouter().publishEvent("Uncaught" + getShortName(throwable.getClass()), asList(throwable));
application.getEventRouter().publishEvent(ApplicationEvent.UNCAUGHT_EXCEPTION_THROWN.getName(), asList(throwable));
}
} catch (Throwable t) {
sanitize(t);
printStacktrace(t);
logError("An error occurred while handling uncaught exception " + throwable + ".", t);
}
}
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public static Throwable sanitize(Throwable throwable) {
try {
if (!isFullStacktraceEnabled()) {
deepSanitize(throwable);
}
} catch (Throwable t) {
// don't let the exception get thrown out, will cause infinite looping!
}
return throwable;
}
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public static StackTraceElement[] sanitize(StackTraceElement[] stackTrace) {
try {
if (!isFullStacktraceEnabled()) {
Throwable t = new Throwable();
t.setStackTrace(stackTrace);
sanitize(t);
stackTrace = t.getStackTrace();
}
} catch (Throwable o) {
// don't let the exception get thrown out, will cause infinite looping!
}
return stackTrace;
}
public static boolean isOutputEnabled() {
return Boolean.getBoolean(GRIFFON_EXCEPTION_OUTPUT);
}
public static void configure(Map<String, Object> config) {
for (String option : CONFIG_OPTIONS) {
if (config.containsKey(option)) {
System.setProperty(option, String.valueOf(config.get(option)));
}
}
}
public static void registerExceptionHandler() {
Thread.setDefaultUncaughtExceptionHandler(new GriffonExceptionHandler());
System.setProperty("sun.awt.exception.handler", GriffonExceptionHandler.class.getName());
}
public static void handleThrowable(Throwable t) {
Thread.getDefaultUncaughtExceptionHandler().uncaughtException(
Thread.currentThread(),
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
*/
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public static Throwable deepSanitize(Throwable t) {
Throwable current = t;
while (current.getCause() != null) {
current = doSanitize(current.getCause());
}
return doSanitize(t);
}
private static Throwable doSanitize(Throwable t) {
StackTraceElement[] trace = t.getStackTrace();
List<StackTraceElement> newTrace = new ArrayList<>();
for (StackTraceElement stackTraceElement : trace) {
if (isApplicationClass(stackTraceElement.getClassName())) {
newTrace.add(stackTraceElement);
}
}
StackTraceElement[] clean = new StackTraceElement[newTrace.size()];
newTrace.toArray(clean);
t.setStackTrace(clean);
return t;
}
public static boolean isFullStacktraceEnabled() {
return Boolean.getBoolean(GRIFFON_FULL_STACKTRACE);
}
private static void printStacktrace(Throwable throwable) {
if (isOutputEnabled()) {
if (!isFullStacktraceEnabled()) {
System.err.println(SANITIZED_STACKTRACE_MSG);
}
throwable.printStackTrace(System.err);
}
}
private static void logError(String message, Throwable throwable) {
if (!isFullStacktraceEnabled()) {
message += " " + SANITIZED_STACKTRACE_MSG;
}
LOG.error(message, throwable);
}
private static boolean isApplicationClass(String className) {
for (CallableWithArgs<Boolean> test : TESTS) {
if (test.call(className)) {
return false;
}
}
for (String excludedPackage : GRIFFON_PACKAGES) {
if (className.startsWith(excludedPackage)) {
return false;
}
}
return true;
}
}