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) 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()) 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 } | 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.ee"; 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) 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()) 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 } |