1 /* 2 * Copyright (c) 2017, 2018, 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.ModuleDescriptor.*; 36 import java.lang.module.ModuleFinder; 37 import java.lang.module.ModuleReference; 38 import java.lang.module.ResolvedModule; 39 import java.nio.file.Files; 40 import java.nio.file.Path; 41 import java.util.ArrayDeque; 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.Deque; 45 import java.util.HashSet; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Objects; 49 import java.util.Set; 50 import java.util.TreeSet; 51 import java.util.function.Function; 52 import java.util.stream.Collectors; 53 import java.util.stream.Stream; 54 55 /** 56 * Generate dot graph for modules 57 */ 58 public class ModuleDotGraph { 59 private final Map<String, Configuration> configurations; 60 private final boolean apiOnly; 61 public ModuleDotGraph(JdepsConfiguration config, boolean apiOnly) { 62 this(config.rootModules().stream() 63 .map(Module::name) 64 .sorted() 65 .collect(toMap(Function.identity(), mn -> config.resolve(Set.of(mn)))), 66 apiOnly); 67 } 68 69 public ModuleDotGraph(Map<String, Configuration> configurations, boolean apiOnly) { 70 this.configurations = configurations; 71 this.apiOnly = apiOnly; 72 } 73 74 /** 75 * Generate dotfile for all modules 76 * 77 * @param dir output directory 78 */ 79 public boolean genDotFiles(Path dir) throws IOException { 80 return genDotFiles(dir, DotGraphAttributes.DEFAULT); 81 } 82 83 public boolean genDotFiles(Path dir, Attributes attributes) 84 throws IOException 85 { 86 Files.createDirectories(dir); 87 for (String mn : configurations.keySet()) { 88 Path path = dir.resolve(mn + ".dot"); 89 genDotFile(path, mn, configurations.get(mn), attributes); 90 } 91 return true; 92 } 93 94 /** 95 * Generate dotfile of the given path 96 */ 97 public void genDotFile(Path path, String name, 98 Configuration configuration, 99 Attributes attributes) 100 throws IOException 101 { 102 // transitive reduction 103 Graph<String> graph = apiOnly 104 ? requiresTransitiveGraph(configuration, Set.of(name)) 105 : gengraph(configuration); 106 107 DotGraphBuilder builder = new DotGraphBuilder(name, graph, attributes); 108 builder.subgraph("se", "java", attributes.javaSubgraphColor(), 109 DotGraphBuilder.JAVA_SE_SUBGRAPH) 110 .subgraph("jdk", "jdk", attributes.jdkSubgraphColor(), 111 DotGraphBuilder.JDK_SUBGRAPH) 112 .modules(graph.nodes().stream() 113 .map(mn -> configuration.findModule(mn).get() 114 .reference().descriptor())); 115 // build dot file 116 builder.build(path); 117 } 118 119 /** 120 * Returns a Graph of the given Configuration after transitive reduction. 121 * 122 * Transitive reduction of requires transitive edge and requires edge have 123 * to be applied separately to prevent the requires transitive edges 124 * (e.g. U -> V) from being reduced by a path (U -> X -> Y -> V) 125 * in which V would not be re-exported from U. 126 */ 127 private Graph<String> gengraph(Configuration cf) { 128 Graph.Builder<String> builder = new Graph.Builder<>(); 129 cf.modules().stream() 130 .forEach(rm -> { 131 String mn = rm.name(); 132 builder.addNode(mn); 133 rm.reads().stream() 134 .map(ResolvedModule::name) 135 .forEach(target -> builder.addEdge(mn, target)); 136 }); 137 138 Graph<String> rpg = requiresTransitiveGraph(cf, builder.nodes); 139 return builder.build().reduce(rpg); 140 } 141 142 143 /** 144 * Returns a Graph containing only requires transitive edges 145 * with transitive reduction. 146 */ 147 public Graph<String> requiresTransitiveGraph(Configuration cf, 148 Set<String> roots) 149 { 150 Deque<String> deque = new ArrayDeque<>(roots); 151 Set<String> visited = new HashSet<>(); 152 Graph.Builder<String> builder = new Graph.Builder<>(); 153 154 while (deque.peek() != null) { 155 String mn = deque.pop(); 156 if (visited.contains(mn)) 157 continue; 158 159 visited.add(mn); 160 builder.addNode(mn); 161 cf.findModule(mn).get() 162 .reference().descriptor().requires().stream() 163 .filter(d -> d.modifiers().contains(TRANSITIVE) 164 || d.name().equals("java.base")) 165 .map(Requires::name) 166 .forEach(d -> { 167 deque.add(d); 168 builder.addEdge(mn, d); 169 }); 170 } 171 172 return builder.build().reduce(); 173 } 174 175 public interface Attributes { 176 static final String ORANGE = "#e76f00"; 177 static final String BLUE = "#437291"; 178 static final String BLACK = "#000000"; 179 static final String DARK_GRAY = "#999999"; 180 static final String LIGHT_GRAY = "#dddddd"; 181 182 int fontSize(); 183 String fontName(); 184 String fontColor(); 185 186 int arrowSize(); 187 int arrowWidth(); 188 String arrowColor(); 189 190 default double rankSep() { 191 return 1; 192 } 193 194 default List<Set<String>> ranks() { 195 return Collections.emptyList(); 196 } 197 198 default int weightOf(String s, String t) { 199 return 1; 200 } 201 202 default String requiresMandatedColor() { 203 return LIGHT_GRAY; 204 } 205 206 default String javaSubgraphColor() { 207 return ORANGE; 208 } 209 210 default String jdkSubgraphColor() { 211 return BLUE; 212 } 213 } 214 215 static class DotGraphAttributes implements Attributes { 216 static final DotGraphAttributes DEFAULT = new DotGraphAttributes(); 217 218 static final String FONT_NAME = "DejaVuSans"; 219 static final int FONT_SIZE = 12; 220 static final int ARROW_SIZE = 1; 221 static final int ARROW_WIDTH = 2; 222 223 @Override 224 public int fontSize() { 225 return FONT_SIZE; 226 } 227 228 @Override 229 public String fontName() { 230 return FONT_NAME; 231 } 232 233 @Override 234 public String fontColor() { 235 return BLACK; 236 } 237 238 @Override 239 public int arrowSize() { 240 return ARROW_SIZE; 241 } 242 243 @Override 244 public int arrowWidth() { 245 return ARROW_WIDTH; 246 } 247 248 @Override 249 public String arrowColor() { 250 return DARK_GRAY; 251 } 252 } 253 254 private static class DotGraphBuilder { 255 static final String REEXPORTS = ""; 256 static final String REQUIRES = "style=\"dashed\""; 257 258 static final Set<String> JAVA_SE_SUBGRAPH = javaSE(); 259 static final Set<String> JDK_SUBGRAPH = jdk(); 260 261 private static Set<String> javaSE() { 262 String root = "java.se"; 263 ModuleFinder system = ModuleFinder.ofSystem(); 264 if (system.find(root).isPresent()) { 265 return Stream.concat(Stream.of(root), 266 Configuration.empty().resolve(system, 267 ModuleFinder.of(), 268 Set.of(root)) 269 .findModule(root).get() 270 .reads().stream() 271 .map(ResolvedModule::name)) 272 .collect(toSet()); 273 } else { 274 // approximation 275 return system.findAll().stream() 276 .map(ModuleReference::descriptor) 277 .map(ModuleDescriptor::name) 278 .filter(name -> name.startsWith("java.") && 279 !name.equals("java.smartcardio")) 280 .collect(Collectors.toSet()); 281 } 282 } 283 284 private static Set<String> jdk() { 285 return ModuleFinder.ofSystem().findAll().stream() 286 .map(ModuleReference::descriptor) 287 .map(ModuleDescriptor::name) 288 .filter(name -> !JAVA_SE_SUBGRAPH.contains(name) && 289 (name.startsWith("java.") || 290 name.startsWith("jdk.") || 291 name.startsWith("javafx."))) 292 .collect(Collectors.toSet()); 293 } 294 295 static class SubGraph { 296 final String name; 297 final String group; 298 final String color; 299 final Set<String> nodes; 300 SubGraph(String name, String group, String color, Set<String> nodes) { 301 this.name = Objects.requireNonNull(name); 302 this.group = Objects.requireNonNull(group); 303 this.color = Objects.requireNonNull(color); 304 this.nodes = Objects.requireNonNull(nodes); 305 } 306 } 307 308 private final String name; 309 private final Graph<String> graph; 310 private final Set<ModuleDescriptor> descriptors = new TreeSet<>(); 311 private final List<SubGraph> subgraphs = new ArrayList<>(); 312 private final Attributes attributes; 313 public DotGraphBuilder(String name, 314 Graph<String> graph, 315 Attributes attributes) { 316 this.name = name; 317 this.graph = graph; 318 this.attributes = attributes; 319 } 320 321 public DotGraphBuilder modules(Stream<ModuleDescriptor> descriptors) { 322 descriptors.forEach(this.descriptors::add); 323 return this; 324 } 325 326 public void build(Path filename) throws IOException { 327 try (BufferedWriter writer = Files.newBufferedWriter(filename); 328 PrintWriter out = new PrintWriter(writer)) { 329 330 out.format("digraph \"%s\" {%n", name); 331 out.format(" nodesep=.5;%n"); 332 out.format(" ranksep=%f;%n", attributes.rankSep()); 333 out.format(" pencolor=transparent;%n"); 334 out.format(" node [shape=plaintext, fontcolor=\"%s\", fontname=\"%s\"," 335 + " fontsize=%d, margin=\".2,.2\"];%n", 336 attributes.fontColor(), 337 attributes.fontName(), 338 attributes.fontSize()); 339 out.format(" edge [penwidth=%d, color=\"%s\", arrowhead=open, arrowsize=%d];%n", 340 attributes.arrowWidth(), 341 attributes.arrowColor(), 342 attributes.arrowSize()); 343 344 // same RANKS 345 attributes.ranks().stream() 346 .map(nodes -> descriptors.stream() 347 .map(ModuleDescriptor::name) 348 .filter(nodes::contains) 349 .map(mn -> "\"" + mn + "\"") 350 .collect(joining(","))) 351 .filter(group -> group.length() > 0) 352 .forEach(group -> out.format(" {rank=same %s}%n", group)); 353 354 subgraphs.forEach(subgraph -> { 355 out.format(" subgraph %s {%n", subgraph.name); 356 descriptors.stream() 357 .map(ModuleDescriptor::name) 358 .filter(subgraph.nodes::contains) 359 .forEach(mn -> printNode(out, mn, subgraph.color, subgraph.group)); 360 out.format(" }%n"); 361 }); 362 363 descriptors.stream() 364 .filter(md -> graph.contains(md.name()) && 365 !graph.adjacentNodes(md.name()).isEmpty()) 366 .forEach(md -> printNode(out, md, graph.adjacentNodes(md.name()))); 367 368 out.println("}"); 369 } 370 } 371 372 public DotGraphBuilder subgraph(String name, String group, String color, 373 Set<String> nodes) { 374 subgraphs.add(new SubGraph(name, group, color, nodes)); 375 return this; 376 } 377 378 public void printNode(PrintWriter out, String node, String color, String group) { 379 out.format(" \"%s\" [fontcolor=\"%s\", group=%s];%n", 380 node, color, group); 381 } 382 383 public void printNode(PrintWriter out, ModuleDescriptor md, Set<String> edges) { 384 Set<String> requiresTransitive = md.requires().stream() 385 .filter(d -> d.modifiers().contains(TRANSITIVE)) 386 .map(d -> d.name()) 387 .collect(toSet()); 388 389 String mn = md.name(); 390 edges.stream().forEach(dn -> { 391 String attr; 392 if (dn.equals("java.base")) { 393 attr = "color=\"" + attributes.requiresMandatedColor() + "\""; 394 } else { 395 attr = (requiresTransitive.contains(dn) ? REEXPORTS : REQUIRES); 396 } 397 398 int w = attributes.weightOf(mn, dn); 399 if (w > 1) { 400 if (!attr.isEmpty()) 401 attr += ", "; 402 403 attr += "weight=" + w; 404 } 405 out.format(" \"%s\" -> \"%s\" [%s];%n", mn, dn, attr); 406 }); 407 } 408 409 } 410 }