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