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