mirror of
https://github.com/openjdk/jdk.git
synced 2026-03-15 18:33:41 +00:00
432 lines
15 KiB
Java
432 lines
15 KiB
Java
/*
|
|
* Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation. Oracle designates this
|
|
* particular file as subject to the "Classpath" exception as provided
|
|
* by Oracle in the LICENSE file that accompanied this code.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
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.Locale;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import java.util.TreeSet;
|
|
import java.util.function.Function;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Stream;
|
|
|
|
/**
|
|
* Generate dot graph for modules
|
|
*/
|
|
public class ModuleDotGraph {
|
|
private final JdepsConfiguration config;
|
|
private final Map<String, Configuration> configurations;
|
|
private final boolean apiOnly;
|
|
public ModuleDotGraph(JdepsConfiguration config, boolean apiOnly) {
|
|
this(config,
|
|
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(null, configurations, apiOnly);
|
|
}
|
|
|
|
private ModuleDotGraph(JdepsConfiguration config,
|
|
Map<String, Configuration> configurations,
|
|
boolean apiOnly) {
|
|
this.configurations = configurations;
|
|
this.apiOnly = apiOnly;
|
|
this.config = config;
|
|
}
|
|
|
|
/**
|
|
* Generate dotfile for all modules
|
|
*
|
|
* @param dir output directory
|
|
*/
|
|
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(toDotFileBaseName(mn) + ".dot");
|
|
genDotFile(path, mn, configurations.get(mn), attributes);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private String toDotFileBaseName(String mn) {
|
|
if (config == null)
|
|
return mn;
|
|
|
|
Optional<Path> path = config.findModule(mn).flatMap(Module::path);
|
|
if (path.isPresent())
|
|
return path.get().getFileName().toString();
|
|
else
|
|
return mn;
|
|
}
|
|
/**
|
|
* Generate dotfile of the given path
|
|
*/
|
|
public void genDotFile(Path path, String name,
|
|
Configuration configuration,
|
|
Attributes attributes)
|
|
throws IOException
|
|
{
|
|
// transitive reduction
|
|
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()));
|
|
// build dot file
|
|
builder.build(path);
|
|
}
|
|
|
|
/**
|
|
* Returns a Graph of the given Configuration after transitive reduction.
|
|
*
|
|
* Transitive reduction of requires transitive edge and requires edge have
|
|
* to be applied separately to prevent the requires transitive edges
|
|
* (e.g. U -> V) from being reduced by a path (U -> X -> Y -> V)
|
|
* in which V would not be re-exported from U.
|
|
*/
|
|
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);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a Graph containing only requires transitive edges
|
|
* with transitive reduction.
|
|
*/
|
|
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";
|
|
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 {
|
|
// approximation
|
|
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((Locale)null, " 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());
|
|
|
|
// same RANKS
|
|
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);
|
|
});
|
|
}
|
|
|
|
}
|
|
}
|