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