Skip to content

Instantly share code, notes, and snippets.

@mtdowling
Created April 10, 2019 16:21
Show Gist options
  • Save mtdowling/39905d445829ab1ac58d0cafecf6bc9e to your computer and use it in GitHub Desktop.
Save mtdowling/39905d445829ab1ac58d0cafecf6bc9e to your computer and use it in GitHub Desktop.
Module Graph example
package com.example;
import java.io.File;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Represents a graph of modules loaded from source sets of files.
*
* <p>Libraries that need to utilize problematic dependencies that are not
* compatible with the module-path due to things like split packages
* can still use modularized dependencies but they themselves must not
* define a truly modular jar via a module-info.java. Instead, these
* libraries should utilize problematic libraries on the classpath
* while utilizing modularized libraries on the module-path, and this
* ModuleGraph abstraction will correctly separate the two paths automatically.
*
* <p>This graph determines which sources are required to be on the
* module-path and which sources can remain on the classpath using the
* following rules:
*
* <ol>
* <li>Any provided "roots" are always added to the module-path. These
* roots are useful, for example, to setup a test runner to be able to
* read from a module under test.
* </li>
* <li>All sources that explicitly defines a module-info.java are always
* added to the module-path. Modularized sources must be run from the
* module-path or they will fail to find their dependencies.</li>
* <li>Any dependency of a modularized jar that is referenced through a
* "reads" or "opens" in their module-info.java is added to the
* module-path. This includes sources that use an Automatic-Module-Name
* and sources that use a derived module name based on the rules defined
* in {@link ModuleFinder}.
* </li>
* <li>The recursive dependencies of all "roots" and modular jars are
* added to the module-path.
* </li>
* <li>All other sources are omitted from the module-path and should be
* used in the class-path.
* </li>
* </ol>
*/
public final class ModuleGraph {
private final Map<URI, ModuleReference> byUri = new LinkedHashMap<>();
private final Map<String, ModuleReference> byName = new LinkedHashMap<>();
private final ConcurrentMap<String, Set<ModuleReference>> cache = new ConcurrentHashMap<>();
private ModuleGraph(List<ModuleReference> references) {
for (var reference : references) {
byName.put(reference.descriptor().name(), reference);
reference.location().ifPresent(uri -> byUri.put(uri, reference));
}
}
/**
* Creates a ModuleGraph from the given class-path style string.
*
* @param path Path to parse into a ModuleGraph.
* @return Returns the create ModuleGraph.
*/
public static ModuleGraph fromPath(String path) {
return fromStrings(Arrays.asList(path.split(":")));
}
/**
* Creates a ModuleGraph from a list of paths.
*
* @param paths Paths to files to find and resolve modules against.
* @return Returns the created graph.
*/
public static ModuleGraph fromPaths(List<Path> paths) {
Path[] array = new Path[paths.size()];
for (var i = 0; i < paths.size(); i++) {
array[i] = paths.get(i);
}
return new ModuleGraph(new ArrayList<>(ModuleFinder.of(array).findAll()));
}
/**
* Creates a ModuleGraph from a list of file names.
*
* @param files Paths to files to find and resolve modules against.
* @return Returns the created graph.
*/
public static ModuleGraph fromStrings(List<String> files) {
return fromPaths(files.stream().map(Paths::get).collect(Collectors.toList()));
}
/**
* Gets all source set locations.
*
* @return Returns a stream of location URIs.
*/
public Stream<URI> locations() {
return byName.values().stream().flatMap(moduleReference -> moduleReference.location().stream());
}
/**
* Gets all files found in the source set.
*
* @return Returns a stream of files.
*/
public Stream<File> files() {
return locations().map(File::new);
}
/**
* Gets all module names resolved from the source set.
*
* @return Returns the resolved module names.
*/
public Set<String> getModuleNames() {
return byName.keySet();
}
/**
* Gets a module reference by source location.
*
* @param uri Module/source location.
* @return Returns the optionally found reference.
*/
public Optional<ModuleReference> getReferenceByUri(URI uri) {
return Optional.ofNullable(byUri.get(uri));
}
/**
* Gets a module reference by source location.
*
* @param file Module/source location.
* @return Returns the optionally found reference.
*/
public Optional<ModuleReference> getReferenceByFile(File file) {
return getReferenceByUri(file.toURI());
}
/**
* Gets a module reference by module name.
*
* @param name Name of the reference to retrieve.
* @return Returns the optionally found reference.
*/
public Optional<ModuleReference> getReferenceByName(String name) {
return Optional.ofNullable(byName.get(name));
}
/**
* Determines which sources are required to be on the module-path.
*
* @param roots Roots that are always placed on the module-path.
* @return Returns the resolved references that must be on the module-path.
*/
public Set<ModuleReference> getModulePathReferences(String... roots) {
Set<ModuleReference> modules = new LinkedHashSet<>();
// Always add explicitly defined modules to the module-path.
for (var loaded : byName.values()) {
if (!loaded.descriptor().isAutomatic()) {
modules.add(loaded);
modules.addAll(getRecursiveEdges(loaded.descriptor().name()));
}
}
for (var root : roots) {
getReferenceByName(root).ifPresent(ref -> {
modules.add(ref);
modules.addAll(getRecursiveEdges(root));
});
}
return modules;
}
/**
* Determines which sources are required to be on the module-path and
* creates a module-path string that separates sources using ":".
*
* @param roots Roots that are always placed on the module-path.
* @return Returns the resolved module-path.
*/
public String getModulePath(String... roots) {
return createPath(getModulePathReferences(roots));
}
/**
* Determines which sources are not required to be on the module path.
*
* @param roots Roots that are always placed on the module-path.
* @return Returns the resolved references that can be in the class-path.
*/
public Set<ModuleReference> getClassPathReferences(String... roots) {
var edges = getModulePathReferences(roots);
return byName.values().stream()
.filter(Predicate.not(edges::contains))
.collect(Collectors.toSet());
}
/**
* Determines which sources are not required to be on the module path
* and creates a class-path string separating sources with ":".
*
* @param roots Roots that are always placed on the module-path.
* @return Returns the resolved class-path string.
*/
public String getClassPath(String... roots) {
return createPath(getClassPathReferences(roots));
}
private Set<ModuleReference> getRecursiveEdges(String rootName) {
return cache.computeIfAbsent(rootName, name -> {
Set<ModuleReference> references = new LinkedHashSet<>();
Deque<ModuleReference> queue = moduleEdges(rootName)
.distinct()
.collect(Collectors.toCollection(ArrayDeque::new));
while (!queue.isEmpty()) {
var ref = queue.removeFirst();
if (!references.contains(ref)) {
references.add(ref);
queue.addAll(moduleEdges(ref.descriptor().name()).collect(Collectors.toList()));
}
}
return references;
});
}
private Stream<ModuleReference> moduleEdges(String moduleName) {
return getReferenceByName(moduleName).map(ref -> {
List<ModuleReference> edges = new ArrayList<>();
// Get required modules.
ref.descriptor().requires().stream()
.map(ModuleDescriptor.Requires::name)
.flatMap(name -> getReferenceByName(name).stream())
.forEach(edges::add);
// Get declared "opens" to module names.
ref.descriptor().opens().stream()
.flatMap(opens -> opens.targets().stream())
.flatMap(name -> getReferenceByName(name).stream())
.forEach(edges::add);
return edges.stream();
}).stream().flatMap(Function.identity());
}
private String createPath(Set<ModuleReference> references) {
return references.stream()
.flatMap(ref -> ref.location().stream())
.map(Paths::get)
.map(Path::toAbsolutePath)
.map(Path::toString)
.sorted()
.collect(Collectors.joining(":"));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment