// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.tools.bugreport;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import org.openstreetmap.josm.actions.ShowStatusReportAction;
/**
* This class contains utility methods to create and handle a bug report.
* <p>
* It allows you to configure the format and request to send the bug report.
* <p>
* It also contains the main entry point for all components to use the bug report system: Call {@link #intercept(Throwable)} to start handling an
* exception.
* <h1> Handling Exceptions </h1>
* In your code, you should add try...catch blocks for any runtime exceptions that might happen. It is fine to catch throwable there.
* <p>
* 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
* or other local variables. Make sure that no excpetions may occur while computing the values. It is best to send plain local variables to
* put(...). If you need to do computations, put them into a lambda expression. Then simply throw the throwable you got from the bug report.
* The global exception handler will do the rest.
* <pre>
* int id = ...;
* String tag = "...";
* try {
* ... your code ...
* } catch (RuntimeException t) {
* throw BugReport.intercept(t).put("id", id).put("tag", () -> x.getTag());
* }
* </pre>
*
* Instead of re-throwing, you can call {@link ReportedException#warn()}. This will display a warning to the user and allow it to either report
* the exception or ignore it.
*
* @author Michael Zangl
* @since 10285
*/
public final class BugReport implements Serializable {
private static final long serialVersionUID = 1L;
private boolean includeStatusReport = true;
private boolean includeData = true;
private boolean includeAllStackTraces;
private final ReportedException exception;
private final CopyOnWriteArrayList<BugReportListener> listeners = new CopyOnWriteArrayList<>();
/**
* Create a new bug report
* @param e The {@link ReportedException} to use. No more data should be added after creating the report.
*/
BugReport(ReportedException e) {
this.exception = e;
includeAllStackTraces = e.mayHaveConcurrentSource();
}
/**
* Determines if this report should include a system status report
* @return <code>true</code> to include it.
* @since 10597
*/
public boolean isIncludeStatusReport() {
return includeStatusReport;
}
/**
* Set if this report should include a system status report
* @param includeStatusReport if the status report should be included
* @since 10585
*/
public void setIncludeStatusReport(boolean includeStatusReport) {
this.includeStatusReport = includeStatusReport;
fireChange();
}
/**
* Determines if this report should include the data that was traced.
* @return <code>true</code> to include it.
* @since 10597
*/
public boolean isIncludeData() {
return includeData;
}
/**
* Set if this report should include the data that was traced.
* @param includeData if data should be included
* @since 10585
*/
public void setIncludeData(boolean includeData) {
this.includeData = includeData;
fireChange();
}
/**
* Determines if this report should include the stack traces for all other threads.
* @return <code>true</code> to include it.
* @since 10597
*/
public boolean isIncludeAllStackTraces() {
return includeAllStackTraces;
}
/**
* Sets if this report should include the stack traces for all other threads.
* @param includeAllStackTraces if all stack traces should be included
* @since 10585
*/
public void setIncludeAllStackTraces(boolean includeAllStackTraces) {
this.includeAllStackTraces = includeAllStackTraces;
fireChange();
}
/**
* Gets the full string that should be send as error report.
* @return The string.
* @since 10585
*/
public String getReportText() {
StringWriter stringWriter = new StringWriter();
PrintWriter out = new PrintWriter(stringWriter);
if (isIncludeStatusReport()) {
try {
out.println(ShowStatusReportAction.getReportHeader());
} catch (RuntimeException e) { // NOPMD
out.println("Could not generate status report: " + e.getMessage());
}
}
if (isIncludeData()) {
exception.printReportDataTo(out);
}
exception.printReportStackTo(out);
if (isIncludeAllStackTraces()) {
exception.printReportThreadsTo(out);
}
return stringWriter.toString().replaceAll("\r", "");
}
/**
* Add a new change listener.
* @param listener The listener
* @since 10585
*/
public void addChangeListener(BugReportListener listener) {
listeners.add(listener);
}
/**
* Remove a change listener.
* @param listener The listener
* @since 10585
*/
public void removeChangeListener(BugReportListener listener) {
listeners.remove(listener);
}
private void fireChange() {
listeners.stream().forEach(l -> l.bugReportChanged(this));
}
/**
* This should be called whenever you want to add more information to a given exception.
* @param t The throwable that was thrown.
* @return A {@link ReportedException} to which you can add additional information.
*/
public static ReportedException intercept(Throwable t) {
ReportedException e;
if (t instanceof ReportedException) {
e = (ReportedException) t;
} else {
e = new ReportedException(t);
}
e.startSection(getCallingMethod(2));
return e;
}
/**
* Find the method that called us.
*
* @param offset
* How many methods to look back in the stack trace. 1 gives the method calling this method, 0 gives you getCallingMethod().
* @return The method name.
*/
public static String getCallingMethod(int offset) {
StackTraceElement found = getCallingMethod(offset + 1, BugReport.class.getName(), "getCallingMethod"::equals);
if (found != null) {
return found.getClassName().replaceFirst(".*\\.", "") + '#' + found.getMethodName();
} else {
return "?";
}
}
/**
* Find the method that called the given method on the current stack trace.
* @param offset
* How many methods to look back in the stack trace.
* 1 gives the method calling this method, 0 gives you the method with the given name..
* @param className The name of the class to search for
* @param methodName The name of the method to search for
* @return The class and method name or null if it is unknown.
*/
public static StackTraceElement getCallingMethod(int offset, String className, Predicate<String> methodName) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (int i = 0; i < stackTrace.length - offset; i++) {
StackTraceElement element = stackTrace[i];
if (className.equals(element.getClassName()) && methodName.test(element.getMethodName())) {
return stackTrace[i + offset];
}
}
return null;
}
/**
* A listener that listens to changes to this report.
* @author Michael Zangl
* @since 10585
*/
@FunctionalInterface
public interface BugReportListener {
/**
* Called whenever this bug report was changed, e.g. the data to be included in it.
* @param report The report that was changed.
*/
void bugReportChanged(BugReport report);
}
}