Bug 544924 - Update plugin classpath when annotation provider changes

Whenever relevant changes happen in plugin registry, recompute the
classpath contributions of affected projects.

Change-Id: I034f43c6c39eb6676262c2a9484046fe48a23946
Signed-off-by: Peter Nehrer <pnehrer@eclipticalsoftware.com>
diff --git a/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/Activator.java b/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/Activator.java
index af3b20a..601462e 100644
--- a/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/Activator.java
+++ b/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/Activator.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2012, 2017 Ecliptical Software Inc. and others.
+ * Copyright (c) 2012, 2019 Ecliptical Software Inc. and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -61,6 +61,7 @@
 
 	@Override
 	public void stop(BundleContext context) throws Exception {
+		DSLibPluginModelListener.dispose();
 		dsPrefListener.dispose();
 
 		synchronized (projectPrefListeners) {
diff --git a/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/DSAnnotationClasspathContributor.java b/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/DSAnnotationClasspathContributor.java
index fa153c6..a49e9c5 100644
--- a/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/DSAnnotationClasspathContributor.java
+++ b/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/DSAnnotationClasspathContributor.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2015, 2017 Ecliptical Software Inc. and others.
+ * Copyright (c) 2015, 2019 Ecliptical Software Inc. and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -48,44 +48,47 @@
 		IPluginModelBase model = PluginRegistry.findModel(project);
 		if (model != null) {
 			IResource resource = model.getUnderlyingResource();
-			if (resource != null && !WorkspaceModelManager.isBinaryProject(resource.getProject())) {
-				IPreferencesService prefs = Platform.getPreferencesService();
-				IScopeContext[] scope = new IScopeContext[] { new ProjectScope(resource.getProject()),
-						InstanceScope.INSTANCE, DefaultScope.INSTANCE };
-				boolean enabled = prefs.getBoolean(Activator.PLUGIN_ID, Activator.PREF_ENABLED, false, scope);
-				if (enabled) {
-					boolean autoClasspath = prefs.getBoolean(Activator.PLUGIN_ID, Activator.PREF_CLASSPATH, true,
-							scope);
-					if (autoClasspath) {
-						DSAnnotationVersion specVersion;
-						try {
-							specVersion = DSAnnotationVersion.valueOf(prefs.getString(Activator.PLUGIN_ID,
-									Activator.PREF_SPEC_VERSION, DSAnnotationVersion.V1_3.name(), scope));
-						} catch (IllegalArgumentException e) {
-							specVersion = DSAnnotationVersion.V1_3;
-						}
+			if (resource == null || WorkspaceModelManager.isBinaryProject(resource.getProject())) {
+				return Collections.emptyList();
+			}
 
-						String libBundleName;
-						if (specVersion == DSAnnotationVersion.V1_3) {
-							libBundleName = "org.eclipse.pde.ds.lib"; //$NON-NLS-1$
-						} else {
-							libBundleName = "org.eclipse.pde.ds1_2.lib"; //$NON-NLS-1$
-						}
+			IPreferencesService prefs = Platform.getPreferencesService();
+			IScopeContext[] scope = new IScopeContext[] { new ProjectScope(resource.getProject()),
+					InstanceScope.INSTANCE, DefaultScope.INSTANCE };
+			boolean enabled = prefs.getBoolean(Activator.PLUGIN_ID, Activator.PREF_ENABLED, false, scope);
+			if (enabled) {
+				boolean autoClasspath = prefs.getBoolean(Activator.PLUGIN_ID, Activator.PREF_CLASSPATH, true, scope);
+				if (autoClasspath) {
+					DSAnnotationVersion specVersion;
+					try {
+						specVersion = DSAnnotationVersion.valueOf(prefs.getString(Activator.PLUGIN_ID,
+								Activator.PREF_SPEC_VERSION, DSAnnotationVersion.V1_3.name(), scope));
+					} catch (IllegalArgumentException e) {
+						specVersion = DSAnnotationVersion.V1_3;
+					}
 
-						IPluginModelBase bundle = PluginRegistry.findModel(libBundleName);
-						if (bundle != null && bundle.isEnabled()) {
-							String location = bundle.getInstallLocation();
-							if (location != null) {
-								IPath srcPath = getSrcPath(libBundleName);
-								IClasspathEntry entry = JavaCore.newLibraryEntry(
-										new Path(location + "/annotations.jar"), srcPath, Path.ROOT, //$NON-NLS-1$
-										ANNOTATION_ACCESS_RULES, DS_ATTRS, false);
-								return Collections.singletonList(entry);
-							}
+					String libBundleName;
+					if (specVersion == DSAnnotationVersion.V1_3) {
+						libBundleName = "org.eclipse.pde.ds.lib"; //$NON-NLS-1$
+					} else {
+						libBundleName = "org.eclipse.pde.ds1_2.lib"; //$NON-NLS-1$
+					}
+
+					IPluginModelBase bundle = PluginRegistry.findModel(libBundleName);
+					if (bundle != null && bundle.isEnabled()) {
+						String location = bundle.getInstallLocation();
+						if (location != null) {
+							IPath srcPath = getSrcPath(libBundleName);
+							IClasspathEntry entry = JavaCore.newLibraryEntry(new Path(location + "/annotations.jar"), //$NON-NLS-1$
+									srcPath, Path.ROOT, ANNOTATION_ACCESS_RULES, DS_ATTRS, false);
+							DSLibPluginModelListener.addProject(JavaCore.create(resource.getProject()), libBundleName);
+							return Collections.singletonList(entry);
 						}
 					}
 				}
 			}
+
+			DSLibPluginModelListener.removeProject(JavaCore.create(resource.getProject()));
 		}
 
 		return Collections.emptyList();
diff --git a/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/DSAnnotationPropertyPage.java b/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/DSAnnotationPropertyPage.java
index 1080fe1..1134dc3 100644
--- a/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/DSAnnotationPropertyPage.java
+++ b/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/DSAnnotationPropertyPage.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2012, 2017 Ecliptical Software Inc. and others.
+ * Copyright (c) 2012, 2019 Ecliptical Software Inc. and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -428,12 +428,13 @@
 	public boolean performOk() {
 		IEclipsePreferences prefs;
 		if (isProjectPreferencePage()) {
-			prefs = wcManager.getWorkingCopy(new ProjectScope(getProject()).getNode(Activator.PLUGIN_ID));
-			if (!useProjectSettings()) {
+			IProject project = getProject();
+			prefs = wcManager.getWorkingCopy(new ProjectScope(project).getNode(Activator.PLUGIN_ID));
+			if (useProjectSettings()) {
+				Activator.getDefault().listenForClasspathPreferenceChanges(JavaCore.create(project));
+			} else {
 				try {
-					for (String key : prefs.keys()) {
-						prefs.remove(key);
-					}
+					prefs.clear();
 				} catch (BackingStoreException e) {
 					Activator.log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Unable to reset project preferences.", e)); //$NON-NLS-1$
 				}
@@ -476,11 +477,6 @@
 			return false;
 		}
 
-		IProject project = getProject();
-		if (project != null) {
-			Activator.getDefault().listenForClasspathPreferenceChanges(JavaCore.create(project));
-		}
-
 		return true;
 	}
 }
\ No newline at end of file
diff --git a/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/DSLibPluginModelListener.java b/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/DSLibPluginModelListener.java
new file mode 100644
index 0000000..a383061
--- /dev/null
+++ b/ds/org.eclipse.pde.ds.annotations/src/org/eclipse/pde/ds/internal/annotations/DSLibPluginModelListener.java
@@ -0,0 +1,168 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Ecliptical Software Inc. and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Ecliptical Software Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.ds.internal.annotations;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+import org.eclipse.core.resources.WorkspaceJob;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.core.runtime.jobs.ISchedulingRule;
+import org.eclipse.core.runtime.jobs.MultiRule;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.pde.core.plugin.ModelEntry;
+import org.eclipse.pde.internal.core.IPluginModelListener;
+import org.eclipse.pde.internal.core.PluginModelDelta;
+import org.eclipse.pde.internal.core.PluginModelManager;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.PlatformUI;
+
+@SuppressWarnings("restriction")
+public class DSLibPluginModelListener implements IPluginModelListener {
+
+	private static DSLibPluginModelListener INSTANCE;
+
+	private final HashMap<IJavaProject, String> projects = new HashMap<>();
+
+	private final HashMap<String, Integer> counts = new HashMap<>();
+
+	private DSLibPluginModelListener() {
+		PluginModelManager.getInstance().addPluginModelListener(this);
+	}
+
+	private synchronized static DSLibPluginModelListener getInstance(boolean create) {
+		if (create && INSTANCE == null) {
+			INSTANCE = new DSLibPluginModelListener();
+		}
+
+		return INSTANCE;
+	}
+
+	private void decrementCount(String modelId) {
+		Integer oldCount = counts.get(modelId);
+		if (oldCount != null) {
+			if (oldCount.intValue() <= 1) {
+				counts.remove(modelId);
+			} else {
+				counts.put(modelId, oldCount.intValue() - 1);
+			}
+		}
+	}
+
+	public static void addProject(IJavaProject project, String modelId) {
+		DSLibPluginModelListener instance = getInstance(true);
+		synchronized (instance.projects) {
+			String oldModelId = instance.projects.put(project, modelId);
+			Integer count = instance.counts.getOrDefault(modelId, Integer.valueOf(0));
+			instance.counts.put(modelId, count.intValue() + 1);
+			if (oldModelId != null) {
+				instance.decrementCount(oldModelId);
+			}
+		}
+	}
+
+	public static void removeProject(IJavaProject project) {
+		DSLibPluginModelListener instance = getInstance(false);
+		if (instance != null) {
+			synchronized (instance.projects) {
+				String oldModelId = instance.projects.remove(project);
+				if (oldModelId != null) {
+					instance.decrementCount(oldModelId);
+				}
+			}
+		}
+	}
+
+	private boolean containsModel(ModelEntry[] entries, String id) {
+		for (ModelEntry entry : entries) {
+			if ("org.eclipse.pde.ds.lib".equals(entry.getId())) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	@Override
+	public void modelsChanged(PluginModelDelta delta) {
+		synchronized (projects) {
+			HashSet<String> modelIds = new HashSet<>(2);
+		for (String modelId : counts.keySet()) {
+			if (((delta.getKind() & PluginModelDelta.ADDED) != 0 && containsModel(delta.getAddedEntries(), modelId))
+					|| ((delta.getKind() & PluginModelDelta.CHANGED) != 0
+							&& containsModel(delta.getChangedEntries(), modelId))
+					|| ((delta.getKind() & PluginModelDelta.REMOVED) != 0
+							&& containsModel(delta.getRemovedEntries(), modelId))) {
+				modelIds.add(modelId);
+			}
+		}
+
+			ArrayList<IJavaProject> toUpdate = new ArrayList<>(projects.size());
+			if (!modelIds.isEmpty()) {
+				for (Map.Entry<IJavaProject, String> entry : projects.entrySet()) {
+					IJavaProject project = entry.getKey();
+					String modelId = entry.getValue();
+					if (modelIds.contains(modelId)) {
+						toUpdate.add(project);
+					}
+				}
+			}
+
+			if (!toUpdate.isEmpty()) {
+				requestClasspathUpdate(toUpdate);
+			}
+		}
+	}
+
+	private void requestClasspathUpdate(final Collection<IJavaProject> changedProjects) {
+		WorkspaceJob job = new WorkspaceJob(Messages.ProjectClasspathPreferenceChangeListener_jobName) {
+			@Override
+			public IStatus runInWorkspace(IProgressMonitor monitor) {
+				SubMonitor progress = SubMonitor.convert(monitor, Messages.DSAnnotationPreferenceListener_taskName,
+						changedProjects.size());
+				for (IJavaProject project : changedProjects) {
+					ProjectClasspathPreferenceChangeListener.updateClasspathContainer(project, progress.newChild(1));
+				}
+
+				return Status.OK_STATUS;
+			};
+		};
+		
+		job.setSystem(true);
+
+		ISchedulingRule[] rules = changedProjects.stream().map(IJavaProject::getProject)
+				.toArray(size -> new ISchedulingRule[size]);
+		job.setRule(new MultiRule(rules));
+
+		Display display = Display.getCurrent();
+		if (display != null) {
+			PlatformUI.getWorkbench().getProgressService().showInDialog(display.getActiveShell(), job);
+		}
+
+		job.schedule();
+	}
+
+	public static void dispose() {
+		DSLibPluginModelListener instance = getInstance(false);
+		if (instance != null) {
+			PluginModelManager.getInstance().removePluginModelListener(instance);
+		}
+	}
+}