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