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