package com.sun.tools.jdeps;
import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
import static java.util.stream.Collectors.*;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.module.Configuration;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.*;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.lang.module.ResolvedModule;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ModuleDotGraph {
private final Map<String, Configuration> configurations;
private final boolean apiOnly;
public ModuleDotGraph(JdepsConfiguration config, boolean apiOnly) {
this(config.rootModules().stream()
.map(Module::name)
.sorted()
.collect(toMap(Function.identity(), mn -> config.resolve(Set.of(mn)))),
apiOnly);
}
public ModuleDotGraph(Map<String, Configuration> configurations, boolean apiOnly) {
this.configurations = configurations;
this.apiOnly = apiOnly;
}
public boolean genDotFiles(Path dir) throws IOException {
return genDotFiles(dir, DotGraphAttributes.DEFAULT);
}
public boolean genDotFiles(Path dir, Attributes attributes)
throws IOException
{
Files.createDirectories(dir);
for (String mn : configurations.keySet()) {
Path path = dir.resolve(mn + ".dot");
genDotFile(path, mn, configurations.get(mn), attributes);
}
return true;
}
public void genDotFile(Path path, String name,
Configuration configuration,
Attributes attributes)
throws IOException
{
Graph<String> graph = apiOnly
? requiresTransitiveGraph(configuration, Set.of(name))
: gengraph(configuration);
DotGraphBuilder builder = new DotGraphBuilder(name, graph, attributes);
builder.subgraph("se", "java", attributes.javaSubgraphColor(),
DotGraphBuilder.JAVA_SE_SUBGRAPH)
.subgraph("jdk", "jdk", attributes.jdkSubgraphColor(),
DotGraphBuilder.JDK_SUBGRAPH)
.modules(graph.nodes().stream()
.map(mn -> configuration.findModule(mn).get()
.reference().descriptor()));
builder.build(path);
}
private Graph<String> gengraph(Configuration cf) {
Graph.Builder<String> builder = new Graph.Builder<>();
cf.modules().stream()
.forEach(rm -> {
String mn = rm.name();
builder.addNode(mn);
rm.reads().stream()
.map(ResolvedModule::name)
.forEach(target -> builder.addEdge(mn, target));
});
Graph<String> rpg = requiresTransitiveGraph(cf, builder.nodes);
return builder.build().reduce(rpg);
}
public Graph<String> requiresTransitiveGraph(Configuration cf,
Set<String> roots)
{
Deque<String> deque = new ArrayDeque<>(roots);
Set<String> visited = new HashSet<>();
Graph.Builder<String> builder = new Graph.Builder<>();
while (deque.peek() != null) {
String mn = deque.pop();
if (visited.contains(mn))
continue;
visited.add(mn);
builder.addNode(mn);
cf.findModule(mn).get()
.reference().descriptor().requires().stream()
.filter(d -> d.modifiers().contains(TRANSITIVE)
|| d.name().equals("java.base"))
.map(Requires::name)
.forEach(d -> {
deque.add(d);
builder.addEdge(mn, d);
});
}
return builder.build().reduce();
}
public interface Attributes {
static final String ORANGE = "#e76f00";
static final String BLUE = "#437291";
static final String BLACK = "#000000";
static final String DARK_GRAY = "#999999";
static final String LIGHT_GRAY = "#dddddd";
int fontSize();
String fontName();
String fontColor();
int arrowSize();
int arrowWidth();
String arrowColor();
default double rankSep() {
return 1;
}
default List<Set<String>> ranks() {
return Collections.emptyList();
}
default int weightOf(String s, String t) {
return 1;
}
default String requiresMandatedColor() {
return LIGHT_GRAY;
}
default String javaSubgraphColor() {
return ORANGE;
}
default String jdkSubgraphColor() {
return BLUE;
}
}
static class DotGraphAttributes implements Attributes {
static final DotGraphAttributes DEFAULT = new DotGraphAttributes();
static final String FONT_NAME = "DejaVuSans";
static final int FONT_SIZE = 12;
static final int ARROW_SIZE = 1;
static final int ARROW_WIDTH = 2;
@Override
public int fontSize() {
return FONT_SIZE;
}
@Override
public String fontName() {
return FONT_NAME;
}
@Override
public String fontColor() {
return BLACK;
}
@Override
public int arrowSize() {
return ARROW_SIZE;
}
@Override
public int arrowWidth() {
return ARROW_WIDTH;
}
@Override
public String arrowColor() {
return DARK_GRAY;
}
}
private static class DotGraphBuilder {
static final String REEXPORTS = "";
static final String REQUIRES = "style=\"dashed\"";
static final Set<String> JAVA_SE_SUBGRAPH = javaSE();
static final Set<String> JDK_SUBGRAPH = jdk();
private static Set<String> javaSE() {
String root = "java.se.ee";
ModuleFinder system = ModuleFinder.ofSystem();
if (system.find(root).isPresent()) {
return Stream.concat(Stream.of(root),
Configuration.empty().resolve(system,
ModuleFinder.of(),
Set.of(root))
.findModule(root).get()
.reads().stream()
.map(ResolvedModule::name))
.collect(toSet());
} else {
return system.findAll().stream()
.map(ModuleReference::descriptor)
.map(ModuleDescriptor::name)
.filter(name -> name.startsWith("java.") &&
!name.equals("java.smartcardio"))
.collect(Collectors.toSet());
}
}
private static Set<String> jdk() {
return ModuleFinder.ofSystem().findAll().stream()
.map(ModuleReference::descriptor)
.map(ModuleDescriptor::name)
.filter(name -> !JAVA_SE_SUBGRAPH.contains(name) &&
(name.startsWith("java.") ||
name.startsWith("jdk.") ||
name.startsWith("javafx.")))
.collect(Collectors.toSet());
}
static class SubGraph {
final String name;
final String group;
final String color;
final Set<String> nodes;
SubGraph(String name, String group, String color, Set<String> nodes) {
this.name = Objects.requireNonNull(name);
this.group = Objects.requireNonNull(group);
this.color = Objects.requireNonNull(color);
this.nodes = Objects.requireNonNull(nodes);
}
}
private final String name;
private final Graph<String> graph;
private final Set<ModuleDescriptor> descriptors = new TreeSet<>();
private final List<SubGraph> subgraphs = new ArrayList<>();
private final Attributes attributes;
public DotGraphBuilder(String name,
Graph<String> graph,
Attributes attributes) {
this.name = name;
this.graph = graph;
this.attributes = attributes;
}
public DotGraphBuilder modules(Stream<ModuleDescriptor> descriptors) {
descriptors.forEach(this.descriptors::add);
return this;
}
public void build(Path filename) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(filename);
PrintWriter out = new PrintWriter(writer)) {
out.format("digraph \"%s\" {%n", name);
out.format(" nodesep=.5;%n");
out.format(" ranksep=%f;%n", attributes.rankSep());
out.format(" pencolor=transparent;%n");
out.format(" node [shape=plaintext, fontcolor=\"%s\", fontname=\"%s\","
+ " fontsize=%d, margin=\".2,.2\"];%n",
attributes.fontColor(),
attributes.fontName(),
attributes.fontSize());
out.format(" edge [penwidth=%d, color=\"%s\", arrowhead=open, arrowsize=%d];%n",
attributes.arrowWidth(),
attributes.arrowColor(),
attributes.arrowSize());
attributes.ranks().stream()
.map(nodes -> descriptors.stream()
.map(ModuleDescriptor::name)
.filter(nodes::contains)
.map(mn -> "\"" + mn + "\"")
.collect(joining(",")))
.filter(group -> group.length() > 0)
.forEach(group -> out.format(" {rank=same %s}%n", group));
subgraphs.forEach(subgraph -> {
out.format(" subgraph %s {%n", subgraph.name);
descriptors.stream()
.map(ModuleDescriptor::name)
.filter(subgraph.nodes::contains)
.forEach(mn -> printNode(out, mn, subgraph.color, subgraph.group));
out.format(" }%n");
});
descriptors.stream()
.filter(md -> graph.contains(md.name()) &&
!graph.adjacentNodes(md.name()).isEmpty())
.forEach(md -> printNode(out, md, graph.adjacentNodes(md.name())));
out.println("}");
}
}
public DotGraphBuilder subgraph(String name, String group, String color,
Set<String> nodes) {
subgraphs.add(new SubGraph(name, group, color, nodes));
return this;
}
public void printNode(PrintWriter out, String node, String color, String group) {
out.format(" \"%s\" [fontcolor=\"%s\", group=%s];%n",
node, color, group);
}
public void printNode(PrintWriter out, ModuleDescriptor md, Set<String> edges) {
Set<String> requiresTransitive = md.requires().stream()
.filter(d -> d.modifiers().contains(TRANSITIVE))
.map(d -> d.name())
.collect(toSet());
String mn = md.name();
edges.stream().forEach(dn -> {
String attr;
if (dn.equals("java.base")) {
attr = "color=\"" + attributes.requiresMandatedColor() + "\"";
} else {
attr = (requiresTransitive.contains(dn) ? REEXPORTS : REQUIRES);
}
int w = attributes.weightOf(mn, dn);
if (w > 1) {
if (!attr.isEmpty())
attr += ", ";
attr += "weight=" + w;
}
out.format(" \"%s\" -> \"%s\" [%s];%n", mn, dn, attr);
});
}
}
}