| /* |
| * Copyright (c) 2017, 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.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; |
| |
| /** |
| * Generate dot graph for modules |
| */ |
| 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; |
| } |
| |
| /** |
| * 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(mn + ".dot"); |
| genDotFile(path, mn, configurations.get(mn), attributes); |
| } |
| return true; |
| } |
| |
| /** |
| * 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.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 { |
| // 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(" 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); |
| }); |
| } |
| |
| } |
| } |