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