001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools.bugreport;
003
004import java.io.PrintWriter;
005import java.io.Serializable;
006import java.lang.reflect.InvocationTargetException;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collection;
010import java.util.Collections;
011import java.util.ConcurrentModificationException;
012import java.util.HashMap;
013import java.util.IdentityHashMap;
014import java.util.Iterator;
015import java.util.LinkedList;
016import java.util.Map;
017import java.util.Map.Entry;
018import java.util.NoSuchElementException;
019import java.util.Set;
020import java.util.function.Supplier;
021
022import org.openstreetmap.josm.tools.Logging;
023import org.openstreetmap.josm.tools.StreamUtils;
024
025/**
026 * This is a special exception that cannot be directly thrown.
027 * <p>
028 * It is used to capture more information about an exception that was already thrown.
029 *
030 * @author Michael Zangl
031 * @see BugReport
032 * @since 10285
033 */
034public class ReportedException extends RuntimeException {
035    /**
036     * How many entries of a collection to include in the bug report.
037     */
038    private static final int MAX_COLLECTION_ENTRIES = 30;
039
040    private static final long serialVersionUID = 737333873766201033L;
041
042    /**
043     * We capture all stack traces on exception creation. This allows us to trace synchonization problems better.
044     * We cannot be really sure what happened but we at least see which threads
045     */
046    private final transient Map<Thread, StackTraceElement[]> allStackTraces = new HashMap<>();
047    private final LinkedList<Section> sections = new LinkedList<>();
048    private final transient Thread caughtOnThread;
049    private String methodWarningFrom;
050
051    ReportedException(Throwable exception) {
052        this(exception, Thread.currentThread());
053    }
054
055    ReportedException(Throwable exception, Thread caughtOnThread) {
056        super(exception);
057
058        try {
059            allStackTraces.putAll(Thread.getAllStackTraces());
060        } catch (SecurityException e) {
061            Logging.log(Logging.LEVEL_ERROR, "Unable to get thread stack traces", e);
062        }
063        this.caughtOnThread = caughtOnThread;
064    }
065
066    /**
067     * Displays a warning for this exception. The program can then continue normally. Does not block.
068     */
069    public void warn() {
070        methodWarningFrom = BugReport.getCallingMethod(2);
071        try {
072            BugReportQueue.getInstance().submit(this);
073        } catch (RuntimeException e) { // NOPMD
074            e.printStackTrace();
075        }
076    }
077
078    /**
079     * Starts a new debug data section. This normally does not need to be called manually.
080     *
081     * @param sectionName
082     *            The section name.
083     */
084    public void startSection(String sectionName) {
085        sections.add(new Section(sectionName));
086    }
087
088    /**
089     * Prints the captured data of this report to a {@link PrintWriter}.
090     *
091     * @param out
092     *            The writer to print to.
093     */
094    public void printReportDataTo(PrintWriter out) {
095        out.println("=== REPORTED CRASH DATA ===");
096        for (Section s : sections) {
097            s.printSection(out);
098            out.println();
099        }
100
101        if (methodWarningFrom != null) {
102            out.println("Warning issued by: " + methodWarningFrom);
103            out.println();
104        }
105    }
106
107    /**
108     * Prints the stack trace of this report to a {@link PrintWriter}.
109     *
110     * @param out
111     *            The writer to print to.
112     */
113    public void printReportStackTo(PrintWriter out) {
114        out.println("=== STACK TRACE ===");
115        out.println(niceThreadName(caughtOnThread));
116        getCause().printStackTrace(out);
117        out.println();
118    }
119
120    /**
121     * Prints the stack traces for other threads of this report to a {@link PrintWriter}.
122     *
123     * @param out
124     *            The writer to print to.
125     */
126    public void printReportThreadsTo(PrintWriter out) {
127        out.println("=== RUNNING THREADS ===");
128        for (Entry<Thread, StackTraceElement[]> thread : allStackTraces.entrySet()) {
129            out.println(niceThreadName(thread.getKey()));
130            if (caughtOnThread.equals(thread.getKey())) {
131                out.println("Stacktrace see above.");
132            } else {
133                for (StackTraceElement e : thread.getValue()) {
134                    out.println(e);
135                }
136            }
137            out.println();
138        }
139    }
140
141    private static String niceThreadName(Thread thread) {
142        StringBuilder name = new StringBuilder("Thread: ").append(thread.getName()).append(" (").append(thread.getId()).append(')');
143        ThreadGroup threadGroup = thread.getThreadGroup();
144        if (threadGroup != null) {
145            name.append(" of ").append(threadGroup.getName());
146        }
147        return name.toString();
148    }
149
150    /**
151     * Checks if this exception is considered the same as an other exception. This is the case if both have the same cause and message.
152     *
153     * @param e
154     *            The exception to check against.
155     * @return <code>true</code> if they are considered the same.
156     */
157    public boolean isSame(ReportedException e) {
158        if (!getMessage().equals(e.getMessage())) {
159            return false;
160        }
161
162        return hasSameStackTrace(new CauseTraceIterator(), e.getCause());
163    }
164
165    private static boolean hasSameStackTrace(CauseTraceIterator causeTraceIterator, Throwable e2) {
166        if (!causeTraceIterator.hasNext()) {
167            // all done.
168            return true;
169        }
170        Throwable e1 = causeTraceIterator.next();
171        StackTraceElement[] t1 = e1.getStackTrace();
172        StackTraceElement[] t2 = e2.getStackTrace();
173
174        if (!Arrays.equals(t1, t2)) {
175            return false;
176        }
177
178        Throwable c1 = e1.getCause();
179        Throwable c2 = e2.getCause();
180        if ((c1 == null) != (c2 == null)) {
181            return false;
182        } else if (c1 != null) {
183            return hasSameStackTrace(causeTraceIterator, c2);
184        } else {
185            return true;
186        }
187    }
188
189    /**
190     * Adds some debug values to this exception. The value is converted to a string. Errors during conversion are handled.
191     *
192     * @param key
193     *            The key to add this for. Does not need to be unique but it would be nice.
194     * @param value
195     *            The value.
196     * @return This exception for easy chaining.
197     */
198    public ReportedException put(String key, Object value) {
199        return put(key, () -> value);
200    }
201
202    /**
203    * Adds some debug values to this exception. This method automatically catches errors that occur during the production of the value.
204    *
205    * @param key
206    *            The key to add this for. Does not need to be unique but it would be nice.
207    * @param valueSupplier
208    *            A supplier that is called once to get the value.
209    * @return This exception for easy chaining.
210    * @since 10586
211    */
212    public ReportedException put(String key, Supplier<Object> valueSupplier) {
213        String string;
214        try {
215            Object value = valueSupplier.get();
216            if (value == null) {
217                string = "null";
218            } else if (value instanceof Collection) {
219                string = makeCollectionNice((Collection<?>) value);
220            } else if (value.getClass().isArray()) {
221                string = makeCollectionNice(Arrays.asList(value));
222            } else {
223                string = value.toString();
224            }
225        } catch (RuntimeException t) { // NOPMD
226            Logging.warn(t);
227            string = "<Error calling toString()>";
228        }
229        sections.getLast().put(key, string);
230        return this;
231    }
232
233    private static String makeCollectionNice(Collection<?> value) {
234        int lines = 0;
235        StringBuilder str = new StringBuilder(32);
236        for (Object e : value) {
237            str.append("\n    - ");
238            if (lines <= MAX_COLLECTION_ENTRIES) {
239                str.append(e);
240            } else {
241                str.append("\n    ... (")
242                   .append(value.size())
243                   .append(" entries)");
244                break;
245            }
246        }
247        return str.toString();
248    }
249
250    @Override
251    public String toString() {
252        return "ReportedException [thread=" + caughtOnThread + ", exception=" + getCause()
253                + ", methodWarningFrom=" + methodWarningFrom + ']';
254    }
255
256    /**
257     * Check if this exception may be caused by a threading issue.
258     * @return <code>true</code> if it is.
259     * @since 10585
260     */
261    public boolean mayHaveConcurrentSource() {
262        return StreamUtils.toStream(CauseTraceIterator::new)
263                .anyMatch(t -> t instanceof ConcurrentModificationException || t instanceof InvocationTargetException);
264    }
265
266    /**
267     * Check if this is caused by an out of memory situaition
268     * @return <code>true</code> if it is.
269     * @since 10819
270     */
271    public boolean isOutOfMemory() {
272        return StreamUtils.toStream(CauseTraceIterator::new).anyMatch(t -> t instanceof OutOfMemoryError);
273    }
274
275    /**
276     * Iterates over the causes for this exception. Ignores cycles and aborts iteration then.
277     * @author Michal Zangl
278     * @since 10585
279     */
280    private final class CauseTraceIterator implements Iterator<Throwable> {
281        private Throwable current = getCause();
282        private final Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
283
284        @Override
285        public boolean hasNext() {
286            return current != null;
287        }
288
289        @Override
290        public Throwable next() {
291            if (!hasNext()) {
292                throw new NoSuchElementException();
293            }
294            Throwable toReturn = current;
295            advance();
296            return toReturn;
297        }
298
299        private void advance() {
300            dejaVu.add(current);
301            current = current.getCause();
302            if (current != null && dejaVu.contains(current)) {
303                current = null;
304            }
305        }
306    }
307
308    private static class SectionEntry implements Serializable {
309
310        private static final long serialVersionUID = 1L;
311
312        private final String key;
313        private final String value;
314
315        SectionEntry(String key, String value) {
316            this.key = key;
317            this.value = value;
318        }
319
320        /**
321         * Prints this entry to the output stream in a line.
322         * @param out The stream to print to.
323         */
324        public void print(PrintWriter out) {
325            out.print(" - ");
326            out.print(key);
327            out.print(": ");
328            out.println(value);
329        }
330    }
331
332    private static class Section implements Serializable {
333
334        private static final long serialVersionUID = 1L;
335
336        private final String sectionName;
337        private final ArrayList<SectionEntry> entries = new ArrayList<>();
338
339        Section(String sectionName) {
340            this.sectionName = sectionName;
341        }
342
343        /**
344         * Add a key/value entry to this section.
345         * @param key The key. Need not be unique.
346         * @param value The value.
347         */
348        public void put(String key, String value) {
349            entries.add(new SectionEntry(key, value));
350        }
351
352        /**
353         * Prints this section to the output stream.
354         * @param out The stream to print to.
355         */
356        public void printSection(PrintWriter out) {
357            out.println(sectionName + ':');
358            if (entries.isEmpty()) {
359                out.println("No data collected.");
360            } else {
361                for (SectionEntry e : entries) {
362                    e.print(out);
363                }
364            }
365        }
366    }
367}