/* * 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 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 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 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 gengraph(Configuration cf) { Graph.Builder 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 rpg = requiresTransitiveGraph(cf, builder.nodes); return builder.build().reduce(rpg); } /** * Returns a Graph containing only requires transitive edges * with transitive reduction. */ public Graph requiresTransitiveGraph(Configuration cf, Set roots) { Deque deque = new ArrayDeque<>(roots); Set visited = new HashSet<>(); Graph.Builder 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> 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 JAVA_SE_SUBGRAPH = javaSE(); static final Set JDK_SUBGRAPH = jdk(); private static Set 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 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 nodes; SubGraph(String name, String group, String color, Set 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 graph; private final Set descriptors = new TreeSet<>(); private final List subgraphs = new ArrayList<>(); private final Attributes attributes; public DotGraphBuilder(String name, Graph graph, Attributes attributes) { this.name = name; this.graph = graph; this.attributes = attributes; } public DotGraphBuilder modules(Stream 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 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 edges) { Set 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); }); } } }