Bug 417869 - Avoid adding dev class path entries that are dups

There are cases where PDE will add class path to the dev settings which
are duplicate paths specified on the Bundle-ClassPath header. The
DevClassLoadingHook should avoid doing adding the duplicate paths which
the framework will add itself.

Change-Id: Ie069f2bbf137a2de16fe1168d251669843266cb6
diff --git a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/bundles/SystemBundleTests.java b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/bundles/SystemBundleTests.java
index 2c3c875..018a040 100755
--- a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/bundles/SystemBundleTests.java
+++ b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/bundles/SystemBundleTests.java
@@ -2416,7 +2416,8 @@
 		return manifest;
 	}
 
-	static File createBundle(File outputDir, String bundleName, Map<String, String> headers, Map<String, String>... entries) throws IOException {
+	public static File createBundle(File outputDir, String bundleName, Map<String, String> headers,
+			Map<String, String>... entries) throws IOException {
 		Manifest m = new Manifest();
 		Attributes attributes = m.getMainAttributes();
 		attributes.putValue("Manifest-Version", "1.0");
diff --git a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/hooks/framework/AllFrameworkHookTests.java b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/hooks/framework/AllFrameworkHookTests.java
index 3e35b93..6b21ff2 100644
--- a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/hooks/framework/AllFrameworkHookTests.java
+++ b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/hooks/framework/AllFrameworkHookTests.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2013, 2015 IBM Corporation and others.
+ * Copyright (c) 2013, 2020 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -26,6 +26,7 @@
 		suite.addTest(new TestSuite(DevClassPathWithExtensionTests.class));
 		suite.addTest(new TestSuite(EmbeddedEquinoxWithURLInClassLoadTests.class));
 		suite.addTest(new TestSuite(ActivatorOrderTest.class));
+		suite.addTest(new TestSuite(DevClassPathDuplicateTests.class));
 		return suite;
 	}
 }
diff --git a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/hooks/framework/DevClassPathDuplicateTests.java b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/hooks/framework/DevClassPathDuplicateTests.java
new file mode 100644
index 0000000..ad19f46
--- /dev/null
+++ b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/hooks/framework/DevClassPathDuplicateTests.java
@@ -0,0 +1,83 @@
+/*******************************************************************************
+ * Copyright (c) 2020 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.osgi.tests.hooks.framework;
+
+import static org.eclipse.osgi.tests.bundles.AbstractBundleTests.stopQuietly;
+
+import java.io.File;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import org.eclipse.osgi.internal.framework.EquinoxConfiguration;
+import org.eclipse.osgi.tests.OSGiTestsActivator;
+import org.eclipse.osgi.tests.bundles.SystemBundleTests;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.framework.launch.Framework;
+
+public class DevClassPathDuplicateTests extends AbstractFrameworkHookTests {
+	private Map<String, String> configuration;
+	private Framework framework;
+	private String location;
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		File store = OSGiTestsActivator.getContext().getDataFile(getName());
+		configuration = new HashMap<>();
+		configuration.put(Constants.FRAMEWORK_STORAGE, store.getAbsolutePath());
+		configuration.put(EquinoxConfiguration.PROP_DEV, "duplicate/");
+		framework = createFramework(configuration);
+
+		Map<String, String> headers = new HashMap<>();
+		headers.put(Constants.BUNDLE_MANIFESTVERSION, "2");
+		headers.put(Constants.BUNDLE_SYMBOLICNAME, "b.dup.cp");
+		headers.put(Constants.BUNDLE_CLASSPATH, "duplicate/");
+		Map<String, String> entries = new HashMap<>();
+		entries.put("duplicate/", null);
+		entries.put("duplicate/resource.txt", "hello");
+		File testBundle = SystemBundleTests.createBundle(store, "b.dup.cp", headers, entries);
+		location = testBundle.toURI().toASCIIString();
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		stopQuietly(framework);
+		super.tearDown();
+	}
+
+	private void initAndStartFramework() throws Exception {
+		initAndStart(framework);
+	}
+
+	private Bundle installBundle() throws Exception {
+		return framework.getBundleContext().installBundle(location);
+	}
+
+	public void testDevClassPathWithExtension() throws Exception {
+		initAndStartFramework();
+
+		Bundle b = installBundle();
+		b.start();
+		Enumeration<URL> resources = b.getResources("resource.txt");
+		assertNotNull("no resources", resources);
+		int cnt = 0;
+		while (resources.hasMoreElements()) {
+			cnt++;
+			resources.nextElement();
+		}
+		assertEquals("Wrong number of resources.", 1, cnt);
+	}
+}
diff --git a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hooks/DevClassLoadingHook.java b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hooks/DevClassLoadingHook.java
index 8d26f1f..d7ec50b 100644
--- a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hooks/DevClassLoadingHook.java
+++ b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hooks/DevClassLoadingHook.java
@@ -16,6 +16,11 @@
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.osgi.container.ModuleCapability;
+import org.eclipse.osgi.container.namespaces.EquinoxModuleDataNamespace;
 import org.eclipse.osgi.framework.util.KeyedElement;
 import org.eclipse.osgi.internal.framework.EquinoxConfiguration;
 import org.eclipse.osgi.internal.hookregistry.ClassLoaderHook;
@@ -50,8 +55,22 @@
 		// check that dev classpath entries have not already been added; we mark this in the first entry below
 		if (cpEntries.size() > 0 && cpEntries.get(0).getUserObject(KEY) != null)
 			return false; // this source has already had its dev classpath entries added.
+
+		// get the specified classpath from the Bundle-ClassPath header to check for dups
+		List<ModuleCapability> moduleDatas = sourceGeneration.getRevision().getModuleCapabilities(EquinoxModuleDataNamespace.MODULE_DATA_NAMESPACE);
+		@SuppressWarnings("unchecked")
+		List<String> specifiedCP = Optional.ofNullable(moduleDatas.isEmpty()
+				?
+				null
+				: (List<String>) moduleDatas.get(0).getAttributes().get(EquinoxModuleDataNamespace.CAPABILITY_CLASSPATH))
+				.orElse(Collections.singletonList(".")); //$NON-NLS-1$
 		boolean result = false;
 		for (String devClassPath : devClassPaths) {
+			if (specifiedCP.contains(devClassPath)) {
+				// dev properties contained a duplicate of an entry on the Bundle-ClassPath header
+				// don't add anything, the framework will do it for us
+				continue;
+			}
 			if (hostmanager.addClassPathEntry(cpEntries, devClassPath, hostmanager, sourceGeneration)) {
 				result = true;
 			} else {