Bug 507795 - Functional dynamic dependencies

Add support for functional dynamic dependencies.

Change-Id: I38286e4bcb877613264cfc53cc75ff23d02483d9
Signed-off-by: Stefan Xenos <sxenos@gmail.com>
diff --git a/bundles/org.eclipse.core.resources/schema/builders.exsd b/bundles/org.eclipse.core.resources/schema/builders.exsd
index 8db1747..03a7a18 100644
--- a/bundles/org.eclipse.core.resources/schema/builders.exsd
+++ b/bundles/org.eclipse.core.resources/schema/builders.exsd
@@ -69,6 +69,7 @@
    <element name="builder">
       <complexType>
          <sequence>
+            <element ref="dynamicReference" minOccurs="0" maxOccurs="1"/>
             <element ref="run" minOccurs="0" maxOccurs="1"/>
          </sequence>
          <attribute name="hasNature" type="boolean">
@@ -154,6 +155,24 @@
       </complexType>
    </element>
 
+   <element name="dynamicReference">
+      <complexType>
+         <attribute name="class" type="string">
+            <annotation>
+               <documentation>
+                  The fully-qualified name of a class that implements &lt;samp&gt;org.eclipse.core.resources.IDynamicReferenceProvider&lt;/samp&gt;.
+                  If supplied, the given class will asked to return a list of other projects that
+                  must be built before this one when the build system is computing the build order.
+               </documentation>
+               <appInfo>
+                  <meta.attribute kind="java" basedOn=":org.eclipse.core.resources.IDynamicReferenceProvider"/>
+               </appInfo>
+            </annotation>
+         </attribute>
+      </complexType>
+   </element>
+
+
    <annotation>
       <appInfo>
          <meta.section type="examples"/>
@@ -188,7 +207,6 @@
       </documentation>
    </annotation>
 
-
    <annotation>
       <appInfo>
          <meta.section type="implementation"/>
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java
index 89580954..0cc9d2d 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java
@@ -470,6 +470,21 @@
 	}
 
 	@Override
+	public void clearCachedDynamicReferences() {
+		ResourceInfo info = getResourceInfo(false, false);
+		if (info == null) {
+			// If the project is not open there is no cached state and so there is nothing to do.
+			return;
+		}
+		ProjectDescription description = ((ProjectInfo) info).getDescription();
+		if (description == null) {
+			// If the project is in the process of being created there is no cached state and nothing to do.
+			return;
+		}
+		description.clearCachedDynamicReferences(null);
+	}
+
+	@Override
 	public IProject[] getReferencingProjects() {
 		IProject[] projects = workspace.getRoot().getProjects(IContainer.INCLUDE_HIDDEN);
 		List<IProject> result = new ArrayList<>(projects.length);
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescription.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescription.java
index 7bbc9df..1af05fa 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescription.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescription.java
@@ -61,8 +61,21 @@
 	protected volatile IBuildConfiguration[] cachedBuildConfigs;
 	// Cached build configuration references. Not persisted.
 	protected Map<String, IBuildConfiguration[]> cachedConfigRefs = Collections.synchronizedMap(new HashMap<String, IBuildConfiguration[]>(1));
-	// Cached project level references.
-	protected volatile IProject[] cachedRefs = null;
+	/**
+	 * Cached project level references. Synchronize on {@link #cachedRefsMutex} before reading or writing. Increment
+	 * {@link #cachedRefsDirtyCount} whenever this is dirtied.
+	 */
+	protected IProject[] cachedRefs;
+	/**
+	 * Counts the number of times {@link #cachedRefs} has been dirtied. Can be used to determine if dynamic dependencies have
+	 * changed during an operation that is intended to be atomic with respect to dynamic dependencies. Synchronize on
+	 * {@link #cachedRefsMutex} before accessing.
+	 */
+	protected int cachedRefsDirtyCount;
+	/**
+	 * Mutex used to protect {@link #cachedRefs} and {@link #cachedRefsDirtyCount}.
+	 */
+	protected final Object cachedRefsMutex = new Object();
 
 	/**
 	 * Map of (IPath -> LinkDescription) pairs for each linked resource
@@ -103,7 +116,7 @@
 		clone.buildSpec = getBuildSpec(true);
 		clone.dynamicConfigRefs = (HashMap<String, IBuildConfiguration[]>) dynamicConfigRefs.clone();
 		clone.cachedConfigRefs = Collections.synchronizedMap(new HashMap<String, IBuildConfiguration[]>(1));
-		clone.clearCachedReferences(null);
+		clone.clearCachedDynamicReferences(null);
 		return clone;
 	}
 
@@ -111,12 +124,15 @@
 	 * Clear cached references for the specified build config name
 	 * or all if configName is null.
 	 */
-	private void clearCachedReferences(String configName) {
-		if (configName == null)
-			cachedConfigRefs.clear();
-		else
-			cachedConfigRefs.remove(configName);
-		cachedRefs = null;
+	public void clearCachedDynamicReferences(String configName) {
+		synchronized (cachedRefsMutex) {
+			if (configName == null)
+				cachedConfigRefs.clear();
+			else
+				cachedConfigRefs.remove(configName);
+			cachedRefs = null;
+			cachedRefsDirtyCount++;
+		}
 	}
 
 	/**
@@ -189,8 +205,17 @@
 	 * @see #getAllBuildConfigReferences(String, boolean)
 	 */
 	public IProject[] getAllReferences(boolean makeCopy) {
-		IProject[] projRefs = cachedRefs;
-		if (projRefs == null) {
+		int dirtyCount;
+		IProject[] projRefs;
+
+		synchronized (cachedRefsMutex) {
+			projRefs = cachedRefs;
+			dirtyCount = cachedRefsDirtyCount;
+		}
+		// Retry this computation until we're able to proceed to the end without someone dirtying the cache.
+		// This loop is here to prevent us from caching a stale result if someone dirties the cache between
+		// the time we invoke getAllBuildConfigReferences and the time we can write to cachedRefs.
+		while (projRefs == null) {
 			IBuildConfiguration[] refs;
 			if (hasBuildConfig(activeConfiguration))
 				refs = getAllBuildConfigReferences(activeConfiguration, false);
@@ -200,7 +225,16 @@
 				// No build configuration => fall-back to default
 				refs = getAllBuildConfigReferences(IBuildConfiguration.DEFAULT_CONFIG_NAME, false);
 			Collection<IProject> l = getProjectsFromBuildConfigRefs(refs);
-			projRefs = cachedRefs = l.toArray(new IProject[l.size()]);
+
+			synchronized (cachedRefsMutex) {
+				// If nobody dirtied the cache since the start of this operation then we can cache the
+				// new result and end the loop.
+				if (cachedRefsDirtyCount == dirtyCount) {
+					cachedRefs = l.toArray(new IProject[l.size()]);
+				}
+				projRefs = cachedRefs;
+				dirtyCount = cachedRefsDirtyCount;
+			}
 		}
 		//still need to copy the result to prevent tampering with the cache
 		return makeCopy ? (IProject[]) projRefs.clone() : projRefs;
@@ -224,7 +258,15 @@
 		if (refs == null) {
 			Set<IBuildConfiguration> references = new LinkedHashSet<>();
 			IBuildConfiguration[] dynamicBuildConfigs = dynamicConfigRefs.containsKey(configName) ? dynamicConfigRefs.get(configName) : EMPTY_BUILD_CONFIG_REFERENCE_ARRAY;
-			Collection<BuildConfiguration> dynamic = getBuildConfigReferencesFromProjects(dynamicRefs);
+			IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(getName());
+			Collection<BuildConfiguration> dynamic;
+			try {
+				IBuildConfiguration buildConfig = project.getBuildConfig(configName);
+				dynamic = getBuildConfigReferencesFromProjects(computeDynamicReferencesForProject(buildConfig, getBuildSpec()));
+			} catch (CoreException e) {
+				dynamic = Collections.emptyList();
+			}
+			Collection<BuildConfiguration> legacyDynamic = getBuildConfigReferencesFromProjects(dynamicRefs);
 			Collection<BuildConfiguration> statik = getBuildConfigReferencesFromProjects(staticRefs);
 
 			// Combine all references:
@@ -232,6 +274,7 @@
 			references.addAll(Arrays.asList(dynamicBuildConfigs));
 			// We preserve the previous order of static project references before dynamic project references
 			references.addAll(statik);
+			references.addAll(legacyDynamic);
 			references.addAll(dynamic);
 			refs = references.toArray(new IBuildConfiguration[references.size()]);
 			cachedConfigRefs.put(configName, refs);
@@ -539,7 +582,7 @@
 	public void setActiveBuildConfig(String configName) {
 		Assert.isNotNull(configName);
 		if (!configName.equals(activeConfiguration))
-			clearCachedReferences(null);
+			clearCachedDynamicReferences(null);
 		activeConfiguration = configName;
 	}
 
@@ -567,16 +610,17 @@
 		comment = value;
 	}
 
+	@Deprecated
 	@Override
 	public void setDynamicReferences(IProject[] value) {
 		Assert.isLegal(value != null);
 		dynamicRefs = copyAndRemoveDuplicates(value);
-		clearCachedReferences(null);
+		clearCachedDynamicReferences(null);
 	}
 
 	public void setBuildConfigReferences(HashMap<String, IBuildConfiguration[]> refs) {
 		dynamicConfigRefs = new HashMap<>(refs);
-		clearCachedReferences(null);
+		clearCachedDynamicReferences(null);
 	}
 
 	@Override
@@ -586,7 +630,7 @@
 		if (!hasBuildConfig(configName))
 			return;
 		dynamicConfigRefs.put(configName, copyAndRemoveDuplicates(references));
-		clearCachedReferences(configName);
+		clearCachedDynamicReferences(configName);
 	}
 
 	@Override
@@ -613,7 +657,7 @@
 		// Remove references for deleted buildConfigs
 		boolean modified = dynamicConfigRefs.keySet().retainAll(buildConfigNames);
 		if (modified)
-			clearCachedReferences(null);
+			clearCachedDynamicReferences(null);
 		// Clear the cached IBuildConfiguration[]
 		cachedBuildConfigs = null;
 	}
@@ -813,7 +857,7 @@
 	public void setReferencedProjects(IProject[] value) {
 		Assert.isLegal(value != null);
 		staticRefs = copyAndRemoveDuplicates(value);
-		clearCachedReferences(null);
+		clearCachedDynamicReferences(null);
 	}
 
 	/**
@@ -871,7 +915,46 @@
 			dynamicConfigRefs = new HashMap<>(description.dynamicConfigRefs);
 		}
 		if (changed)
-			clearCachedReferences(null);
+			clearCachedDynamicReferences(null);
 		return changed;
 	}
+
+	/**
+	 * Computes the dynamic references for the given project + configuration.
+	 */
+	private static IProject[] computeDynamicReferencesForProject(IBuildConfiguration buildConfig, ICommand[] buildSpec) {
+		List<IProject> result = new ArrayList<>();
+		for (ICommand command : buildSpec) {
+			IExtension extension = Platform.getExtensionRegistry().getExtension(ResourcesPlugin.PI_RESOURCES, ResourcesPlugin.PT_BUILDERS, command.getBuilderName());
+
+			if (extension == null) {
+				continue;
+			}
+
+			IConfigurationElement[] configurationElements = extension.getConfigurationElements();
+
+			if (configurationElements.length == 0) {
+				continue;
+			}
+
+			IConfigurationElement element = configurationElements[0];
+
+			Object executableExtension;
+			try {
+				IConfigurationElement[] children = element.getChildren("dynamicReference"); //$NON-NLS-1$
+				if (children.length != 0) {
+					executableExtension = children[0].createExecutableExtension("class"); //$NON-NLS-1$
+					if (executableExtension instanceof IDynamicReferenceProvider) {
+						IDynamicReferenceProvider provider = (IDynamicReferenceProvider) executableExtension;
+
+						result.addAll(provider.getDependentProjects(buildConfig));
+					}
+				}
+			} catch (CoreException e) {
+				String problemElement = element.toString();
+				ResourcesPlugin.getPlugin().getLog().log(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, "Unable to load dynamic reference provider: " + problemElement, e)); //$NON-NLS-1$
+			}
+		}
+		return result.toArray(new IProject[0]);
+	}
 }
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IDynamicReferenceProvider.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IDynamicReferenceProvider.java
new file mode 100644
index 0000000..7392f8a
--- /dev/null
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IDynamicReferenceProvider.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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.core.resources;
+
+import java.util.List;
+import org.eclipse.core.runtime.CoreException;
+
+/**
+ * Implementations of this interface are capable of determining a set
+ * of projects which a given project depends upon. Unless otherwise stated,
+ * all arguments and return values are non-null.
+ *
+ * @since 3.12
+ */
+public interface IDynamicReferenceProvider {
+	/**
+	 * Returns the set of projects which the given project depends upon. If the return
+	 * value of a previous call to this method ever changes, it will fire an event to
+	 * the listeners. This method my be invoked from any thread and may be invoked
+	 * in parallel by multiple threads.
+	 *
+	 * @param buildConfiguration the build configuration being queried.
+	 * @return the set of projects which the given projects depends upon.
+	 */
+	public List<IProject> getDependentProjects(IBuildConfiguration buildConfiguration) throws CoreException;
+}
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProject.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProject.java
index 9702a5b..0ea186a 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProject.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProject.java
@@ -101,7 +101,7 @@
 	 * @see IncrementalProjectBuilder#CLEAN_BUILD
 	 * @see IResourceRuleFactory#buildRule()
 	 */
-	public void build(int kind, String builderName, Map<String,String> args, IProgressMonitor monitor) throws CoreException;
+	public void build(int kind, String builderName, Map<String, String> args, IProgressMonitor monitor) throws CoreException;
 
 	/**
 	 * Builds this project. Does nothing if the project is closed.
@@ -611,6 +611,20 @@
 	public IProject[] getReferencedProjects() throws CoreException;
 
 	/**
+	 * Clears the cache of dynamic project references for this project. Invoking this
+	 * method will cause the dynamic project references to be recomputed the next time
+	 * they are accessed (for example, in a call to {@link #getReferencedProjects()}.
+	 * It is not necessary to hold the workspace lock when invoking this method. Plugins
+	 * that provide an {@link IDynamicReferenceProvider} should invoke this method to
+	 * inform the rest of the application when one or more dynamic project references
+	 * may have changed. This will also clear any other cached data that is derived from
+	 * the dynamic references.
+	 *
+	 * @since 3.12
+	 */
+	public void clearCachedDynamicReferences();
+
+	/**
 	 * Returns the list of all open projects which reference
 	 * this project. This project may or may not exist. Returns
 	 * an empty array if there are no referencing projects.
@@ -744,8 +758,7 @@
 	 * @see #saveSnapshot(int, URI, IProgressMonitor)
 	 * @since 3.6
 	 */
-	public void loadSnapshot(int options, URI snapshotLocation,
-			IProgressMonitor monitor) throws CoreException;
+	public void loadSnapshot(int options, URI snapshotLocation, IProgressMonitor monitor) throws CoreException;
 
 	/**
 	 * Renames this project so that it is located at the name in
@@ -894,8 +907,7 @@
 	 * @see #loadSnapshot(int, URI, IProgressMonitor)
 	 * @since 3.6
 	 */
-	public void saveSnapshot(int options, URI snapshotLocation,
-			IProgressMonitor monitor) throws CoreException;
+	public void saveSnapshot(int options, URI snapshotLocation, IProgressMonitor monitor) throws CoreException;
 
 	/**
 	 * Changes this project resource to match the given project
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProjectDescription.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProjectDescription.java
index 8cb8f08..dbc4516 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProjectDescription.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProjectDescription.java
@@ -275,12 +275,14 @@
 	 * Users must call {@link IProject#setDescription(IProjectDescription, int, IProgressMonitor)}
 	 * before changes made to this description take effect.
 	 * </p>
+	 * @deprecated please use {@link IDynamicReferenceProvider} with the builders extension point to supply dynamic references
 	 * @see #getDynamicReferences()
 	 * @see #setBuildConfigReferences(String, IBuildConfiguration[])
 	 * @see IProject#setDescription(IProjectDescription, int, IProgressMonitor)
 	 * @param projects list of projects
 	 * @since 3.0
 	 */
+	@Deprecated
 	public void setDynamicReferences(IProject[] projects);
 
 	/**