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 010import org.openstreetmap.josm.actions.ShowStatusReportAction; 011 012/** 013 * This class contains utility methods to create and handle a bug report. 014 * <p> 015 * It allows you to configure the format and request to send the bug report. 016 * <p> 017 * It also contains the main entry point for all components to use the bug report system: Call {@link #intercept(Throwable)} to start handling an 018 * exception. 019 * <h1> Handling Exceptions </h1> 020 * In your code, you should add try...catch blocks for any runtime exceptions that might happen. It is fine to catch throwable there. 021 * <p> 022 * 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 023 * or other local variables. Make sure that no excpetions may occur while computing the values. It is best to send plain local variables to 024 * put(...). If you need to do computations, put them into a lambda expression. Then simply throw the throwable you got from the bug report. 025 * The global exception handler will do the rest. 026 * <pre> 027 * int id = ...; 028 * String tag = "..."; 029 * try { 030 * ... your code ... 031 * } catch (RuntimeException t) { 032 * throw BugReport.intercept(t).put("id", id).put("tag", () -> x.getTag()); 033 * } 034 * </pre> 035 * 036 * Instead of re-throwing, you can call {@link ReportedException#warn()}. This will display a warning to the user and allow it to either report 037 * the exception or ignore it. 038 * 039 * @author Michael Zangl 040 * @since 10285 041 */ 042public final class BugReport implements Serializable { 043 private static final long serialVersionUID = 1L; 044 045 private boolean includeStatusReport = true; 046 private boolean includeData = true; 047 private boolean includeAllStackTraces; 048 private final ReportedException exception; 049 private final CopyOnWriteArrayList<BugReportListener> listeners = new CopyOnWriteArrayList<>(); 050 051 /** 052 * Create a new bug report 053 * @param e The {@link ReportedException} to use. No more data should be added after creating the report. 054 */ 055 BugReport(ReportedException e) { 056 this.exception = e; 057 includeAllStackTraces = e.mayHaveConcurrentSource(); 058 } 059 060 /** 061 * Determines if this report should include a system status report 062 * @return <code>true</code> to include it. 063 * @since 10597 064 */ 065 public boolean isIncludeStatusReport() { 066 return includeStatusReport; 067 } 068 069 /** 070 * Set if this report should include a system status report 071 * @param includeStatusReport if the status report should be included 072 * @since 10585 073 */ 074 public void setIncludeStatusReport(boolean includeStatusReport) { 075 this.includeStatusReport = includeStatusReport; 076 fireChange(); 077 } 078 079 /** 080 * Determines if this report should include the data that was traced. 081 * @return <code>true</code> to include it. 082 * @since 10597 083 */ 084 public boolean isIncludeData() { 085 return includeData; 086 } 087 088 /** 089 * Set if this report should include the data that was traced. 090 * @param includeData if data should be included 091 * @since 10585 092 */ 093 public void setIncludeData(boolean includeData) { 094 this.includeData = includeData; 095 fireChange(); 096 } 097 098 /** 099 * Determines if this report should include the stack traces for all other threads. 100 * @return <code>true</code> to include it. 101 * @since 10597 102 */ 103 public boolean isIncludeAllStackTraces() { 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); 125 if (isIncludeStatusReport()) { 126 try { 127 out.println(ShowStatusReportAction.getReportHeader()); 128 } catch (RuntimeException e) { 129 out.println("Could not generate status report: " + e.getMessage()); 130 } 131 } 132 if (isIncludeData()) { 133 exception.printReportDataTo(out); 134 } 135 exception.printReportStackTo(out); 136 if (isIncludeAllStackTraces()) { 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 /** 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 */ 187 public static String getCallingMethod(int offset) { 188 StackTraceElement found = getCallingMethod(offset + 1, BugReport.class.getName(), "getCallingMethod"::equals); 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 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.. 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) { 206 StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); 207 for (int i = 0; i < stackTrace.length - offset; i++) { 208 StackTraceElement element = stackTrace[i]; 209 if (className.equals(element.getClassName()) && methodName.test(element.getMethodName())) { 210 return stackTrace[i + offset]; 211 } 212 } 213 return null; 214 } 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 } 229}