001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools.bugreport; 003 004import java.io.PrintWriter; 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.Collection; 008import java.util.Collections; 009import java.util.IdentityHashMap; 010import java.util.LinkedList; 011import java.util.Map; 012import java.util.Map.Entry; 013import java.util.Set; 014 015import org.openstreetmap.josm.Main; 016 017/** 018 * This is a special exception that cannot be directly thrown. 019 * <p> 020 * It is used to capture more information about an exception that was already thrown. 021 * 022 * @author Michael Zangl 023 * @see BugReport 024 * @since 10285 025 */ 026public class ReportedException extends RuntimeException { 027 private static final int MAX_COLLECTION_ENTRIES = 30; 028 /** 029 * 030 */ 031 private static final long serialVersionUID = 737333873766201033L; 032 /** 033 * We capture all stack traces on exception creation. This allows us to trace synchonization problems better. We cannot be really sure what 034 * happened but we at least see which threads 035 */ 036 private final transient Map<Thread, StackTraceElement[]> allStackTraces; 037 private final transient LinkedList<Section> sections = new LinkedList<>(); 038 private final transient Thread caughtOnThread; 039 private final Throwable exception; 040 private String methodWarningFrom; 041 042 ReportedException(Throwable exception) { 043 this(exception, Thread.currentThread()); 044 } 045 046 ReportedException(Throwable exception, Thread caughtOnThread) { 047 super(exception); 048 this.exception = exception; 049 050 allStackTraces = Thread.getAllStackTraces(); 051 this.caughtOnThread = caughtOnThread; 052 } 053 054 /** 055 * Displays a warning for this exception. The program can then continue normally. Does not block. 056 */ 057 public void warn() { 058 methodWarningFrom = BugReport.getCallingMethod(2); 059 // TODO: Open the dialog. 060 } 061 062 /** 063 * Starts a new debug data section. This normally does not need to be called manually. 064 * 065 * @param sectionName 066 * The section name. 067 */ 068 public void startSection(String sectionName) { 069 sections.add(new Section(sectionName)); 070 } 071 072 /** 073 * Prints the captured data of this report to a {@link PrintWriter}. 074 * 075 * @param out 076 * The writer to print to. 077 */ 078 public void printReportDataTo(PrintWriter out) { 079 out.println("=== REPORTED CRASH DATA ==="); 080 for (Section s : sections) { 081 s.printSection(out); 082 out.println(); 083 } 084 085 if (methodWarningFrom != null) { 086 out.println("Warning issued by: " + methodWarningFrom); 087 out.println(); 088 } 089 } 090 091 /** 092 * Prints the stack trace of this report to a {@link PrintWriter}. 093 * 094 * @param out 095 * The writer to print to. 096 */ 097 public void printReportStackTo(PrintWriter out) { 098 out.println("=== STACK TRACE ==="); 099 out.println(niceThreadName(caughtOnThread)); 100 getCause().printStackTrace(out); 101 out.println(); 102 } 103 104 /** 105 * Prints the stack traces for other threads of this report to a {@link PrintWriter}. 106 * 107 * @param out 108 * The writer to print to. 109 */ 110 public void printReportThreadsTo(PrintWriter out) { 111 out.println("=== RUNNING THREADS ==="); 112 for (Entry<Thread, StackTraceElement[]> thread : allStackTraces.entrySet()) { 113 out.println(niceThreadName(thread.getKey())); 114 if (caughtOnThread.equals(thread.getKey())) { 115 out.println("Stacktrace see above."); 116 } else { 117 for (StackTraceElement e : thread.getValue()) { 118 out.println(e); 119 } 120 } 121 out.println(); 122 } 123 } 124 125 private static String niceThreadName(Thread thread) { 126 String name = "Thread: " + thread.getName() + " (" + thread.getId() + ')'; 127 ThreadGroup threadGroup = thread.getThreadGroup(); 128 if (threadGroup != null) { 129 name += " of " + threadGroup.getName(); 130 } 131 return name; 132 } 133 134 /** 135 * Checks if this exception is considered the same as an other exception. This is the case if both have the same cause and message. 136 * 137 * @param e 138 * The exception to check against. 139 * @return <code>true</code> if they are considered the same. 140 */ 141 public boolean isSame(ReportedException e) { 142 if (!getMessage().equals(e.getMessage())) { 143 return false; 144 } 145 146 Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>()); 147 return hasSameStackTrace(dejaVu, this.exception, e.exception); 148 } 149 150 private static boolean hasSameStackTrace(Set<Throwable> dejaVu, Throwable e1, Throwable e2) { 151 if (dejaVu.contains(e1)) { 152 // cycle. If it was the same until here, we assume both have that cycle. 153 return true; 154 } 155 dejaVu.add(e1); 156 157 StackTraceElement[] t1 = e1.getStackTrace(); 158 StackTraceElement[] t2 = e2.getStackTrace(); 159 160 if (!Arrays.equals(t1, t2)) { 161 return false; 162 } 163 164 Throwable c1 = e1.getCause(); 165 Throwable c2 = e2.getCause(); 166 if ((c1 == null) != (c2 == null)) { 167 return false; 168 } else if (c1 != null) { 169 return hasSameStackTrace(dejaVu, c1, c2); 170 } else { 171 return true; 172 } 173 } 174 175 /** 176 * Adds some debug values to this exception. 177 * 178 * @param key 179 * The key to add this for. Does not need to be unique but it would be nice. 180 * @param value 181 * The value. 182 * @return This exception for easy chaining. 183 */ 184 public ReportedException put(String key, Object value) { 185 String string; 186 try { 187 if (value == null) { 188 string = "null"; 189 } else if (value instanceof Collection) { 190 string = makeCollectionNice((Collection<?>) value); 191 } else if (value.getClass().isArray()) { 192 string = makeCollectionNice(Arrays.asList(value)); 193 } else { 194 string = value.toString(); 195 } 196 } catch (RuntimeException t) { 197 Main.warn(t); 198 string = "<Error calling toString()>"; 199 } 200 sections.getLast().put(key, string); 201 return this; 202 } 203 204 private static String makeCollectionNice(Collection<?> value) { 205 int lines = 0; 206 StringBuilder str = new StringBuilder(); 207 for (Object e : value) { 208 str.append("\n - "); 209 if (lines <= MAX_COLLECTION_ENTRIES) { 210 str.append(e); 211 } else { 212 str.append("\n ... (") 213 .append(value.size()) 214 .append(" entries)"); 215 break; 216 } 217 } 218 return str.toString(); 219 } 220 221 @Override 222 public String toString() { 223 return new StringBuilder(48) 224 .append("CrashReportedException [on thread ") 225 .append(caughtOnThread) 226 .append(']') 227 .toString(); 228 } 229 230 private static class SectionEntry { 231 private final String key; 232 private final String value; 233 234 SectionEntry(String key, String value) { 235 this.key = key; 236 this.value = value; 237 } 238 239 /** 240 * Prints this entry to the output stream in a line. 241 * @param out The stream to print to. 242 */ 243 public void print(PrintWriter out) { 244 out.print(" - "); 245 out.print(key); 246 out.print(": "); 247 out.println(value); 248 } 249 } 250 251 private static class Section { 252 253 private final String sectionName; 254 private final ArrayList<SectionEntry> entries = new ArrayList<>(); 255 256 Section(String sectionName) { 257 this.sectionName = sectionName; 258 } 259 260 /** 261 * Add a key/value entry to this section. 262 * @param key The key. Need not be unique. 263 * @param value The value. 264 */ 265 public void put(String key, String value) { 266 entries.add(new SectionEntry(key, value)); 267 } 268 269 /** 270 * Prints this section to the output stream. 271 * @param out The stream to print to. 272 */ 273 public void printSection(PrintWriter out) { 274 out.println(sectionName + ':'); 275 if (entries.isEmpty()) { 276 out.println("No data collected."); 277 } else { 278 for (SectionEntry e : entries) { 279 e.print(out); 280 } 281 } 282 } 283 } 284}