Bug 551480 - java.lang.module.FindException: Module jdk.crypto.ec not
found, required by jdk.crypto.cryptoki

Change-Id: Iaaa2588fcce02f4b776d5af2140115c5f5751a57
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
index 7c73eb5..8ed5ac9 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
@@ -77,6 +77,7 @@
 import org.eclipse.jdt.debug.tests.core.LineTrackerTests;
 import org.eclipse.jdt.debug.tests.core.LiteralTests17;
 import org.eclipse.jdt.debug.tests.core.LocalVariableTests;
+import org.eclipse.jdt.debug.tests.core.ModuleOptionsTests;
 import org.eclipse.jdt.debug.tests.core.ProcessTests;
 import org.eclipse.jdt.debug.tests.core.RuntimeClasspathEntryTests;
 import org.eclipse.jdt.debug.tests.core.StaticVariableTests;
@@ -233,6 +234,9 @@
 			addTest(new TestSuite(BootpathTests.class));
 		}
 		addTest(new TestSuite(EEDefinitionTests.class));
+		if (JavaProjectHelper.isJava9Compatible()) {
+			addTest(new TestSuite(ModuleOptionsTests.class));
+		}
 
 	//VM Install/Environment tests
 		addTest(new TestSuite(VMInstallTests.class));
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/ModuleOptionsTests.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/ModuleOptionsTests.java
new file mode 100644
index 0000000..4aadcbd
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/ModuleOptionsTests.java
@@ -0,0 +1,182 @@
+/*******************************************************************************
+ * Copyright (c) 2019 GK Software SE 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:
+ *     Stephan Herrmann - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.debug.tests.core;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.jdt.core.IClasspathAttribute;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.debug.tests.AbstractDebugTest;
+import org.eclipse.jdt.launching.JavaRuntime;
+
+public class ModuleOptionsTests extends AbstractDebugTest {
+
+	private static final String ASSUMED_DEFAULT_MODULES_9 = "java.se," //
+			+ "javafx.base,javafx.controls,javafx.fxml,javafx.graphics,javafx.media,javafx.swing,javafx.web," // REMOVED in 10
+			+ "jdk.accessibility,jdk.attach,jdk.compiler,jdk.dynalink,jdk.httpserver,"//
+			+ "jdk.incubator.httpclient," //
+			+ "jdk.jartool,jdk.javadoc,jdk.jconsole,jdk.jdi,"//
+			+ "jdk.jfr," // intermittent
+			+ "jdk.jshell,jdk.jsobject,jdk.management,"//
+			+ "jdk.management.cmm,jdk.management.jfr,jdk.management.resource," // REMOVED later
+			+ "jdk.net," //
+			+ "jdk.packager,jdk.packager.services,jdk.plugin.dom," // NOT in openjdk
+			+ "jdk.scripting.nashorn,jdk.sctp,jdk.security.auth,jdk.security.jgss,jdk.unsupported," //
+			+ "jdk.xml.dom,"//
+			+ "oracle.desktop,oracle.net"; // NOT in openjdk
+	private static final String ASSUMED_DEFAULT_MODULES_10 = "java.se," //
+			+ "jdk.accessibility,jdk.attach,jdk.compiler,jdk.dynalink,jdk.httpserver," //
+			+ "jdk.incubator.httpclient," // REMOVED later
+			+ "jdk.jartool,jdk.javadoc,jdk.jconsole,jdk.jdi," //
+			+ "jdk.jshell,jdk.jsobject,jdk.management," //
+			+ "jdk.net," //
+			+ "jdk.scripting.nashorn,jdk.sctp,jdk.security.auth,jdk.security.jgss,jdk.unsupported," //
+			+ "jdk.xml.dom";
+	private static final String ASSUMED_DEFAULT_MODULES_12 = "java.se," //
+			+ "jdk.accessibility,jdk.attach,jdk.compiler,jdk.dynalink,jdk.httpserver," //
+			+ "jdk.jartool,jdk.javadoc,jdk.jconsole,jdk.jdi," //
+			+ "jdk.jfr," // intermittent
+			+ "jdk.jshell,jdk.jsobject,jdk.management," //
+			+ "jdk.management.jfr," // intermittent
+			+ "jdk.net," //
+			+ "jdk.scripting.nashorn,jdk.sctp,jdk.security.auth,jdk.security.jgss,jdk.unsupported," //
+			+ "jdk.unsupported.desktop," // NEW
+			+ "jdk.xml.dom";
+
+	public ModuleOptionsTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected IJavaProject getProjectContext() {
+		return get9Project();
+	}
+
+	protected void addClasspathAttributesToSystemLibrary(IJavaProject project, IClasspathAttribute[] extraAttributes) throws JavaModelException {
+		IClasspathEntry[] rawClasspath = project.getRawClasspath();
+		int i = indexOfJREContainer(rawClasspath);
+		rawClasspath[i] = JavaCore.newContainerEntry(rawClasspath[i].getPath(), null, extraAttributes, false);
+		project.setRawClasspath(rawClasspath, null);
+		waitForBuild();
+	}
+
+	protected void removeClasspathAttributesFromSystemLibrary(IJavaProject project) throws JavaModelException {
+		IClasspathEntry[] rawClasspath = project.getRawClasspath();
+		int i = indexOfJREContainer(rawClasspath);
+		rawClasspath[i] = JavaCore.newContainerEntry(rawClasspath[i].getPath(), null, new IClasspathAttribute[0], false);
+		project.setRawClasspath(rawClasspath, null);
+		waitForBuild();
+	}
+
+	private List<String> getDefaultModules(IJavaProject javaProject) throws JavaModelException {
+		IClasspathEntry[] rawClasspath = javaProject.getRawClasspath();
+		int i = indexOfJREContainer(rawClasspath);
+		List<String> list = JavaCore.defaultRootModules(Arrays.asList(javaProject.findUnfilteredPackageFragmentRoots(rawClasspath[i])));
+		list.sort(String::compareTo);
+		return list;
+	}
+
+	private int indexOfJREContainer(IClasspathEntry[] rawClasspath) {
+		for (int i = 0; i < rawClasspath.length; i++) {
+			IClasspathEntry classpathEntry = rawClasspath[i];
+			if (classpathEntry.getEntryKind() == IClasspathEntry.CPE_CONTAINER
+					&& JavaRuntime.JRE_CONTAINER.equals(classpathEntry.getPath().segment(0))) {
+				return i;
+			}
+		}
+		return -1;
+	}
+
+	public void testAddModules1() throws JavaModelException {
+		IJavaProject javaProject = getProjectContext();
+		List<String> defaultModules = getDefaultModules(javaProject);
+		defaultModules.add("jdk.crypto.cryptoki"); // requires jdk.crypto.ec
+		try {
+			IClasspathAttribute[] attributes = {
+					JavaCore.newClasspathAttribute(IClasspathAttribute.LIMIT_MODULES, String.join(",", defaultModules))
+			};
+			addClasspathAttributesToSystemLibrary(javaProject, attributes);
+
+			ILaunchConfiguration launchConfiguration = getLaunchConfiguration(javaProject, "LogicalStructures");
+			String cliOptions = JavaRuntime.getModuleCLIOptions(launchConfiguration);
+			assertEquals("Unexpectd cli options", "--add-modules jdk.crypto.cryptoki,jdk.crypto.ec", cliOptions);
+		} finally {
+			removeClasspathAttributesFromSystemLibrary(javaProject);
+		}
+	}
+
+	public void testLimitModules1() throws JavaModelException {
+		IJavaProject javaProject = getProjectContext();
+		List<String> defaultModules = getDefaultModules(javaProject);
+		String expectedModules;
+		switch (String.join(",", defaultModules)) {
+			case ASSUMED_DEFAULT_MODULES_9:
+				expectedModules = "java.se," //
+						+ "javafx.fxml,javafx.swing,javafx.web," //
+						+ "jdk.accessibility,jdk.httpserver,jdk.incubator.httpclient,"
+						+ "jdk.jartool,jdk.jconsole,jdk.jshell," //
+						+ "jdk.management.cmm,jdk.management.jfr,jdk.management.resource," //
+						+ "jdk.net," //
+						+ "jdk.packager,jdk.packager.services,jdk.plugin.dom," //
+						+ "jdk.scripting.nashorn,jdk.sctp,"
+						+ "jdk.security.auth,jdk.security.jgss,jdk.unsupported," //
+						+ "oracle.desktop,oracle.net";
+				break;
+			case ASSUMED_DEFAULT_MODULES_10:
+				expectedModules = "java.se," //
+						+ "jdk.accessibility,jdk.httpserver,jdk.incubator.httpclient," //
+						+ "jdk.jartool,jdk.jconsole,jdk.jshell," //
+						+ "jdk.jsobject," // previously pulled in via javafx.*
+						+ "jdk.net," //
+						+ "jdk.scripting.nashorn,jdk.sctp," //
+						+ "jdk.security.auth,jdk.security.jgss,jdk.unsupported," //
+						+ "jdk.xml.dom";
+				break;
+			case ASSUMED_DEFAULT_MODULES_12:
+				expectedModules = "java.se," //
+						+ "jdk.accessibility,jdk.httpserver," //
+						+ "jdk.jartool,jdk.jconsole,jdk.jshell," //
+						+ "jdk.jsobject," //
+						+ "jdk.management.jfr," // intermittent
+						+ "jdk.net," //
+						+ "jdk.scripting.nashorn,jdk.sctp," //
+						+ "jdk.security.auth,jdk.security.jgss,jdk.unsupported," //
+						+ "jdk.unsupported.desktop," // NEW
+						+ "jdk.xml.dom";
+				break;
+			default:
+				fail("Unknown set of default modules " + String.join(",", defaultModules));
+				return;
+		}
+		if (!defaultModules.remove("jdk.javadoc")) { // requires java.compiler and jdk.compiler but is required by no default module
+			fail("expected module was not in defaultModules");
+		}
+		try {
+			IClasspathAttribute[] attributes = {
+					JavaCore.newClasspathAttribute(IClasspathAttribute.LIMIT_MODULES, String.join(",", defaultModules)) };
+			addClasspathAttributesToSystemLibrary(javaProject, attributes);
+
+			ILaunchConfiguration launchConfiguration = getLaunchConfiguration(javaProject, "LogicalStructures");
+			String cliOptions = JavaRuntime.getModuleCLIOptions(launchConfiguration);
+			assertEquals("Unexpectd cli options", "--limit-modules " + expectedModules, cliOptions);
+		} finally {
+			removeClasspathAttributesFromSystemLibrary(javaProject);
+		}
+	}
+}
diff --git a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/JavaRuntime.java b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/JavaRuntime.java
index 8b4d0cb..c9c9ec8 100644
--- a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/JavaRuntime.java
+++ b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/JavaRuntime.java
@@ -35,7 +35,9 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import javax.xml.parsers.DocumentBuilder;
 
@@ -3460,7 +3462,7 @@
 			}
 		}
 		catch (CoreException e) {
-			e.printStackTrace();
+			LaunchingPlugin.log(e);
 		}
 		return cliOptionString.toString().trim();
 	}
@@ -3541,7 +3543,7 @@
 			try {
 				absPaths[i] = toAbsolutePath(resource, root);
 			} catch (JavaModelException e) {
-				// JavaPlugin.log(e);
+				LaunchingPlugin.log(e);
 			}
 			if (absPaths[i] == null) {
 				absPaths[i] = paths[i];
@@ -3576,18 +3578,25 @@
 			Set<String> selected = new HashSet<>(Arrays.asList(modules));
 			List<IPackageFragmentRoot> allSystemRoots = Arrays.asList(prj.findUnfilteredPackageFragmentRoots(systemLibrary));
 			Set<String> defaultModules = getDefaultModules(allSystemRoots);
-			Set<String> limit = new HashSet<>(defaultModules);
-			if (limit.retainAll(selected)) { // limit = selected ∩ default -- only add the option, if limit ⊂ default
+			Set<String> limit = new HashSet<>(defaultModules); // contains some redundancy, but is no full closure
+
+			// selected contains the minimal representation, now compute the transitive closure for comparison with semi-closed defaultModules:
+			Map<String, IModuleDescription> allModules = allSystemRoots.stream() //
+					.map(r -> r.getModuleDescription()) //
+					.filter(Objects::nonNull) //
+					.collect(Collectors.toMap(IModuleDescription::getElementName, module -> module));
+			Set<String> selectedClosure = closure(selected, new HashSet<>(), allModules);
+
+			if (limit.retainAll(selectedClosure)) { // limit = selected ∩ default -- only add the option, if limit ⊂ default
 				if (limit.isEmpty()) {
 					throw new IllegalArgumentException("Cannot hide all modules, at least java.base is required"); //$NON-NLS-1$
 				}
-				buf.append(LIMIT_MODULES).append(joinedSortedList(limit)).append(BLANK);
+				buf.append(LIMIT_MODULES).append(joinedSortedList(reduceNames(limit, allModules.values()))).append(BLANK);
 			}
 
-			Set<String> add = new HashSet<>(selected);
-			add.removeAll(defaultModules);
-			if (!add.isEmpty()) { // add = selected \ default
-				buf.append(ADD_MODULES).append(joinedSortedList(add)).append(BLANK);
+			selectedClosure.removeAll(defaultModules);
+			if (!selectedClosure.isEmpty()) { // add = selected \ default
+				buf.append(ADD_MODULES).append(joinedSortedList(selectedClosure)).append(BLANK);
 			}
 		} else {
 			Arrays.sort(modules);
@@ -3595,6 +3604,58 @@
 		}
 	}
 
+	private static Set<String> closure(Collection<String> moduleNames, Set<String> collected, Map<String, IModuleDescription> allModules) {
+		for (String name : moduleNames) {
+			if (collected.add(name)) {
+				IModuleDescription module = allModules.get(name);
+				if (module != null) {
+					try {
+						closure(Arrays.asList(module.getRequiredModuleNames()), collected, allModules);
+					} catch (JavaModelException e) {
+						LaunchingPlugin.log(e);
+					}
+				}
+			}
+		}
+		return collected;
+	}
+
+	private static Collection<String> reduceNames(Collection<String> names, Collection<IModuleDescription> allModules) {
+		// build a reverse dependency tree:
+		Map<String, List<String>> moduleRequiredByModules = new HashMap<>();
+		for (IModuleDescription module : allModules) {
+			if (!names.contains(module.getElementName())) {
+				continue;
+			}
+			try {
+				for (String required : module.getRequiredModuleNames()) {
+					List<String> dominators = moduleRequiredByModules.get(required);
+					if (dominators == null) {
+						moduleRequiredByModules.put(required, dominators = new ArrayList<>());
+					}
+					dominators.add(module.getElementName());
+				}
+			} catch (CoreException e) {
+				LaunchingPlugin.log(e);
+				return names; // unreduced
+			}
+		}
+		// use the tree to find and eliminate redundancy:
+		List<String> reduced = new ArrayList<>();
+		outer: for (String name : names) {
+			List<String> dominators = moduleRequiredByModules.get(name);
+			if (dominators != null) {
+				for (String dominator : dominators) {
+					if (names.contains(dominator)) {
+						continue outer;
+					}
+				}
+			}
+			reduced.add(name);
+		}
+		return reduced;
+	}
+
 	private static Set<String> getDefaultModules(List<IPackageFragmentRoot> allSystemRoots) throws JavaModelException {
 		HashMap<String, String[]> moduleDescriptions = new HashMap<>();
 		for (IPackageFragmentRoot packageFragmentRoot : allSystemRoots) {