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