GriffonExceptionHandler.java
001 /*
002  * Copyright 2008-2016 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 javax.annotation.Nullable;
022 import java.util.ArrayList;
023 import java.util.List;
024 import java.util.Map;
025 
026 import static griffon.util.GriffonNameUtils.getShortName;
027 import static java.util.Arrays.asList;
028 
029 /**
030  * Catches and sanitizes all uncaught exceptions.
031  *
032  @author Danno Ferrin
033  @author Andres Almiray
034  */
035 public class GriffonExceptionHandler implements ExceptionHandler {
036     private static final Logger LOG = LoggerFactory.getLogger(GriffonExceptionHandler.class);
037 
038     private static final String[] CONFIG_OPTIONS = {
039         GRIFFON_FULL_STACKTRACE,
040         GRIFFON_EXCEPTION_OUTPUT
041     };
042 
043     private static final String[] GRIFFON_PACKAGES =
044         System.getProperty("griffon.sanitized.stacktraces",
045             "org.codehaus.groovy.," +
046                 "org.codehaus.griffon.," +
047                 "groovy.," +
048                 "java.," +
049                 "javax.," +
050                 "sun.," +
051                 "com.sun.,"
052         ).split("(\\s|,)+");
053 
054     private static final List<CallableWithArgs<Boolean>> TESTS = new ArrayList<>();
055 
056     public static void addClassTest(CallableWithArgs<Boolean> test) {
057         TESTS.add(test);
058     }
059 
060     private GriffonApplication application;
061 
062     public GriffonExceptionHandler() {
063         Thread.setDefaultUncaughtExceptionHandler(this);
064     }
065 
066     // @Inject
067     public GriffonExceptionHandler(GriffonApplication application) {
068         this.application = application;
069         Thread.setDefaultUncaughtExceptionHandler(this);
070     }
071 
072     @Nullable
073     protected GriffonApplication getApplication() {
074         return application;
075     }
076 
077     @Override
078     public void uncaughtException(Thread t, Throwable e) {
079         handle(e);
080     }
081 
082     @Override
083     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
084     public void handle(Throwable throwable) {
085         try {
086             sanitize(throwable);
087             if (isOutputEnabled()) throwable.printStackTrace(System.err);
088             LOG.error("Uncaught Exception", throwable);
089             if (application != null) {
090                 application.getEventRouter().publishEvent("Uncaught" + getShortName(throwable.getClass()), asList(throwable));
091                 application.getEventRouter().publishEvent(ApplicationEvent.UNCAUGHT_EXCEPTION_THROWN.getName(), asList(throwable));
092             }
093         catch (Throwable t) {
094             sanitize(t);
095             if (isOutputEnabled()) t.printStackTrace(System.err);
096             LOG.error("An error occurred while handling uncaught exception " + throwable, t);
097         }
098     }
099 
100     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
101     public static Throwable sanitize(Throwable throwable) {
102         try {
103             if (!Boolean.getBoolean(GRIFFON_FULL_STACKTRACE)) {
104                 deepSanitize(throwable);
105             }
106         catch (Throwable t) {
107             // don't let the exception get thrown out, will cause infinite looping!
108         }
109         return throwable;
110     }
111 
112     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
113     public static StackTraceElement[] sanitize(StackTraceElement[] stackTrace) {
114         try {
115             if (!Boolean.getBoolean(GRIFFON_FULL_STACKTRACE)) {
116                 Throwable t = new Throwable();
117                 t.setStackTrace(stackTrace);
118                 sanitize(t);
119                 stackTrace = t.getStackTrace();
120             }
121         catch (Throwable o) {
122             // don't let the exception get thrown out, will cause infinite looping!
123         }
124         return stackTrace;
125     }
126 
127     public static boolean isOutputEnabled() {
128         return Boolean.getBoolean(GRIFFON_EXCEPTION_OUTPUT);
129     }
130 
131     public static void configure(Map<String, Object> config) {
132         for (String option : CONFIG_OPTIONS) {
133             if (config.containsKey(option)) {
134                 System.setProperty(option, String.valueOf(config.get(option)));
135             }
136         }
137     }
138 
139     public static void registerExceptionHandler() {
140         Thread.setDefaultUncaughtExceptionHandler(new GriffonExceptionHandler());
141         System.setProperty("sun.awt.exception.handler", GriffonExceptionHandler.class.getName());
142     }
143 
144     public static void handleThrowable(Throwable t) {
145         Thread.getDefaultUncaughtExceptionHandler().uncaughtException(
146             Thread.currentThread(),
147             t
148         );
149     }
150 
151     /**
152      * Sanitize the exception and ALL nested causes
153      <p/>
154      * This will MODIFY the stacktrace of the exception instance and all its causes irreversibly
155      *
156      @param t a throwable
157      @return The root cause exception instances, with stack trace modified to filter out groovy runtime classes
158      */
159     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
160     public static Throwable deepSanitize(Throwable t) {
161         Throwable current = t;
162         while (current.getCause() != null) {
163             current = doSanitize(current.getCause());
164         }
165         return doSanitize(t);
166     }
167 
168     private static Throwable doSanitize(Throwable t) {
169         StackTraceElement[] trace = t.getStackTrace();
170         List<StackTraceElement> newTrace = new ArrayList<>();
171         for (StackTraceElement stackTraceElement : trace) {
172             if (isApplicationClass(stackTraceElement.getClassName())) {
173                 newTrace.add(stackTraceElement);
174             }
175         }
176 
177         StackTraceElement[] clean = new StackTraceElement[newTrace.size()];
178         newTrace.toArray(clean);
179         t.setStackTrace(clean);
180         return t;
181     }
182 
183     private static boolean isApplicationClass(String className) {
184         for (CallableWithArgs<Boolean> test : TESTS) {
185             if (test.call(className)) {
186                 return false;
187             }
188         }
189 
190         for (String excludedPackage : GRIFFON_PACKAGES) {
191             if (className.startsWith(excludedPackage)) {
192                 return false;
193             }
194         }
195         return true;
196     }
197 }