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