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", () -> 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}