Bug 284158 - Dismiss exception breakpoint per instance

- ask user once per breakpoint and store answer in the breakpoint

Change-Id: I02d2e206e046d25bf0bfbcc27e173cf018a8abec
Signed-off-by: Stephan Herrmann <stephan.herrmann@berlin.de>
diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/DebugUIMessages.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/DebugUIMessages.java
index 628eec6..18f6620 100644
--- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/DebugUIMessages.java
+++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/DebugUIMessages.java
@@ -179,6 +179,7 @@
 	public static String thread_suspended_linebreakpoint;
 	public static String thread_suspended_methodentry;
 	public static String thread_suspended_exception;
+	public static String thread_suspended_exception_uncaught;
 	public static String thread_suspended_methodexit;
 	public static String thread_suspended_fieldmodification;
 	public static String thread_suspended_runtoline;
@@ -311,7 +312,15 @@
 	public static String JavaDebugOptionsManager_Method_breakpoint___2;
 	public static String JavaDebugOptionsManager_Watchpoint___3;
 	public static String JavaDebugOptionsManager_0;
+
+	public static String JavaDebugOptionsManager_exceptionRecurrence_dialogTitle;
+	public static String JavaDebugOptionsManager_exceptionRecurrence_dialogMessage;
+	public static String JavaDebugOptionsManager_cancel_buttonLabel;
+	public static String JavaDebugOptionsManager_skip_buttonLabel;
+	public static String JavaDebugOptionsManager_suspend_buttonLabel;
+
 	public static String JavaDebugOptionsManager_Line_breakpoint___4;
+
 	public static String JavaBreakpointWorkbenchAdapterFactory_1;
 
 	public static String HotCodeReplaceErrorDialog_0;
diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/DebugUIMessages.properties b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/DebugUIMessages.properties
index 861d79c..03b9ef4 100644
--- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/DebugUIMessages.properties
+++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/DebugUIMessages.properties
@@ -128,6 +128,7 @@
 thread_suspended_linebreakpoint=Thread [{0}] (Suspended (breakpoint at line {1} in {2}))
 thread_suspended_methodentry=Thread [{0}] (Suspended (entry into method {1} in {2}))
 thread_suspended_exception=Thread [{0}] (Suspended (exception {1}))
+thread_suspended_exception_uncaught=Thread [{0}] (Suspended (uncaught exception {1}))
 thread_suspended_methodexit=Thread [{0}] (Suspended (exit of method {1} in {2}))
 thread_suspended_fieldmodification=Thread [{0}] (Suspended (modification of field {1} in {2}))
 thread_suspended_runtoline=Thread [{0}] (Suspended (run to line {1} in {2}))
@@ -267,7 +268,20 @@
 JavaDebugOptionsManager_Method_breakpoint___2=Method breakpoint:
 JavaDebugOptionsManager_Watchpoint___3=Watchpoint:
 JavaDebugOptionsManager_0=Initialize Java Debug Options
+JavaDebugOptionsManager_exceptionRecurrence_dialogTitle=Repeated exception occurrence
+JavaDebugOptionsManager_exceptionRecurrence_dialogMessage=The debugger is about to suspend on the exception breakpoint for {0}, \
+but the current exception instance has already caused suspending before.\n\
+\n\
+Please choose how this breakpoint should react to repeated occurrences of the same exception instance:\n\
+ \u2022 Skip:\t\t fire only once per exception instance\n\
+ \u2022 Suspend:\t fire always\n\
+ \u2022 Cancel:\t suspend now, but don't remember this decision\n\n\
+This choice does not affect the global preference regarding uncaught exceptions. 
 JavaDebugOptionsManager_Line_breakpoint___4=Line breakpoint:
+JavaDebugOptionsManager_skip_buttonLabel=Skip
+JavaDebugOptionsManager_suspend_buttonLabel=Suspend
+JavaDebugOptionsManager_cancel_buttonLabel=Cancel
+
 JavaBreakpointWorkbenchAdapterFactory_1=\ [line:
 
 HotCodeReplaceErrorDialog_0=&Continue
diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JDIModelPresentation.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JDIModelPresentation.java
index f243689..49f06cf 100644
--- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JDIModelPresentation.java
+++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JDIModelPresentation.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2017 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -77,6 +77,7 @@
 import org.eclipse.jdt.internal.debug.core.model.JDIReferenceListValue;
 import org.eclipse.jdt.internal.debug.core.model.JDIReferenceListVariable;
 import org.eclipse.jdt.internal.debug.core.model.JDIThread;
+import org.eclipse.jdt.internal.debug.ui.breakpoints.SuspendOnUncaughtExceptionListener;
 import org.eclipse.jdt.internal.debug.ui.display.JavaInspectExpression;
 import org.eclipse.jdt.internal.debug.ui.monitors.JavaContendedMonitor;
 import org.eclipse.jdt.internal.debug.ui.monitors.JavaOwnedMonitor;
@@ -368,6 +369,9 @@
 				// check args == null in case the exception is a compilation error
 				if (breakpoint instanceof IJavaExceptionBreakpoint && args == null) {
 					key.append("_exception"); //$NON-NLS-1$
+					if (isUncaughtExceptionsBreakpoint(breakpoint)) {
+						key.append("_uncaught"); //$NON-NLS-1$
+					}
 					String exName = ((IJavaExceptionBreakpoint)breakpoint).getExceptionTypeName();
 					if (exName == null) {
 						exName = typeName;
@@ -432,6 +436,19 @@
 		return DebugUIMessages.JDIModelPresentation_unknown_name__1;
 	}
 
+	private boolean isUncaughtExceptionsBreakpoint(IJavaBreakpoint breakpoint) {
+		try {
+			for (String id : breakpoint.getBreakpointListeners()) {
+				if (SuspendOnUncaughtExceptionListener.ID_UNCAUGHT_EXCEPTION_LISTENER.equals(id)) {
+					return true;
+				}
+			}
+		} catch (CoreException e) {
+			DebugUIPlugin.log(e);
+		}
+		return false;
+	}
+
 	/**
 	 * Build the text for an IJavaDebugTarget.
 	 */
diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JavaDebugOptionsManager.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JavaDebugOptionsManager.java
index 93d63bf..541ee0c 100644
--- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JavaDebugOptionsManager.java
+++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JavaDebugOptionsManager.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2015 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -55,6 +55,7 @@
 import org.eclipse.jdt.debug.core.IJavaBreakpointListener;
 import org.eclipse.jdt.debug.core.IJavaDebugTarget;
 import org.eclipse.jdt.debug.core.IJavaExceptionBreakpoint;
+import org.eclipse.jdt.debug.core.IJavaExceptionBreakpoint.SuspendOnRecurrenceStrategy;
 import org.eclipse.jdt.debug.core.IJavaLineBreakpoint;
 import org.eclipse.jdt.debug.core.IJavaMethodBreakpoint;
 import org.eclipse.jdt.debug.core.IJavaMethodEntryBreakpoint;
@@ -67,10 +68,12 @@
 import org.eclipse.jdt.internal.debug.core.breakpoints.JavaExceptionBreakpoint;
 import org.eclipse.jdt.internal.debug.core.logicalstructures.IJavaStructuresListener;
 import org.eclipse.jdt.internal.debug.core.logicalstructures.JavaLogicalStructures;
+import org.eclipse.jdt.internal.debug.core.model.JDIThread;
 import org.eclipse.jdt.internal.debug.ui.actions.JavaBreakpointPropertiesAction;
 import org.eclipse.jdt.internal.debug.ui.breakpoints.SuspendOnCompilationErrorListener;
 import org.eclipse.jdt.internal.debug.ui.breakpoints.SuspendOnUncaughtExceptionListener;
 import org.eclipse.jdt.internal.debug.ui.snippeteditor.ScrapbookLauncher;
+import org.eclipse.jface.dialogs.MessageDialog;
 import org.eclipse.jface.preference.IPreferenceStore;
 import org.eclipse.jface.util.IPropertyChangeListener;
 import org.eclipse.jface.util.PropertyChangeEvent;
@@ -81,6 +84,7 @@
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Shell;
 
+import com.ibm.icu.text.MessageFormat;
 import com.sun.jdi.InvocationException;
 import com.sun.jdi.ObjectReference;
 
@@ -567,10 +571,74 @@
 	 */
 	@Override
 	public int breakpointHit(IJavaThread thread, IJavaBreakpoint breakpoint) {
+		if (thread instanceof JDIThread && breakpoint instanceof IJavaExceptionBreakpoint) {
+			if (shouldSkipSubsequentOccurrence((JDIThread) thread, (IJavaExceptionBreakpoint) breakpoint)) {
+				return DONT_SUSPEND;
+			}
+		}
 		return DONT_CARE;
 	}
 
 	/**
+	 * Figure out whether suspending on an exceptionBreakpoint should be skipped due to the user's choice regarding
+	 * {@link IJavaExceptionBreakpoint#setSuspendOnRecurrenceStrategy(int)}.
+	 * <p>
+	 * If the current hit is indeed recurrence of an already-seen exception instance, and if the user has not yet made a choice for this breakpoint,
+	 * then a question dialog will be opened to request the user's choice.
+	 * </p>
+	 *
+	 * @param thread
+	 *            the thread where an exception occurred
+	 * @param exceptionBreakpoint
+	 *            the exceptionBreakpoint that just fired
+	 * @return {@code true} if the current breakpoint hit should be skipped.
+	 */
+	public boolean shouldSkipSubsequentOccurrence(JDIThread thread, IJavaExceptionBreakpoint exceptionBreakpoint) {
+		if (exceptionBreakpoint == fSuspendOnExceptionBreakpoint) {
+			// this breakpoint doesn't have the recurrence property, wait until we are called from SuspendOnUncaughtExceptionListener itself
+			return false;
+		}
+		SuspendOnRecurrenceStrategy skip = thread.shouldSkipExceptionRecurrence(exceptionBreakpoint);
+		if (skip == null) {
+			return false; // not a recurrence
+		}
+		if (skip == SuspendOnRecurrenceStrategy.RECURRENCE_UNCONFIGURED) {
+			skip = askUserExceptionRecurrence(exceptionBreakpoint);
+			try {
+				exceptionBreakpoint.setSuspendOnRecurrenceStrategy(skip);
+			} catch (CoreException e) {
+				JDIDebugUIPlugin.log(e);
+			}
+		}
+		switch (skip) {
+			case SKIP_RECURRENCES:
+				return true;
+			default:
+				return false;
+		}
+	}
+
+	private static SuspendOnRecurrenceStrategy askUserExceptionRecurrence(IJavaExceptionBreakpoint breakpoint) {
+		Shell shell = JDIDebugUIPlugin.getShell();
+		MessageDialog question = new MessageDialog(shell, DebugUIMessages.JavaDebugOptionsManager_exceptionRecurrence_dialogTitle, null, //
+				MessageFormat.format(DebugUIMessages.JavaDebugOptionsManager_exceptionRecurrence_dialogMessage, breakpoint.getExceptionTypeName()), //
+				MessageDialog.QUESTION, 0, //
+				DebugUIMessages.JavaDebugOptionsManager_skip_buttonLabel, //
+				DebugUIMessages.JavaDebugOptionsManager_suspend_buttonLabel, //
+				DebugUIMessages.JavaDebugOptionsManager_cancel_buttonLabel);
+		int answer[] = { -1 };
+		shell.getDisplay().syncExec(() -> answer[0] = question.open());
+		switch (answer[0]) {
+			case 0:
+				return SuspendOnRecurrenceStrategy.SKIP_RECURRENCES;
+			case 1:
+				return SuspendOnRecurrenceStrategy.SUSPEND_ALWAYS;
+			default:
+				return SuspendOnRecurrenceStrategy.RECURRENCE_UNCONFIGURED;
+		}
+	}
+
+	/**
 	 * @see IJavaBreakpointListener#breakpointInstalled(IJavaDebugTarget, IJavaBreakpoint)
 	 */
 	@Override
diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/breakpoints/ExceptionBreakpointEditor.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/breakpoints/ExceptionBreakpointEditor.java
index 8de056b..ccf78ee 100644
--- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/breakpoints/ExceptionBreakpointEditor.java
+++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/breakpoints/ExceptionBreakpointEditor.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2009, 2011 IBM Corporation and others.
+ * Copyright (c) 2009, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -13,13 +13,23 @@
  *******************************************************************************/
 package org.eclipse.jdt.internal.debug.ui.breakpoints;
 
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.debug.internal.ui.SWTFactory;
 import org.eclipse.jdt.debug.core.IJavaBreakpoint;
 import org.eclipse.jdt.debug.core.IJavaExceptionBreakpoint;
+import org.eclipse.jdt.debug.core.IJavaExceptionBreakpoint.SuspendOnRecurrenceStrategy;
 import org.eclipse.jdt.internal.debug.core.breakpoints.JavaExceptionBreakpoint;
 import org.eclipse.jdt.internal.debug.ui.propertypages.PropertyPageMessages;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 
@@ -34,11 +44,14 @@
     public static final int PROP_CAUGHT = 0x1020;
     public static final int PROP_UNCAUGHT = 0x1021;
     public static final int PROP_SUBCLASSES = 0x1022;
+	public static final int PROP_RECURRENCE = 0x1023;
 
 	// editors
 	private Button fCaught;
 	private Button fUncaught;
 	private Button fSubclasses;
+	private Combo fRecurrence;
+	private Map<SuspendOnRecurrenceStrategy, String> fRecurrenceOptions;
 
 	/* (non-Javadoc)
 	 * @see org.eclipse.jdt.internal.debug.ui.breakpoints.StandardJavaBreakpointEditor#createControl(org.eclipse.swt.widgets.Composite)
@@ -53,9 +66,27 @@
 		fCaught = createSusupendPropertyEditor(composite, processMnemonics(PropertyPageMessages.ExceptionBreakpointEditor_1), PROP_CAUGHT);
 		fUncaught = createSusupendPropertyEditor(composite, processMnemonics(PropertyPageMessages.ExceptionBreakpointEditor_2), PROP_UNCAUGHT);
 		fSubclasses = createSusupendPropertyEditor(composite, processMnemonics(PropertyPageMessages.ExceptionBreakpointEditor_3), PROP_SUBCLASSES);
+		composite = SWTFactory.createComposite(container, parent.getFont(), 2, 1, 0, 0, 0);
+		fRecurrence = createRecurrenceEditor(composite, processMnemonics(PropertyPageMessages.ExceptionBreakpointEditor_recurrence_label), PROP_RECURRENCE);
 		return container;
 	}
 
+	private Combo createRecurrenceEditor(Composite parent, String labelText, int propId) {
+		fRecurrenceOptions = new HashMap<>();
+		fRecurrenceOptions.put(SuspendOnRecurrenceStrategy.RECURRENCE_UNCONFIGURED, PropertyPageMessages.ExceptionBreakpointEditor_recurrence_unconfigured);
+		fRecurrenceOptions.put(SuspendOnRecurrenceStrategy.SUSPEND_ALWAYS, PropertyPageMessages.ExceptionBreakpointEditor_recurrence_always);
+		fRecurrenceOptions.put(SuspendOnRecurrenceStrategy.SKIP_RECURRENCES, PropertyPageMessages.ExceptionBreakpointEditor_recurrence_onlyOnce);
+		SWTFactory.createLabel(parent, labelText, 1);
+		Combo box = SWTFactory.createCombo(parent, SWT.READ_ONLY, 1, GridData.FILL_HORIZONTAL, fRecurrenceOptions.values().toArray(new String[fRecurrenceOptions.size()]));
+		box.addSelectionListener(new SelectionAdapter() {
+			@Override
+			public void widgetSelected(SelectionEvent e) {
+				setDirty(propId);
+			}
+		});
+		return box;
+	}
+
 	/* (non-Javadoc)
 	 * @see org.eclipse.jdt.internal.debug.ui.breakpoints.StandardJavaBreakpointEditor#setBreakpoint(org.eclipse.jdt.debug.core.IJavaBreakpoint)
 	 */
@@ -67,13 +98,16 @@
 			fCaught.setEnabled(true);
 			fUncaught.setEnabled(true);
 			fSubclasses.setEnabled(true);
+			fRecurrence.setEnabled(true);
 			fCaught.setSelection(ex.isCaught());
 			fUncaught.setSelection(ex.isUncaught());
 			fSubclasses.setSelection(((JavaExceptionBreakpoint)ex).isSuspendOnSubclasses());
+			fRecurrence.setText(fRecurrenceOptions.get(ex.getSuspendOnRecurrenceStrategy()));
 		} else {
 			fCaught.setEnabled(false);
 			fUncaught.setEnabled(false);
 			fSubclasses.setEnabled(false);
+			fRecurrence.setEnabled(false);
 		}
 	}
 
@@ -89,6 +123,16 @@
 			ex.setCaught(fCaught.getSelection());
 			ex.setUncaught(fUncaught.getSelection());
 			((JavaExceptionBreakpoint)ex).setSuspendOnSubclasses(fSubclasses.getSelection());
+			ex.setSuspendOnRecurrenceStrategy(stringToRecurrence(fRecurrence.getText()));
 		}
 	}
+
+	private SuspendOnRecurrenceStrategy stringToRecurrence(String text) {
+		for (Entry<SuspendOnRecurrenceStrategy, String> entry : fRecurrenceOptions.entrySet()) {
+			if (entry.getValue().equals(text)) {
+				return entry.getKey();
+			}
+		}
+		return SuspendOnRecurrenceStrategy.RECURRENCE_UNCONFIGURED;
+	}
 }
diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/propertypages/PropertyPageMessages.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/propertypages/PropertyPageMessages.java
index 2c8c3b1..5f9e88f 100644
--- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/propertypages/PropertyPageMessages.java
+++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/propertypages/PropertyPageMessages.java
@@ -23,6 +23,11 @@
 	public static String ExceptionBreakpointEditor_2;
 	public static String ExceptionBreakpointEditor_3;
 
+	public static String ExceptionBreakpointEditor_recurrence_label;
+	public static String ExceptionBreakpointEditor_recurrence_always;
+	public static String ExceptionBreakpointEditor_recurrence_onlyOnce;
+	public static String ExceptionBreakpointEditor_recurrence_unconfigured;
+
 	public static String ExceptionFilterEditor_5;
 	public static String ExceptionFilterEditor_6;
 	public static String ExceptionFilterEditor_7;
diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/propertypages/PropertyPageMessages.properties b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/propertypages/PropertyPageMessages.properties
index 904b158..b3f1417 100644
--- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/propertypages/PropertyPageMessages.properties
+++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/propertypages/PropertyPageMessages.properties
@@ -15,6 +15,10 @@
 ExceptionBreakpointEditor_1=&Caught locations
 ExceptionBreakpointEditor_2=&Uncaught locations
 ExceptionBreakpointEditor_3=Subclasses of this e&xception
+ExceptionBreakpointEditor_recurrence_label=Suspend on &recurrence per instance:
+ExceptionBreakpointEditor_recurrence_always=always
+ExceptionBreakpointEditor_recurrence_onlyOnce=only once
+ExceptionBreakpointEditor_recurrence_unconfigured=unconfigured
 ExceptionFilterEditor_5=Re&strict to Selected Location(s):\nChecked locations are inclusive (stop in the specified location)\nUnchecked locations are exclusive (do not stop in the specified location)
 ExceptionFilterEditor_6=&Add
 ExceptionFilterEditor_7=Add a fully qualified type name or package name expression as a filter pattern for this breakpoint
diff --git a/org.eclipse.jdt.debug/META-INF/MANIFEST.MF b/org.eclipse.jdt.debug/META-INF/MANIFEST.MF
index 6911b7a..dd3ebe3 100644
--- a/org.eclipse.jdt.debug/META-INF/MANIFEST.MF
+++ b/org.eclipse.jdt.debug/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.jdt.debug; singleton:=true
-Bundle-Version: 3.13.200.qualifier
+Bundle-Version: 3.14.0.qualifier
 Bundle-ClassPath: jdi.jar,
  jdimodel.jar,
  tools.jar
diff --git a/org.eclipse.jdt.debug/model/org/eclipse/jdt/debug/core/IJavaExceptionBreakpoint.java b/org.eclipse.jdt.debug/model/org/eclipse/jdt/debug/core/IJavaExceptionBreakpoint.java
index fd11cf7..93c19c2 100644
--- a/org.eclipse.jdt.debug/model/org/eclipse/jdt/debug/core/IJavaExceptionBreakpoint.java
+++ b/org.eclipse.jdt.debug/model/org/eclipse/jdt/debug/core/IJavaExceptionBreakpoint.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2011 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -196,4 +196,53 @@
 	 */
 	@Deprecated
 	public boolean isInclusiveFiltered() throws CoreException;
+
+	/**
+	 * Constants for telling the debugger for each exception breakpoint how to handle multiple occurrences of the same exception instance, which can
+	 * happen via re-throwing or multiple finally clauses in the call stack.
+	 *
+	 * @since 3.14
+	 * @see IJavaExceptionBreakpoint#getSuspendOnRecurrenceStrategy()
+	 * @see IJavaExceptionBreakpoint#setSuspendOnRecurrenceStrategy(SuspendOnRecurrenceStrategy)
+	 */
+	enum SuspendOnRecurrenceStrategy {
+		/**
+		 * Signals that this setting has not yet been configured for a given breakpoint
+		 */
+		RECURRENCE_UNCONFIGURED,
+		/**
+		 * Signals that the exception breakpoint should always cause suspending.
+		 */
+		SUSPEND_ALWAYS,
+		/**
+		 * Signals that the breakpoint should not cause suspending more than once. This does not influence the way how the debugger reacts to uncaught
+		 * exceptions.
+		 */
+		SKIP_RECURRENCES
+	}
+
+	/**
+	 * Define this breakpoint's {@link SuspendOnRecurrenceStrategy strategy} for suspending on recurrences of the same exception instance.
+	 *
+	 * @param strategy
+	 *            the new strategy
+	 *
+	 * @throws CoreException
+	 *             if accessing the breakpoint's marker failed
+	 * @since 3.14
+	 * @see #getSuspendOnRecurrenceStrategy()
+	 */
+	void setSuspendOnRecurrenceStrategy(SuspendOnRecurrenceStrategy strategy) throws CoreException;
+
+	/**
+	 * Answer this breakpoint's {@link SuspendOnRecurrenceStrategy strategy} for suspending on recurrences of the same exception instance.
+	 *
+	 * @return the strategy
+	 *
+	 * @throws CoreException
+	 *             if accessing the breakpoint's marker failed
+	 * @since 3.14
+	 * @see #setSuspendOnRecurrenceStrategy(SuspendOnRecurrenceStrategy)
+	 */
+	SuspendOnRecurrenceStrategy getSuspendOnRecurrenceStrategy() throws CoreException;
 }
diff --git a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/breakpoints/JavaExceptionBreakpoint.java b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/breakpoints/JavaExceptionBreakpoint.java
index bb2139e..9b1b622 100644
--- a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/breakpoints/JavaExceptionBreakpoint.java
+++ b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/breakpoints/JavaExceptionBreakpoint.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2017 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -105,6 +105,14 @@
 	protected static final String SUSPEND_ON_SUBCLASSES = "org.eclipse.jdt.debug.core.suspend_on_subclasses"; //$NON-NLS-1$
 
 	/**
+	 * Allows the user to specify that each exception instance matching this breakpoint should suspend only once, i.e., re-throws and finally clauses
+	 * will not again suspend on an exception instance that had already caused a suspend.
+	 *
+	 * @since 3.14
+	 */
+	protected static final String SUSPEND_ON_RECURRENCE = "org.eclipse.jdt.debug.core.suspend_on_recurrence"; //$NON-NLS-1$
+
+	/**
 	 * Name of the exception that was actually hit (could be a sub-type of the
 	 * type that is being caught).
 	 */
@@ -322,6 +330,21 @@
 		recreate();
 	}
 
+	@Override
+	public SuspendOnRecurrenceStrategy getSuspendOnRecurrenceStrategy() throws CoreException {
+		int valueIndex = ensureMarker().getAttribute(SUSPEND_ON_RECURRENCE, SuspendOnRecurrenceStrategy.RECURRENCE_UNCONFIGURED.ordinal());
+		return SuspendOnRecurrenceStrategy.values()[valueIndex];
+	}
+
+	@Override
+	public void setSuspendOnRecurrenceStrategy(SuspendOnRecurrenceStrategy strategy) throws CoreException {
+		if (strategy == getSuspendOnRecurrenceStrategy()) {
+			return;
+		}
+		setAttribute(SUSPEND_ON_RECURRENCE, strategy.ordinal());
+		// don't re-create, the change only affects the debugger, not the target
+	}
+
 	/**
 	 * @see IJavaExceptionBreakpoint#isChecked()
 	 */
diff --git a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIThread.java b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIThread.java
index 45ee0ad..2e9a6a7 100644
--- a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIThread.java
+++ b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIThread.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2018 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -47,6 +47,8 @@
 import org.eclipse.jdt.debug.core.IEvaluationRunnable;
 import org.eclipse.jdt.debug.core.IJavaBreakpoint;
 import org.eclipse.jdt.debug.core.IJavaBreakpointListener;
+import org.eclipse.jdt.debug.core.IJavaExceptionBreakpoint;
+import org.eclipse.jdt.debug.core.IJavaExceptionBreakpoint.SuspendOnRecurrenceStrategy;
 import org.eclipse.jdt.debug.core.IJavaObject;
 import org.eclipse.jdt.debug.core.IJavaStackFrame;
 import org.eclipse.jdt.debug.core.IJavaThread;
@@ -58,6 +60,7 @@
 import org.eclipse.jdt.internal.debug.core.JDIDebugPlugin;
 import org.eclipse.jdt.internal.debug.core.breakpoints.ConditionalBreakpointHandler;
 import org.eclipse.jdt.internal.debug.core.breakpoints.JavaBreakpoint;
+import org.eclipse.jdt.internal.debug.core.breakpoints.JavaExceptionBreakpoint;
 import org.eclipse.jdt.internal.debug.core.breakpoints.JavaLineBreakpoint;
 import org.eclipse.jdt.internal.debug.core.model.MethodResult.ResultType;
 
@@ -307,6 +310,11 @@
 	private MethodResult fMethodResult;
 
 	/**
+	 * If previous suspend was on an exception breakpoint, this variable holds that Java exception instance, else {@code null}.
+	 */
+	private IJavaObject fPreviousException;
+
+	/**
 	 * Creates a new thread on the underlying thread reference in the given
 	 * debug target.
 	 *
@@ -1506,6 +1514,33 @@
 	}
 
 	/**
+	 * To be called whenever a Java exception breakpoint is called. This thread will remember the Java exception instance associated with this event
+	 * and will answer if suspending should be skipped as per the breakpoint's {@link IJavaExceptionBreakpoint.SuspendOnRecurrenceStrategy suspend on
+	 * recurrence} strategy.
+	 *
+	 * @param breakpoint
+	 *            the breakpoint about to trigger
+	 * @return either {@code null} to signal that this breakpoint hit is not a recurrence, or one of
+	 *         {@link SuspendOnRecurrenceStrategy#RECURRENCE_UNCONFIGURED}, {@link SuspendOnRecurrenceStrategy#SKIP_RECURRENCES}, or
+	 *         {@link SuspendOnRecurrenceStrategy#SUSPEND_ALWAYS}.
+	 */
+	public SuspendOnRecurrenceStrategy shouldSkipExceptionRecurrence(IJavaExceptionBreakpoint breakpoint) {
+		if (breakpoint instanceof JavaExceptionBreakpoint) {
+			JavaExceptionBreakpoint exceptionBreakpoint = (JavaExceptionBreakpoint) breakpoint;
+			try {
+				IJavaObject lastException = exceptionBreakpoint.getLastException();
+				if (fPreviousException != null && fPreviousException.equals(lastException)) {
+					return exceptionBreakpoint.getSuspendOnRecurrenceStrategy();
+				}
+				fPreviousException = lastException;
+			} catch (CoreException e) {
+				// ignore
+			}
+		}
+		return null; // skipping not applicable, since this is no recurrence
+	}
+
+	/**
 	 * Called after an event set with a breakpoint is done being processed.
 	 * Updates thread state based on the result of handling the event set.
 	 * Aborts any step in progress and fires a suspend event is suspending.
diff --git a/org.eclipse.jdt.debug/pom.xml b/org.eclipse.jdt.debug/pom.xml
index c14300e..7193858 100644
--- a/org.eclipse.jdt.debug/pom.xml
+++ b/org.eclipse.jdt.debug/pom.xml
@@ -18,6 +18,6 @@
   </parent>
   <groupId>org.eclipse.jdt</groupId>
   <artifactId>org.eclipse.jdt.debug</artifactId>
-  <version>3.13.200-SNAPSHOT</version>
+  <version>3.14.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 </project>