1 /* 2 * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package com.sun.tools.jdeps; 26 27 import static java.lang.module.ModuleDescriptor.Requires.Modifier.*; 28 import static java.util.stream.Collectors.*; 29 30 import java.io.BufferedWriter; 31 import java.io.IOException; 32 import java.io.PrintWriter; 33 import java.lang.module.Configuration; 34 import java.lang.module.ModuleDescriptor; 35 import java.lang.module.ModuleFinder; 36 import java.lang.module.ModuleReference; 37 import java.lang.module.ResolvedModule; 38 import java.nio.file.Files; 39 import java.nio.file.Path; 40 import java.util.ArrayDeque; 41 import java.util.ArrayList; 42 import java.util.Deque; 43 import java.util.HashMap; 44 import java.util.HashSet; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Objects; 48 import java.util.Set; 49 import java.util.TreeSet; 50 import java.util.function.Function; 51 import java.util.stream.Collectors; 52 import java.util.stream.Stream; 53 54 /** 55 * Generate dot graph for modules 56 */ 57 public class ModuleDotGraph { 58 private final Map<String, Configuration> configurations; 59 private final boolean apiOnly; 60 public ModuleDotGraph(JdepsConfiguration config, boolean apiOnly) { 61 this(config.rootModules().stream() 62 .map(Module::name) 63 .sorted() 64 .collect(toMap(Function.identity(), mn -> config.resolve(Set.of(mn)))), 65 apiOnly); 66 } 67 68 public ModuleDotGraph(Map<String, Configuration> configurations, boolean apiOnly) { 69 this.configurations = configurations; 70 this.apiOnly = apiOnly; 71 } 72 73 /** 74 * Generate dotfile for all modules 75 * 76 * @param dir output directory 77 */ 78 public boolean genDotFiles(Path dir) throws IOException { 79 Files.createDirectories(dir); 80 for (String mn : configurations.keySet()) { 81 Path path = dir.resolve(mn + ".dot"); 82 genDotFile(path, mn, configurations.get(mn)); 83 } 84 return true; 85 } 86 87 /** 88 * Generate dotfile of the given path 89 */ 90 public void genDotFile(Path path, String name, Configuration configuration) 91 throws IOException 92 { 93 // transitive reduction 94 Graph<String> graph = apiOnly 95 ? requiresTransitiveGraph(configuration, Set.of(name)) 96 : gengraph(configuration); 97 98 DotGraphBuilder builder = new DotGraphBuilder(name, graph); 99 builder.subgraph("se", "java", DotGraphBuilder.ORANGE, 100 DotGraphBuilder.JAVA_SE_SUBGRAPH) 101 .subgraph("jdk", "jdk", DotGraphBuilder.BLUE, 102 DotGraphBuilder.JDK_SUBGRAPH) 103 .descriptors(graph.nodes().stream() 104 .map(mn -> configuration.findModule(mn).get() 105 .reference().descriptor())); 106 // build dot file 107 builder.build(path); 108 } 109 110 /** 111 * Returns a Graph of the given Configuration after transitive reduction. 112 * 113 * Transitive reduction of requires transitive edge and requires edge have 114 * to be applied separately to prevent the requires transitive edges 115 * (e.g. U -> V) from being reduced by a path (U -> X -> Y -> V) 116 * in which V would not be re-exported from U. 117 */ 118 private Graph<String> gengraph(Configuration cf) { 119 Graph.Builder<String> builder = new Graph.Builder<>(); 120 cf.modules().stream() 121 .forEach(resolvedModule -> { 122 String mn = resolvedModule.reference().descriptor().name(); 123 builder.addNode(mn); 124 resolvedModule.reads().stream() 125 .map(ResolvedModule::name) 126 .forEach(target -> builder.addEdge(mn, target)); 127 }); 128 129 Graph<String> rpg = requiresTransitiveGraph(cf, builder.nodes); 130 return builder.build().reduce(rpg); 131 } 132 133 134 /** 135 * Returns a Graph containing only requires transitive edges 136 * with transitive reduction. 137 */ 138 public Graph<String> requiresTransitiveGraph(Configuration cf, 139 Set<String> roots) 140 { 141 Deque<String> deque = new ArrayDeque<>(roots); 142 Set<String> visited = new HashSet<>(); 143 Graph.Builder<String> builder = new Graph.Builder<>(); 144 145 while (deque.peek() != null) { 146 String mn = deque.pop(); 147 if (visited.contains(mn)) 148 continue; 149 150 visited.add(mn); 151 builder.addNode(mn); 152 ModuleDescriptor descriptor = cf.findModule(mn).get() 153 .reference().descriptor(); 154 descriptor.requires().stream() 155 .filter(d -> d.modifiers().contains(TRANSITIVE) 156 || d.name().equals("java.base")) 157 .map(d -> d.name()) 158 .forEach(d -> { 159 deque.add(d); 160 builder.addEdge(mn, d); 161 }); 162 } 163 164 return builder.build().reduce(); 165 } 166 167 public static class DotGraphBuilder { 168 static final Set<String> JAVA_SE_SUBGRAPH = javaSE(); 169 static final Set<String> JDK_SUBGRAPH = jdk(); 170 171 private static Set<String> javaSE() { 172 String root = "java.se.ee"; 173 ModuleFinder system = ModuleFinder.ofSystem(); 174 if (system.find(root).isPresent()) { 175 return Stream.concat(Stream.of(root), 176 Configuration.empty().resolve(system, 177 ModuleFinder.of(), 178 Set.of(root)) 179 .findModule(root).get() 180 .reads().stream() 181 .map(ResolvedModule::name)) 182 .collect(toSet()); 183 } else { 184 // approximation 185 return system.findAll().stream() 186 .map(ModuleReference::descriptor) 187 .map(ModuleDescriptor::name) 188 .filter(name -> name.startsWith("java.") && 189 !name.equals("java.smartcardio")) 190 .collect(Collectors.toSet()); 191 } 192 } 193 194 private static Set<String> jdk() { 195 return ModuleFinder.ofSystem().findAll().stream() 196 .map(ModuleReference::descriptor) 197 .map(ModuleDescriptor::name) 198 .filter(name -> !JAVA_SE_SUBGRAPH.contains(name) && 199 (name.startsWith("java.") || 200 name.startsWith("jdk.") || 201 name.startsWith("javafx."))) 202 .collect(Collectors.toSet()); 203 } 204 205 static class SubGraph { 206 final String name; 207 final String group; 208 final String color; 209 final Set<String> nodes; 210 SubGraph(String name, String group, String color, Set<String> nodes) { 211 this.name = Objects.requireNonNull(name); 212 this.group = Objects.requireNonNull(group); 213 this.color = Objects.requireNonNull(color); 214 this.nodes = Objects.requireNonNull(nodes); 215 } 216 } 217 218 static final String ORANGE = "#e76f00"; 219 static final String BLUE = "#437291"; 220 static final String GRAY = "#dddddd"; 221 static final String BLACK = "#000000"; 222 223 static final String FONT_NAME = "DejaVuSans"; 224 static final int FONT_SIZE = 12; 225 static final int ARROW_SIZE = 1; 226 static final int ARROW_WIDTH = 2; 227 static final int RANK_SEP = 1; 228 229 static final String REEXPORTS = ""; 230 static final String REQUIRES = "style=\"dashed\""; 231 static final String REQUIRES_BASE = "color=\"" + GRAY + "\""; 232 233 // can be configured 234 static double rankSep = RANK_SEP; 235 static String fontColor = BLACK; 236 static String fontName = FONT_NAME; 237 static int fontsize = FONT_SIZE; 238 static int arrowWidth = ARROW_WIDTH; 239 static int arrowSize = ARROW_SIZE; 240 static final Map<String, Integer> weights = new HashMap<>(); 241 static final List<Set<String>> ranks = new ArrayList<>(); 242 243 private final String name; 244 private final Graph<String> graph; 245 private final Set<ModuleDescriptor> descriptors = new TreeSet<>(); 246 private final List<SubGraph> subgraphs = new ArrayList<>(); 247 public DotGraphBuilder(String name, Graph<String> graph) { 248 this.name = name; 249 this.graph = graph; 250 } 251 252 public DotGraphBuilder descriptors(Stream<ModuleDescriptor> descriptors) { 253 descriptors.forEach(this.descriptors::add); 254 return this; 255 } 256 257 public void build(Path filename) throws IOException { 258 try (BufferedWriter writer = Files.newBufferedWriter(filename); 259 PrintWriter out = new PrintWriter(writer)) { 260 261 out.format("digraph \"%s\" {%n", name); 262 out.format(" nodesep=.5;%n"); 263 out.format(" ranksep=%f;%n", rankSep); 264 out.format(" pencolor=transparent;%n"); 265 out.format(" node [shape=plaintext, fontname=\"%s\", fontsize=%d, margin=\".2,.2\"];%n", 266 fontName, fontsize); 267 out.format(" edge [penwidth=%d, color=\"#999999\", arrowhead=open, arrowsize=%d];%n", 268 arrowWidth, arrowSize); 269 270 // same RANKS 271 ranks.stream() 272 .map(nodes -> descriptors.stream() 273 .map(ModuleDescriptor::name) 274 .filter(nodes::contains) 275 .map(mn -> "\"" + mn + "\"") 276 .collect(joining(","))) 277 .filter(group -> group.length() > 0) 278 .forEach(group -> out.format(" {rank=same %s}%n", group)); 279 280 subgraphs.forEach(subgraph -> { 281 out.format(" subgraph %s {%n", subgraph.name); 282 descriptors.stream() 283 .map(ModuleDescriptor::name) 284 .filter(subgraph.nodes::contains) 285 .forEach(mn -> printNode(out, mn, subgraph.color, subgraph.group)); 286 out.format(" }%n"); 287 }); 288 289 descriptors.stream() 290 .filter(md -> graph.contains(md.name()) && 291 !graph.adjacentNodes(md.name()).isEmpty()) 292 .forEach(md -> printNode(out, md, graph.adjacentNodes(md.name()))); 293 294 out.println("}"); 295 } 296 } 297 298 public DotGraphBuilder subgraph(String name, String group, String color, 299 Set<String> nodes) { 300 subgraphs.add(new SubGraph(name, group, color, nodes)); 301 return this; 302 } 303 304 public void printNode(PrintWriter out, String node, String color, String group) { 305 out.format(" \"%s\" [fontcolor=\"%s\", group=%s];%n", 306 node, color, group); 307 } 308 309 public void printNode(PrintWriter out, ModuleDescriptor md, Set<String> edges) { 310 Set<String> requiresTransitive = md.requires().stream() 311 .filter(d -> d.modifiers().contains(TRANSITIVE)) 312 .map(d -> d.name()) 313 .collect(toSet()); 314 315 String mn = md.name(); 316 edges.stream().forEach(dn -> { 317 String attr = dn.equals("java.base") ? REQUIRES_BASE 318 : (requiresTransitive.contains(dn) ? REEXPORTS : REQUIRES); 319 320 int w = weightOf(mn, dn); 321 if (w > 1) { 322 if (!attr.isEmpty()) 323 attr += ", "; 324 325 attr += "weight=" + w; 326 } 327 out.format(" \"%s\" -> \"%s\" [%s];%n", mn, dn, attr); 328 }); 329 } 330 331 public int weightOf(String s, String t) { 332 int w = weights.getOrDefault(s + ":" + t, 1); 333 if (w != 1) 334 return w; 335 if (s.startsWith("java.") && t.startsWith("java.")) 336 return 10; 337 return 1; 338 } 339 340 public static void sameRankNodes(Set<String> nodes) { 341 ranks.add(nodes); 342 } 343 344 public static void weight(String s, String t, int w) { 345 weights.put(s + ":" + t, w); 346 } 347 348 public static void setRankSep(double value) { 349 rankSep = value; 350 } 351 352 public static void setFontSize(int size) { 353 fontsize = size; 354 } 355 356 public static void setFontColor(String color) { 357 fontColor = color; 358 } 359 360 public static void setArrowSize(int size) { 361 arrowSize = size; 362 } 363 364 public static void setArrowWidth(int width) { 365 arrowWidth = width; 366 } 367 } 368 }