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 */
25package com.sun.tools.jdeps;
26
27import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
28import static java.util.stream.Collectors.*;
29
30import java.io.BufferedWriter;
31import java.io.IOException;
32import java.io.PrintWriter;
33import java.lang.module.Configuration;
34import java.lang.module.ModuleDescriptor;
35import java.lang.module.ModuleDescriptor.*;
36import java.lang.module.ModuleFinder;
37import java.lang.module.ModuleReference;
38import java.lang.module.ResolvedModule;
39import java.nio.file.Files;
40import java.nio.file.Path;
41import java.util.ArrayDeque;
42import java.util.ArrayList;
43import java.util.Collections;
44import java.util.Deque;
45import java.util.HashSet;
46import java.util.List;
47import java.util.Map;
48import java.util.Objects;
49import java.util.Set;
50import java.util.TreeSet;
51import java.util.function.Function;
52import java.util.stream.Collectors;
53import java.util.stream.Stream;
54
55/**
56 * Generate dot graph for modules
57 */
58public 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)
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}
411