[10285] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
| 2 | package org.openstreetmap.josm.tools.bugreport;
|
---|
| 3 |
|
---|
[10585] | 4 | import java.io.PrintWriter;
|
---|
[10649] | 5 | import java.io.Serializable;
|
---|
[10585] | 6 | import java.io.StringWriter;
|
---|
| 7 | import java.util.concurrent.CopyOnWriteArrayList;
|
---|
[10899] | 8 | import java.util.function.Predicate;
|
---|
[10585] | 9 |
|
---|
| 10 | import org.openstreetmap.josm.actions.ShowStatusReportAction;
|
---|
| 11 |
|
---|
[10285] | 12 | /**
|
---|
| 13 | * This class contains utility methods to create and handle a bug report.
|
---|
| 14 | * <p>
|
---|
| 15 | * It allows you to configure the format and request to send the bug report.
|
---|
| 16 | * <p>
|
---|
| 17 | * It also contains the main entry point for all components to use the bug report system: Call {@link #intercept(Throwable)} to start handling an
|
---|
| 18 | * exception.
|
---|
| 19 | * <h1> Handling Exceptions </h1>
|
---|
| 20 | * In your code, you should add try...catch blocks for any runtime exceptions that might happen. It is fine to catch throwable there.
|
---|
| 21 | * <p>
|
---|
| 22 | * 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
|
---|
| 23 | * or other local variables. Make sure that no excpetions may occur while computing the values. It is best to send plain local variables to
|
---|
[10586] | 24 | * put(...). If you need to do computations, put them into a lambda expression. Then simply throw the throwable you got from the bug report.
|
---|
| 25 | * The global exception handler will do the rest.
|
---|
[10285] | 26 | * <pre>
|
---|
| 27 | * int id = ...;
|
---|
| 28 | * String tag = "...";
|
---|
| 29 | * try {
|
---|
| 30 | * ... your code ...
|
---|
[10585] | 31 | * } catch (RuntimeException t) {
|
---|
[10745] | 32 | * throw BugReport.intercept(t).put("id", id).put("tag", () -> x.getTag());
|
---|
[10285] | 33 | * }
|
---|
| 34 | * </pre>
|
---|
| 35 | *
|
---|
| 36 | * Instead of re-throwing, you can call {@link ReportedException#warn()}. This will display a warning to the user and allow it to either report
|
---|
[10586] | 37 | * the exception or ignore it.
|
---|
[10285] | 38 | *
|
---|
| 39 | * @author Michael Zangl
|
---|
| 40 | * @since 10285
|
---|
| 41 | */
|
---|
[10649] | 42 | public final class BugReport implements Serializable {
|
---|
| 43 | private static final long serialVersionUID = 1L;
|
---|
| 44 |
|
---|
[10585] | 45 | private boolean includeStatusReport = true;
|
---|
| 46 | private boolean includeData = true;
|
---|
| 47 | private boolean includeAllStackTraces;
|
---|
[10598] | 48 | private final ReportedException exception;
|
---|
[10585] | 49 | private final CopyOnWriteArrayList<BugReportListener> listeners = new CopyOnWriteArrayList<>();
|
---|
| 50 |
|
---|
[10285] | 51 | /**
|
---|
| 52 | * Create a new bug report
|
---|
| 53 | * @param e The {@link ReportedException} to use. No more data should be added after creating the report.
|
---|
| 54 | */
|
---|
[10585] | 55 | BugReport(ReportedException e) {
|
---|
| 56 | this.exception = e;
|
---|
| 57 | includeAllStackTraces = e.mayHaveConcurrentSource();
|
---|
[10285] | 58 | }
|
---|
| 59 |
|
---|
| 60 | /**
|
---|
[10597] | 61 | * Determines if this report should include a system status report
|
---|
[10585] | 62 | * @return <code>true</code> to include it.
|
---|
[10597] | 63 | * @since 10597
|
---|
[10585] | 64 | */
|
---|
[10597] | 65 | public boolean isIncludeStatusReport() {
|
---|
[10585] | 66 | return includeStatusReport;
|
---|
| 67 | }
|
---|
| 68 |
|
---|
| 69 | /**
|
---|
| 70 | * Set if this report should include a system status report
|
---|
| 71 | * @param includeStatusReport if the status report should be included
|
---|
| 72 | * @since 10585
|
---|
| 73 | */
|
---|
| 74 | public void setIncludeStatusReport(boolean includeStatusReport) {
|
---|
| 75 | this.includeStatusReport = includeStatusReport;
|
---|
| 76 | fireChange();
|
---|
| 77 | }
|
---|
| 78 |
|
---|
| 79 | /**
|
---|
[10597] | 80 | * Determines if this report should include the data that was traced.
|
---|
[10585] | 81 | * @return <code>true</code> to include it.
|
---|
[10597] | 82 | * @since 10597
|
---|
[10585] | 83 | */
|
---|
[10597] | 84 | public boolean isIncludeData() {
|
---|
[10585] | 85 | return includeData;
|
---|
| 86 | }
|
---|
| 87 |
|
---|
| 88 | /**
|
---|
| 89 | * Set if this report should include the data that was traced.
|
---|
| 90 | * @param includeData if data should be included
|
---|
| 91 | * @since 10585
|
---|
| 92 | */
|
---|
| 93 | public void setIncludeData(boolean includeData) {
|
---|
| 94 | this.includeData = includeData;
|
---|
| 95 | fireChange();
|
---|
| 96 | }
|
---|
| 97 |
|
---|
| 98 | /**
|
---|
[10597] | 99 | * Determines if this report should include the stack traces for all other threads.
|
---|
[10585] | 100 | * @return <code>true</code> to include it.
|
---|
[10597] | 101 | * @since 10597
|
---|
[10585] | 102 | */
|
---|
[10597] | 103 | public boolean isIncludeAllStackTraces() {
|
---|
[10585] | 104 | return includeAllStackTraces;
|
---|
| 105 | }
|
---|
| 106 |
|
---|
| 107 | /**
|
---|
| 108 | * Sets if this report should include the stack traces for all other threads.
|
---|
| 109 | * @param includeAllStackTraces if all stack traces should be included
|
---|
| 110 | * @since 10585
|
---|
| 111 | */
|
---|
| 112 | public void setIncludeAllStackTraces(boolean includeAllStackTraces) {
|
---|
| 113 | this.includeAllStackTraces = includeAllStackTraces;
|
---|
| 114 | fireChange();
|
---|
| 115 | }
|
---|
| 116 |
|
---|
| 117 | /**
|
---|
| 118 | * Gets the full string that should be send as error report.
|
---|
| 119 | * @return The string.
|
---|
| 120 | * @since 10585
|
---|
| 121 | */
|
---|
| 122 | public String getReportText() {
|
---|
| 123 | StringWriter stringWriter = new StringWriter();
|
---|
| 124 | PrintWriter out = new PrintWriter(stringWriter);
|
---|
[10597] | 125 | if (isIncludeStatusReport()) {
|
---|
[10886] | 126 | try {
|
---|
| 127 | out.println(ShowStatusReportAction.getReportHeader());
|
---|
| 128 | } catch (RuntimeException e) {
|
---|
| 129 | out.println("Could not generate status report: " + e.getMessage());
|
---|
| 130 | }
|
---|
[10585] | 131 | }
|
---|
[10597] | 132 | if (isIncludeData()) {
|
---|
[10585] | 133 | exception.printReportDataTo(out);
|
---|
| 134 | }
|
---|
| 135 | exception.printReportStackTo(out);
|
---|
[10597] | 136 | if (isIncludeAllStackTraces()) {
|
---|
[10585] | 137 | exception.printReportThreadsTo(out);
|
---|
| 138 | }
|
---|
| 139 | return stringWriter.toString().replaceAll("\r", "");
|
---|
| 140 | }
|
---|
| 141 |
|
---|
| 142 | /**
|
---|
| 143 | * Add a new change listener.
|
---|
| 144 | * @param listener The listener
|
---|
| 145 | * @since 10585
|
---|
| 146 | */
|
---|
| 147 | public void addChangeListener(BugReportListener listener) {
|
---|
| 148 | listeners.add(listener);
|
---|
| 149 | }
|
---|
| 150 |
|
---|
| 151 | /**
|
---|
| 152 | * Remove a change listener.
|
---|
| 153 | * @param listener The listener
|
---|
| 154 | * @since 10585
|
---|
| 155 | */
|
---|
| 156 | public void removeChangeListener(BugReportListener listener) {
|
---|
| 157 | listeners.remove(listener);
|
---|
| 158 | }
|
---|
| 159 |
|
---|
| 160 | private void fireChange() {
|
---|
| 161 | listeners.stream().forEach(l -> l.bugReportChanged(this));
|
---|
| 162 | }
|
---|
| 163 |
|
---|
| 164 | /**
|
---|
[10285] | 165 | * This should be called whenever you want to add more information to a given exception.
|
---|
| 166 | * @param t The throwable that was thrown.
|
---|
| 167 | * @return A {@link ReportedException} to which you can add additional information.
|
---|
| 168 | */
|
---|
| 169 | public static ReportedException intercept(Throwable t) {
|
---|
| 170 | ReportedException e;
|
---|
| 171 | if (t instanceof ReportedException) {
|
---|
| 172 | e = (ReportedException) t;
|
---|
| 173 | } else {
|
---|
| 174 | e = new ReportedException(t);
|
---|
| 175 | }
|
---|
| 176 | e.startSection(getCallingMethod(2));
|
---|
| 177 | return e;
|
---|
| 178 | }
|
---|
| 179 |
|
---|
| 180 | /**
|
---|
| 181 | * Find the method that called us.
|
---|
| 182 | *
|
---|
| 183 | * @param offset
|
---|
| 184 | * How many methods to look back in the stack trace. 1 gives the method calling this method, 0 gives you getCallingMethod().
|
---|
| 185 | * @return The method name.
|
---|
| 186 | */
|
---|
[10412] | 187 | public static String getCallingMethod(int offset) {
|
---|
[10902] | 188 | StackTraceElement found = getCallingMethod(offset + 1, BugReport.class.getName(), "getCallingMethod"::equals);
|
---|
[10899] | 189 | if (found != null) {
|
---|
| 190 | return found.getClassName().replaceFirst(".*\\.", "") + '#' + found.getMethodName();
|
---|
| 191 | } else {
|
---|
| 192 | return "?";
|
---|
| 193 | }
|
---|
| 194 | }
|
---|
| 195 |
|
---|
| 196 | /**
|
---|
| 197 | * Find the method that called the given method on the current stack trace.
|
---|
| 198 | * @param offset
|
---|
[10902] | 199 | * How many methods to look back in the stack trace.
|
---|
| 200 | * 1 gives the method calling this method, 0 gives you the method with the given name..
|
---|
[10899] | 201 | * @param className The name of the class to search for
|
---|
| 202 | * @param methodName The name of the method to search for
|
---|
| 203 | * @return The class and method name or null if it is unknown.
|
---|
| 204 | */
|
---|
| 205 | public static StackTraceElement getCallingMethod(int offset, String className, Predicate<String> methodName) {
|
---|
[10285] | 206 | StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
---|
| 207 | for (int i = 0; i < stackTrace.length - offset; i++) {
|
---|
| 208 | StackTraceElement element = stackTrace[i];
|
---|
[10899] | 209 | if (className.equals(element.getClassName()) && methodName.test(element.getMethodName())) {
|
---|
[10902] | 210 | return stackTrace[i + offset];
|
---|
[10285] | 211 | }
|
---|
| 212 | }
|
---|
[10899] | 213 | return null;
|
---|
[10285] | 214 | }
|
---|
[10585] | 215 |
|
---|
| 216 | /**
|
---|
| 217 | * A listener that listens to changes to this report.
|
---|
| 218 | * @author Michael Zangl
|
---|
| 219 | * @since 10585
|
---|
| 220 | */
|
---|
| 221 | @FunctionalInterface
|
---|
| 222 | public interface BugReportListener {
|
---|
| 223 | /**
|
---|
| 224 | * Called whenever this bug report was changed, e.g. the data to be included in it.
|
---|
| 225 | * @param report The report that was changed.
|
---|
| 226 | */
|
---|
| 227 | void bugReportChanged(BugReport report);
|
---|
| 228 | }
|
---|
[10285] | 229 | }
|
---|