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.util.Collection; 36 import java.util.Comparator; 37 import java.util.HashMap; 38 import java.util.Map; 39 40 public 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 private 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 boolean jdkunsupported = Module.JDK_UNSUPPORTED.equals(module.name()); 322 if (module.isExported(pn) && !jdkunsupported) { 323 return showProfileOrModule(module); 324 } 325 326 // JDK internal API 327 if (!source.getModule().isJDK() && module.isJDK()){ 328 return "JDK internal API (" + module.name() + ")"; 329 } 330 331 // qualified exports or inaccessible 332 boolean isExported = module.isExported(pn, source.getModule().name()); 333 return module.name() + (isExported ? " (qualified)" : " (internal)"); 334 } 335 336 String showProfileOrModule(Module m) { 337 String tag = ""; 338 if (showProfile) { 339 Profile p = Profile.getProfile(m); 340 if (p != null) { 341 tag = p.profileName(); 342 } 343 } else if (showModule) { 344 tag = m.name(); 345 } 346 return tag; 347 } 348 349 Profile getProfile(String name) { 350 String pn = name; 351 if (type == CLASS || type == VERBOSE) { 352 int i = name.lastIndexOf('.'); 353 pn = i > 0 ? name.substring(0, i) : ""; 354 } 355 return Profile.getProfile(pn); 356 } 357 358 }