View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.impl;
18  
19  import java.io.Serializable;
20  import java.net.URL;
21  import java.security.CodeSource;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.Stack;
30  
31  import org.apache.logging.log4j.core.util.Loader;
32  import org.apache.logging.log4j.status.StatusLogger;
33  import org.apache.logging.log4j.util.ReflectionUtil;
34  import org.apache.logging.log4j.util.Strings;
35  
36  /**
37   * Wraps a Throwable to add packaging information about each stack trace element.
38   * 
39   * <p>
40   * A proxy is used to represent a throwable that may not exist in a different class loader or JVM. When an application
41   * deserializes a ThrowableProxy, the throwable may not be set, but the throwable's information is preserved in other
42   * fields of the proxy like the message and stack trace.
43   * </p>
44   * 
45   * <p>
46   * TODO: Move this class to org.apache.logging.log4j.core because it is used from LogEvent.
47   * </p>
48   * <p>
49   * TODO: Deserialize: Try to rebuild Throwable if the target exception is in this class loader?
50   * </p>
51   */
52  public class ThrowableProxy implements Serializable {
53  
54  	private static final String CAUSED_BY_LABEL = "Caused by: ";
55  	private static final String SUPPRESSED_LABEL = "Suppressed: ";
56      private static final String WRAPPED_BY_LABEL = "Wrapped by: ";
57  
58  	/**
59       * Cached StackTracePackageElement and ClassLoader.
60       * <p>
61       * Consider this class private.
62       * </p>
63       */
64      static class CacheEntry {
65          private final ExtendedClassInfo element;
66          private final ClassLoader loader;
67  
68          public CacheEntry(final ExtendedClassInfo element, final ClassLoader loader) {
69              this.element = element;
70              this.loader = loader;
71          }
72      }
73  
74      private static final ThrowableProxy[] EMPTY_THROWABLE_PROXY_ARRAY = new ThrowableProxy[0];
75  
76      private static final char EOL = '\n';
77  
78      private static final long serialVersionUID = -2752771578252251910L;
79  
80      private final ThrowableProxy causeProxy;
81  
82      private int commonElementCount;
83  
84      private final ExtendedStackTraceElement[] extendedStackTrace;
85  
86      private final String localizedMessage;
87  
88      private final String message;
89  
90      private final String name;
91  
92      private final ThrowableProxy[] suppressedProxies;
93  
94      private final transient Throwable throwable;
95  
96      /**
97       * For JSON and XML IO via Jackson.
98       */
99      @SuppressWarnings("unused")
100     private ThrowableProxy() {
101         this.throwable = null;
102         this.name = null;
103         this.extendedStackTrace = null;
104         this.causeProxy = null;
105         this.message = null;
106         this.localizedMessage = null;
107         this.suppressedProxies = EMPTY_THROWABLE_PROXY_ARRAY;
108     }
109 
110     /**
111      * Constructs the wrapper for the Throwable that includes packaging data.
112      * 
113      * @param throwable
114      *        The Throwable to wrap, must not be null.
115      */
116     public ThrowableProxy(final Throwable throwable) {
117         this(throwable, null);
118     }
119 
120     /**
121      * Constructs the wrapper for the Throwable that includes packaging data.
122      * 
123      * @param throwable
124      *        The Throwable to wrap, must not be null.
125      * @param visited
126      *        The set of visited suppressed exceptions.
127      */
128     private ThrowableProxy(final Throwable throwable, final Set<Throwable> visited) {
129         this.throwable = throwable;
130         this.name = throwable.getClass().getName();
131         this.message = throwable.getMessage();
132         this.localizedMessage = throwable.getLocalizedMessage();
133         final Map<String, CacheEntry> map = new HashMap<>();
134         final Stack<Class<?>> stack = ReflectionUtil.getCurrentStackTrace();
135         this.extendedStackTrace = this.toExtendedStackTrace(stack, map, null, throwable.getStackTrace());
136         final Throwable throwableCause = throwable.getCause();
137         final Set<Throwable> causeVisited = new HashSet<>(1);
138         this.causeProxy = throwableCause == null ? null : new ThrowableProxy(throwable, stack, map, throwableCause, visited, causeVisited);
139         this.suppressedProxies = this.toSuppressedProxies(throwable, visited);
140     }
141 
142     /**
143      * Constructs the wrapper for a Throwable that is referenced as the cause by another Throwable.
144      * 
145      * @param parent
146      *        The Throwable referencing this Throwable.
147      * @param stack
148      *        The Class stack.
149      * @param map
150      *        The cache containing the packaging data.
151      * @param cause
152      *        The Throwable to wrap.
153      * @param suppressedVisited TODO
154      * @param causeVisited TODO
155      */
156     private ThrowableProxy(final Throwable parent, final Stack<Class<?>> stack, final Map<String, CacheEntry> map,
157             final Throwable cause, final Set<Throwable> suppressedVisited, final Set<Throwable> causeVisited) {
158         causeVisited.add(cause);
159         this.throwable = cause;
160         this.name = cause.getClass().getName();
161         this.message = this.throwable.getMessage();
162         this.localizedMessage = this.throwable.getLocalizedMessage();
163         this.extendedStackTrace = this.toExtendedStackTrace(stack, map, parent.getStackTrace(), cause.getStackTrace());
164         final Throwable causeCause = cause.getCause();
165         this.causeProxy = causeCause == null || causeVisited.contains(causeCause) ? null : new ThrowableProxy(parent,
166                 stack, map, causeCause, suppressedVisited, causeVisited);
167         this.suppressedProxies = this.toSuppressedProxies(cause, suppressedVisited);
168     }
169 
170     @Override
171     public boolean equals(final Object obj) {
172         if (this == obj) {
173             return true;
174         }
175         if (obj == null) {
176             return false;
177         }
178         if (this.getClass() != obj.getClass()) {
179             return false;
180         }
181         final ThrowableProxy other = (ThrowableProxy) obj;
182         if (this.causeProxy == null) {
183             if (other.causeProxy != null) {
184                 return false;
185             }
186         } else if (!this.causeProxy.equals(other.causeProxy)) {
187             return false;
188         }
189         if (this.commonElementCount != other.commonElementCount) {
190             return false;
191         }
192         if (this.name == null) {
193             if (other.name != null) {
194                 return false;
195             }
196         } else if (!this.name.equals(other.name)) {
197             return false;
198         }
199         if (!Arrays.equals(this.extendedStackTrace, other.extendedStackTrace)) {
200             return false;
201         }
202         if (!Arrays.equals(this.suppressedProxies, other.suppressedProxies)) {
203             return false;
204         }
205         return true;
206     }
207 
208 	private void formatCause(final StringBuilder sb, final String prefix, final ThrowableProxy cause, final List<String> ignorePackages) {
209 		formatThrowableProxy(sb, prefix, CAUSED_BY_LABEL, cause, ignorePackages);
210 	}
211 
212 	private void formatThrowableProxy(final StringBuilder sb, final String prefix, final String causeLabel,
213 			final ThrowableProxy throwableProxy, final List<String> ignorePackages) {
214 		if (throwableProxy == null) {
215 			return;
216 		}
217 		sb.append(prefix).append(causeLabel).append(throwableProxy).append(EOL);
218 		this.formatElements(sb, prefix, throwableProxy.commonElementCount,
219 				throwableProxy.getStackTrace(), throwableProxy.extendedStackTrace, ignorePackages);
220 		this.formatSuppressed(sb, prefix + "\t", throwableProxy.suppressedProxies, ignorePackages);
221 		this.formatCause(sb, prefix, throwableProxy.causeProxy, ignorePackages);
222 	}
223 
224 	private void formatSuppressed(final StringBuilder sb, final String prefix, final ThrowableProxy[] suppressedProxies,
225 			final List<String> ignorePackages) {
226 		if (suppressedProxies == null) {
227 			return;
228 		}
229 		for (final ThrowableProxy suppressedProxy : suppressedProxies) {
230 			final ThrowableProxy cause = suppressedProxy;
231 			formatThrowableProxy(sb, prefix, SUPPRESSED_LABEL, cause, ignorePackages);
232 		}
233 	}
234 
235 	private void formatElements(final StringBuilder sb, final String prefix, final int commonCount,
236 			final StackTraceElement[] causedTrace, final ExtendedStackTraceElement[] extStackTrace,
237 			final List<String> ignorePackages) {
238 		if (ignorePackages == null || ignorePackages.isEmpty()) {
239 			for (final ExtendedStackTraceElement element : extStackTrace) {
240 				this.formatEntry(element, sb, prefix);
241 			}
242 		} else {
243 			int count = 0;
244 			for (int i = 0; i < extStackTrace.length; ++i) {
245 				if (!this.ignoreElement(causedTrace[i], ignorePackages)) {
246 					if (count > 0) {
247 						appendSuppressedCount(sb, prefix, count);
248 						count = 0;
249 					}
250 					this.formatEntry(extStackTrace[i], sb, prefix);
251 				} else {
252 					++count;
253 				}
254 			}
255 			if (count > 0) {
256 				appendSuppressedCount(sb, prefix, count);
257 			}
258 		}
259 		if (commonCount != 0) {
260 			sb.append(prefix).append("\t... ").append(commonCount).append(" more").append(EOL);
261 		}
262 	}
263 
264     private void appendSuppressedCount(final StringBuilder sb, final String prefix, final int count) {
265     	sb.append(prefix);
266         if (count == 1) {
267             sb.append("\t....").append(EOL);
268         } else {
269             sb.append("\t... suppressed ").append(count).append(" lines").append(EOL);
270         }
271     }
272 
273     private void formatEntry(final ExtendedStackTraceElement extStackTraceElement, final StringBuilder sb, final String prefix) {
274         sb.append(prefix);
275         sb.append("\tat ");
276         sb.append(extStackTraceElement);
277         sb.append(EOL);
278     }
279 
280     /**
281      * Formats the specified Throwable.
282      * 
283      * @param sb
284      *        StringBuilder to contain the formatted Throwable.
285      * @param cause
286      *        The Throwable to format.
287      */
288     public void formatWrapper(final StringBuilder sb, final ThrowableProxy cause) {
289         this.formatWrapper(sb, cause, null);
290     }
291 
292     /**
293      * Formats the specified Throwable.
294      * 
295      * @param sb
296      *        StringBuilder to contain the formatted Throwable.
297      * @param cause
298      *        The Throwable to format.
299      * @param packages
300      *        The List of packages to be suppressed from the trace.
301      */
302     @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
303     public void formatWrapper(final StringBuilder sb, final ThrowableProxy cause, final List<String> packages) {
304         final Throwable caused = cause.getCauseProxy() != null ? cause.getCauseProxy().getThrowable() : null;
305         if (caused != null) {
306             this.formatWrapper(sb, cause.causeProxy);
307             sb.append(WRAPPED_BY_LABEL);
308         }
309         sb.append(cause).append(EOL);
310         this.formatElements(sb, "", cause.commonElementCount,
311                 cause.getThrowable().getStackTrace(), cause.extendedStackTrace, packages);
312     }
313 
314     public ThrowableProxy getCauseProxy() {
315         return this.causeProxy;
316     }
317 
318     /**
319      * Format the Throwable that is the cause of this Throwable.
320      * 
321      * @return The formatted Throwable that caused this Throwable.
322      */
323     public String getCauseStackTraceAsString() {
324         return this.getCauseStackTraceAsString(null);
325     }
326 
327     /**
328      * Format the Throwable that is the cause of this Throwable.
329      * 
330      * @param packages
331      *        The List of packages to be suppressed from the trace.
332      * @return The formatted Throwable that caused this Throwable.
333      */
334     public String getCauseStackTraceAsString(final List<String> packages) {
335         final StringBuilder sb = new StringBuilder();
336         if (this.causeProxy != null) {
337             this.formatWrapper(sb, this.causeProxy);
338             sb.append(WRAPPED_BY_LABEL);
339         }
340         sb.append(this.toString());
341         sb.append(EOL);
342         this.formatElements(sb, "", 0, this.throwable.getStackTrace(), this.extendedStackTrace, packages);
343         return sb.toString();
344     }
345 
346     /**
347      * Return the number of elements that are being omitted because they are common with the parent Throwable's stack
348      * trace.
349      * 
350      * @return The number of elements omitted from the stack trace.
351      */
352     public int getCommonElementCount() {
353         return this.commonElementCount;
354     }
355 
356     /**
357      * Gets the stack trace including packaging information.
358      * 
359      * @return The stack trace including packaging information.
360      */
361     public ExtendedStackTraceElement[] getExtendedStackTrace() {
362         return this.extendedStackTrace;
363     }
364 
365     /**
366      * Format the stack trace including packaging information.
367      * 
368      * @return The formatted stack trace including packaging information.
369      */
370     public String getExtendedStackTraceAsString() {
371         return this.getExtendedStackTraceAsString(null);
372     }
373 
374     /**
375      * Format the stack trace including packaging information.
376      * 
377      * @param ignorePackages
378      *        List of packages to be ignored in the trace.
379      * @return The formatted stack trace including packaging information.
380      */
381     public String getExtendedStackTraceAsString(final List<String> ignorePackages) {
382         final StringBuilder sb = new StringBuilder(this.name);
383         final String msg = this.message;
384         if (msg != null) {
385             sb.append(": ").append(msg);
386         }
387         sb.append(EOL);
388         final StackTraceElement[] causedTrace = this.throwable != null ? this.throwable.getStackTrace() : null;
389         this.formatElements(sb, "", 0, causedTrace, this.extendedStackTrace, ignorePackages);
390         this.formatSuppressed(sb, "\t", this.suppressedProxies, ignorePackages);
391         this.formatCause(sb, "", this.causeProxy, ignorePackages);
392         return sb.toString();
393     }
394 
395     public String getLocalizedMessage() {
396         return this.localizedMessage;
397     }
398 
399     public String getMessage() {
400         return this.message;
401     }
402 
403     /**
404      * Return the FQCN of the Throwable.
405      * 
406      * @return The FQCN of the Throwable.
407      */
408     public String getName() {
409         return this.name;
410     }
411 
412     public StackTraceElement[] getStackTrace() {
413         return this.throwable == null ? null : this.throwable.getStackTrace();
414     }
415 
416     /**
417      * Gets proxies for suppressed exceptions.
418      * 
419      * @return proxies for suppressed exceptions.
420      */
421     public ThrowableProxy[] getSuppressedProxies() {
422         return this.suppressedProxies;
423     }
424 
425     /**
426      * Format the suppressed Throwables.
427      * 
428      * @return The formatted suppressed Throwables.
429      */
430     public String getSuppressedStackTrace() {
431         final ThrowableProxy[] suppressed = this.getSuppressedProxies();
432         if (suppressed == null || suppressed.length == 0) {
433             return Strings.EMPTY;
434         }
435         final StringBuilder sb = new StringBuilder("Suppressed Stack Trace Elements:").append(EOL);
436         for (final ThrowableProxy proxy : suppressed) {
437             sb.append(proxy.getExtendedStackTraceAsString());
438         }
439         return sb.toString();
440     }
441 
442     /**
443      * The throwable or null if this object is deserialized from XML or JSON.
444      * 
445      * @return The throwable or null if this object is deserialized from XML or JSON.
446      */
447     public Throwable getThrowable() {
448         return this.throwable;
449     }
450 
451     @Override
452     public int hashCode() {
453         final int prime = 31;
454         int result = 1;
455         result = prime * result + (this.causeProxy == null ? 0 : this.causeProxy.hashCode());
456         result = prime * result + this.commonElementCount;
457         result = prime * result + (this.extendedStackTrace == null ? 0 : Arrays.hashCode(this.extendedStackTrace));
458         result = prime * result + (this.suppressedProxies == null ? 0 : Arrays.hashCode(this.suppressedProxies));
459         result = prime * result + (this.name == null ? 0 : this.name.hashCode());
460         return result;
461     }
462 
463     private boolean ignoreElement(final StackTraceElement element, final List<String> ignorePackages) {
464         final String className = element.getClassName();
465         for (final String pkg : ignorePackages) {
466             if (className.startsWith(pkg)) {
467                 return true;
468             }
469         }
470         return false;
471     }
472 
473     /**
474      * Loads classes not located via Reflection.getCallerClass.
475      * 
476      * @param lastLoader
477      *        The ClassLoader that loaded the Class that called this Class.
478      * @param className
479      *        The name of the Class.
480      * @return The Class object for the Class or null if it could not be located.
481      */
482     private Class<?> loadClass(final ClassLoader lastLoader, final String className) {
483         // XXX: this is overly complicated
484         Class<?> clazz;
485         if (lastLoader != null) {
486             try {
487                 clazz = Loader.initializeClass(className, lastLoader);
488                 if (clazz != null) {
489                     return clazz;
490                 }
491             } catch (final Throwable ignore) {
492                 // Ignore exception.
493             }
494         }
495         try {
496             clazz = Loader.loadClass(className);
497         } catch (final ClassNotFoundException ignored) {
498             return initializeClass(className);
499         } catch (final NoClassDefFoundError ignored) {
500             return initializeClass(className);
501         }
502         return clazz;
503     }
504 
505     private Class<?> initializeClass(final String className) {
506         try {
507             return Loader.initializeClass(className, this.getClass().getClassLoader());
508         } catch (final ClassNotFoundException ignore) {
509             return null;
510         } catch (final NoClassDefFoundError ignore) {
511             return null;
512         }
513     }
514 
515     /**
516      * Construct the CacheEntry from the Class's information.
517      * 
518      * @param stackTraceElement
519      *        The stack trace element
520      * @param callerClass
521      *        The Class.
522      * @param exact
523      *        True if the class was obtained via Reflection.getCallerClass.
524      * 
525      * @return The CacheEntry.
526      */
527     private CacheEntry toCacheEntry(final StackTraceElement stackTraceElement, final Class<?> callerClass,
528             final boolean exact) {
529         String location = "?";
530         String version = "?";
531         ClassLoader lastLoader = null;
532         if (callerClass != null) {
533             try {
534                 final CodeSource source = callerClass.getProtectionDomain().getCodeSource();
535                 if (source != null) {
536                     final URL locationURL = source.getLocation();
537                     if (locationURL != null) {
538                         final String str = locationURL.toString().replace('\\', '/');
539                         int index = str.lastIndexOf("/");
540                         if (index >= 0 && index == str.length() - 1) {
541                             index = str.lastIndexOf("/", index - 1);
542                             location = str.substring(index + 1);
543                         } else {
544                             location = str.substring(index + 1);
545                         }
546                     }
547                 }
548             } catch (final Exception ex) {
549                 // Ignore the exception.
550             }
551             final Package pkg = callerClass.getPackage();
552             if (pkg != null) {
553                 final String ver = pkg.getImplementationVersion();
554                 if (ver != null) {
555                     version = ver;
556                 }
557             }
558             lastLoader = callerClass.getClassLoader();
559         }
560         return new CacheEntry(new ExtendedClassInfo(exact, location, version), lastLoader);
561     }
562 
563     /**
564      * Resolve all the stack entries in this stack trace that are not common with the parent.
565      * 
566      * @param stack
567      *        The callers Class stack.
568      * @param map
569      *        The cache of CacheEntry objects.
570      * @param rootTrace
571      *        The first stack trace resolve or null.
572      * @param stackTrace
573      *        The stack trace being resolved.
574      * @return The StackTracePackageElement array.
575      */
576     ExtendedStackTraceElement[] toExtendedStackTrace(final Stack<Class<?>> stack, final Map<String, CacheEntry> map,
577             final StackTraceElement[] rootTrace, final StackTraceElement[] stackTrace) {
578         int stackLength;
579         if (rootTrace != null) {
580             int rootIndex = rootTrace.length - 1;
581             int stackIndex = stackTrace.length - 1;
582             while (rootIndex >= 0 && stackIndex >= 0 && rootTrace[rootIndex].equals(stackTrace[stackIndex])) {
583                 --rootIndex;
584                 --stackIndex;
585             }
586             this.commonElementCount = stackTrace.length - 1 - stackIndex;
587             stackLength = stackIndex + 1;
588         } else {
589             this.commonElementCount = 0;
590             stackLength = stackTrace.length;
591         }
592         final ExtendedStackTraceElement[] extStackTrace = new ExtendedStackTraceElement[stackLength];
593         Class<?> clazz = stack.isEmpty() ? null : stack.peek();
594         ClassLoader lastLoader = null;
595         for (int i = stackLength - 1; i >= 0; --i) {
596             final StackTraceElement stackTraceElement = stackTrace[i];
597             final String className = stackTraceElement.getClassName();
598             // The stack returned from getCurrentStack may be missing entries for java.lang.reflect.Method.invoke()
599             // and its implementation. The Throwable might also contain stack entries that are no longer
600             // present as those methods have returned.
601             ExtendedClassInfo extClassInfo;
602             if (clazz != null && className.equals(clazz.getName())) {
603                 final CacheEntry entry = this.toCacheEntry(stackTraceElement, clazz, true);
604                 extClassInfo = entry.element;
605                 lastLoader = entry.loader;
606                 stack.pop();
607                 clazz = stack.isEmpty() ? null : stack.peek();
608             } else {
609                 final CacheEntry cacheEntry = map.get(className);
610                 if (cacheEntry != null) {
611                     final CacheEntry entry = cacheEntry;
612                     extClassInfo = entry.element;
613                     if (entry.loader != null) {
614                         lastLoader = entry.loader;
615                     }
616                 } else {
617                     final CacheEntry entry = this.toCacheEntry(stackTraceElement,
618                             this.loadClass(lastLoader, className), false);
619                     extClassInfo = entry.element;
620                     map.put(stackTraceElement.toString(), entry);
621                     if (entry.loader != null) {
622                         lastLoader = entry.loader;
623                     }
624                 }
625             }
626             extStackTrace[i] = new ExtendedStackTraceElement(stackTraceElement, extClassInfo);
627         }
628         return extStackTrace;
629     }
630 
631     @Override
632     public String toString() {
633         final String msg = this.message;
634         return msg != null ? this.name + ": " + msg : this.name;
635     }
636 
637     private ThrowableProxy[] toSuppressedProxies(final Throwable thrown, Set<Throwable> suppressedVisited) {
638         try {
639             final Throwable[] suppressed = thrown.getSuppressed();
640             if (suppressed == null) {
641                 return EMPTY_THROWABLE_PROXY_ARRAY;
642             }
643             final List<ThrowableProxy> proxies = new ArrayList<>(suppressed.length);
644             if (suppressedVisited == null) {
645                 suppressedVisited = new HashSet<>(proxies.size());
646             }
647             for (int i = 0; i < suppressed.length; i++) {
648                 final Throwable candidate = suppressed[i];
649                 if (!suppressedVisited.contains(candidate)) {
650                     suppressedVisited.add(candidate);
651                     proxies.add(new ThrowableProxy(candidate, suppressedVisited));
652                 }
653             }
654             return proxies.toArray(new ThrowableProxy[proxies.size()]);
655         } catch (final Exception e) {
656             StatusLogger.getLogger().error(e);
657         }
658         return null;
659     }
660 }