GriffonExceptionHandler.java
001 /*
002  * Copyright 2008-2017 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     private static final String SANITIZED_STACKTRACE_MSG = "Stacktrace was sanitized. Set System property '" + GRIFFON_FULL_STACKTRACE + "' to 'true' for full report.";
057 
058     public static void addClassTest(CallableWithArgs<Boolean> test) {
059         TESTS.add(test);
060     }
061 
062     private GriffonApplication application;
063 
064     public GriffonExceptionHandler() {
065         Thread.setDefaultUncaughtExceptionHandler(this);
066     }
067 
068     // @Inject
069     public GriffonExceptionHandler(GriffonApplication application) {
070         this.application = application;
071         Thread.setDefaultUncaughtExceptionHandler(this);
072     }
073 
074     @Nullable
075     protected GriffonApplication getApplication() {
076         return application;
077     }
078 
079     @Override
080     public void uncaughtException(Thread t, Throwable e) {
081         handle(e);
082     }
083 
084     @Override
085     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
086     public void handle(Throwable throwable) {
087         try {
088             sanitize(throwable);
089             printStacktrace(throwable);
090             logError("Uncaught Exception.", throwable);
091             if (application != null) {
092                 application.getEventRouter().publishEvent("Uncaught" + getShortName(throwable.getClass()), asList(throwable));
093                 application.getEventRouter().publishEvent(ApplicationEvent.UNCAUGHT_EXCEPTION_THROWN.getName(), asList(throwable));
094             }
095         catch (Throwable t) {
096             sanitize(t);
097             printStacktrace(t);
098             logError("An error occurred while handling uncaught exception " + throwable + ".", t);
099         }
100     }
101 
102     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
103     public static Throwable sanitize(Throwable throwable) {
104         try {
105             if (!isFullStacktraceEnabled()) {
106                 deepSanitize(throwable);
107             }
108         catch (Throwable t) {
109             // don't let the exception get thrown out, will cause infinite looping!
110         }
111         return throwable;
112     }
113 
114     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
115     public static StackTraceElement[] sanitize(StackTraceElement[] stackTrace) {
116         try {
117             if (!isFullStacktraceEnabled()) {
118                 Throwable t = new Throwable();
119                 t.setStackTrace(stackTrace);
120                 sanitize(t);
121                 stackTrace = t.getStackTrace();
122             }
123         catch (Throwable o) {
124             // don't let the exception get thrown out, will cause infinite looping!
125         }
126         return stackTrace;
127     }
128 
129     public static boolean isOutputEnabled() {
130         return Boolean.getBoolean(GRIFFON_EXCEPTION_OUTPUT);
131     }
132 
133     public static void configure(Map<String, Object> config) {
134         for (String option : CONFIG_OPTIONS) {
135             if (config.containsKey(option)) {
136                 System.setProperty(option, String.valueOf(config.get(option)));
137             }
138         }
139     }
140 
141     public static void registerExceptionHandler() {
142         Thread.setDefaultUncaughtExceptionHandler(new GriffonExceptionHandler());
143         System.setProperty("sun.awt.exception.handler", GriffonExceptionHandler.class.getName());
144     }
145 
146     public static void handleThrowable(Throwable t) {
147         Thread.getDefaultUncaughtExceptionHandler().uncaughtException(
148             Thread.currentThread(),
149             t
150         );
151     }
152 
153     /**
154      * Sanitize the exception and ALL nested causes
155      <p/>
156      * This will MODIFY the stacktrace of the exception instance and all its causes irreversibly
157      *
158      @param t a throwable
159      *
160      @return The root cause exception instances, with stack trace modified to filter out groovy runtime classes
161      */
162     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
163     public static Throwable deepSanitize(Throwable t) {
164         Throwable current = t;
165         while (current.getCause() != null) {
166             current = doSanitize(current.getCause());
167         }
168         return doSanitize(t);
169     }
170 
171     private static Throwable doSanitize(Throwable t) {
172         StackTraceElement[] trace = t.getStackTrace();
173         List<StackTraceElement> newTrace = new ArrayList<>();
174         for (StackTraceElement stackTraceElement : trace) {
175             if (isApplicationClass(stackTraceElement.getClassName())) {
176                 newTrace.add(stackTraceElement);
177             }
178         }
179 
180         StackTraceElement[] clean = new StackTraceElement[newTrace.size()];
181         newTrace.toArray(clean);
182         t.setStackTrace(clean);
183         return t;
184     }
185 
186     public static boolean isFullStacktraceEnabled() {
187         return Boolean.getBoolean(GRIFFON_FULL_STACKTRACE);
188     }
189 
190     private static void printStacktrace(Throwable throwable) {
191         if (isOutputEnabled()) {
192             if (!isFullStacktraceEnabled()) {
193                 System.err.println(SANITIZED_STACKTRACE_MSG);
194             }
195             throwable.printStackTrace(System.err);
196         }
197     }
198 
199     private static void logError(String message, Throwable throwable) {
200         if (!isFullStacktraceEnabled()) {
201             message += " " + SANITIZED_STACKTRACE_MSG;
202         }
203         LOG.error(message, throwable);
204     }
205 
206     private static boolean isApplicationClass(String className) {
207         for (CallableWithArgs<Boolean> test : TESTS) {
208             if (test.call(className)) {
209                 return false;
210             }
211         }
212 
213         for (String excludedPackage : GRIFFON_PACKAGES) {
214             if (className.startsWith(excludedPackage)) {
215                 return false;
216             }
217         }
218         return true;
219     }
220 }