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