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.Arrays;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Stack;
027
028import org.apache.logging.log4j.core.util.Loader;
029import org.apache.logging.log4j.core.util.Throwables;
030import org.apache.logging.log4j.status.StatusLogger;
031import org.apache.logging.log4j.util.ReflectionUtil;
032import 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 */
050public 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}