1/*
2 * Copyright (c) 2015, 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 com.sun.tools.jdeps.Analyzer.Type.*;
28
29import java.io.IOException;
30import java.io.PrintWriter;
31import java.io.UncheckedIOException;
32import java.lang.module.ModuleDescriptor.Requires;
33import java.nio.file.Files;
34import java.nio.file.Path;
35import java.util.Collection;
36import java.util.Comparator;
37import java.util.HashMap;
38import java.util.Map;
39
40public abstract class JdepsWriter {
41    public static JdepsWriter newDotWriter(Path outputdir, Analyzer.Type type) {
42        return new DotFileWriter(outputdir, type, false, true, false);
43    }
44
45    public static JdepsWriter newSimpleWriter(PrintWriter writer,  Analyzer.Type type) {
46        return new SimpleWriter(writer, type, false, true);
47    }
48
49    final Analyzer.Type type;
50    final boolean showProfile;
51    final boolean showModule;
52
53    JdepsWriter(Analyzer.Type type, boolean showProfile, boolean showModule) {
54        this.type = type;
55        this.showProfile = showProfile;
56        this.showModule = showModule;
57    }
58
59    abstract void generateOutput(Collection<Archive> archives, Analyzer analyzer) throws IOException;
60
61    static class DotFileWriter extends JdepsWriter {
62        final boolean showLabel;
63        final Path outputDir;
64        DotFileWriter(Path dir, Analyzer.Type type,
65                      boolean showProfile, boolean showModule, boolean showLabel) {
66            super(type, showProfile, showModule);
67            this.showLabel = showLabel;
68            this.outputDir = dir;
69        }
70
71        @Override
72        void generateOutput(Collection<Archive> archives, Analyzer analyzer)
73                throws IOException
74        {
75            Files.createDirectories(outputDir);
76
77            // output individual .dot file for each archive
78            if (type != SUMMARY && type != MODULE) {
79                archives.stream()
80                        .filter(analyzer::hasDependences)
81                        .forEach(archive -> {
82                            Path dotfile = outputDir.resolve(archive.getName() + ".dot");
83                            try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile));
84                                 DotFileFormatter formatter = new DotFileFormatter(pw, archive)) {
85                                analyzer.visitDependences(archive, formatter);
86                            } catch (IOException e) {
87                                throw new UncheckedIOException(e);
88                            }
89                        });
90            }
91            // generate summary dot file
92            generateSummaryDotFile(archives, analyzer);
93        }
94
95        private void generateSummaryDotFile(Collection<Archive> archives, Analyzer analyzer)
96                throws IOException
97        {
98            // If verbose mode (-v or -verbose option),
99            // the summary.dot file shows package-level dependencies.
100            boolean isSummary =  type == PACKAGE || type == SUMMARY || type == MODULE;
101            Analyzer.Type summaryType = isSummary ? SUMMARY : PACKAGE;
102            Path summary = outputDir.resolve("summary.dot");
103            try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary));
104                 SummaryDotFile dotfile = new SummaryDotFile(sw, summaryType)) {
105                for (Archive archive : archives) {
106                    if (isSummary) {
107                        if (showLabel) {
108                            // build labels listing package-level dependencies
109                            analyzer.visitDependences(archive, dotfile.labelBuilder(), PACKAGE);
110                        }
111                    }
112                    analyzer.visitDependences(archive, dotfile, summaryType);
113                }
114            }
115        }
116
117        class DotFileFormatter implements Analyzer.Visitor, AutoCloseable {
118            private final PrintWriter writer;
119            private final String name;
120            DotFileFormatter(PrintWriter writer, Archive archive) {
121                this.writer = writer;
122                this.name = archive.getName();
123                writer.format("digraph \"%s\" {%n", name);
124                writer.format("    // Path: %s%n", archive.getPathName());
125            }
126
127            @Override
128            public void close() {
129                writer.println("}");
130            }
131
132            @Override
133            public void visitDependence(String origin, Archive originArchive,
134                                        String target, Archive targetArchive) {
135                String tag = toTag(originArchive, target, targetArchive);
136                writer.format("   %-50s -> \"%s\";%n",
137                              String.format("\"%s\"", origin),
138                              tag.isEmpty() ? target
139                                            : String.format("%s (%s)", target, tag));
140            }
141        }
142
143        class SummaryDotFile implements Analyzer.Visitor, AutoCloseable {
144            private final PrintWriter writer;
145            private final Analyzer.Type type;
146            private final Map<Archive, Map<Archive,StringBuilder>> edges = new HashMap<>();
147            SummaryDotFile(PrintWriter writer, Analyzer.Type type) {
148                this.writer = writer;
149                this.type = type;
150                writer.format("digraph \"summary\" {%n");
151            }
152
153            @Override
154            public void close() {
155                writer.println("}");
156            }
157
158            @Override
159            public void visitDependence(String origin, Archive originArchive,
160                                        String target, Archive targetArchive) {
161
162                String targetName = type == PACKAGE ? target : targetArchive.getName();
163                if (targetArchive.getModule().isJDK()) {
164                    Module m = (Module)targetArchive;
165                    String n = showProfileOrModule(m);
166                    if (!n.isEmpty()) {
167                        targetName += " (" + n + ")";
168                    }
169                } else if (type == PACKAGE) {
170                    targetName += " (" + targetArchive.getName() + ")";
171                }
172                String label = getLabel(originArchive, targetArchive);
173                writer.format("  %-50s -> \"%s\"%s;%n",
174                        String.format("\"%s\"", origin), targetName, label);
175            }
176
177            String getLabel(Archive origin, Archive target) {
178                if (edges.isEmpty())
179                    return "";
180
181                StringBuilder label = edges.get(origin).get(target);
182                return label == null ? "" : String.format(" [label=\"%s\",fontsize=9]", label.toString());
183            }
184
185            Analyzer.Visitor labelBuilder() {
186                // show the package-level dependencies as labels in the dot graph
187                return new Analyzer.Visitor() {
188                    @Override
189                    public void visitDependence(String origin, Archive originArchive,
190                                                String target, Archive targetArchive)
191                    {
192                        edges.putIfAbsent(originArchive, new HashMap<>());
193                        edges.get(originArchive).putIfAbsent(targetArchive, new StringBuilder());
194                        StringBuilder sb = edges.get(originArchive).get(targetArchive);
195                        String tag = toTag(originArchive, target, targetArchive);
196                        addLabel(sb, origin, target, tag);
197                    }
198
199                    void addLabel(StringBuilder label, String origin, String target, String tag) {
200                        label.append(origin).append(" -> ").append(target);
201                        if (!tag.isEmpty()) {
202                            label.append(" (" + tag + ")");
203                        }
204                        label.append("\\n");
205                    }
206                };
207            }
208        }
209    }
210
211    static class SimpleWriter extends JdepsWriter {
212        final PrintWriter writer;
213        SimpleWriter(PrintWriter writer, Analyzer.Type type,
214                     boolean showProfile, boolean showModule) {
215            super(type, showProfile, showModule);
216            this.writer = writer;
217        }
218
219        @Override
220        void generateOutput(Collection<Archive> archives, Analyzer analyzer) {
221            RawOutputFormatter depFormatter = new RawOutputFormatter(writer);
222            RawSummaryFormatter summaryFormatter = new RawSummaryFormatter(writer);
223            archives.stream()
224                .filter(analyzer::hasDependences)
225                .sorted(Comparator.comparing(Archive::getName))
226                .forEach(archive -> {
227                    if (showModule && archive.getModule().isNamed() && type != SUMMARY) {
228                        // print module-info except -summary
229                        summaryFormatter.printModuleDescriptor(archive.getModule());
230                    }
231                    // print summary
232                    analyzer.visitDependences(archive, summaryFormatter, SUMMARY);
233
234                    if (analyzer.hasDependences(archive) && type != SUMMARY) {
235                        // print the class-level or package-level dependences
236                        analyzer.visitDependences(archive, depFormatter);
237                    }
238            });
239        }
240
241        class RawOutputFormatter implements Analyzer.Visitor {
242            private final PrintWriter writer;
243            private String pkg = "";
244
245            RawOutputFormatter(PrintWriter writer) {
246                this.writer = writer;
247            }
248
249            @Override
250            public void visitDependence(String origin, Archive originArchive,
251                                        String target, Archive targetArchive) {
252                String tag = toTag(originArchive, target, targetArchive);
253                if (showModule || type == VERBOSE) {
254                    writer.format("   %-50s -> %-50s %s%n", origin, target, tag);
255                } else {
256                    if (!origin.equals(pkg)) {
257                        pkg = origin;
258                        writer.format("   %s (%s)%n", origin, originArchive.getName());
259                    }
260                    writer.format("      -> %-50s %s%n", target, tag);
261                }
262            }
263        }
264
265        class RawSummaryFormatter implements Analyzer.Visitor {
266            private final PrintWriter writer;
267
268            RawSummaryFormatter(PrintWriter writer) {
269                this.writer = writer;
270            }
271
272            @Override
273            public void visitDependence(String origin, Archive originArchive,
274                                        String target, Archive targetArchive) {
275
276                String targetName = targetArchive.getPathName();
277                if (targetArchive.getModule().isNamed()) {
278                    targetName = targetArchive.getModule().name();
279                }
280                writer.format("%s -> %s", originArchive.getName(), targetName);
281                if (showProfile && targetArchive.getModule().isJDK()) {
282                    writer.format(" (%s)", target);
283                }
284                writer.format("%n");
285            }
286
287            public void printModuleDescriptor(Module module) {
288                if (!module.isNamed())
289                    return;
290
291                writer.format("%s%s%n", module.name(), module.isAutomatic() ? " automatic" : "");
292                writer.format(" [%s]%n", module.location());
293                module.descriptor().requires()
294                        .stream()
295                        .sorted(Comparator.comparing(Requires::name))
296                        .forEach(req -> writer.format("   requires %s%n", req));
297            }
298        }
299    }
300
301    /**
302     * If the given archive is JDK archive, this method returns the profile name
303     * only if -profile option is specified; it accesses a private JDK API and
304     * the returned value will have "JDK internal API" prefix
305     *
306     * For non-JDK archives, this method returns the file name of the archive.
307     */
308    String toTag(Archive source, String name, Archive target) {
309        if (source == target || !target.getModule().isNamed()) {
310            return target.getName();
311        }
312
313        Module module = target.getModule();
314        String pn = name;
315        if ((type == CLASS || type == VERBOSE)) {
316            int i = name.lastIndexOf('.');
317            pn = i > 0 ? name.substring(0, i) : "";
318        }
319
320        // exported API
321        if (module.isExported(pn) && !module.isJDKUnsupported()) {
322            return showProfileOrModule(module);
323        }
324
325        // JDK internal API
326        if (!source.getModule().isJDK() && module.isJDK()){
327            return "JDK internal API (" + module.name() + ")";
328        }
329
330        // qualified exports or inaccessible
331        boolean isExported = module.isExported(pn, source.getModule().name());
332        return module.name() + (isExported ?  " (qualified)" : " (internal)");
333    }
334
335    String showProfileOrModule(Module m) {
336        String tag = "";
337        if (showProfile) {
338            Profile p = Profile.getProfile(m);
339            if (p != null) {
340                tag = p.profileName();
341            }
342        } else if (showModule) {
343            tag = m.name();
344        }
345        return tag;
346    }
347
348    Profile getProfile(String name) {
349        String pn = name;
350        if (type == CLASS || type == VERBOSE) {
351            int i = name.lastIndexOf('.');
352            pn = i > 0 ? name.substring(0, i) : "";
353        }
354        return Profile.getProfile(pn);
355    }
356
357}
358