Bug 508524 - [hcr] Changing code in unrelated project triggers
"Hot Code Replace Failed" dialog

Filter types from unrelated projects from HCR.

Change-Id: I2ad7350cd34e32f8f1ad14e5679c10f659e69be3
Signed-off-by: Andrey Loskutov <loskutov@gmx.de>
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
index 45d6540..b106fc4 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
@@ -185,6 +185,7 @@
 	public static final String ONE_SEVEN_PROJECT_NAME = "OneSeven";
 	public static final String ONE_EIGHT_PROJECT_NAME = "OneEight";
 	public static final String BOUND_JRE_PROJECT_NAME = "BoundJRE";
+	public static final String CLONE_SUFFIX = "Clone";
 
 	final String[] LAUNCH_CONFIG_NAMES_1_4 = {"LargeSourceFile", "LotsOfFields", "Breakpoints", "InstanceVariablesTests", "LocalVariablesTests", "LocalVariableTests2", "StaticVariablesTests",
  "DropTests", "ThrowsNPE", "ThrowsException", "org.eclipse.debug.tests.targets.Watchpoint",
@@ -399,6 +400,7 @@
 				cfgs.add(createLaunchConfiguration(jp, "a.b.c.bug403028"));
 				cfgs.add(createLaunchConfiguration(jp, "a.b.c.bug484686"));
 				cfgs.add(createLaunchConfiguration(jp, "a.b.c.GenericMethodEntryTest"));
+				cfgs.add(createLaunchConfiguration(jp, "org.eclipse.debug.tests.targets.HcrClass", true));
 				loaded15 = true;
 				waitForBuild();
 	        }
@@ -1183,6 +1185,21 @@
 	}
 
 	/**
+	 * Launches the type with the given name, and waits for a breakpoint-caused
+	 * suspend event in that program. Returns the thread in which the suspend
+	 * event occurred.
+	 *
+	 * @param mainTypeName the program to launch
+	 * @param register whether to register the launch
+	 * @return thread in which the first suspend event occurred
+	 */
+	protected IJavaThread launchToBreakpoint(IJavaProject project, String mainTypeName, String launchName, boolean register) throws Exception {
+		ILaunchConfiguration config = getLaunchConfiguration(project, launchName);
+		assertNotNull("Could not locate launch configuration for " + mainTypeName, config); //$NON-NLS-1$
+		return launchToBreakpoint(config, register);
+	}
+
+	/**
 	 * Launches the given configuration in debug mode, and waits for a breakpoint-caused
 	 * suspend event in that program. Returns the thread in which the suspend
 	 * event occurred.
@@ -2446,8 +2463,19 @@
      * Creates a shared launch configuration for the type with the given name.
      */
     protected ILaunchConfiguration createLaunchConfiguration(IJavaProject project, String mainTypeName) throws Exception {
+		return createLaunchConfiguration(project, mainTypeName, false);
+	}
+
+	/**
+	 * Creates a shared launch configuration for the type with the given name.
+	 *
+	 * @param clone
+	 *            true if the launch config name should be different from the main type name
+	 */
+	protected ILaunchConfiguration createLaunchConfiguration(IJavaProject project, String mainTypeName, boolean clone) throws Exception {
         ILaunchConfigurationType type = getLaunchManager().getLaunchConfigurationType(IJavaLaunchConfigurationConstants.ID_JAVA_APPLICATION);
-        ILaunchConfigurationWorkingCopy config = type.newInstance(project.getProject().getFolder(LAUNCHCONFIGURATIONS), mainTypeName);
+		String configName = clone ? mainTypeName + CLONE_SUFFIX : mainTypeName;
+		ILaunchConfigurationWorkingCopy config = type.newInstance(project.getProject().getFolder(LAUNCHCONFIGURATIONS), configName);
         config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, mainTypeName);
         config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, project.getElementName());
 		Set<String> modes = new HashSet<>();
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/HcrTests.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/HcrTests.java
index 70a3a7d..55cd091 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/HcrTests.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/HcrTests.java
@@ -10,12 +10,17 @@
  *******************************************************************************/
 package org.eclipse.jdt.debug.tests.core;
 
+import static org.junit.Assert.assertNotEquals;
+
 import org.eclipse.debug.core.DebugEvent;
 import org.eclipse.debug.core.DebugException;
 import org.eclipse.jdt.core.IBuffer;
 import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
 import org.eclipse.jdt.debug.core.IJavaDebugTarget;
 import org.eclipse.jdt.debug.core.IJavaHotCodeReplaceListener;
+import org.eclipse.jdt.debug.core.IJavaLineBreakpoint;
 import org.eclipse.jdt.debug.core.IJavaStackFrame;
 import org.eclipse.jdt.debug.core.IJavaThread;
 import org.eclipse.jdt.debug.core.IJavaVariable;
@@ -1214,4 +1219,74 @@
 			JDIDebugModel.removeHotCodeReplaceListener(listener);
 		}
 	}
+
+	/**
+	 * Tests that HCR is NOT triggered if the code change is happened on an unrelated type, see bug 508524 and 5188
+	 */
+	public void testNoHcrOnUnrelatedType() throws Exception {
+		String typeName = "org.eclipse.debug.tests.targets.HcrClass";
+		IJavaProject unrelatedProject = get14Project();
+		IJavaProject debuggedProject = get15Project();
+		IType type1 = unrelatedProject.findType(typeName);
+		IType type2 = debuggedProject.findType(typeName);
+		assertNotEquals(type1, type2);
+
+		// Types FQNs are same
+		assertEquals(type1.getFullyQualifiedName(), type2.getFullyQualifiedName());
+
+		// Paths are same, except the project part
+		assertEquals(type1.getResource().getFullPath().removeFirstSegments(1), type2.getResource().getFullPath().removeFirstSegments(1));
+
+		final int lineNumber = 39;
+		IJavaLineBreakpoint bp1 = createLineBreakpoint(type1, lineNumber);
+		IJavaLineBreakpoint bp2 = createLineBreakpoint(type2, lineNumber);
+		assertNotEquals(bp1, bp2);
+
+		HCRListener listener = new HCRListener();
+		HCRListener listener2 = new HCRListener();
+		JDIDebugModel.addHotCodeReplaceListener(listener);
+		IJavaThread thread = null;
+		try {
+			// We start debugging on the one project but do modifications on the unrelated one!
+			thread = launchToBreakpoint(debuggedProject, typeName, typeName + CLONE_SUFFIX, true);
+			assertNotNull("Breakpoint not hit within timeout period", thread);
+
+			IJavaDebugTarget target = (IJavaDebugTarget) thread.getDebugTarget();
+			if (target.supportsHotCodeReplace()) {
+				target.addHotCodeReplaceListener(listener2);
+				// look at the value of 'x' - it should be "One"
+				IJavaStackFrame frame = (IJavaStackFrame) thread.getTopStackFrame();
+				IJavaVariable variable = findVariable(frame, "x");
+				assertNotNull("Could not find 'x'", variable);
+				assertEquals("value of 'x' should be 'One'", "One", variable.getValue().getValueString());
+				removeAllBreakpoints();
+				// now modify the source in *unrelated* project => NO HCR should happen, even if type names are same!
+				ICompilationUnit cu = getCompilationUnit(unrelatedProject, "src", "org.eclipse.debug.tests.targets", "HcrClass.java");
+				cu = cu.getPrimary();
+				if (!cu.isWorkingCopy()) {
+					cu = cu.getWorkingCopy(null);
+				}
+				assertTrue("HcrClass.java does not exist", cu.exists());
+				IBuffer buffer = cu.getBuffer();
+				String contents = buffer.getContents();
+				int index = contents.indexOf("\"One\"");
+				assertTrue("Could not find code to replace", index > 0);
+				String newCode = contents.substring(0, index) + "\"Two\"" + contents.substring(index + 5);
+				buffer.setContents(newCode);
+
+				// save contents
+				cu.commitWorkingCopy(true, null);
+				waitForBuild();
+				assertFalse("Specific listener should not have been notified", listener2.waitNotification());
+				assertFalse("General listener should not have been notified", listener.wasNotified());
+			} else {
+				System.err.println("Warning: HCR test skipped since target VM does not support HCR.");
+			}
+		}
+		finally {
+			terminateAndRemove(thread);
+			removeAllBreakpoints();
+			JDIDebugModel.removeHotCodeReplaceListener(listener);
+		}
+	}
 }
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/JavaDebugTargetTests.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/JavaDebugTargetTests.java
index 82681a9..b6b3e07 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/JavaDebugTargetTests.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/JavaDebugTargetTests.java
@@ -10,9 +10,14 @@
  *******************************************************************************/
 package org.eclipse.jdt.debug.tests.core;
 
+import static org.junit.Assert.assertNotEquals;
+
 import java.lang.reflect.Method;
 
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
 import org.eclipse.jdt.debug.core.IJavaDebugTarget;
+import org.eclipse.jdt.debug.core.IJavaLineBreakpoint;
 import org.eclipse.jdt.debug.core.IJavaThread;
 import org.eclipse.jdt.debug.tests.AbstractDebugTest;
 import org.eclipse.jdt.internal.debug.core.model.JDIDebugTarget;
@@ -111,6 +116,62 @@
 		}
 	}
 
+	/**
+	 * Tests that debug target ignores breakpoints from unrelated projects, see bugs 5188 and 508524
+	 */
+	public void testSupportsResource() throws Exception {
+		String typeName = "org.eclipse.debug.tests.targets.HcrClass";
+		IJavaProject project1 = get14Project();
+		IJavaProject project2 = get15Project();
+		IType type1 = project1.findType(typeName);
+		IType type2 = project2.findType(typeName);
+		assertNotEquals(type1, type2);
+
+		// Types FQNs are same
+		assertEquals(type1.getFullyQualifiedName(), type2.getFullyQualifiedName());
+
+		// Paths are same, except the project part
+		assertEquals(type1.getResource().getFullPath().removeFirstSegments(1), type2.getResource().getFullPath().removeFirstSegments(1));
+
+		final int lineNumber = 21;
+		IJavaLineBreakpoint bp1 = createLineBreakpoint(type1, lineNumber);
+		IJavaLineBreakpoint bp2 = createLineBreakpoint(type2, lineNumber);
+		assertNotEquals(bp1, bp2);
+
+		IJavaThread thread = null;
+		try {
+			// Launch the first project config: the breakpoint from second one shouldn't be supported
+			thread = launchToBreakpoint(project1, typeName, typeName, true);
+			assertNotNull("Breakpoint not hit within timeout period", thread);
+			JDIDebugTarget target = (JDIDebugTarget) thread.getDebugTarget();
+			assertTrue(target.isAvailable());
+			assertEquals(1, target.getBreakpoints().size());
+			assertEquals(bp1, target.getBreakpoints().get(0));
+			assertTrue(target.supportsResource(() -> typeName, type1.getResource()));
+			assertFalse(target.supportsResource(() -> typeName, type2.getResource()));
+			terminateAndRemove(thread);
+			// Line above *deletes all breakpoints!*
+
+			bp1 = createLineBreakpoint(type1, lineNumber);
+			bp2 = createLineBreakpoint(type2, lineNumber);
+			assertNotEquals(bp1, bp2);
+
+			// Launch the second project config: the breakpoint from first one shouldn't be supported
+			thread = launchToBreakpoint(project2, typeName, typeName + CLONE_SUFFIX, true);
+			assertNotNull("Breakpoint not hit within timeout period", thread);
+			target = (JDIDebugTarget) thread.getDebugTarget();
+			assertTrue(target.isAvailable());
+			assertEquals(1, target.getBreakpoints().size());
+			assertEquals(bp2, target.getBreakpoints().get(0));
+			assertFalse(target.supportsResource(() -> typeName, type1.getResource()));
+			assertTrue(target.supportsResource(() -> typeName, type2.getResource()));
+		}
+		finally {
+			terminateAndRemove(thread);
+			removeAllBreakpoints();
+		}
+	}
+
 	static private class JDIDebugTargetProxy {
 
 		private JDIDebugTarget target;
diff --git a/org.eclipse.jdt.debug.tests/testsource-j2se-1.5/org/eclipse/debug/tests/targets/HcrClass.java b/org.eclipse.jdt.debug.tests/testsource-j2se-1.5/org/eclipse/debug/tests/targets/HcrClass.java
new file mode 100644
index 0000000..9d0b89f
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/testsource-j2se-1.5/org/eclipse/debug/tests/targets/HcrClass.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2005 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ * 
+ * Contributors:
+ *     IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.debug.tests.targets;
+
+/**
+ * Class used to test hot code replace
+ */
+public class HcrClass {
+	
+	protected String instVar = null;
+	
+	public static void main(String[] args) {
+		new HcrClass().one();
+	}
+	
+	public void one() {
+		instVar = "One";
+		two();
+	}
+	
+	public void two() {
+		three();
+	}
+	
+	public void three() {
+		four();
+	}
+	
+	public void four() {
+		String x = instVar;
+		System.out.println(x);
+	}
+
+}
diff --git a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/hcr/JavaHotCodeReplaceManager.java b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/hcr/JavaHotCodeReplaceManager.java
index c1b48cd..ef7576a 100644
--- a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/hcr/JavaHotCodeReplaceManager.java
+++ b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/hcr/JavaHotCodeReplaceManager.java
@@ -469,8 +469,15 @@
 			// unloaded types on a per-target basis.
 			List<IResource> resourcesToReplace = new ArrayList<>(resources);
 			List<String> qualifiedNamesToReplace = new ArrayList<>(qualifiedNames);
-			filterUnloadedTypes(target, resourcesToReplace,
-					qualifiedNamesToReplace);
+
+			// Make sure we only try to replace types from related projects
+			filterUnrelatedResources(target, resourcesToReplace, qualifiedNamesToReplace);
+			if (qualifiedNamesToReplace.isEmpty()) {
+				// If none of the changed types are related to our target, do nothing.
+				continue;
+			}
+
+			filterUnloadedTypes(target, resourcesToReplace, qualifiedNamesToReplace);
 			if (qualifiedNamesToReplace.isEmpty()) {
 				// If none of the changed types are loaded, do nothing.
 				continue;
@@ -541,6 +548,18 @@
 		fDeltaCache.clear();
 	}
 
+	private void filterUnrelatedResources(JDIDebugTarget target, List<IResource> resourcesToReplace, List<String> qualifiedNamesToReplace) {
+		Iterator<IResource> resources = resourcesToReplace.iterator();
+		Iterator<String> names = qualifiedNamesToReplace.iterator();
+		while (resources.hasNext()) {
+			boolean supported = target.supportsResource(() -> names.next(), resources.next());
+			if (!supported) {
+				resources.remove();
+				names.remove();
+			}
+		}
+	}
+
 	/**
 	 * Returns whether the given exception, which occurred during HCR, should be
 	 * logged. We anticipate that we can get IncompatibleThreadStateExceptions
diff --git a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIDebugTarget.java b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIDebugTarget.java
index 5c49532..f76bac1 100644
--- a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIDebugTarget.java
+++ b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIDebugTarget.java
@@ -23,6 +23,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.eclipse.core.resources.IFile;
@@ -1391,7 +1392,15 @@
 			return true;
 		}
 
-		IResource resource = marker.getResource();
+		return supportsResource(() -> jBreakpoint.getTypeName(), marker.getResource());
+	}
+
+
+	public boolean supportsResource(Callable<String> typeNameSupplier, IResource resource) {
+		if (fScope == null) {
+			// No checks, everything in scope: the filtering is disabled
+			return true;
+		}
 		// Java exception breakpoints have wsp root as resource
 		if(resource == null || resource == ResourcesPlugin.getWorkspace().getRoot()) {
 			return true;
@@ -1427,7 +1436,7 @@
 		// This can be also an incomplete resource mapping.
 		// Try to see if the type available multiple times in workspace
 		try {
-			String typeName = jBreakpoint.getTypeName();
+			String typeName = typeNameSupplier.call();
 			if(typeName != null){
 				Boolean known = knownTypes.get(typeName);
 				if(known != null){
@@ -1437,7 +1446,8 @@
 				knownTypes.put(typeName, Boolean.valueOf(supportedBreakpoint));
 				return supportedBreakpoint;
 			}
-		} catch (CoreException e) {
+		}
+		catch (Exception e) {
 			logError(e);
 		}
 		// we don't know why computation failed, so let assume the breakpoint is supported.