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