Bug 522332 - Add Required Plug-ins: resolve unsatisfied constraints

Collect dependencies recursively until no additional dependency can be
computed.
Then perform a validation operation to detect unsatisfied constraints
and try to resolve bundles from the target platform state that are
satisfying these
constraints.

Change-Id: Ib4a1bf310240bc2df5db8993639aa478ea1d8cb9
Signed-off-by: Karsten Thoms <karsten.thoms@itemis.de>
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/DependencyManager.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/DependencyManager.java
index 8fe37c5..eb4756d 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/DependencyManager.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/DependencyManager.java
@@ -7,14 +7,16 @@
  *
  *  Contributors:
  *     IBM Corporation - initial API and implementation
+ *     Karsten Thoms <karsten.thoms@itemis.de> - Bug 522332
  *******************************************************************************/
 package org.eclipse.pde.internal.core;
 
 import java.util.*;
+import java.util.stream.Collectors;
 import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.NullProgressMonitor;
 import org.eclipse.osgi.service.resolver.*;
-import org.eclipse.pde.core.plugin.IPluginExtension;
-import org.eclipse.pde.core.plugin.IPluginModelBase;
+import org.eclipse.pde.core.plugin.*;
 import org.eclipse.pde.core.target.ITargetPlatformService;
 import org.eclipse.pde.core.target.NameVersionDescriptor;
 import org.osgi.framework.Constants;
@@ -27,7 +29,6 @@
  * @noinstantiate This class is not intended to be instantiated by clients.
  */
 public class DependencyManager {
-
 	/**
 	 * Returns a {@link Set} of bundle ids for the dependents of the given
 	 * {@link IPluginModelBase}. The set includes the id of the given model base
@@ -106,29 +107,46 @@
 	}
 
 	/**
-	 * Returns a {@link Set} of bundle ids for the dependents of the given
-	 * objects from the given {@link State}.
-	 * The set additionally only includes the given set of implicit dependencies.
+	 * Returns a {@link Set} of bundle ids for the dependents of the given objects
+	 * from the given {@link State}. The set additionally only includes the given
+	 * set of implicit dependencies.
 	 *
-	 * @param selected selected the group of objects to compute dependencies for. Any items
-	 * in this array that are not {@link IPluginModelBase}s are ignored.
-	 * @param implicit the array of additional implicit dependencies to add to the {@link Set}
-	 * @param state the {@link State} to compute the dependencies in
-	 * @param removeSelf if the id of one of the bundles were are computing dependencies for should be
-	 * included in the result {@link Set} or not
-	 * @param includeOptional if optional bundle ids should be included
-	 * @param excludeFragments a collection of <b>fragment</b> bundle symbolic names to exclude from the dependency resolution
+	 * @param selected
+	 *            selected the group of objects to compute dependencies for. Any
+	 *            items in this array that are not {@link IPluginModelBase}s are
+	 *            ignored.
+	 * @param implicit
+	 *            the array of additional implicit dependencies to add to the
+	 *            {@link Set}
+	 * @param state
+	 *            the {@link State} to compute the dependencies in
+	 * @param removeSelf
+	 *            if the id of one of the bundles were are computing dependencies
+	 *            for should be included in the result {@link Set} or not
+	 * @param includeOptional
+	 *            if optional bundle ids should be included
+	 * @param excludeFragments
+	 *            a collection of <b>fragment</b> bundle symbolic names to exclude
+	 *            from the dependency resolution
 	 * @return a set of bundle IDs
 	 */
-	private static Set<String> getDependencies(Object[] selected, String[] implicit, State state, boolean removeSelf, boolean includeOptional, Set<String> excludeFragments) {
+	private static Set<String> getDependencies(Object[] selected, String[] implicit, State state, boolean removeSelf,
+			boolean includeOptional, Set<String> excludeFragments) {
 		Set<String> set = new TreeSet<>();
+		Set<IPluginModelBase> models = new HashSet<>();
+
+		// For all selected bundles add their bundle dependencies.
+		// Also consider plugin extensions and their dependencies.
 		for (int i = 0; i < selected.length; i++) {
 			if (!(selected[i] instanceof IPluginModelBase))
 				continue;
 			IPluginModelBase model = (IPluginModelBase) selected[i];
+			models.add(model);
 			addBundleAndDependencies(model.getBundleDescription(), set, includeOptional, excludeFragments);
 			IPluginExtension[] extensions = model.getPluginBase().getExtensions();
 			for (IPluginExtension extension : extensions) {
+				// TODO: this loop might be useless, because dependencies are already defined in
+				// the manifest
 				String point = extension.getPoint();
 				if (point != null) {
 					int dot = point.lastIndexOf('.');
@@ -153,6 +171,29 @@
 				set.remove(model.getPluginBase().getId());
 			}
 		}
+
+		if (!set.isEmpty()) {
+			// validate all models and try to add bundles that resolve constraint violations
+			for (IPluginModelBase model : DependencyManager.getDependencies(TargetPlatformHelper.getState(),
+					models.toArray(new IPluginModelBase[models.size()]))) {
+				set.add(model.getBundleDescription().getSymbolicName());
+			}
+
+			// build array with all selected plus calculated dependencies and recurse
+			// loop ends when no more additional dependencies are calculated
+			for (String id : set) {
+				ModelEntry entry = PluginRegistry.findEntry(id);
+				if (entry != null) {
+					models.add(entry.getModel());
+				}
+			}
+
+			Set<String> additionalIds = getDependencies(models.toArray(), implicit, state, removeSelf, includeOptional,
+					excludeFragments);
+			set.addAll(additionalIds);
+		}
+
+
 		return set;
 	}
 
@@ -221,4 +262,73 @@
 		}
 	}
 
+	/**
+	 * Validates the given models and retrieves bundle IDs that satisfy violated
+	 * constraints. This method uses the {@link BundleValidationOperation} to
+	 * determine unsatisfied constraints for the given plugin models.
+	 *
+	 * @param state
+	 *            the {@link State} to compute the dependencies in
+	 * @param models
+	 *            the array of {@link IPluginModelBase}s to compute dependencies for
+	 *
+	 * @return a set of bundle IDs
+	 */
+	private static Set<IPluginModelBase> getDependencies(State state, IPluginModelBase[] models) {
+		Set<IPluginModelBase> dependencies = new HashSet<>();
+		BundleValidationOperation operation = new BundleValidationOperation(models);
+		try {
+			operation.run(new NullProgressMonitor());
+			Map<Object, Object[]> input = operation.getResolverErrors();
+			// extract the unsatisfied constraints from the operation's result structure
+			VersionConstraint[] unsatisfiedConstraints = input.values().stream()
+					.filter(ResolverError[].class::isInstance)
+					.map(ResolverError[].class::cast)
+					.flatMap(arr -> Arrays.stream(arr))
+					.filter(err -> err.getUnsatisfiedConstraint() != null)
+					.map(err -> err.getUnsatisfiedConstraint())
+					.toArray(VersionConstraint[]::new);
+
+			for (VersionConstraint constraint : unsatisfiedConstraints) {
+				// first try to find a solution in the set of additionally computed
+				// bundles that satisfy constraints.
+				if (dependencies.stream()
+						.anyMatch(pmb -> satisfiesConstraint(pmb.getBundleDescription(), constraint))) {
+					continue;
+				}
+				// determine all bundles from the target platform state that satisfy the current
+				// constraint
+				List<BundleDescription> satisfyingBundles = Arrays.stream(state.getBundles())
+						.filter(desc -> satisfiesConstraint(desc, constraint)).collect(Collectors.toList());
+
+				// It is possible to have none, exactly one, or in rare cases multiple bundles
+				// that satisfy the constraint.
+				for (BundleDescription bundle : satisfyingBundles) {
+					ModelEntry entry = PluginRegistry.findEntry(bundle.getSymbolicName());
+					if (entry != null) {
+						dependencies.add(entry.getModel());
+					}
+				}
+			}
+			return dependencies;
+		} catch (CoreException e) {
+			PDECore.log(e);
+			return Collections.emptySet();
+		}
+	}
+
+	private static boolean satisfiesConstraint(BundleDescription desc, VersionConstraint constraint) {
+		if (constraint instanceof GenericSpecification) {
+			for (GenericDescription description : desc.getGenericCapabilities()) {
+				if (constraint.isSatisfiedBy(description)) {
+					return true;
+				}
+			}
+		} else if (constraint instanceof BundleSpecification) {
+			return constraint.getName().equals(desc.getName())
+					&& constraint.getVersionRange().isIncluded(desc.getVersion());
+		}
+		return false;
+	}
+
 }
diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/AbstractPluginBlock.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/AbstractPluginBlock.java
index d3f47ec..2d7450a 100644
--- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/AbstractPluginBlock.java
+++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/AbstractPluginBlock.java
@@ -10,6 +10,7 @@
  *     Ian Bull <irbull@cs.uvic.ca> - bug 204404 and bug 207064
  *     EclipseSource Corporation - ongoing enhancements
  *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 487943
+ *     Karsten Thoms <karsten.thoms@itemis.de> - Bug 522332
  *******************************************************************************/
 package org.eclipse.pde.internal.ui.launcher;
 
@@ -17,6 +18,7 @@
 
 import java.util.*;
 import java.util.List;
+import java.util.stream.Collectors;
 import org.eclipse.core.resources.IProject;
 import org.eclipse.core.resources.IResource;
 import org.eclipse.core.runtime.*;
@@ -828,31 +830,19 @@
 	 */
 	protected void addRequiredPlugins() {
 		Object[] checked = fPluginTreeViewer.getCheckedElements();
-		ArrayList<Object> toCheck = new ArrayList<>(checked.length);
-		for (Object checkedElement : checked) {
-			if (checkedElement instanceof IPluginModelBase) {
-				toCheck.add(checkedElement);
-			}
-		}
+		Set<IPluginModelBase> toCheck = Arrays.stream(checked).filter(IPluginModelBase.class::isInstance)
+				.map(IPluginModelBase.class::cast).collect(Collectors.toSet());
 
-		Set<?> additionalIds = DependencyManager.getDependencies(checked, fIncludeOptionalButton.getSelection(), null);
+		Set<String> additionalIds = DependencyManager.getDependencies(toCheck.toArray(),
+				fIncludeOptionalButton.getSelection(), null);
 
-		Iterator<?> it = additionalIds.iterator();
-		while (it.hasNext()) {
-			String id = (String) it.next();
-			if (findPlugin(id) == null) {
-				ModelEntry entry = PluginRegistry.findEntry(id);
-				if (entry != null) {
-					IPluginModelBase model = entry.getModel();
-					if (model != null) {
-						toCheck.add(model);
-					}
-				}
-			}
-		}
+		additionalIds.stream().map(id -> PluginRegistry.findEntry(id))
+				.filter(Objects::nonNull).map(entry -> entry.getModel())
+				.forEach(model -> toCheck.add(model));
 
 		checked = toCheck.toArray();
 		setCheckedElements(checked);
+
 		fNumExternalChecked = 0;
 		fNumWorkspaceChecked = 0;
 		for (Object checkedElement : checked) {