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 }