001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools.bugreport; 003 004import java.util.ArrayList; 005import java.util.LinkedList; 006import java.util.Objects; 007import java.util.concurrent.CopyOnWriteArrayList; 008import java.util.function.Predicate; 009 010import org.openstreetmap.josm.tools.Logging; 011 012/** 013 * This class handles the display of the bug report dialog. 014 * @author Michael Zangl 015 * @since 10819 016 */ 017public class BugReportQueue { 018 019 /** 020 * The fallback bug report handler if none is set. Prints the stacktrace on standard error stream. 021 * @since 12770 022 */ 023 public static final BugReportHandler FALLBACK_BUGREPORT_HANDLER = (e, index) -> { 024 e.printStackTrace(); 025 return BugReportQueue.SuppressionMode.NONE; 026 }; 027 028 private static final BugReportQueue INSTANCE = new BugReportQueue(); 029 030 private final LinkedList<ReportedException> reportsToDisplay = new LinkedList<>(); 031 private boolean suppressAllMessages; 032 private final ArrayList<ReportedException> suppressFor = new ArrayList<>(); 033 private Thread displayThread; 034 private BugReportHandler bugReportHandler = FALLBACK_BUGREPORT_HANDLER; 035 private final CopyOnWriteArrayList<Predicate<ReportedException>> handlers = new CopyOnWriteArrayList<>(); 036 private int displayedErrors; 037 038 private boolean inReportDialog; 039 040 /** 041 * Class that handles reporting a bug to the user. 042 */ 043 public interface BugReportHandler { 044 /** 045 * Handle the bug report for a given exception 046 * @param e The exception to display 047 * @param exceptionCounter A counter of how many exceptions have already been worked on 048 * @return The new suppression status 049 */ 050 SuppressionMode handle(ReportedException e, int exceptionCounter); 051 } 052 053 /** 054 * The suppression mode that should be used after the dialog was closed. 055 */ 056 public enum SuppressionMode { 057 /** 058 * Suppress no dialogs. 059 */ 060 NONE, 061 /** 062 * Suppress only the ones that are for the same error 063 */ 064 SAME, 065 /** 066 * Suppress all report dialogs 067 */ 068 ALL 069 } 070 071 /** 072 * Submit a new error to be displayed 073 * @param report The error to display 074 */ 075 public synchronized void submit(ReportedException report) { 076 Logging.logWithStackTrace(Logging.LEVEL_ERROR, "Handled by bug report queue", report.getCause()); 077 if (suppressAllMessages || suppressFor.stream().anyMatch(report::isSame)) { 078 Logging.info("User requested to skip error " + report); 079 } else if (reportsToDisplay.size() > 100 || reportsToDisplay.stream().filter(report::isSame).count() >= 10) { 080 Logging.warn("Too many errors. Dropping " + report); 081 } else { 082 reportsToDisplay.add(report); 083 if (displayThread == null) { 084 displayThread = new Thread(new BugReportDisplayRunnable(), "bug-report-display"); 085 displayThread.start(); 086 } 087 notifyAll(); 088 } 089 } 090 091 private class BugReportDisplayRunnable implements Runnable { 092 093 private volatile boolean running = true; 094 095 @Override 096 public void run() { 097 try { 098 while (running) { 099 ReportedException e = getNext(); 100 handleDialogResult(e, displayFor(e)); 101 } 102 } catch (InterruptedException e) { 103 displayFor(BugReport.intercept(e)); 104 Thread.currentThread().interrupt(); 105 } 106 } 107 } 108 109 private synchronized void handleDialogResult(ReportedException e, SuppressionMode suppress) { 110 if (suppress == SuppressionMode.ALL) { 111 suppressAllMessages = true; 112 reportsToDisplay.clear(); 113 } else if (suppress == SuppressionMode.SAME) { 114 suppressFor.add(e); 115 reportsToDisplay.removeIf(e::isSame); 116 } 117 displayedErrors++; 118 inReportDialog = false; 119 } 120 121 private synchronized ReportedException getNext() throws InterruptedException { 122 while (reportsToDisplay.isEmpty()) { 123 wait(); 124 } 125 inReportDialog = true; 126 return reportsToDisplay.removeFirst(); 127 } 128 129 private SuppressionMode displayFor(ReportedException e) { 130 if (handlers.stream().anyMatch(p -> p.test(e))) { 131 Logging.trace("Intercepted by handler."); 132 return SuppressionMode.NONE; 133 } 134 return bugReportHandler.handle(e, getDisplayedErrors()); 135 } 136 137 private synchronized int getDisplayedErrors() { 138 return displayedErrors; 139 } 140 141 /** 142 * Check if the dialog is shown. Should only be used for e.g. debugging. 143 * @return <code>true</code> if the exception handler is still showing the exception to the user. 144 */ 145 public synchronized boolean exceptionHandlingInProgress() { 146 return !reportsToDisplay.isEmpty() || inReportDialog; 147 } 148 149 /** 150 * Sets the {@link BugReportHandler} for this queue. 151 * @param bugReportHandler the handler in charge of displaying the bug report. Must not be null 152 * @since 12770 153 */ 154 public void setBugReportHandler(BugReportHandler bugReportHandler) { 155 this.bugReportHandler = Objects.requireNonNull(bugReportHandler, "bugReportHandler"); 156 } 157 158 /** 159 * Allows you to peek or even intercept the bug reports. 160 * @param handler The handler. It can return false to stop all further handling of the exception. 161 * @since 10886 162 */ 163 public void addBugReportHandler(Predicate<ReportedException> handler) { 164 handlers.add(handler); 165 } 166 167 /** 168 * Gets the global bug report queue 169 * @return The queue 170 * @since 10886 171 */ 172 public static BugReportQueue getInstance() { 173 return INSTANCE; 174 } 175}