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.io.StringWriter;
007import java.util.concurrent.CopyOnWriteArrayList;
008import java.util.function.Predicate;
009
010/**
011 * This class contains utility methods to create and handle a bug report.
012 * <p>
013 * It allows you to configure the format and request to send the bug report.
014 * <p>
015 * It also contains the main entry point for all components to use the bug report system: Call {@link #intercept(Throwable)} to start handling an
016 * exception.
017 * <h1> Handling Exceptions </h1>
018 * In your code, you should add try...catch blocks for any runtime exceptions that might happen. It is fine to catch throwable there.
019 * <p>
020 * You should then add some debug information there. This can be the OSM ids that caused the error, information on the data you were working on
021 * or other local variables. Make sure that no exceptions may occur while computing the values. It is best to send plain local variables to
022 * put(...). If you need to do computations, put them into a lambda expression. Then simply throw the throwable you got from the bug report.
023 * The global exception handler will do the rest.
024 * <pre>
025 * int id = ...;
026 * String tag = "...";
027 * try {
028 *   ... your code ...
029 * } catch (RuntimeException t) {
030 *   throw BugReport.intercept(t).put("id", id).put("tag", () -&gt; x.getTag());
031 * }
032 * </pre>
033 *
034 * Instead of re-throwing, you can call {@link ReportedException#warn()}. This will display a warning to the user and allow it to either report
035 * the exception or ignore it.
036 *
037 * @author Michael Zangl
038 * @since 10285
039 */
040public final class BugReport implements Serializable {
041    private static final long serialVersionUID = 1L;
042
043    private boolean includeStatusReport = true;
044    private boolean includeData = true;
045    private boolean includeAllStackTraces;
046    private final ReportedException exception;
047    private final CopyOnWriteArrayList<BugReportListener> listeners = new CopyOnWriteArrayList<>();
048
049    /**
050     * Create a new bug report
051     * @param e The {@link ReportedException} to use. No more data should be added after creating the report.
052     */
053    public BugReport(ReportedException e) {
054        this.exception = e;
055        includeAllStackTraces = e.mayHaveConcurrentSource();
056    }
057
058    /**
059     * Determines if this report should include a system status report
060     * @return <code>true</code> to include it.
061     * @since 10597
062     */
063    public boolean isIncludeStatusReport() {
064        return includeStatusReport;
065    }
066
067    /**
068     * Set if this report should include a system status report
069     * @param includeStatusReport if the status report should be included
070     * @since 10585
071     */
072    public void setIncludeStatusReport(boolean includeStatusReport) {
073        this.includeStatusReport = includeStatusReport;
074        fireChange();
075    }
076
077    /**
078     * Determines if this report should include the data that was traced.
079     * @return <code>true</code> to include it.
080     * @since 10597
081     */
082    public boolean isIncludeData() {
083        return includeData;
084    }
085
086    /**
087     * Set if this report should include the data that was traced.
088     * @param includeData if data should be included
089     * @since 10585
090     */
091    public void setIncludeData(boolean includeData) {
092        this.includeData = includeData;
093        fireChange();
094    }
095
096    /**
097     * Determines if this report should include the stack traces for all other threads.
098     * @return <code>true</code> to include it.
099     * @since 10597
100     */
101    public boolean isIncludeAllStackTraces() {
102        return includeAllStackTraces;
103    }
104
105    /**
106     * Sets if this report should include the stack traces for all other threads.
107     * @param includeAllStackTraces if all stack traces should be included
108     * @since 10585
109     */
110    public void setIncludeAllStackTraces(boolean includeAllStackTraces) {
111        this.includeAllStackTraces = includeAllStackTraces;
112        fireChange();
113    }
114
115    /**
116     * Gets the full string that should be send as error report.
117     * @param header header text for the error report
118     * @return The string.
119     * @since 10585
120     */
121    public String getReportText(String header) {
122        StringWriter stringWriter = new StringWriter();
123        PrintWriter out = new PrintWriter(stringWriter);
124        if (isIncludeStatusReport()) {
125            try {
126                out.println(header);
127            } catch (RuntimeException e) { // NOPMD
128                out.println("Could not generate status report: " + e.getMessage());
129            }
130        }
131        if (isIncludeData()) {
132            exception.printReportDataTo(out);
133        }
134        exception.printReportStackTo(out);
135        if (isIncludeAllStackTraces()) {
136            exception.printReportThreadsTo(out);
137        }
138        return stringWriter.toString().replaceAll("\r", "");
139    }
140
141    /**
142     * Add a new change listener.
143     * @param listener The listener
144     * @since 10585
145     */
146    public void addChangeListener(BugReportListener listener) {
147        listeners.add(listener);
148    }
149
150    /**
151     * Remove a change listener.
152     * @param listener The listener
153     * @since 10585
154     */
155    public void removeChangeListener(BugReportListener listener) {
156        listeners.remove(listener);
157    }
158
159    private void fireChange() {
160        listeners.stream().forEach(l -> l.bugReportChanged(this));
161    }
162
163    /**
164     * This should be called whenever you want to add more information to a given exception.
165     * @param t The throwable that was thrown.
166     * @return A {@link ReportedException} to which you can add additional information.
167     */
168    public static ReportedException intercept(Throwable t) {
169        ReportedException e;
170        if (t instanceof ReportedException) {
171            e = (ReportedException) t;
172        } else {
173            e = new ReportedException(t);
174        }
175        e.startSection(getCallingMethod(2));
176        return e;
177    }
178
179    /**
180     * Find the method that called us.
181     *
182     * @param offset
183     *            How many methods to look back in the stack trace. 1 gives the method calling this method, 0 gives you getCallingMethod().
184     * @return The method name.
185     */
186    public static String getCallingMethod(int offset) {
187        StackTraceElement found = getCallingMethod(offset + 1, BugReport.class.getName(), "getCallingMethod"::equals);
188        if (found != null) {
189            return found.getClassName().replaceFirst(".*\\.", "") + '#' + found.getMethodName();
190        } else {
191            return "?";
192        }
193    }
194
195    /**
196     * Find the method that called the given method on the current stack trace.
197     * @param offset
198     *           How many methods to look back in the stack trace.
199     *           1 gives the method calling this method, 0 gives you the method with the given name..
200     * @param className The name of the class to search for
201     * @param methodName The name of the method to search for
202     * @return The class and method name or null if it is unknown.
203     */
204    public static StackTraceElement getCallingMethod(int offset, String className, Predicate<String> methodName) {
205        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
206        for (int i = 0; i < stackTrace.length - offset; i++) {
207            StackTraceElement element = stackTrace[i];
208            if (className.equals(element.getClassName()) && methodName.test(element.getMethodName())) {
209                return stackTrace[i + offset];
210            }
211        }
212        return null;
213    }
214
215    /**
216     * A listener that listens to changes to this report.
217     * @author Michael Zangl
218     * @since 10585
219     */
220    @FunctionalInterface
221    public interface BugReportListener {
222        /**
223         * Called whenever this bug report was changed, e.g. the data to be included in it.
224         * @param report The report that was changed.
225         */
226        void bugReportChanged(BugReport report);
227    }
228}