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}