Bug 578990 - react to Bundle-Root setting changes

Change-Id: I2cfa7841c7a64ac59fbd24dfc874de040057ca6a
Signed-off-by: Hannes Wellmann <wellmann.hannes1@gmx.net>
Reviewed-on: https://git.eclipse.org/r/c/pde/eclipse.pde.ui/+/191246
Tested-by: PDE Bot <pde-bot@eclipse.org>
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java
index bf810b5..8cd46e9 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2018 IBM Corporation and others.
+ * Copyright (c) 2000, 2022 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -1239,21 +1239,4 @@
 	public void removeExtensionDeltaListener(IExtensionDeltaListener listener) {
 		fWorkspaceManager.removeExtensionDeltaListener(listener);
 	}
-
-	/**
-	 * Called when the bundle root for a project is changed.
-	 *
-	 * @param project
-	 */
-	public void bundleRootChanged(IProject project) {
-		fWorkspaceManager.initialize();
-		fWorkspaceManager.removeModel(project);
-		if (fWorkspaceManager.isInterestingProject(project)) {
-			fWorkspaceManager.createModel(project, false);
-			IPluginModelBase model = fWorkspaceManager.getModel(project);
-			fWorkspaceManager.addChange(model, IModelProviderEvent.MODELS_CHANGED);
-		}
-		fWorkspaceManager.processModelChanges();
-	}
-
 }
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/WorkspaceModelManager.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/WorkspaceModelManager.java
index f313971..3e0740c 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/WorkspaceModelManager.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/WorkspaceModelManager.java
@@ -10,6 +10,7 @@
  *
  *  Contributors:
  *     IBM Corporation - initial API and implementation
+ *     Hannes Wellmann - react to changes of Bundle-Root setting and in derived folders
  *******************************************************************************/
 package org.eclipse.pde.internal.core;
 
@@ -17,11 +18,14 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.ListIterator;
 import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Stream;
 import org.eclipse.core.resources.IFile;
 import org.eclipse.core.resources.IFolder;
 import org.eclipse.core.resources.IProject;
@@ -31,11 +35,18 @@
 import org.eclipse.core.resources.IResourceDelta;
 import org.eclipse.core.resources.IResourceDeltaVisitor;
 import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ProjectScope;
+import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
 import org.eclipse.pde.core.IModel;
 import org.eclipse.pde.core.IModelProviderEvent;
 import org.eclipse.pde.internal.core.project.PDEProject;
 import org.eclipse.team.core.RepositoryProvider;
+import org.osgi.service.prefs.BackingStoreException;
+import org.osgi.service.prefs.Preferences;
 
 public abstract class WorkspaceModelManager<T> extends AbstractModelManager
 		implements IResourceChangeListener, IResourceDeltaVisitor {
@@ -96,6 +107,7 @@
 
 	private Map<IProject, T> fModels = null;
 	private ArrayList<ModelChange> fChangedModels;
+	private final IPreferenceChangeListener bundleRootChangedListener = createBundleRootChangeListener();
 
 	protected Map<IProject, T> getModelsMap() {
 		ensureModelsMapCreated();
@@ -133,6 +145,9 @@
 	protected void addListeners() {
 		IWorkspace workspace = PDECore.getWorkspace();
 		workspace.addResourceChangeListener(this, IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.POST_CHANGE);
+		if (bundleRootChangedListener != null) {
+			Arrays.stream(workspace.getRoot().getProjects()).forEach(this::addBundleRootChangedListener);
+		}
 	}
 
 	@Override
@@ -180,6 +195,9 @@
 					IProject project = (IProject) resource;
 					boolean addedOrOpened = delta.getKind() == IResourceDelta.ADDED
 							|| (delta.getFlags() & IResourceDelta.OPEN) != 0;
+					if (addedOrOpened && bundleRootChangedListener != null) {
+						addBundleRootChangedListener(project);
+					}
 					if (isInterestingProject(project) && addedOrOpened) {
 						createModel(project, true);
 						return false;
@@ -202,6 +220,39 @@
 		return false;
 	}
 
+	private void addBundleRootChangedListener(IProject project) {
+		IEclipsePreferences pdeNode = new ProjectScope(project).getNode(PDECore.PLUGIN_ID);
+		// Always add the same listener instance to not add multiple listeners
+		// in case of repetitive project opening/closing
+		pdeNode.addPreferenceChangeListener(bundleRootChangedListener);
+	}
+
+	protected IPreferenceChangeListener createBundleRootChangeListener() {
+		return e -> {
+			if (PDEProject.BUNDLE_ROOT_PATH.equals(e.getKey()) && !isInRemovedBranch(e.getNode())) {
+				String projectName = Path.forPosix(e.getNode().absolutePath()).segment(1);
+				IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
+
+				// bundle-root changed (null value means default): try to
+				// (re-)load model from new bundle-root path (may delete it)
+				if (getModel(project) != null) {
+					removeModel(project);
+				}
+				createModel(project, true);
+			}
+		};
+	}
+
+	private static boolean isInRemovedBranch(Preferences node) {
+		return !Stream.iterate(node, Objects::nonNull, Preferences::parent).allMatch(n -> {
+			try { // Returns true if existing node is about to be removed
+				return n.nodeExists(""); //$NON-NLS-1$
+			} catch (BackingStoreException e1) {
+				return false;
+			}
+		});
+	}
+
 	private boolean isContentChange(IResourceDelta delta) {
 		int kind = delta.getKind();
 		return (kind == IResourceDelta.ADDED || kind == IResourceDelta.REMOVED || (kind == IResourceDelta.CHANGED && (delta.getFlags() & IResourceDelta.CONTENT) != 0));
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/WorkspacePluginModelManager.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/WorkspacePluginModelManager.java
index e60e790..a726211 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/WorkspacePluginModelManager.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/WorkspacePluginModelManager.java
@@ -449,6 +449,8 @@
 	 */
 	@Override
 	protected void addListeners() {
+		super.addListeners();
+		// Overwrite resource-change event-mask set in the super implementation:
 		PDECore.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.PRE_CLOSE);
 		// PDE must process the POST_CHANGE events before the Java model
 		// for the PDE container classpath update to proceed smoothly
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/project/PDEProject.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/project/PDEProject.java
index 7173de2..53f6feb 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/project/PDEProject.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/project/PDEProject.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2010, 2013 IBM Corporation and others.
+ * Copyright (c) 2010, 2022 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -135,8 +135,7 @@
 			}
 			try {
 				node.flush();
-				// update model manager
-				PDECore.getDefault().getModelManager().bundleRootChanged(project);
+				// WorkspacePluginModelManager reacts to setting changes
 			} catch (BackingStoreException e) {
 				throw new CoreException(Status.error(e.getMessage(), e));
 			}
diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/WorkspaceModelManagerTest.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/WorkspaceModelManagerTest.java
index b3ba65a..b6b34c4 100644
--- a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/WorkspaceModelManagerTest.java
+++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/WorkspaceModelManagerTest.java
@@ -170,6 +170,67 @@
 		assertNull(mm.getModel(project));
 	}
 
+	@Test
+	public void testBundleRootHandling_bundleRootChangedFromDefaultToOthersAndReverse() throws Exception {
+		TestWorkspaceModelManager mm = createWorkspaceModelManager();
+		IProject project = createModelProject("plugin.a", "1.0.0");
+		copyFile(manifest(project), manifestIn(project, "otherRoot"), replaceVersionTo("2.0.0"));
+		copyFile(manifest(project), manifestIn(project, "root2"), replaceVersionTo("3.0.0"));
+
+		setBundleRoot(project, "otherRoot");
+
+		IPluginModelBase model1 = mm.getModel(project);
+		assertExistingModel("plugin.a", "2.0.0", model1);
+		assertEquals(manifestIn(project, "otherRoot"), model1.getUnderlyingResource());
+
+		setBundleRoot(project, "root2");
+
+		IPluginModelBase model2 = mm.getModel(project);
+		assertExistingModel("plugin.a", "3.0.0", model2);
+		assertEquals(manifestIn(project, "root2"), model2.getUnderlyingResource());
+
+		setBundleRoot(project, null);
+
+		IPluginModelBase model0 = mm.getModel(project);
+		assertExistingModel("plugin.a", "1.0.0", model0);
+		assertEquals(manifest(project), model0.getUnderlyingResource());
+	}
+
+	@Test
+	public void testBundleRootHandling_bundleRootChangedFromNoneToOther() throws Exception {
+		TestWorkspaceModelManager mm = createWorkspaceModelManager();
+		IProject project = createModelProject("plugin.a", "1.0.0");
+
+		copyFile(manifest(project), manifestIn(project, "otherRoot"), replaceVersionTo("2.0.0"));
+
+		manifest(project).delete(true, null);
+		assertNull(mm.getModel(project));
+
+		setBundleRoot(project, "otherRoot");
+
+		IPluginModelBase model = mm.getModel(project);
+		assertExistingModel("plugin.a", "2.0.0", model);
+		assertEquals(manifestIn(project, "otherRoot"), model.getUnderlyingResource());
+	}
+
+	@Test
+	public void testBundleRootHandling_bundleRootChangedFromNoneToDefault() throws Exception {
+		TestWorkspaceModelManager mm = createWorkspaceModelManager();
+		IProject project = createModelProject("plugin.a", "1.0.0");
+
+		copyFile(manifest(project), manifestIn(project, "otherRoot"), replaceVersionTo("2.0.0"));
+		setBundleRoot(project, "otherRoot");
+
+		manifestIn(project, "otherRoot").delete(true, null);
+		assertNull(mm.getModel(project));
+
+		setBundleRoot(project, null);
+
+		IPluginModelBase model = mm.getModel(project);
+		assertExistingModel("plugin.a", "1.0.0", model);
+		assertEquals(manifest(project), model.getUnderlyingResource());
+	}
+
 	// --- utilities ---
 
 	// This class tests tests the abstract WorkspaceModelManager using the
diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/views/features/model/WorkspaceProductModelManager.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/views/features/model/WorkspaceProductModelManager.java
index d6bb856..4b127ad 100644
--- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/views/features/model/WorkspaceProductModelManager.java
+++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/views/features/model/WorkspaceProductModelManager.java
@@ -17,6 +17,7 @@
 import java.util.Collection;
 import org.eclipse.core.resources.*;
 import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
 import org.eclipse.jdt.core.JavaCore;
 import org.eclipse.pde.core.IModelProviderEvent;
 import org.eclipse.pde.internal.core.WorkspaceModelManager;
@@ -28,6 +29,11 @@
 	public final static String PRODUCT_FILENAME_SUFFIX = ".product"; //$NON-NLS-1$
 
 	@Override
+	protected IPreferenceChangeListener createBundleRootChangeListener() {
+		return null; // ignore bundle-root changes
+	}
+
+ 	@Override
 	protected boolean isInterestingProject(IProject project) {
 		return (project.isOpen() && !findProductFiles(project, false).isEmpty());
 	}