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