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