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