Bug 577118 - Handle multiple Plug-in versions in launching facility

- Take only the latest version of a workspace/target bundle if multiple
versions are present and no version is specified in the launch-config
- Replace selected target-bundles only by a workspace-bundle if it has
the same Major-Minor-Micro version
- When writing bundle entries for launch-configurations, check if there
are other bundles in the same container (workspace or target-platform)
to decide if the version is written and not only check the number of
'active' models
- write versioned-entries to the dev.properties to enable different dev
classpath-entries for different versions of a bundle

Change-Id: Ib559c47ba6dfa5a56da4259c92dbe5d95370a45a
Signed-off-by: Hannes Wellmann <wellmann.hannes1@gmx.net>
Reviewed-on: https://git.eclipse.org/r/c/pde/eclipse.pde.ui/+/187493
Tested-by: PDE Bot <pde-bot@eclipse.org>
Reviewed-by: Julian Honnen <julian.honnen@vector.com>
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ClasspathHelper.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ClasspathHelper.java
index 16655d2..48176ab 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ClasspathHelper.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ClasspathHelper.java
@@ -12,6 +12,7 @@
  *     IBM Corporation - initial API and implementation
  *     Hannes Wellmann - Bug 577541 - Clean up ClasspathHelper and TargetWeaver
  *     Hannes Wellmann - Bug 577543 - Only weave dev.properties for secondary launches if plug-in is from Running-Platform
+ *     Hannes Wellmann - Bug 577118 - Handle multiple Plug-in versions in launching facility
  *******************************************************************************/
 package org.eclipse.pde.internal.core;
 
@@ -20,9 +21,9 @@
 import java.io.IOException;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -51,6 +52,7 @@
 import org.eclipse.pde.core.build.IBuild;
 import org.eclipse.pde.core.build.IBuildEntry;
 import org.eclipse.pde.core.plugin.IFragmentModel;
+import org.eclipse.pde.core.plugin.IPluginBase;
 import org.eclipse.pde.core.plugin.IPluginLibrary;
 import org.eclipse.pde.core.plugin.IPluginModelBase;
 import org.eclipse.pde.core.plugin.PluginRegistry;
@@ -64,19 +66,19 @@
 
 	private static final String DOT = "."; //$NON-NLS-1$
 	private static final String FRAGMENT_ANNOTATION = "@fragment@"; //$NON-NLS-1$
+	private static final String DEV_CLASSPATH_ENTRY_SEPARATOR = ","; //$NON-NLS-1$
+	private static final String DEV_CLASSPATH_VERSION_SEPARATOR = ";"; //$NON-NLS-1$
 
 	public static String getDevEntriesProperties(String fileName, boolean checkExcluded) throws CoreException {
 		IPluginModelBase[] models = PluginRegistry.getWorkspaceModels();
-		Map<String, IPluginModelBase> bundleModels = new HashMap<>();
-		for (IPluginModelBase model : models) {
-			bundleModels.put(model.getPluginBase().getId(), model);
-		}
+		Map<String, List<IPluginModelBase>> bundleModels = Arrays.stream(models)
+				.collect(Collectors.groupingBy(m -> m.getPluginBase().getId()));
 
 		Properties properties = getDevEntriesProperties(bundleModels, checkExcluded);
 		return writeDevEntries(fileName, properties);
 	}
 
-	public static String getDevEntriesProperties(String fileName, Map<String, IPluginModelBase> map)
+	public static String getDevEntriesProperties(String fileName, Map<String, List<IPluginModelBase>> map)
 			throws CoreException {
 		Properties properties = getDevEntriesProperties(map, true);
 		return writeDevEntries(fileName, properties);
@@ -99,26 +101,70 @@
 		}
 	}
 
-	public static Properties getDevEntriesProperties(Map<String, IPluginModelBase> bundlesMap, boolean checkExcluded) {
-		Properties properties = new Properties();
+	public static Properties getDevEntriesProperties(Map<String, List<IPluginModelBase>> bundlesMap,
+			boolean checkExcluded) {
+
+		Set<IPluginModelBase> launchedPlugins = bundlesMap.values().stream().flatMap(Collection::stream)
+				.collect(Collectors.toCollection(LinkedHashSet::new));
+
+		Map<IPluginModelBase, String> modelEntries = new LinkedHashMap<>();
 		// account for cascading workspaces
-		TargetWeaver.weaveRunningPlatformDevProperties(properties, bundlesMap.values());
-		for (IPluginModelBase model : bundlesMap.values()) {
-			if (model.getUnderlyingResource() != null) {
-				String entry = formatEntry(getDevPaths(model, checkExcluded, bundlesMap.keySet()));
-				if (!entry.isEmpty()) {
-					// overwrite entry, if plug-in from primary Eclipse is also
-					// imported into workspace of secondary eclipse
-					properties.put(model.getPluginBase().getId(), entry);
+		TargetWeaver.weaveRunningPlatformDevProperties(modelEntries, launchedPlugins);
+
+		for (List<IPluginModelBase> models : bundlesMap.values()) {
+			for (IPluginModelBase model : models) {
+				if (model.getUnderlyingResource() != null) {
+					String entry = formatEntry(getDevPaths(model, checkExcluded, launchedPlugins));
+					if (!entry.isEmpty()) {
+						// overwrite entry, if plug-in from primary Eclipse is
+						// also imported into workspace of secondary eclipse
+						modelEntries.put(model, entry);
+					}
+				}
+			}
+			// Check if there is an entry of a workspace-model or
+			// a target model woven from a primary-workspace plugin with same id
+			if (models.stream().anyMatch(modelEntries::containsKey)) {
+				for (IPluginModelBase model : models) {
+					// in case of multiple models with same id add empty entries
+					// for target-bundles to ensure the non-version entry is not
+					// used to falsely extend their class-path
+					modelEntries.putIfAbsent(model, ""); //$NON-NLS-1$
 				}
 			}
 		}
+
+		Properties properties = new Properties();
+		modelEntries.forEach((m, cp) -> addDevClasspath(m.getPluginBase(), properties, cp, false));
 		properties.put("@ignoredot@", "true"); //$NON-NLS-1$ //$NON-NLS-2$
 		return properties;
 	}
 
 	private static String formatEntry(Collection<IPath> paths) {
-		return paths.stream().map(IPath::toString).collect(Collectors.joining(",")); //$NON-NLS-1$
+		return paths.stream().map(IPath::toString).collect(Collectors.joining(DEV_CLASSPATH_ENTRY_SEPARATOR));
+	}
+
+	public static void addDevClasspath(IPluginBase model, Properties devProperties, String devCP, boolean append) {
+		// add entries with & without version to be backward-compatible with
+		// 'old' Equinox, that doesn't consider versions, too.
+		String id = model.getId();
+		if (!devCP.isEmpty()) {
+			addDevCPEntry(id, devCP, devProperties, append);
+		}
+		addDevCPEntry(id + DEV_CLASSPATH_VERSION_SEPARATOR + model.getVersion(), devCP, devProperties, append);
+	}
+
+	private static void addDevCPEntry(String id, String devCP, Properties devProperties, boolean append) {
+		if (append) {
+			devProperties.merge(id, devCP, (vOld, vNew) -> vOld + DEV_CLASSPATH_ENTRY_SEPARATOR + vNew);
+		} else {
+			devProperties.put(id, devCP);
+		}
+	}
+
+	public static String getDevClasspath(Properties devProperties, String id, String version) {
+		Object cp = devProperties.get(id + ClasspathHelper.DEV_CLASSPATH_VERSION_SEPARATOR + version);
+		return (String) (cp != null ? cp : devProperties.get(id)); // prefer version-entry
 	}
 
 	// creates a map whose key is a Path to the source directory/jar and the value is a Path output directory or jar.
@@ -213,7 +259,7 @@
 		return paths;
 	}
 
-	private static Set<IPath> getDevPaths(IPluginModelBase model, boolean checkExcluded, Set<String> plugins) {
+	private static Set<IPath> getDevPaths(IPluginModelBase model, boolean checkExcluded, Set<IPluginModelBase> plugins) {
 		IProject project = model.getUnderlyingResource().getProject();
 		try {
 			if (project.hasNature(JavaCore.NATURE_ID)) {
@@ -263,10 +309,10 @@
 	}
 
 	// looks for fragments for a plug-in.  Then searches the fragments for a specific library.  Will return paths which are absolute (required by runtime)
-	private static List<IPath> findLibraryFromFragments(String libName, IPluginModelBase model, boolean checkExcluded, Set<String> plugins) {
+	private static List<IPath> findLibraryFromFragments(String libName, IPluginModelBase model, boolean checkExcluded, Set<IPluginModelBase> plugins) {
 		IFragmentModel[] frags = PDEManager.findFragmentsFor(model);
 		for (int i = 0; i < frags.length; i++) {
-			if (!plugins.contains(frags[i].getBundleDescription().getSymbolicName())) {
+			if (!plugins.contains(frags[i])) {
 				continue;
 			}
 			// look in project first
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/P2Utils.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/P2Utils.java
index e24b015..17e3330 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/P2Utils.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/P2Utils.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2007, 2018 IBM Corporation and others.
+ * Copyright (c) 2007, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -27,6 +27,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.StringTokenizer;
+import java.util.stream.Collectors;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.IStatus;
@@ -381,7 +382,7 @@
 	 *
 	 * @throws CoreException if the profile cannot be generated
 	 */
-	public static void createProfile(String profileID, File p2DataArea, Collection<?> bundles) throws CoreException {
+	public static void createProfile(String profileID, File p2DataArea, Collection<List<IPluginModelBase>> bundles) throws CoreException {
 		// Acquire the required p2 services, creating an agent in the target p2 metadata area
 		IProvisioningAgentProvider provider = PDECore.getDefault().acquireService(IProvisioningAgentProvider.class);
 		if (provider == null) {
@@ -418,12 +419,9 @@
 		profile = registry.addProfile(profileID, props);
 
 		// Create metadata for the bundles
-		Collection<IInstallableUnit> ius = new ArrayList<>(bundles.size());
-		for (final Object name : bundles) {
-			IPluginModelBase model = (IPluginModelBase) name;
-			BundleDescription bundle = model.getBundleDescription();
-			ius.add(createBundleIU(bundle));
-		}
+		Collection<IInstallableUnit> ius = bundles.stream().flatMap(Collection::stream)
+				.map(IPluginModelBase::getBundleDescription).map(P2Utils::createBundleIU) //
+				.collect(Collectors.toList());
 
 		// Add the metadata to the profile
 		ProvisioningContext context = new ProvisioningContext(agent);
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/TargetPlatformHelper.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/TargetPlatformHelper.java
index 7b4a17c..e791225 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/TargetPlatformHelper.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/TargetPlatformHelper.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2017 IBM Corporation and others.
+ * Copyright (c) 2000, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -26,7 +26,6 @@
 import java.util.Dictionary;
 import java.util.HashMap;
 import java.util.Hashtable;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -234,13 +233,12 @@
 		return null;
 	}
 
-	public static void checkPluginPropertiesConsistency(Map<?, ?> map, File configDir) {
+	public static void checkPluginPropertiesConsistency(Map<String, List<IPluginModelBase>> map, File configDir) {
 		File runtimeDir = new File(configDir, IPDEBuildConstants.BUNDLE_CORE_RUNTIME);
 		if (runtimeDir.exists() && runtimeDir.isDirectory()) {
 			long timestamp = runtimeDir.lastModified();
-			Iterator<?> iter = map.values().iterator();
-			while (iter.hasNext()) {
-				if (hasChanged((IPluginModelBase) iter.next(), timestamp)) {
+			for (List<IPluginModelBase> models : map.values()) {
+				if (models.stream().anyMatch(m -> hasChanged(m, timestamp))) {
 					CoreUtility.deleteContent(runtimeDir);
 					break;
 				}
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/TargetWeaver.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/TargetWeaver.java
index 1d6da8b..8f2081a 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/TargetWeaver.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/TargetWeaver.java
@@ -13,6 +13,7 @@
  *     EclipseSource Corporation - ongoing enhancements
  *     Hannes Wellmann - Bug 577541 - Clean up ClasspathHelper and TargetWeaver
  *     Hannes Wellmann - Bug 577543 - Only weave dev.properties for secondary launches if plug-in is from Running-Platform
+ *     Hannes Wellmann - Bug 577118 - Handle multiple Plug-in versions in launching facility
  *******************************************************************************/
 package org.eclipse.pde.internal.core;
 
@@ -126,14 +127,14 @@
 	 * @param launchDevProperties dev.properties
 	 * @param launchedPlugins the bundles that participate in secondary runtime
 	 */
-	static void weaveRunningPlatformDevProperties(Properties launchDevProperties,
+	static void weaveRunningPlatformDevProperties(Map<IPluginModelBase, String> launchDevProperties,
 			Iterable<IPluginModelBase> launchedPlugins) {
 		if (fgDevPropertiesURL != null) {
 			Properties platformDevProperties = getDevProperties();
 			for (IPluginModelBase launchedPlugin : launchedPlugins) {
 				String devCP = getDevProperty(launchedPlugin, platformDevProperties);
 				if (devCP != null) {
-					launchDevProperties.setProperty(launchedPlugin.getPluginBase().getId(), devCP);
+					launchDevProperties.put(launchedPlugin, devCP);
 				}
 			}
 		}
@@ -186,7 +187,7 @@
 	}
 
 	private static String getDevProperty(Path bundleLocation, String id, String version, Properties devProperties) {
-		String devCP = (String) devProperties.get(id);
+		String devCP = ClasspathHelper.getDevClasspath(devProperties, id, version);
 		return devCP != null && isBundleOfRunningPlatform(bundleLocation, id, version) ? devCP : null;
 	}
 
diff --git a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/BundleLauncherHelper.java b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/BundleLauncherHelper.java
index 57e588d..f2491cf 100644
--- a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/BundleLauncherHelper.java
+++ b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/BundleLauncherHelper.java
@@ -12,6 +12,7 @@
  *     IBM Corporation - initial API and implementation
  *     EclipseSource Corporation - ongoing enhancements
  *     Hannes Wellmann - Bug 576885: Unify methods to parse bundle-sets from launch-configs
+ *     Hannes Wellmann - Bug 577118 - Handle multiple Plug-in versions in launching facility
  *******************************************************************************/
 package org.eclipse.pde.internal.launching.launcher;
 
@@ -19,8 +20,9 @@
 
 import java.util.*;
 import java.util.Map.Entry;
+import java.util.function.BiPredicate;
 import java.util.function.Function;
-import java.util.function.Predicate;
+import java.util.stream.Stream;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.debug.core.ILaunchConfiguration;
 import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
@@ -29,6 +31,7 @@
 import org.eclipse.pde.internal.build.IPDEBuildConstants;
 import org.eclipse.pde.internal.core.*;
 import org.eclipse.pde.internal.core.ifeature.*;
+import org.eclipse.pde.internal.core.util.VersionUtil;
 import org.eclipse.pde.internal.launching.IPDEConstants;
 import org.eclipse.pde.launching.IPDELauncherConstants;
 import org.osgi.framework.Version;
@@ -48,11 +51,7 @@
 	public static final char VERSION_SEPARATOR = '*';
 
 	public static Map<IPluginModelBase, String> getWorkspaceBundleMap(ILaunchConfiguration configuration) throws CoreException {
-		return getWorkspaceBundleMap(configuration, null);
-	}
-
-	public static Map<IPluginModelBase, String> getTargetBundleMap(ILaunchConfiguration configuration) throws CoreException {
-		return getTargetBundleMap(configuration, null);
+		return getWorkspaceBundleMap(configuration, new HashMap<>());
 	}
 
 	public static Map<IPluginModelBase, String> getMergedBundleMap(ILaunchConfiguration configuration, boolean osgi) throws CoreException {
@@ -79,9 +78,13 @@
 			return getMergedBundleMapFeatureBased(wc, osgi);
 		}
 
-		Set<String> set = new HashSet<>();
-		Map<IPluginModelBase, String> map = getWorkspaceBundleMap(wc, set);
-		map.putAll(getTargetBundleMap(wc, set));
+		return getAllSelectedPluginBundles(wc);
+	}
+
+	public static Map<IPluginModelBase, String> getAllSelectedPluginBundles(ILaunchConfiguration config) throws CoreException {
+		Map<String, List<Version>> idVersions = new HashMap<>();
+		Map<IPluginModelBase, String> map = getWorkspaceBundleMap(config, idVersions);
+		map.putAll(getTargetBundleMap(config, idVersions));
 		return map;
 	}
 
@@ -310,37 +313,33 @@
 		return map.keySet().toArray(new IPluginModelBase[map.size()]);
 	}
 
-	public static Map<IPluginModelBase, String> getWorkspaceBundleMap(ILaunchConfiguration configuration, Set<String> pluginIds) throws CoreException {
+	private static final BiPredicate<List<Version>, Version> CONTAINS_SAME_VERSION = List::contains;
+	private static final BiPredicate<List<Version>, Version> CONTAINS_SAME_MMM_VERSION = (versions, toAdd) -> versions.stream().anyMatch(v -> VersionUtil.compareMacroMinorMicro(toAdd, v) == 0);
+
+	private static Map<IPluginModelBase, String> getWorkspaceBundleMap(ILaunchConfiguration configuration, Map<String, List<Version>> idVersions) throws CoreException {
 		Set<String> workspaceBundles = configuration.getAttribute(IPDELauncherConstants.SELECTED_WORKSPACE_BUNDLES, emptySet());
 
-		Map<IPluginModelBase, String> map = getBundleMap(workspaceBundles, ModelEntry::getWorkspaceModels, pluginIds, id -> true);
+		Map<IPluginModelBase, String> map = getBundleMap(workspaceBundles, ModelEntry::getWorkspaceModels, CONTAINS_SAME_VERSION, idVersions);
 
 		if (configuration.getAttribute(IPDELauncherConstants.AUTOMATIC_ADD, true)) {
 			Set<String> deselectedWorkspaceBundles = configuration.getAttribute(IPDELauncherConstants.DESELECTED_WORKSPACE_BUNDLES, emptySet());
-			Set<IPluginModelBase> deselectedPlugins = getBundleMap(deselectedWorkspaceBundles, ModelEntry::getWorkspaceModels, null, id -> true).keySet();
+			Set<IPluginModelBase> deselectedPlugins = getBundleMap(deselectedWorkspaceBundles, ModelEntry::getWorkspaceModels, null, null).keySet();
 			IPluginModelBase[] models = PluginRegistry.getWorkspaceModels();
 			for (IPluginModelBase model : models) {
-				String id = model.getPluginBase().getId();
-				if (id != null && !deselectedPlugins.contains(model)) {
-					if (pluginIds != null) {
-						pluginIds.add(id);
-					}
-					if (!map.containsKey(model)) {
-						addBundleToMap(map, model, "default:default"); //$NON-NLS-1$
-					}
+				if (model.getPluginBase().getId() != null && !deselectedPlugins.contains(model) && !map.containsKey(model)) {
+					addPlugin(map, model, "default:default", idVersions, CONTAINS_SAME_VERSION); //$NON-NLS-1$
 				}
 			}
 		}
 		return map;
 	}
 
-	public static Map<IPluginModelBase, String> getTargetBundleMap(ILaunchConfiguration configuration, Set<String> pluginIds) throws CoreException {
+	private static Map<IPluginModelBase, String> getTargetBundleMap(ILaunchConfiguration configuration, Map<String, List<Version>> idVersions) throws CoreException {
 		Set<String> targetBundles = configuration.getAttribute(IPDELauncherConstants.SELECTED_TARGET_BUNDLES, emptySet());
-		Predicate<String> idFilter = pluginIds != null ? id -> !pluginIds.contains(id) : id -> true;
-		return getBundleMap(targetBundles, ModelEntry::getExternalModels, null, idFilter);
+		return getBundleMap(targetBundles, ModelEntry::getExternalModels, CONTAINS_SAME_MMM_VERSION, idVersions); // don't add same major-minor-micro-version more than once
 	}
 
-	private static Map<IPluginModelBase, String> getBundleMap(Set<String> entries, Function<ModelEntry, IPluginModelBase[]> getModels, Set<String> pluginIds, Predicate<String> idFilter) {
+	private static Map<IPluginModelBase, String> getBundleMap(Set<String> entries, Function<ModelEntry, IPluginModelBase[]> getModels, BiPredicate<List<Version>, Version> versionFilter, Map<String, List<Version>> idVersions) {
 		Map<IPluginModelBase, String> map = new LinkedHashMap<>();
 		for (String bundleEntry : entries) {
 			int index = bundleEntry.indexOf('@');
@@ -353,36 +352,46 @@
 			String id = (versionIndex > 0) ? idVersion.substring(0, versionIndex) : idVersion;
 			String version = (versionIndex > 0) ? idVersion.substring(versionIndex + 1) : null;
 
-			if (idFilter.test(id)) {
-				if (pluginIds != null) {
-					pluginIds.add(id);
-				}
-				ModelEntry entry = PluginRegistry.findEntry(id);
-				if (entry != null) {
-					IPluginModelBase[] models = getModels.apply(entry);
-					String startData = bundleEntry.substring(index + 1);
-					addPluginModel(models, version, startData, map);
+			ModelEntry entry = PluginRegistry.findEntry(id);
+			if (entry != null) {
+				IPluginModelBase[] models = getModels.apply(entry);
+				String startData = bundleEntry.substring(index + 1);
+				for (IPluginModelBase model : getSelectedModels(models, version, versionFilter == null)) {
+					addPlugin(map, model, startData, idVersions, versionFilter);
 				}
 			}
 		}
 		return map;
 	}
 
-	private static void addPluginModel(IPluginModelBase[] models, String version, String startData, Map<IPluginModelBase, String> map) {
-		Set<String> versions = new HashSet<>();
-		for (IPluginModelBase model : models) {
-			if (model.isEnabled()) { // always true for workspace models, external might be disabled
-				IPluginBase base = model.getPluginBase();
-				String v = base.getVersion();
-				if (versions.add(v)) { // don't add exact same version more than once
-					// match only if...
-					// a) if we have the same version
-					// b) no version
-					// c) all else fails, if there's just one bundle available, use it
-					if (base.getVersion().equals(version) || version == null || models.length == 1) {
-						addBundleToMap(map, model, startData);
-					}
-				}
+	static final Comparator<IPluginModelBase> VERSION = Comparator.comparing(m -> m.getBundleDescription().getVersion());
+
+	private static Iterable<IPluginModelBase> getSelectedModels(IPluginModelBase[] models, String version, boolean greedy) {
+		// match only if...
+		// a) if we have the same version
+		// b) no version (if greedy take latest, else take all)
+		// c) all else fails, if there's just one bundle available, use it
+		Stream<IPluginModelBase> selectedModels = Arrays.stream(models).filter(IPluginModelBase::isEnabled); // workspace models are always enabled, external might be disabled
+		if (version == null) {
+			if (!greedy) {
+				IPluginModelBase latestModel = selectedModels.max(VERSION).orElseThrow();
+				selectedModels = Stream.of(latestModel); // take only  latest
+			} // Otherwise be greedy and take all if versionFilter is null
+		} else {
+			selectedModels = selectedModels.filter(m -> m.getPluginBase().getVersion().equals(version) || models.length == 1);
+		}
+		return selectedModels::iterator;
+	}
+
+	private static void addPlugin(Map<IPluginModelBase, String> map, IPluginModelBase model, String startData, Map<String, List<Version>> idVersions, BiPredicate<List<Version>, Version> containsVersion) {
+		if (containsVersion == null) { // be greedy and just take all (idVersions is null as well)
+			addBundleToMap(map, model, startData);
+		} else {
+			List<Version> pluginVersions = idVersions.computeIfAbsent(model.getPluginBase().getId(), n -> new ArrayList<>());
+			Version version = model.getBundleDescription().getVersion();
+			if (!containsVersion.test(pluginVersions, version)) { // apply version filter    
+				pluginVersions.add(version);
+				addBundleToMap(map, model, startData);
 			}
 		}
 	}
@@ -464,22 +473,28 @@
 		StringBuilder buffer = new StringBuilder(id);
 
 		ModelEntry entry = PluginRegistry.findEntry(id);
-		if (entry != null && entry.getActiveModels().length > 1) {
-			buffer.append(VERSION_SEPARATOR);
-			buffer.append(model.getPluginBase().getVersion());
+		if (entry != null) {
+			boolean isWorkspacePlugin = model.getUnderlyingResource() != null;
+			IPluginModelBase[] entryModels = isWorkspacePlugin ? entry.getWorkspaceModels() : entry.getExternalModels();
+			if (entryModels.length > 1) {
+				buffer.append(VERSION_SEPARATOR);
+				buffer.append(model.getPluginBase().getVersion());
+			}
 		}
 
-		boolean hasStartLevel = (startLevel != null && startLevel.length() > 0);
-		boolean hasAutoStart = (autoStart != null && autoStart.length() > 0);
+		boolean hasStartLevel = startLevel != null && !startLevel.isEmpty();
+		boolean hasAutoStart = autoStart != null && !autoStart.isEmpty();
 
-		if (hasStartLevel || hasAutoStart)
+		if (hasStartLevel || hasAutoStart) {
 			buffer.append('@');
-		if (hasStartLevel)
-			buffer.append(startLevel);
-		if (hasStartLevel || hasAutoStart)
+			if (hasStartLevel) {
+				buffer.append(startLevel);
+			}
 			buffer.append(':');
-		if (hasAutoStart)
-			buffer.append(autoStart);
+			if (hasAutoStart) {
+				buffer.append(autoStart);
+			}
+		}
 		return buffer.toString();
 	}
 
diff --git a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LaunchConfigurationHelper.java b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LaunchConfigurationHelper.java
index 658c2dd..044353f 100644
--- a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LaunchConfigurationHelper.java
+++ b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LaunchConfigurationHelper.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2005, 2017 IBM Corporation and others.
+ * Copyright (c) 2005, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -107,7 +107,7 @@
 	 * @return a properties object containing the properties written out to config.ini
 	 * @throws CoreException
 	 */
-	public static Properties createConfigIniFile(ILaunchConfiguration configuration, String productID, Map<String, IPluginModelBase> bundles, Map<IPluginModelBase, String> bundlesWithStartLevels, File configurationDirectory) throws CoreException {
+	public static Properties createConfigIniFile(ILaunchConfiguration configuration, String productID, Map<String, List<IPluginModelBase>> bundles, Map<IPluginModelBase, String> bundlesWithStartLevels, File configurationDirectory) throws CoreException {
 		Properties properties = null;
 		// if we are to generate a config.ini, start with the values in the target platform's config.ini - bug 141918
 		if (configuration.getAttribute(IPDELauncherConstants.CONFIG_GENERATE_DEFAULT, true)) {
@@ -193,7 +193,7 @@
 		return properties;
 	}
 
-	private static void addRequiredProperties(Properties properties, String productID, Map<String, IPluginModelBase> bundles, Map<IPluginModelBase, String> bundlesWithStartLevels) {
+	private static void addRequiredProperties(Properties properties, String productID, Map<String, List<IPluginModelBase>> bundles, Map<IPluginModelBase, String> bundlesWithStartLevels) {
 		if (!properties.containsKey("osgi.install.area")) //$NON-NLS-1$
 			properties.setProperty("osgi.install.area", "file:" + TargetPlatform.getLocation()); //$NON-NLS-1$ //$NON-NLS-2$
 		if (!properties.containsKey("osgi.configuration.cascaded")) //$NON-NLS-1$
@@ -221,7 +221,7 @@
 	 * @param bundlesWithStartLevels map of bundles of start level
 	 * @return string list of osgi bundles
 	 */
-	private static String computeOSGiBundles(String bundleList, Map<String, IPluginModelBase> bundles, Map<IPluginModelBase, String> bundlesWithStartLevels) {
+	private static String computeOSGiBundles(String bundleList, Map<String, List<IPluginModelBase>> bundles, Map<IPluginModelBase, String> bundlesWithStartLevels) {
 
 		// if p2 and only simple configurator and
 		// if simple configurator isn't selected & isn't in bundle list... hack it
@@ -282,7 +282,7 @@
 		return properties;
 	}
 
-	private static void addSplashLocation(Properties properties, String productID, Map<String, IPluginModelBase> map) {
+	private static void addSplashLocation(Properties properties, String productID, Map<String, List<IPluginModelBase>> map) {
 		Properties targetConfig = TargetPlatformHelper.getConfigIniProperties();
 		String targetProduct = targetConfig == null ? null : targetConfig.getProperty("eclipse.product"); //$NON-NLS-1$
 		String targetSplash = targetConfig == null ? null : targetConfig.getProperty("osgi.splashPath"); //$NON-NLS-1$
@@ -290,7 +290,7 @@
 			ArrayList<String> locations = new ArrayList<>();
 			String plugin = getContributingPlugin(productID);
 			locations.add(plugin);
-			IPluginModelBase model = map.get(plugin);
+			IPluginModelBase model = getLatestModel(plugin, map);
 			if (model != null) {
 				BundleDescription desc = model.getBundleDescription();
 				if (desc != null) {
@@ -304,7 +304,7 @@
 			resolveLocationPath(targetSplash, properties, map);
 	}
 
-	private static void resolveLocationPath(String splashPath, Properties properties, Map<String, IPluginModelBase> map) {
+	private static void resolveLocationPath(String splashPath, Properties properties, Map<String, List<IPluginModelBase>> map) {
 		ArrayList<String> locations = new ArrayList<>();
 		StringTokenizer tok = new StringTokenizer(splashPath, ","); //$NON-NLS-1$
 		while (tok.hasMoreTokens())
@@ -312,7 +312,7 @@
 		resolveLocationPath(locations, properties, map);
 	}
 
-	private static void resolveLocationPath(ArrayList<String> locations, Properties properties, Map<String, IPluginModelBase> map) {
+	private static void resolveLocationPath(ArrayList<String> locations, Properties properties, Map<String, List<IPluginModelBase>> map) {
 		StringBuilder buffer = new StringBuilder();
 		for (int i = 0; i < locations.size(); i++) {
 			String location = locations.get(i);
@@ -338,11 +338,17 @@
 	 * @param includeReference whether to prefix the url with 'reference:'
 	 * @return string url for the bundle location
 	 */
-	public static String getBundleURL(String id, Map<String, IPluginModelBase> pluginMap, boolean includeReference) {
-		IPluginModelBase model = pluginMap.get(id.trim());
+	public static String getBundleURL(String id, Map<String, List<IPluginModelBase>> pluginMap, boolean includeReference) {
+		IPluginModelBase model = getLatestModel(id, pluginMap);
 		return getBundleURL(model, includeReference);
 	}
 
+
+	public static IPluginModelBase getLatestModel(String id, Map<String, List<IPluginModelBase>> plugins) {
+		List<IPluginModelBase> models = plugins.getOrDefault(id.trim(), Collections.emptyList());
+		return models.stream().max(BundleLauncherHelper.VERSION).orElse(null);
+	}
+
 	/**
 	 * Returns a string url representing the install location of the given bundle model
 	 * @param model the model to create the url for
@@ -367,7 +373,7 @@
 	 * @param map map of bundles being launched (id mapped to model)
 	 * @param properties properties for config.ini
 	 */
-	private static void setBundleLocations(Map<String, IPluginModelBase> map, Properties properties, boolean defaultAuto) {
+	private static void setBundleLocations(Map<String, List<IPluginModelBase>> map, Properties properties, boolean defaultAuto) {
 		String framework = properties.getProperty(PROP_OSGI_FRAMEWORK);
 		if (framework != null) {
 			framework = TargetPlatformHelper.stripPathInformation(framework);
diff --git a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LaunchPluginValidator.java b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LaunchPluginValidator.java
index 0df5b78..38943ed 100644
--- a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LaunchPluginValidator.java
+++ b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LaunchPluginValidator.java
@@ -39,7 +39,7 @@
 			return models;
 
 		Collection<IPluginModelBase> result = null;
-		Map<IPluginModelBase, String> bundles = BundleLauncherHelper.getWorkspaceBundleMap(configuration, null);
+		Map<IPluginModelBase, String> bundles = BundleLauncherHelper.getWorkspaceBundleMap(configuration);
 		result = bundles.keySet();
 		return result.toArray(new IPluginModelBase[result.size()]);
 	}
diff --git a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LauncherUtils.java b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LauncherUtils.java
index 6d97912..4a9ecd2 100644
--- a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LauncherUtils.java
+++ b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/internal/launching/launcher/LauncherUtils.java
@@ -243,7 +243,7 @@
 	}
 
 	private static void handleSelectedPlugins(ILaunchConfiguration config, String timeStamp, ArrayList<IProject> projects) throws CoreException {
-		Map<IPluginModelBase, String> selectedPlugins = BundleLauncherHelper.getWorkspaceBundleMap(config, null);
+		Map<IPluginModelBase, String> selectedPlugins = BundleLauncherHelper.getWorkspaceBundleMap(config);
 		Iterator<IPluginModelBase> it = selectedPlugins.keySet().iterator();
 		while (it.hasNext()) {
 			IPluginModelBase model = it.next();
@@ -258,7 +258,7 @@
 	}
 
 	private static void handleDeselectedPlugins(ILaunchConfiguration config, String launcherTimeStamp, ArrayList<IProject> projects) throws CoreException {
-		Map<IPluginModelBase, String> deSelectedPlugins = BundleLauncherHelper.getWorkspaceBundleMap(config, null);
+		Map<IPluginModelBase, String> deSelectedPlugins = BundleLauncherHelper.getWorkspaceBundleMap(config);
 		IProject[] projs = ResourcesPlugin.getWorkspace().getRoot().getProjects();
 		for (int i = 0; i < projs.length; i++) {
 			if (!WorkspaceModelManager.isPluginProject(projs[i]))
diff --git a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/EclipseApplicationLaunchConfiguration.java b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/EclipseApplicationLaunchConfiguration.java
index 2566aca..5a1e102 100644
--- a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/EclipseApplicationLaunchConfiguration.java
+++ b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/EclipseApplicationLaunchConfiguration.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2005, 2015 IBM Corporation and others.
+ * Copyright (c) 2005, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -16,6 +16,8 @@
 
 import java.io.File;
 import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.variables.IStringVariableManager;
 import org.eclipse.core.variables.VariablesPlugin;
@@ -23,11 +25,10 @@
 import org.eclipse.debug.core.ILaunchConfiguration;
 import org.eclipse.pde.core.plugin.IPluginModelBase;
 import org.eclipse.pde.core.plugin.TargetPlatform;
-import org.eclipse.pde.internal.core.*;
+import org.eclipse.pde.internal.core.ClasspathHelper;
+import org.eclipse.pde.internal.core.TargetPlatformHelper;
 import org.eclipse.pde.internal.core.util.CoreUtility;
-import org.eclipse.pde.internal.core.util.VersionUtil;
 import org.eclipse.pde.internal.launching.launcher.*;
-import org.osgi.framework.Version;
 
 /**
  * A launch delegate for launching Eclipse applications
@@ -43,8 +44,8 @@
 public class EclipseApplicationLaunchConfiguration extends AbstractPDELaunchConfiguration {
 
 	// used to generate the dev classpath entries
-	// key is bundle ID, value is a model
-	private Map<String, IPluginModelBase> fAllBundles;
+	// key is bundle ID, value is a List of models
+	private Map<String, List<IPluginModelBase>> fAllBundles;
 
 	// key is a model, value is startLevel:autoStart
 	private Map<IPluginModelBase, String> fModels;
@@ -95,11 +96,6 @@
 		// add the output folder names
 		programArgs.add("-dev"); //$NON-NLS-1$
 		programArgs.add(ClasspathHelper.getDevEntriesProperties(getConfigDir(configuration).toString() + "/dev.properties", fAllBundles)); //$NON-NLS-1$
-		// necessary for PDE to know how to load plugins when target platform = host platform
-		// see PluginPathFinder.getPluginPaths() and PluginPathFinder.isDevLaunchMode()
-		IPluginModelBase base = fAllBundles.get(PDECore.PLUGIN_ID);
-		if (base != null && VersionUtil.compareMacroMinorMicro(base.getBundleDescription().getVersion(), new Version("3.3.1")) < 0) //$NON-NLS-1$
-			programArgs.add("-pdelaunch"); //$NON-NLS-1$
 
 		String[] args = super.getProgramArguments(configuration);
 		Collections.addAll(programArgs, args);
@@ -181,12 +177,8 @@
 		fWorkspaceLocation = null;
 
 		fModels = BundleLauncherHelper.getMergedBundleMap(configuration, false);
-		fAllBundles = new HashMap<>(fModels.size());
-		Iterator<IPluginModelBase> iter = fModels.keySet().iterator();
-		while (iter.hasNext()) {
-			IPluginModelBase model = iter.next();
-			fAllBundles.put(model.getPluginBase().getId(), model);
-		}
+		fAllBundles = fModels.keySet().stream().collect(Collectors.groupingBy(m -> m.getPluginBase().getId()));
+
 		validateConfigIni(configuration);
 		super.preLaunchCheck(configuration, launch, monitor);
 	}
@@ -210,16 +202,9 @@
 	@Override
 	public String[] getVMArguments(ILaunchConfiguration configuration) throws CoreException {
 		String[] vmArgs = super.getVMArguments(configuration);
-		IPluginModelBase base = fAllBundles.get(PDECore.PLUGIN_ID);
-		if (base != null && VersionUtil.compareMacroMinorMicro(base.getBundleDescription().getVersion(), new Version("3.3.1")) >= 0) { //$NON-NLS-1$
-			// necessary for PDE to know how to load plugins when target platform = host platform
-			// see PluginPathFinder.getPluginPaths() and PluginPathFinder.isDevLaunchMode()
-			String[] result = new String[vmArgs.length + 1];
-			System.arraycopy(vmArgs, 0, result, 0, vmArgs.length);
-			result[vmArgs.length] = "-Declipse.pde.launch=true"; //$NON-NLS-1$
-			return result;
-		}
-		return vmArgs;
+		// necessary for PDE to know how to load plugins when target platform = host platform
+		// see PluginPathFinder.getPluginPaths() and PluginPathFinder.isDevLaunchMode()
+		return Stream.concat(Arrays.stream(vmArgs), Stream.of("-Declipse.pde.launch=true")).toArray(String[]::new); //$NON-NLS-1$
 	}
 
 }
diff --git a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/EquinoxLaunchConfiguration.java b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/EquinoxLaunchConfiguration.java
index 3dbd6e2..bff34e1 100644
--- a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/EquinoxLaunchConfiguration.java
+++ b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/EquinoxLaunchConfiguration.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2005, 2017 IBM Corporation and others.
+ * Copyright (c) 2005, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -18,6 +18,7 @@
 import java.net.URL;
 import java.util.*;
 import java.util.Map.Entry;
+import java.util.stream.Collectors;
 import org.eclipse.core.runtime.*;
 import org.eclipse.debug.core.ILaunch;
 import org.eclipse.debug.core.ILaunchConfiguration;
@@ -44,8 +45,8 @@
 public class EquinoxLaunchConfiguration extends AbstractPDELaunchConfiguration {
 
 	// used to generate the dev classpath entries
-	// key is bundle ID, value is a model
-	protected Map<String, IPluginModelBase> fAllBundles;
+	// key is bundle ID, value is a List of models
+	protected Map<String, List<IPluginModelBase>> fAllBundles;
 
 	// key is a model, value is startLevel:autoStart
 	private Map<IPluginModelBase, String> fModels;
@@ -84,7 +85,7 @@
 				properties.setProperty("org.eclipse.equinox.simpleconfigurator.configUrl", bundlesTxt.toString()); //$NON-NLS-1$
 			}
 			StringBuilder buffer = new StringBuilder();
-			IPluginModelBase model = fAllBundles.get(IPDEBuildConstants.BUNDLE_SIMPLE_CONFIGURATOR);
+			IPluginModelBase model = LaunchConfigurationHelper.getLatestModel(IPDEBuildConstants.BUNDLE_SIMPLE_CONFIGURATOR, fAllBundles);
 			buffer.append(LaunchConfigurationHelper.getBundleURL(model, true));
 			appendStartData(buffer, fModels.get(model), autostart);
 			bundles = buffer.toString();
@@ -152,19 +153,14 @@
 	@Override
 	protected void preLaunchCheck(ILaunchConfiguration configuration, ILaunch launch, IProgressMonitor monitor) throws CoreException {
 		fModels = BundleLauncherHelper.getMergedBundleMap(configuration, true);
-		fAllBundles = new HashMap<>(fModels.size());
-		Iterator<IPluginModelBase> iter = fModels.keySet().iterator();
-		while (iter.hasNext()) {
-			IPluginModelBase model = iter.next();
-			fAllBundles.put(model.getPluginBase().getId(), model);
-		}
+		fAllBundles = fModels.keySet().stream().collect(Collectors.groupingBy(m -> m.getPluginBase().getId(), HashMap::new, Collectors.toCollection(ArrayList::new)));
 
 		if (!fAllBundles.containsKey(IPDEBuildConstants.BUNDLE_OSGI)) {
 			// implicitly add it
 			IPluginModelBase model = PluginRegistry.findModel(IPDEBuildConstants.BUNDLE_OSGI);
 			if (model != null) {
 				fModels.put(model, "default:default"); //$NON-NLS-1$
-				fAllBundles.put(IPDEBuildConstants.BUNDLE_OSGI, model);
+				fAllBundles.computeIfAbsent(model.getPluginBase().getId(), i -> new ArrayList<>()).add(model);
 			} else {
 				String message = PDEMessages.EquinoxLaunchConfiguration_oldTarget;
 				throw new CoreException(Status.error(message));
diff --git a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/JUnitLaunchConfigurationDelegate.java b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/JUnitLaunchConfigurationDelegate.java
index ccbf7b5..73a10db 100644
--- a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/JUnitLaunchConfigurationDelegate.java
+++ b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/JUnitLaunchConfigurationDelegate.java
@@ -19,6 +19,8 @@
 
 import java.io.File;
 import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.eclipse.core.resources.IProject;
 import org.eclipse.core.runtime.*;
 import org.eclipse.debug.core.*;
@@ -33,7 +35,6 @@
 import org.eclipse.pde.internal.core.util.VersionUtil;
 import org.eclipse.pde.internal.launching.*;
 import org.eclipse.pde.internal.launching.launcher.*;
-import org.osgi.framework.Version;
 
 /**
  * A launch delegate for launching JUnit Plug-in tests.
@@ -58,7 +59,7 @@
 
 	// used to generate the dev classpath entries
 	// key is bundle ID, value is a model
-	private Map<String, IPluginModelBase> fAllBundles;
+	private Map<String, List<IPluginModelBase>> fAllBundles;
 
 	// key is a model, value is startLevel:autoStart
 	private Map<IPluginModelBase, String> fModels;
@@ -78,15 +79,29 @@
 		return "org.eclipse.core.launcher.Main"; //$NON-NLS-1$
 	}
 
-	private String getTestPluginId(ILaunchConfiguration configuration) throws CoreException {
+	private IPluginBase getTestPlugin(ILaunchConfiguration configuration) throws CoreException {
 		IJavaProject javaProject = getJavaProject(configuration);
 		IPluginModelBase model = PluginRegistry.findModel(javaProject.getProject());
-		if (model == null)
+		if (model == null) {
 			abort(NLS.bind(PDEMessages.JUnitLaunchConfiguration_error_notaplugin, javaProject.getProject().getName()), null, IStatus.OK);
-		if (model instanceof IFragmentModel)
-			return ((IFragmentModel) model).getFragment().getPluginId();
+		}
+		if (model instanceof IFragmentModel) {
+			IFragment fragment = ((IFragmentModel) model).getFragment();
+			IPluginBase hostModel = getFragmentHostModel(fragment.getPluginId(), fragment.getPluginVersion(), fragment.getRule());
+			if (hostModel == null) {
+				abort(NLS.bind(PDEMessages.JUnitLaunchConfiguration_error_missingPlugin, fragment.getPluginId()), null, IStatus.OK);
+			}
+			model = hostModel.getPluginModel();
+		}
+		return model.getPluginBase();
+	}
 
-		return model.getPluginBase().getId();
+	private IPluginBase getFragmentHostModel(String hostId, String hostVersion, int hostVersionMatchRule) {
+		// return host plug-in model with matching version from bundles selected for launch 
+		List<IPluginModelBase> hosts = fAllBundles.getOrDefault(hostId, Collections.emptyList());
+		Stream<IPluginBase> hostPlugins = hosts.stream().map(IPluginModelBase::getPluginBase);
+		return hostPlugins.filter(h -> VersionUtil.compare(h.getVersion(), hostVersion, hostVersionMatchRule)) //
+				.max(Comparator.comparing(IPluginBase::getVersion)).orElse(null);
 	}
 
 	@Override
@@ -143,7 +158,7 @@
 
 		// Create the platform configuration for the runtime workbench
 		String productID = LaunchConfigurationHelper.getProductID(configuration);
-		String testPluginId = getTestPluginId(configuration);
+		IPluginBase testPlugin = getTestPlugin(configuration);
 		LaunchConfigurationHelper.createConfigIniFile(configuration, productID, fAllBundles, fModels, getConfigurationDirectory(configuration));
 		TargetPlatformHelper.checkPluginPropertiesConsistency(fAllBundles, getConfigurationDirectory(configuration));
 
@@ -162,7 +177,7 @@
 					.filter(IClasspathEntry::isTest)//
 					.filter(entry -> entry.getOutputLocation() != null).forEach(entry -> {
 						IPath relativePath = entry.getOutputLocation().removeFirstSegments(1).makeRelative();
-						devProperties.merge(testPluginId, relativePath.toString(), (vOld, vNew) -> vOld + "," + vNew); //$NON-NLS-1$
+						ClasspathHelper.addDevClasspath(testPlugin, devProperties, relativePath.toString(), true);
 					});
 		}
 		programArgs.add(ClasspathHelper.writeDevEntries(getConfigurationDirectory(configuration).toString() + "/dev.properties", devProperties)); //$NON-NLS-1$
@@ -200,7 +215,7 @@
 		}
 
 		programArgs.add("-testpluginname"); //$NON-NLS-1$
-		programArgs.add(testPluginId);
+		programArgs.add(testPlugin.getId());
 
 		IVMInstall launcher = VMHelper.createLauncher(configuration);
 		boolean isModular = JavaRuntime.isModularJava(launcher);
@@ -275,10 +290,7 @@
 		String vmArgs = LaunchArgumentsHelper.getUserVMArguments(configuration);
 
 		// necessary for PDE to know how to load plugins when target platform = host platform
-		IPluginModelBase base = fAllBundles.get(PDECore.PLUGIN_ID);
-		if (base != null && VersionUtil.compareMacroMinorMicro(base.getBundleDescription().getVersion(), new Version("3.3.1")) >= 0) { //$NON-NLS-1$
-			vmArgs = concatArg(vmArgs, "-Declipse.pde.launch=true"); //$NON-NLS-1$
-		}
+		vmArgs = concatArg(vmArgs, "-Declipse.pde.launch=true"); //$NON-NLS-1$
 		// For p2 target, add "-Declipse.p2.data.area=@config.dir/p2" unless already specified by user
 		if (fAllBundles.containsKey("org.eclipse.equinox.p2.core")) { //$NON-NLS-1$
 			if (!vmArgs.contains("-Declipse.p2.data.area=")) { //$NON-NLS-1$
@@ -394,12 +406,7 @@
 		fWorkspaceLocation = null;
 		fConfigDir = null;
 		fModels = BundleLauncherHelper.getMergedBundleMap(configuration, false);
-		fAllBundles = new LinkedHashMap<>(fModels.size());
-		Iterator<IPluginModelBase> iter = fModels.keySet().iterator();
-		while (iter.hasNext()) {
-			IPluginModelBase model = iter.next();
-			fAllBundles.put(model.getPluginBase().getId(), model);
-		}
+		fAllBundles = fModels.keySet().stream().collect(Collectors.groupingBy(m -> m.getPluginBase().getId(), LinkedHashMap::new, Collectors.toCollection(ArrayList::new)));
 
 		// implicitly add the plug-ins required for JUnit testing if necessary
 		String[] requiredPlugins = JUnitLaunchConfigurationDelegate.getRequiredPlugins(configuration);
@@ -407,7 +414,7 @@
 			String id = requiredPlugin;
 			if (!fAllBundles.containsKey(id)) {
 				IPluginModelBase model = findRequiredPluginInTargetOrHost(id);
-				fAllBundles.put(id, model);
+				fAllBundles.computeIfAbsent(model.getPluginBase().getId(), i -> new ArrayList<>()).add(model);
 				fModels.put(model, "default:default"); //$NON-NLS-1$
 			}
 		}
diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/classpathresolver/ClasspathResolverTest.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/classpathresolver/ClasspathResolverTest.java
index 36613d4..05bfc3b 100644
--- a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/classpathresolver/ClasspathResolverTest.java
+++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/classpathresolver/ClasspathResolverTest.java
@@ -122,6 +122,7 @@
 
 		String expectedDevCP = project.getFolder("cpe").getLocation().toPortableString();
 		assertEquals(expectedDevCP, properties.get(bundleName));
+		assertEquals(expectedDevCP, properties.get(bundleName + ";1.0.0.qualifier"));
 	}
 
 	/**
@@ -155,7 +156,8 @@
 
 		assertEquals("true", devProperties.getProperty("@ignoredot@"));
 		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID));
-		assertEquals(2, devProperties.size()); // assert no more entries
+		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID + ";2.0.0"));
+		assertEquals(3, devProperties.size()); // assert no more entries
 	}
 
 	@Test
@@ -173,7 +175,8 @@
 
 		assertEquals("true", devProperties.getProperty("@ignoredot@"));
 		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID));
-		assertEquals(2, devProperties.size()); // assert no more entries
+		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID + ";" + hostBundleVersion));
+		assertEquals(3, devProperties.size()); // assert no more entries
 	}
 
 	@Test
@@ -189,8 +192,9 @@
 		Properties devProperties = createDevEntryProperties(List.of(hostModel));
 
 		assertEquals("true", devProperties.getProperty("@ignoredot@"));
-		assertEquals("devPath1", devProperties.getProperty(HOST_BUNDLE_ID));
-		assertEquals(2, devProperties.size()); // assert no more entries
+		assertEquals("devPath2", devProperties.getProperty(HOST_BUNDLE_ID));
+		assertEquals("devPath2", devProperties.getProperty(HOST_BUNDLE_ID + ";" + hostBundleVersion));
+		assertEquals(3, devProperties.size()); // assert no more entries
 	}
 
 	@Test
@@ -247,8 +251,10 @@
 		Properties devProperties = createDevEntryProperties(List.of(hostModel, wsModel));
 
 		assertEquals("true", devProperties.getProperty("@ignoredot@"));
-		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID));
-		assertEquals(2, devProperties.size()); // assert no more entries
+		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID)); // last
+		assertEquals("", devProperties.getProperty(HOST_BUNDLE_ID + ";1.0.0"));
+		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID + ";2.0.0"));
+		assertEquals(4, devProperties.size()); // assert no more entries
 	}
 
 	@Test
@@ -266,8 +272,10 @@
 		Properties devProperties = createDevEntryProperties(List.of(tpModel, hostModel));
 
 		assertEquals("true", devProperties.getProperty("@ignoredot@"));
-		assertEquals("devPath1", devProperties.getProperty(HOST_BUNDLE_ID));
-		assertEquals(2, devProperties.size()); // assert no more entries
+		assertEquals("devPath2", devProperties.getProperty(HOST_BUNDLE_ID)); // last
+		assertEquals("", devProperties.getProperty(HOST_BUNDLE_ID + ";1.0.0"));
+		assertEquals("devPath2", devProperties.getProperty(HOST_BUNDLE_ID + ";" + hostBundleVersion));
+		assertEquals(4, devProperties.size()); // assert no more entries
 	}
 
 	@Test
@@ -284,8 +292,10 @@
 		Properties devProperties = createDevEntryProperties(List.of(hostModel, wsModel));
 
 		assertEquals("true", devProperties.getProperty("@ignoredot@"));
-		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID));
-		assertEquals(2, devProperties.size()); // assert no more entries
+		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID)); // last
+		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID + ";2.0.0"));
+		assertEquals("devPath2", devProperties.getProperty(HOST_BUNDLE_ID + ";" + hostBundleVersion));
+		assertEquals(4, devProperties.size()); // assert no more entries
 	}
 
 	@Test
@@ -301,11 +311,15 @@
 		IPluginModelBase tpModel = findTargetModel(HOST_BUNDLE_ID, "1.0.0");
 		IPluginModelBase wsModel = findWorkspaceModel(HOST_BUNDLE_ID, "2.0.0");
 
-		Properties devProperties = createDevEntryProperties(List.of(hostModel, tpModel, wsModel));
+		Properties devProperties = createDevEntryProperties(List.of(hostModel, wsModel, tpModel));
 
 		assertEquals("true", devProperties.getProperty("@ignoredot@"));
+		// jar-bundle from tp should not be considered for non-version entry
 		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID));
-		assertEquals(2, devProperties.size()); // assert no more entries
+		assertEquals("", devProperties.getProperty(HOST_BUNDLE_ID + ";1.0.0"));
+		assertEquals("bin", devProperties.getProperty(HOST_BUNDLE_ID + ";2.0.0"));
+		assertEquals("devPath2", devProperties.getProperty(HOST_BUNDLE_ID + ";" + hostBundleVersion));
+		assertEquals(5, devProperties.size()); // assert no more entries
 	}
 
 	// --- utility methods ---
@@ -372,8 +386,7 @@
 	private Properties createDevEntryProperties(List<IPluginModelBase> launchedBundles)
 			throws IOException, CoreException {
 		File devPropertiesFile = tempFolder.newFile("dev.properties").getCanonicalFile();
-		Map<String, IPluginModelBase> bundlesMap = Map.of(HOST_BUNDLE_ID,
-				launchedBundles.get(launchedBundles.size() - 1));
+		Map<String, List<IPluginModelBase>> bundlesMap = Map.of(HOST_BUNDLE_ID, launchedBundles);
 		String devPropertiesURL = ClasspathHelper.getDevEntriesProperties(devPropertiesFile.getPath(), bundlesMap);
 		return loadProperties(devPropertiesURL);
 	}
diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/launcher/LaunchConfigurationMigrationTest.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/launcher/LaunchConfigurationMigrationTest.java
index 8b57ff6..604baa1 100644
--- a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/launcher/LaunchConfigurationMigrationTest.java
+++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/launcher/LaunchConfigurationMigrationTest.java
@@ -47,13 +47,12 @@
 
 		assertOldPropertiesRemoved(wc);
 
-		Map<IPluginModelBase, String> workspaceBundles = BundleLauncherHelper.getWorkspaceBundleMap(wc);
-		assertEquals("default:true", workspaceBundles.get(findWorkspaceModel("org.eclipse.pde.plugin1", null)));
-		assertEquals("3:false", workspaceBundles.get(findWorkspaceModel("org.eclipse.pde.plugin2", null)));
+		Map<IPluginModelBase, String> bundles = BundleLauncherHelper.getAllSelectedPluginBundles(wc);
+		assertEquals("default:true", bundles.get(findWorkspaceModel("org.eclipse.pde.plugin1", null)));
+		assertEquals("3:false", bundles.get(findWorkspaceModel("org.eclipse.pde.plugin2", null)));
 
-		Map<IPluginModelBase, String> targetBundles = BundleLauncherHelper.getTargetBundleMap(wc);
-		assertEquals("default:true", targetBundles.get(findTargetModel("org.eclipse.core.runtime", null)));
-		assertEquals("2:false", targetBundles.get(findTargetModel("org.eclipse.ui", null)));
+		assertEquals("default:true", bundles.get(findTargetModel("org.eclipse.core.runtime", null)));
+		assertEquals("2:false", bundles.get(findTargetModel("org.eclipse.ui", null)));
 	}
 
 	@Test
@@ -66,12 +65,11 @@
 
 		assertOldPropertiesRemoved(wc);
 
-		Map<IPluginModelBase, String> workspaceBundles = BundleLauncherHelper.getWorkspaceBundleMap(wc);
-		assertEquals("default:default", workspaceBundles.get(findWorkspaceModel("org.eclipse.pde.plugin1", null)));
+		Map<IPluginModelBase, String> bundles = BundleLauncherHelper.getAllSelectedPluginBundles(wc);
+		assertEquals("default:default", bundles.get(findWorkspaceModel("org.eclipse.pde.plugin1", null)));
 
-		Map<IPluginModelBase, String> targetBundles = BundleLauncherHelper.getTargetBundleMap(wc);
-		assertEquals("default:true", targetBundles.get(findTargetModel("org.eclipse.core.runtime", null)));
-		assertEquals("2:false", targetBundles.get(findTargetModel("org.eclipse.ui", null)));
+		assertEquals("default:true", bundles.get(findTargetModel("org.eclipse.core.runtime", null)));
+		assertEquals("2:false", bundles.get(findTargetModel("org.eclipse.ui", null)));
 	}
 
 	@Test
@@ -84,13 +82,12 @@
 
 		assertOldOsgiPropertiesRemoved(wc);
 
-		Map<IPluginModelBase, String> workspaceBundles = BundleLauncherHelper.getWorkspaceBundleMap(wc);
-		assertEquals("default:true", workspaceBundles.get(findWorkspaceModel("org.eclipse.pde.plugin1", null)));
-		assertEquals("3:false", workspaceBundles.get(findWorkspaceModel("org.eclipse.pde.plugin2", null)));
+		Map<IPluginModelBase, String> bundles = BundleLauncherHelper.getAllSelectedPluginBundles(wc);
+		assertEquals("default:true", bundles.get(findWorkspaceModel("org.eclipse.pde.plugin1", null)));
+		assertEquals("3:false", bundles.get(findWorkspaceModel("org.eclipse.pde.plugin2", null)));
 
-		Map<IPluginModelBase, String> targetBundles = BundleLauncherHelper.getTargetBundleMap(wc);
-		assertEquals("default:true", targetBundles.get(findTargetModel("org.eclipse.core.runtime", null)));
-		assertEquals("2:false", targetBundles.get(findTargetModel("org.eclipse.ui", null)));
+		assertEquals("default:true", bundles.get(findTargetModel("org.eclipse.core.runtime", null)));
+		assertEquals("2:false", bundles.get(findTargetModel("org.eclipse.ui", null)));
 	}
 
 	@SuppressWarnings("deprecation")
diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/launcher/PluginBasedLaunchTest.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/launcher/PluginBasedLaunchTest.java
index 58e69c3..412d055 100644
--- a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/launcher/PluginBasedLaunchTest.java
+++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/launcher/PluginBasedLaunchTest.java
@@ -236,7 +236,6 @@
 		};
 
 		Set<BundleLocationDescriptor> expectedBundles = Set.of( //
-				workspaceBundle("plugin.a", "1.0.0"), //
 				workspaceBundle("plugin.a", "2.0.0"));
 
 		assertGetMergedBundleMap(workspacePlugins, targetPlatformBundles, launchConfigSetup, expectedBundles);
@@ -549,7 +548,6 @@
 			wc.setAttribute(IPDELauncherConstants.SELECTED_TARGET_BUNDLES, Set.of("plugin.b"));
 		};
 		Set<BundleLocationDescriptor> expectedBundles = Set.of(//
-				targetBundle("plugin.b", "1.0.0"), //
 				targetBundle("plugin.b", "2.0.0"));
 
 		assertGetMergedBundleMap(workspacePlugins, targetPlatformBundles, launchConfigSetup, expectedBundles);
@@ -586,12 +584,11 @@
 
 		Consumer<ILaunchConfigurationWorkingCopy> launchConfigSetup = wc -> {
 			wc.setAttribute(IPDELauncherConstants.SELECTED_TARGET_BUNDLES,
-					Set.of("plugin.a*1.0.0.2020", "plugin.a*1.0.0.2021"));
-		};
+					new LinkedHashSet<>(List.of("plugin.a*1.0.0.2020", "plugin.a*1.0.0.2021")));
+		}; // first entry is selected -> LinkedHashSet ensures its the same
 
 		Set<BundleLocationDescriptor> expectedBundles = Set.of( //
-				targetBundle("plugin.a", "1.0.0.2020"), //
-				targetBundle("plugin.a", "1.0.0.2021"));
+				targetBundle("plugin.a", "1.0.0.2020"));
 
 		assertGetMergedBundleMap(workspacePlugins, targetPlatformBundles, launchConfigSetup, expectedBundles);
 	}
@@ -615,7 +612,9 @@
 
 		Set<BundleLocationDescriptor> expectedBundles = Set.of( //
 				workspaceBundle("plugin.a", "1.0.0"), //
-				workspaceBundle("plugin.b", "1.0.0"));
+				workspaceBundle("plugin.b", "1.0.0"), //
+				targetBundle("plugin.a", "1.0.1"), //
+				targetBundle("plugin.b", "2.0.0"));
 
 		assertGetMergedBundleMap(workspacePlugins, targetPlatformBundles, launchConfigSetup, expectedBundles);
 	}
@@ -637,7 +636,9 @@
 
 		Set<BundleLocationDescriptor> expectedBundles = Set.of( //
 				workspaceBundle("plugin.a", "1.0.0"), //
-				workspaceBundle("plugin.b", "2.0.0"));
+				workspaceBundle("plugin.b", "2.0.0"), //
+				targetBundle("plugin.a", "1.0.1"), //
+				targetBundle("plugin.b", "3.0.0"));
 
 		assertGetMergedBundleMap(workspacePlugins, targetPlatformBundles, launchConfigSetup, expectedBundles);
 	}
@@ -655,7 +656,8 @@
 		};
 
 		Set<BundleLocationDescriptor> expectedBundles = Set.of( //
-				workspaceBundle("plugin.a", "1.0.0"));
+				workspaceBundle("plugin.a", "1.0.0"), //
+				targetBundle("plugin.a", "1.0.2"));
 
 		assertGetMergedBundleMap(workspacePlugins, targetPlatformBundles, launchConfigSetup, expectedBundles);
 	}
@@ -702,7 +704,9 @@
 
 		Set<BundleLocationDescriptor> expectedBundles = Set.of( //
 				workspaceBundle("plugin.a", "1.0.0"), //
-				workspaceBundle("plugin.b", "1.0.0"));
+				workspaceBundle("plugin.b", "1.0.0"), //
+				targetBundle("plugin.a", "1.0.1"), //
+				targetBundle("plugin.b", "1.0.1"));
 
 		assertGetMergedBundleMap(workspacePlugins, targetPlatformBundles, launchConfigSetup, expectedBundles);
 	}
@@ -770,7 +774,7 @@
 		IPluginModelBase plugin = targetBundle("plugin.a", "2.0.0").findModel();
 
 		String entry = BundleLauncherHelper.writeBundleEntry(plugin, null, null);
-		assertEquals("plugin.a*2.0.0", entry);
+		assertEquals("plugin.a", entry);
 	}
 
 	@Test
@@ -785,7 +789,7 @@
 		IPluginModelBase plugin = targetBundle("plugin.a", "2.0.0").findModel();
 
 		String entry = BundleLauncherHelper.writeBundleEntry(plugin, null, null);
-		assertEquals("plugin.a", entry);
+		assertEquals("plugin.a*2.0.0", entry);
 	}
 
 	@Test
diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/OSGiBundleBlock.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/OSGiBundleBlock.java
index 1cdb967..099e2ad 100644
--- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/OSGiBundleBlock.java
+++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/OSGiBundleBlock.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2005, 2015 IBM Corporation and others.
+ * Copyright (c) 2005, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -14,7 +14,6 @@
  *******************************************************************************/
 package org.eclipse.pde.internal.ui.launcher;
 
-import java.util.HashMap;
 import java.util.Map;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.debug.core.ILaunchConfiguration;
@@ -74,10 +73,7 @@
 	// TODO deal with the discrepency between save/init states of the two blocks
 
 	private void initializePluginsState(ILaunchConfiguration configuration) throws CoreException {
-		Map<IPluginModelBase, String> selected = new HashMap<>();
-		selected.putAll(BundleLauncherHelper.getWorkspaceBundleMap(configuration));
-		selected.putAll(BundleLauncherHelper.getTargetBundleMap(configuration, null));
-
+		Map<IPluginModelBase, String> selected = BundleLauncherHelper.getAllSelectedPluginBundles(configuration);
 		initializePluginsState(selected);
 	}
 
diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/PluginBlock.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/PluginBlock.java
index cb64cc6..3d355a6 100644
--- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/PluginBlock.java
+++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/launcher/PluginBlock.java
@@ -94,10 +94,7 @@
 	}
 
 	private void initializePluginsState(ILaunchConfiguration config) throws CoreException {
-		Map<IPluginModelBase, String> selected = new HashMap<>();
-		selected.putAll(BundleLauncherHelper.getWorkspaceBundleMap(config, null));
-		selected.putAll(BundleLauncherHelper.getTargetBundleMap(config, null));
-
+		Map<IPluginModelBase, String> selected = BundleLauncherHelper.getAllSelectedPluginBundles(config);
 		initializePluginsState(selected);
 	}
 
diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/imports/PluginImportWizard.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/imports/PluginImportWizard.java
index 64d9655..3514189 100644
--- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/imports/PluginImportWizard.java
+++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/imports/PluginImportWizard.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- *  Copyright (c) 2000, 2015 IBM Corporation and others.
+ *  Copyright (c) 2000, 2021 IBM Corporation and others.
  *
  *  This program and the accompanying materials
  *  are made available under the terms of the Eclipse Public License 2.0
@@ -14,7 +14,8 @@
  *******************************************************************************/
 package org.eclipse.pde.internal.ui.wizards.imports;
 
-import java.util.*;
+import java.util.HashSet;
+import java.util.Map;
 import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.debug.core.*;
@@ -140,9 +141,8 @@
 				if (configuration == null)
 					continue;
 				try {
-					Map<?, ?> workspaceBundleMap = BundleLauncherHelper.getWorkspaceBundleMap(configuration);
-					for (Object key : workspaceBundleMap.keySet()) {
-						IPluginModelBase bm = (IPluginModelBase) key;
+					var workspaceBundles = BundleLauncherHelper.getWorkspaceBundleMap(configuration).keySet();
+					for (IPluginModelBase bm : workspaceBundles) {
 						BundleDescription description = bm.getBundleDescription();
 						if (description != null) {
 							if (imported.contains(description.getSymbolicName())) {
diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/product/ProductFromConfigOperation.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/product/ProductFromConfigOperation.java
index 0226657..3d798fe 100644
--- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/product/ProductFromConfigOperation.java
+++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/product/ProductFromConfigOperation.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2005, 2015 IBM Corporation and others.
+ * Copyright (c) 2005, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -74,9 +74,7 @@
 			}
 
 			// fetch the plug-ins models
-			Set<String> set = new HashSet<>();
-			Map<IPluginModelBase, String> map = BundleLauncherHelper.getWorkspaceBundleMap(fLaunchConfiguration, set);
-			map.putAll(BundleLauncherHelper.getTargetBundleMap(fLaunchConfiguration, set));
+			Map<IPluginModelBase, String> map = BundleLauncherHelper.getAllSelectedPluginBundles(fLaunchConfiguration);
 
 			addPlugins(factory, product, map);
 
diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/ui/launcher/EquinoxLaunchConfiguration.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/ui/launcher/EquinoxLaunchConfiguration.java
index fa3c30b..cf36dde 100644
--- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/ui/launcher/EquinoxLaunchConfiguration.java
+++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/ui/launcher/EquinoxLaunchConfiguration.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2005, 2017 IBM Corporation and others.
+ * Copyright (c) 2005, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -18,6 +18,7 @@
 import java.net.URL;
 import java.util.*;
 import java.util.Map.Entry;
+import java.util.stream.Collectors;
 import org.eclipse.core.runtime.*;
 import org.eclipse.debug.core.ILaunch;
 import org.eclipse.debug.core.ILaunchConfiguration;
@@ -47,8 +48,8 @@
 public class EquinoxLaunchConfiguration extends AbstractPDELaunchConfiguration {
 
 	// used to generate the dev classpath entries
-	// key is bundle ID, value is a model
-	protected Map<String, IPluginModelBase> fAllBundles;
+	// key is bundle ID, value is a List of models
+	protected Map<String, List<IPluginModelBase>> fAllBundles;
 
 	// key is a model, value is startLevel:autoStart
 	private Map<IPluginModelBase, String> fModels;
@@ -86,7 +87,7 @@
 				properties.setProperty("org.eclipse.equinox.simpleconfigurator.configUrl", bundlesTxt.toString()); //$NON-NLS-1$
 			}
 			StringBuilder buffer = new StringBuilder();
-			IPluginModelBase model = fAllBundles.get(IPDEBuildConstants.BUNDLE_SIMPLE_CONFIGURATOR);
+			IPluginModelBase model = LaunchConfigurationHelper.getLatestModel(IPDEBuildConstants.BUNDLE_SIMPLE_CONFIGURATOR, fAllBundles);
 			buffer.append(LaunchConfigurationHelper.getBundleURL(model, true));
 			appendStartData(buffer, fModels.get(model), autostart);
 			bundles = buffer.toString();
@@ -154,19 +155,15 @@
 	@Override
 	protected void preLaunchCheck(ILaunchConfiguration configuration, ILaunch launch, IProgressMonitor monitor) throws CoreException {
 		fModels = BundleLauncherHelper.getMergedBundleMap(configuration, true);
-		fAllBundles = new HashMap<>(fModels.size());
-		Iterator<IPluginModelBase> iter = fModels.keySet().iterator();
-		while (iter.hasNext()) {
-			IPluginModelBase model = iter.next();
-			fAllBundles.put(model.getPluginBase().getId(), model);
-		}
+		fAllBundles = fModels.keySet().stream().collect(Collectors.groupingBy(m -> m.getPluginBase().getId(),
+				HashMap::new, Collectors.toCollection(ArrayList::new)));
 
 		if (!fAllBundles.containsKey(IPDEBuildConstants.BUNDLE_OSGI)) {
 			// implicitly add it
 			IPluginModelBase model = PluginRegistry.findModel(IPDEBuildConstants.BUNDLE_OSGI);
 			if (model != null) {
 				fModels.put(model, "default:default"); //$NON-NLS-1$
-				fAllBundles.put(IPDEBuildConstants.BUNDLE_OSGI, model);
+				fAllBundles.computeIfAbsent(IPDEBuildConstants.BUNDLE_OSGI, i -> new ArrayList<>()).add(model);
 			} else {
 				String message = PDEMessages.EquinoxLaunchConfiguration_oldTarget;
 				throw new CoreException(Status.error((String) message));
diff --git a/ui/org.eclipse.pde.unittest.junit/src/org/eclipse/pde/unittest/junit/launcher/JUnitPluginLaunchConfigurationDelegate.java b/ui/org.eclipse.pde.unittest.junit/src/org/eclipse/pde/unittest/junit/launcher/JUnitPluginLaunchConfigurationDelegate.java
index 0887671..807e90a 100644
--- a/ui/org.eclipse.pde.unittest.junit/src/org/eclipse/pde/unittest/junit/launcher/JUnitPluginLaunchConfigurationDelegate.java
+++ b/ui/org.eclipse.pde.unittest.junit/src/org/eclipse/pde/unittest/junit/launcher/JUnitPluginLaunchConfigurationDelegate.java
@@ -26,15 +26,14 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import org.osgi.framework.Bundle;
 import org.osgi.framework.Constants;
-import org.osgi.framework.Version;
 
 import org.eclipse.osgi.util.NLS;
 import org.eclipse.pde.core.plugin.IFragmentModel;
@@ -46,7 +45,6 @@
 import org.eclipse.pde.internal.core.PDECore;
 import org.eclipse.pde.internal.core.TargetPlatformHelper;
 import org.eclipse.pde.internal.core.util.CoreUtility;
-import org.eclipse.pde.internal.core.util.VersionUtil;
 import org.eclipse.pde.internal.launching.IPDEConstants;
 import org.eclipse.pde.internal.launching.launcher.BundleLauncherHelper;
 import org.eclipse.pde.internal.launching.launcher.EclipsePluginValidationOperation;
@@ -361,12 +359,8 @@
 		fWorkspaceLocation = null;
 		fConfigDir = null;
 		fModels = BundleLauncherHelper.getMergedBundleMap(configuration, false);
-		fAllBundles = new LinkedHashMap<>(fModels.size());
-		Iterator<IPluginModelBase> iter = fModels.keySet().iterator();
-		while (iter.hasNext()) {
-			IPluginModelBase model = iter.next();
-			fAllBundles.put(model.getPluginBase().getId(), model);
-		}
+		fAllBundles = fModels.keySet().stream().collect(Collectors.groupingBy(m -> m.getPluginBase().getId(),
+				LinkedHashMap::new, Collectors.toCollection(ArrayList::new)));
 
 		// implicitly add the plug-ins required for JUnit testing if necessary
 		String[] requiredPlugins = getRequiredPlugins(configuration);
@@ -374,7 +368,7 @@
 			String id = requiredPlugin;
 			if (!fAllBundles.containsKey(id)) {
 				IPluginModelBase model = findRequiredPluginInTargetOrHost(id);
-				fAllBundles.put(id, model);
+				fAllBundles.computeIfAbsent(id, i -> new ArrayList<>()).add(model);
 				fModels.put(model, "default:default"); //$NON-NLS-1$
 			}
 		}
@@ -988,7 +982,7 @@
 
 	// used to generate the dev classpath entries
 	// key is bundle ID, value is a model
-	private Map<String, IPluginModelBase> fAllBundles;
+	private Map<String, List<IPluginModelBase>> fAllBundles;
 
 	// key is a model, value is startLevel:autoStart
 	private Map<IPluginModelBase, String> fModels;
@@ -1079,11 +1073,7 @@
 
 		// necessary for PDE to know how to load plugins when target platform = host
 		// platform
-		IPluginModelBase base = fAllBundles.get(PDECore.PLUGIN_ID);
-		if (base != null && VersionUtil.compareMacroMinorMicro(base.getBundleDescription().getVersion(),
-				new Version("3.3.1")) >= 0) { //$NON-NLS-1$
-			vmArgs = concatArg(vmArgs, "-Declipse.pde.launch=true"); //$NON-NLS-1$
-		}
+		vmArgs = concatArg(vmArgs, "-Declipse.pde.launch=true"); //$NON-NLS-1$
 		// For p2 target, add "-Declipse.p2.data.area=@config.dir/p2" unless already
 		// specified by user
 		if (fAllBundles.containsKey("org.eclipse.equinox.p2.core")) { //$NON-NLS-1$