GriffonExceptionHandler.java
001 /*
002  * Copyright 2008-2015 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *     http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 package griffon.core;
017 
018 import org.slf4j.Logger;
019 import org.slf4j.LoggerFactory;
020 
021 import java.util.ArrayList;
022 import java.util.List;
023 import java.util.Map;
024 
025 import static griffon.util.GriffonNameUtils.getShortName;
026 import static java.util.Arrays.asList;
027 
028 /**
029  * Catches and sanitizes all uncaught exceptions.
030  *
031  @author Danno Ferrin
032  @author Andres Almiray
033  */
034 public class GriffonExceptionHandler implements ExceptionHandler {
035     private static final Logger LOG = LoggerFactory.getLogger(GriffonExceptionHandler.class);
036 
037     private static final String[] CONFIG_OPTIONS = {
038         GRIFFON_FULL_STACKTRACE,
039         GRIFFON_EXCEPTION_OUTPUT
040     };
041 
042     private static final String[] GRIFFON_PACKAGES =
043         System.getProperty("griffon.sanitized.stacktraces",
044             "org.codehaus.groovy.," +
045                 "org.codehaus.griffon.," +
046                 "groovy.," +
047                 "java.," +
048                 "javax.," +
049                 "sun.," +
050                 "com.sun.,"
051         ).split("(\\s|,)+");
052 
053     private static final List<CallableWithArgs<Boolean>> TESTS = new ArrayList<>();
054 
055     public static void addClassTest(CallableWithArgs<Boolean> test) {
056         TESTS.add(test);
057     }
058 
059     private GriffonApplication application;
060 
061     public GriffonExceptionHandler() {
062         Thread.setDefaultUncaughtExceptionHandler(this);
063     }
064 
065     // @Inject
066     public GriffonExceptionHandler(GriffonApplication application) {
067         this.application = application;
068         Thread.setDefaultUncaughtExceptionHandler(this);
069     }
070 
071     @Override
072     public void uncaughtException(Thread t, Throwable e) {
073         handle(e);
074     }
075 
076     @Override
077     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
078     public void handle(Throwable throwable) {
079         try {
080             sanitize(throwable);
081             if (isOutputEnabled()) throwable.printStackTrace(System.err);
082             LOG.error("Uncaught Exception", throwable);
083             if (application != null) {
084                 application.getEventRouter().publishEvent("Uncaught" + getShortName(throwable.getClass()), asList(throwable));
085                 application.getEventRouter().publishEvent(ApplicationEvent.UNCAUGHT_EXCEPTION_THROWN.getName(), asList(throwable));
086             }
087         catch (Throwable t) {
088             sanitize(t);
089             if (isOutputEnabled()) t.printStackTrace(System.err);
090             LOG.error("An error occurred while handling uncaught exception " + throwable, t);
091         }
092     }
093 
094     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
095     public static Throwable sanitize(Throwable throwable) {
096         try {
097             if (!Boolean.getBoolean(GRIFFON_FULL_STACKTRACE)) {
098                 deepSanitize(throwable);
099             }
100         catch (Throwable t) {
101             // don't let the exception get thrown out, will cause infinite looping!
102         }
103         return throwable;
104     }
105 
106     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
107     public static StackTraceElement[] sanitize(StackTraceElement[] stackTrace) {
108         try {
109             if (!Boolean.getBoolean(GRIFFON_FULL_STACKTRACE)) {
110                 Throwable t = new Throwable();
111                 t.setStackTrace(stackTrace);
112                 sanitize(t);
113                 stackTrace = t.getStackTrace();
114             }
115         catch (Throwable o) {
116             // don't let the exception get thrown out, will cause infinite looping!
117         }
118         return stackTrace;
119     }
120 
121     public static boolean isOutputEnabled() {
122         return Boolean.getBoolean(GRIFFON_EXCEPTION_OUTPUT);
123     }
124 
125     public static void configure(Map<String, Object> config) {
126         for (String option : CONFIG_OPTIONS) {
127             if (config.containsKey(option)) {
128                 System.setProperty(option, String.valueOf(config.get(option)));
129             }
130         }
131     }
132 
133     public static void registerExceptionHandler() {
134         Thread.setDefaultUncaughtExceptionHandler(new GriffonExceptionHandler());
135         System.setProperty("sun.awt.exception.handler", GriffonExceptionHandler.class.getName());
136     }
137 
138     public static void handleThrowable(Throwable t) {
139         Thread.getDefaultUncaughtExceptionHandler().uncaughtException(
140             Thread.currentThread(),
141             t
142         );
143     }
144 
145     /**
146      * Sanitize the exception and ALL nested causes
147      <p/>
148      * This will MODIFY the stacktrace of the exception instance and all its causes irreversibly
149      *
150      @param t a throwable
151      @return The root cause exception instances, with stack trace modified to filter out groovy runtime classes
152      */
153     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
154     public static Throwable deepSanitize(Throwable t) {
155         Throwable current = t;
156         while (current.getCause() != null) {
157             current = doSanitize(current.getCause());
158         }
159         return doSanitize(t);
160     }
161 
162     private static Throwable doSanitize(Throwable t) {
163         StackTraceElement[] trace = t.getStackTrace();
164         List<StackTraceElement> newTrace = new ArrayList<>();
165         for (StackTraceElement stackTraceElement : trace) {
166             if (isApplicationClass(stackTraceElement.getClassName())) {
167                 newTrace.add(stackTraceElement);
168             }
169         }
170 
171         StackTraceElement[] clean = new StackTraceElement[newTrace.size()];
172         newTrace.toArray(clean);
173         t.setStackTrace(clean);
174         return t;
175     }
176 
177     private static boolean isApplicationClass(String className) {
178         for (CallableWithArgs<Boolean> test : TESTS) {
179             if (test.call(className)) {
180                 return false;
181             }
182         }
183 
184         for (String excludedPackage : GRIFFON_PACKAGES) {
185             if (className.startsWith(excludedPackage)) {
186                 return false;
187             }
188         }
189         return true;
190     }
191 }