Created
April 10, 2019 16:21
-
-
Save mtdowling/39905d445829ab1ac58d0cafecf6bc9e to your computer and use it in GitHub Desktop.
Module Graph example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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