001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools.bugreport;
003
004import java.io.PrintWriter;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.Collection;
008import java.util.Collections;
009import java.util.IdentityHashMap;
010import java.util.LinkedList;
011import java.util.Map;
012import java.util.Map.Entry;
013import java.util.Set;
014
015import org.openstreetmap.josm.Main;
016
017/**
018 * This is a special exception that cannot be directly thrown.
019 * <p>
020 * It is used to capture more information about an exception that was already thrown.
021 *
022 * @author Michael Zangl
023 * @see BugReport
024 * @since 10285
025 */
026public class ReportedException extends RuntimeException {
027    private static final int MAX_COLLECTION_ENTRIES = 30;
028    /**
029     *
030     */
031    private static final long serialVersionUID = 737333873766201033L;
032    /**
033     * We capture all stack traces on exception creation. This allows us to trace synchonization problems better. We cannot be really sure what
034     * happened but we at least see which threads
035     */
036    private final transient Map<Thread, StackTraceElement[]> allStackTraces;
037    private final transient LinkedList<Section> sections = new LinkedList<>();
038    private final transient Thread caughtOnThread;
039    private final Throwable exception;
040    private String methodWarningFrom;
041
042    ReportedException(Throwable exception) {
043        this(exception, Thread.currentThread());
044    }
045
046    ReportedException(Throwable exception, Thread caughtOnThread) {
047        super(exception);
048        this.exception = exception;
049
050        allStackTraces = Thread.getAllStackTraces();
051        this.caughtOnThread = caughtOnThread;
052    }
053
054    /**
055     * Displays a warning for this exception. The program can then continue normally. Does not block.
056     */
057    public void warn() {
058        methodWarningFrom = BugReport.getCallingMethod(2);
059        // TODO: Open the dialog.
060    }
061
062    /**
063     * Starts a new debug data section. This normally does not need to be called manually.
064     *
065     * @param sectionName
066     *            The section name.
067     */
068    public void startSection(String sectionName) {
069        sections.add(new Section(sectionName));
070    }
071
072    /**
073     * Prints the captured data of this report to a {@link PrintWriter}.
074     *
075     * @param out
076     *            The writer to print to.
077     */
078    public void printReportDataTo(PrintWriter out) {
079        out.println("=== REPORTED CRASH DATA ===");
080        for (Section s : sections) {
081            s.printSection(out);
082            out.println();
083        }
084
085        if (methodWarningFrom != null) {
086            out.println("Warning issued by: " + methodWarningFrom);
087            out.println();
088        }
089    }
090
091    /**
092     * Prints the stack trace of this report to a {@link PrintWriter}.
093     *
094     * @param out
095     *            The writer to print to.
096     */
097    public void printReportStackTo(PrintWriter out) {
098        out.println("=== STACK TRACE ===");
099        out.println(niceThreadName(caughtOnThread));
100        getCause().printStackTrace(out);
101        out.println();
102    }
103
104    /**
105     * Prints the stack traces for other threads of this report to a {@link PrintWriter}.
106     *
107     * @param out
108     *            The writer to print to.
109     */
110    public void printReportThreadsTo(PrintWriter out) {
111        out.println("=== RUNNING THREADS ===");
112        for (Entry<Thread, StackTraceElement[]> thread : allStackTraces.entrySet()) {
113            out.println(niceThreadName(thread.getKey()));
114            if (caughtOnThread.equals(thread.getKey())) {
115                out.println("Stacktrace see above.");
116            } else {
117                for (StackTraceElement e : thread.getValue()) {
118                    out.println(e);
119                }
120            }
121            out.println();
122        }
123    }
124
125    private static String niceThreadName(Thread thread) {
126        String name = "Thread: " + thread.getName() + " (" + thread.getId() + ')';
127        ThreadGroup threadGroup = thread.getThreadGroup();
128        if (threadGroup != null) {
129            name += " of " + threadGroup.getName();
130        }
131        return name;
132    }
133
134    /**
135     * Checks if this exception is considered the same as an other exception. This is the case if both have the same cause and message.
136     *
137     * @param e
138     *            The exception to check against.
139     * @return <code>true</code> if they are considered the same.
140     */
141    public boolean isSame(ReportedException e) {
142        if (!getMessage().equals(e.getMessage())) {
143            return false;
144        }
145
146        Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
147        return hasSameStackTrace(dejaVu, this.exception, e.exception);
148    }
149
150    private static boolean hasSameStackTrace(Set<Throwable> dejaVu, Throwable e1, Throwable e2) {
151        if (dejaVu.contains(e1)) {
152            // cycle. If it was the same until here, we assume both have that cycle.
153            return true;
154        }
155        dejaVu.add(e1);
156
157        StackTraceElement[] t1 = e1.getStackTrace();
158        StackTraceElement[] t2 = e2.getStackTrace();
159
160        if (!Arrays.equals(t1, t2)) {
161            return false;
162        }
163
164        Throwable c1 = e1.getCause();
165        Throwable c2 = e2.getCause();
166        if ((c1 == null) != (c2 == null)) {
167            return false;
168        } else if (c1 != null) {
169            return hasSameStackTrace(dejaVu, c1, c2);
170        } else {
171            return true;
172        }
173    }
174
175    /**
176     * Adds some debug values to this exception.
177     *
178     * @param key
179     *            The key to add this for. Does not need to be unique but it would be nice.
180     * @param value
181     *            The value.
182     * @return This exception for easy chaining.
183     */
184    public ReportedException put(String key, Object value) {
185        String string;
186        try {
187            if (value == null) {
188                string = "null";
189            } else if (value instanceof Collection) {
190                string = makeCollectionNice((Collection<?>) value);
191            } else if (value.getClass().isArray()) {
192                string = makeCollectionNice(Arrays.asList(value));
193            } else {
194                string = value.toString();
195            }
196        } catch (RuntimeException t) {
197            Main.warn(t);
198            string = "<Error calling toString()>";
199        }
200        sections.getLast().put(key, string);
201        return this;
202    }
203
204    private static String makeCollectionNice(Collection<?> value) {
205        int lines = 0;
206        StringBuilder str = new StringBuilder();
207        for (Object e : value) {
208            str.append("\n    - ");
209            if (lines <= MAX_COLLECTION_ENTRIES) {
210                str.append(e);
211            } else {
212                str.append("\n    ... (")
213                   .append(value.size())
214                   .append(" entries)");
215                break;
216            }
217        }
218        return str.toString();
219    }
220
221    @Override
222    public String toString() {
223        return new StringBuilder(48)
224            .append("CrashReportedException [on thread ")
225            .append(caughtOnThread)
226            .append(']')
227            .toString();
228    }
229
230    private static class SectionEntry {
231        private final String key;
232        private final String value;
233
234        SectionEntry(String key, String value) {
235            this.key = key;
236            this.value = value;
237        }
238
239        /**
240         * Prints this entry to the output stream in a line.
241         * @param out The stream to print to.
242         */
243        public void print(PrintWriter out) {
244            out.print(" - ");
245            out.print(key);
246            out.print(": ");
247            out.println(value);
248        }
249    }
250
251    private static class Section {
252
253        private final String sectionName;
254        private final ArrayList<SectionEntry> entries = new ArrayList<>();
255
256        Section(String sectionName) {
257            this.sectionName = sectionName;
258        }
259
260        /**
261         * Add a key/value entry to this section.
262         * @param key The key. Need not be unique.
263         * @param value The value.
264         */
265        public void put(String key, String value) {
266            entries.add(new SectionEntry(key, value));
267        }
268
269        /**
270         * Prints this section to the output stream.
271         * @param out The stream to print to.
272         */
273        public void printSection(PrintWriter out) {
274            out.println(sectionName + ':');
275            if (entries.isEmpty()) {
276                out.println("No data collected.");
277            } else {
278                for (SectionEntry e : entries) {
279                    e.print(out);
280                }
281            }
282        }
283    }
284}