View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements. See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache license, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License. You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the license for the specific language governing permissions and
    15   * limitations under the license.
    16   */
    17  package org.apache.logging.log4j.core.impl;
    18
    19  import java.io.Serializable;
    20  import java.net.URL;
    21  import java.security.CodeSource;
    22  import java.util.Arrays;
    23  import java.util.HashMap;
    24  import java.util.List;
    25  import java.util.Map;
    26  import java.util.Stack;
    27
    28  import org.apache.logging.log4j.core.util.Loader;
    29  import org.apache.logging.log4j.core.util.Throwables;
    30  import org.apache.logging.log4j.status.StatusLogger;
    31  import org.apache.logging.log4j.util.ReflectionUtil;
    32  import org.apache.logging.log4j.util.Strings;
    33
    34  /**
    35   * Wraps a Throwable to add packaging information about each stack trace element.
    36   *
    37   * <p>
    38   * A proxy is used to represent a throwable that may not exist in a different class loader or JVM. When an application
    39   * deserializes a ThrowableProxy, the throwable may not be set, but the throwable's information is preserved in other
    40   * fields of the proxy like the message and stack trace.
    41   * </p>
    42   *
    43   * <p>
    44   * TODO: Move this class to org.apache.logging.log4j.core because it is used from LogEvent.
    45   * </p>
    46   * <p>
    47   * TODO: Deserialize: Try to rebuild Throwable if the target exception is in this class loader?
    48   * </p>
    49   */
    50  public class ThrowableProxy implements Serializable {
    51
    52      /**
    53       * Cached StackTracePackageElement and ClassLoader.
    54       * <p>
    55       * Consider this class private.
    56       * </p>
    57       */
    58      static class CacheEntry {
    59          private final ExtendedClassInfo element;
    60          private final ClassLoader loader;
    61
    62          public CacheEntry(final ExtendedClassInfo element, final ClassLoader loader) {
    63              this.element = element;
    64              this.loader = loader;
    65          }
    66      }
    67
    68      private static final ThrowableProxy[] EMPTY_THROWABLE_PROXY_ARRAY = new ThrowableProxy[0];
    69
    70      private static final char EOL = '\n';
    71
    72      private static final long serialVersionUID = -2752771578252251910L;
    73
    74      private final ThrowableProxy causeProxy;
    75
    76      private int commonElementCount;
    77
    78      private final ExtendedStackTraceElement[] extendedStackTrace;
    79
    80      private final String localizedMessage;
    81
    82      private final String message;
    83
    84      private final String name;
    85
    86      private final ThrowableProxy[] suppressedProxies;
    87
    88      private final transient Throwable throwable;
    89
    90      /**
    91       * For JSON and XML IO via Jackson.
    92       */
    93      @SuppressWarnings("unused")
    94      private ThrowableProxy() {
    95          this.throwable = null;
    96          this.name = null;
    97          this.extendedStackTrace = null;
    98          this.causeProxy = null;
    99          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 }