1 /*
   2  * Copyright (c) 2012, 2013, 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 com.sun.tools.classfile.AccessFlags;
  28 import com.sun.tools.classfile.ClassFile;
  29 import com.sun.tools.classfile.ConstantPoolException;
  30 import com.sun.tools.classfile.Dependencies;
  31 import com.sun.tools.classfile.Dependencies.ClassFileError;
  32 import com.sun.tools.classfile.Dependency;
  33 import java.io.*;
  34 import java.nio.file.DirectoryStream;
  35 import java.nio.file.Files;
  36 import java.nio.file.Path;
  37 import java.nio.file.Paths;
  38 import java.text.MessageFormat;
  39 import java.util.*;
  40 import java.util.regex.Pattern;
  41 
  42 /**
  43  * Implementation for the jdeps tool for static class dependency analysis.
  44  */
  45 class JdepsTask {
  46     static class BadArgs extends Exception {
  47         static final long serialVersionUID = 8765093759964640721L;
  48         BadArgs(String key, Object... args) {
  49             super(JdepsTask.getMessage(key, args));
  50             this.key = key;
  51             this.args = args;
  52         }
  53 
  54         BadArgs showUsage(boolean b) {
  55             showUsage = b;
  56             return this;
  57         }
  58         final String key;
  59         final Object[] args;
  60         boolean showUsage;
  61     }
  62 
  63     static abstract class Option {
  64         Option(boolean hasArg, String... aliases) {
  65             this.hasArg = hasArg;
  66             this.aliases = aliases;
  67         }
  68 
  69         boolean isHidden() {
  70             return false;
  71         }
  72 
  73         boolean matches(String opt) {
  74             for (String a : aliases) {
  75                 if (a.equals(opt))
  76                     return true;
  77                 if (hasArg && opt.startsWith(a + "="))
  78                     return true;
  79             }
  80             return false;
  81         }
  82 
  83         boolean ignoreRest() {
  84             return false;
  85         }
  86 
  87         abstract void process(JdepsTask task, String opt, String arg) throws BadArgs;
  88         final boolean hasArg;
  89         final String[] aliases;
  90     }
  91 
  92     static abstract class HiddenOption extends Option {
  93         HiddenOption(boolean hasArg, String... aliases) {
  94             super(hasArg, aliases);
  95         }
  96 
  97         boolean isHidden() {
  98             return true;
  99         }
 100     }
 101 
 102     static Option[] recognizedOptions = {
 103         new Option(false, "-h", "-?", "-help") {
 104             void process(JdepsTask task, String opt, String arg) {
 105                 task.options.help = true;
 106             }
 107         },
 108         new Option(true, "-dotoutput") {
 109             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 110                 Path p = Paths.get(arg);
 111                 if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) {
 112                     throw new BadArgs("err.dot.output.path", arg);
 113                 }
 114                 task.options.dotOutputDir = arg;
 115             }
 116         },
 117         new Option(false, "-s", "-summary") {
 118             void process(JdepsTask task, String opt, String arg) {
 119                 task.options.showSummary = true;
 120                 task.options.verbose = Analyzer.Type.SUMMARY;
 121             }
 122         },
 123         new Option(false, "-v", "-verbose",
 124                           "-verbose:package",
 125                           "-verbose:class")
 126         {
 127             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 128                 switch (opt) {
 129                     case "-v":
 130                     case "-verbose":
 131                         task.options.verbose = Analyzer.Type.VERBOSE;
 132                         break;
 133                     case "-verbose:package":
 134                             task.options.verbose = Analyzer.Type.PACKAGE;
 135                             break;
 136                     case "-verbose:class":
 137                             task.options.verbose = Analyzer.Type.CLASS;
 138                             break;
 139                     default:
 140                         throw new BadArgs("err.invalid.arg.for.option", opt);
 141                 }
 142             }
 143         },
 144         new Option(true, "-cp", "-classpath") {
 145             void process(JdepsTask task, String opt, String arg) {
 146                 task.options.classpath = arg;
 147             }
 148         },
 149         new Option(true, "-p", "-package") {
 150             void process(JdepsTask task, String opt, String arg) {
 151                 task.options.packageNames.add(arg);
 152             }
 153         },
 154         new Option(true, "-e", "-regex") {
 155             void process(JdepsTask task, String opt, String arg) {
 156                 task.options.regex = arg;
 157             }
 158         },
 159         new Option(true, "-include") {
 160             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 161                 task.options.includePattern = Pattern.compile(arg);
 162             }
 163         },
 164         new Option(false, "-P", "-profile") {
 165             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 166                 task.options.showProfile = true;
 167                 if (Profile.getProfileCount() == 0) {
 168                     throw new BadArgs("err.option.unsupported", opt, getMessage("err.profiles.msg"));
 169                 }
 170             }
 171         },
 172         new Option(false, "-apionly") {
 173             void process(JdepsTask task, String opt, String arg) {
 174                 task.options.apiOnly = true;
 175             }
 176         },
 177         new Option(false, "-recursive") {
 178             void process(JdepsTask task, String opt, String arg) {
 179                 task.options.depth = 0;
 180             }
 181         },
 182         new Option(false, "-version") {
 183             void process(JdepsTask task, String opt, String arg) {
 184                 task.options.version = true;
 185             }
 186         },
 187         new HiddenOption(false, "-fullversion") {
 188             void process(JdepsTask task, String opt, String arg) {
 189                 task.options.fullVersion = true;
 190             }
 191         },
 192         new HiddenOption(true, "-depth") {
 193             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 194                 try {
 195                     task.options.depth = Integer.parseInt(arg);
 196                 } catch (NumberFormatException e) {
 197                     throw new BadArgs("err.invalid.arg.for.option", opt);
 198                 }
 199             }
 200         },
 201     };
 202 
 203     private static final String PROGNAME = "jdeps";
 204     private final Options options = new Options();
 205     private final List<String> classes = new ArrayList<String>();
 206 
 207     private PrintWriter log;
 208     void setLog(PrintWriter out) {
 209         log = out;
 210     }
 211 
 212     /**
 213      * Result codes.
 214      */
 215     static final int EXIT_OK = 0, // Completed with no errors.
 216                      EXIT_ERROR = 1, // Completed but reported errors.
 217                      EXIT_CMDERR = 2, // Bad command-line arguments
 218                      EXIT_SYSERR = 3, // System error or resource exhaustion.
 219                      EXIT_ABNORMAL = 4;// terminated abnormally
 220 
 221     int run(String[] args) {
 222         if (log == null) {
 223             log = new PrintWriter(System.out);
 224         }
 225         try {
 226             handleOptions(args);
 227             if (options.help) {
 228                 showHelp();
 229             }
 230             if (options.version || options.fullVersion) {
 231                 showVersion(options.fullVersion);
 232             }
 233             if (classes.isEmpty() && options.includePattern == null) {
 234                 if (options.help || options.version || options.fullVersion) {
 235                     return EXIT_OK;
 236                 } else {
 237                     showHelp();
 238                     return EXIT_CMDERR;
 239                 }
 240             }
 241             if (options.regex != null && options.packageNames.size() > 0) {
 242                 showHelp();
 243                 return EXIT_CMDERR;
 244             }
 245             if (options.showSummary && options.verbose != Analyzer.Type.SUMMARY) {
 246                 showHelp();
 247                 return EXIT_CMDERR;
 248             }
 249             boolean ok = run();
 250             return ok ? EXIT_OK : EXIT_ERROR;
 251         } catch (BadArgs e) {
 252             reportError(e.key, e.args);
 253             if (e.showUsage) {
 254                 log.println(getMessage("main.usage.summary", PROGNAME));
 255             }
 256             return EXIT_CMDERR;
 257         } catch (IOException e) {
 258             return EXIT_ABNORMAL;
 259         } finally {
 260             log.flush();
 261         }
 262     }
 263 
 264     private final List<Archive> sourceLocations = new ArrayList<>();
 265     private boolean run() throws IOException {
 266         findDependencies();
 267         Analyzer analyzer = new Analyzer(options.verbose);
 268         analyzer.run(sourceLocations);
 269         if (options.dotOutputDir != null) {
 270             Path dir = Paths.get(options.dotOutputDir);
 271             Files.createDirectories(dir);
 272             generateDotFiles(dir, analyzer);
 273         } else {
 274             printRawOutput(log, analyzer);
 275         }
 276         return true;
 277     }
 278 
 279     private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException {
 280         Path summary = dir.resolve("summary.dot");
 281         try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary));
 282              DotFileFormatter formatter = new DotFileFormatter(sw, "summary")) {
 283             for (Archive archive : sourceLocations) {
 284                  analyzer.visitArchiveDependences(archive, formatter);
 285             }
 286         }
 287         if (options.verbose != Analyzer.Type.SUMMARY) {
 288             for (Archive archive : sourceLocations) {
 289                 if (analyzer.hasDependences(archive)) {
 290                     Path dotfile = dir.resolve(archive.getFileName() + ".dot");
 291                     try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile));
 292                          DotFileFormatter formatter = new DotFileFormatter(pw, archive)) {
 293                         analyzer.visitDependences(archive, formatter);
 294                     }
 295                 }
 296             }
 297         }
 298     }
 299 
 300     private void printRawOutput(PrintWriter writer, Analyzer analyzer) {
 301         for (Archive archive : sourceLocations) {
 302             RawOutputFormatter formatter = new RawOutputFormatter(writer);
 303             analyzer.visitArchiveDependences(archive, formatter);
 304             if (options.verbose != Analyzer.Type.SUMMARY) {
 305                 analyzer.visitDependences(archive, formatter);
 306             }
 307         }
 308     }
 309     private boolean isValidClassName(String name) {
 310         if (!Character.isJavaIdentifierStart(name.charAt(0))) {
 311             return false;
 312         }
 313         for (int i=1; i < name.length(); i++) {
 314             char c = name.charAt(i);
 315             if (c != '.'  && !Character.isJavaIdentifierPart(c)) {
 316                 return false;
 317             }
 318         }
 319         return true;
 320     }
 321 
 322     private Dependency.Filter getDependencyFilter() {
 323          if (options.regex != null) {
 324             return Dependencies.getRegexFilter(Pattern.compile(options.regex));
 325         } else if (options.packageNames.size() > 0) {
 326             return Dependencies.getPackageFilter(options.packageNames, false);
 327         } else {
 328             return new Dependency.Filter() {
 329                 @Override
 330                 public boolean accepts(Dependency dependency) {
 331                     return !dependency.getOrigin().equals(dependency.getTarget());
 332                 }
 333             };
 334         }
 335     }
 336 
 337     private boolean matches(String classname, AccessFlags flags) {
 338         if (options.apiOnly && !flags.is(AccessFlags.ACC_PUBLIC)) {
 339             return false;
 340         } else if (options.includePattern != null) {
 341             return options.includePattern.matcher(classname.replace('/', '.')).matches();
 342         } else {
 343             return true;
 344         }
 345     }
 346 
 347     private void findDependencies() throws IOException {
 348         Dependency.Finder finder =
 349             options.apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
 350                             : Dependencies.getClassDependencyFinder();
 351         Dependency.Filter filter = getDependencyFilter();
 352 
 353         List<Archive> archives = new ArrayList<>();
 354         Deque<String> roots = new LinkedList<>();
 355         for (String s : classes) {
 356             Path p = Paths.get(s);
 357             if (Files.exists(p)) {
 358                 archives.add(new Archive(p, ClassFileReader.newInstance(p)));
 359             } else {
 360                 if (isValidClassName(s)) {
 361                     roots.add(s);
 362                 } else {
 363                     warning("warn.invalid.arg", s);
 364                 }
 365             }
 366         }
 367 
 368         List<Archive> classpaths = new ArrayList<>(); // for class file lookup
 369         if (options.includePattern != null) {
 370             archives.addAll(getClassPathArchives(options.classpath));
 371         } else {
 372             classpaths.addAll(getClassPathArchives(options.classpath));
 373         }
 374         classpaths.addAll(PlatformClassPath.getArchives());
 375 
 376         // add all archives to the source locations for reporting
 377         sourceLocations.addAll(archives);
 378         sourceLocations.addAll(classpaths);
 379 
 380         // Work queue of names of classfiles to be searched.
 381         // Entries will be unique, and for classes that do not yet have
 382         // dependencies in the results map.
 383         Deque<String> deque = new LinkedList<>();
 384         Set<String> doneClasses = new HashSet<>();
 385 
 386         // get the immediate dependencies of the input files
 387         for (Archive a : archives) {
 388             for (ClassFile cf : a.reader().getClassFiles()) {
 389                 String classFileName;
 390                 try {
 391                     classFileName = cf.getName();
 392                 } catch (ConstantPoolException e) {
 393                     throw new ClassFileError(e);
 394                 }
 395 
 396                 if (matches(classFileName, cf.access_flags)) {
 397                     if (!doneClasses.contains(classFileName)) {
 398                         doneClasses.add(classFileName);
 399                     }
 400                     for (Dependency d : finder.findDependencies(cf)) {
 401                         if (filter.accepts(d)) {
 402                             String cn = d.getTarget().getName();
 403                             if (!doneClasses.contains(cn) && !deque.contains(cn)) {
 404                                 deque.add(cn);
 405                             }
 406                             a.addClass(d.getOrigin(), d.getTarget());
 407                         }
 408                     }
 409                 }
 410             }
 411         }
 412 
 413         // add Archive for looking up classes from the classpath
 414         // for transitive dependency analysis
 415         Deque<String> unresolved = roots;
 416         int depth = options.depth > 0 ? options.depth : Integer.MAX_VALUE;
 417         do {
 418             String name;
 419             while ((name = unresolved.poll()) != null) {
 420                 if (doneClasses.contains(name)) {
 421                     continue;
 422                 }
 423                 ClassFile cf = null;
 424                 for (Archive a : classpaths) {
 425                     cf = a.reader().getClassFile(name);
 426                     if (cf != null) {
 427                         String classFileName;
 428                         try {
 429                             classFileName = cf.getName();
 430                         } catch (ConstantPoolException e) {
 431                             throw new ClassFileError(e);
 432                         }
 433                         if (!doneClasses.contains(classFileName)) {
 434                             // if name is a fully-qualified class name specified
 435                             // from command-line, this class might already be parsed
 436                             doneClasses.add(classFileName);
 437                             for (Dependency d : finder.findDependencies(cf)) {
 438                                 if (depth == 0) {
 439                                     // ignore the dependency
 440                                     a.addClass(d.getOrigin());
 441                                     break;
 442                                 } else if (filter.accepts(d)) {
 443                                     a.addClass(d.getOrigin(), d.getTarget());
 444                                     String cn = d.getTarget().getName();
 445                                     if (!doneClasses.contains(cn) && !deque.contains(cn)) {
 446                                         deque.add(cn);
 447                                     }
 448                                 }
 449                             }
 450                         }
 451                         break;
 452                     }
 453                 }
 454                 if (cf == null) {
 455                     doneClasses.add(name);
 456                 }
 457             }
 458             unresolved = deque;
 459             deque = new LinkedList<>();
 460         } while (!unresolved.isEmpty() && depth-- > 0);
 461     }
 462 
 463     public void handleOptions(String[] args) throws BadArgs {
 464         // process options
 465         for (int i=0; i < args.length; i++) {
 466             if (args[i].charAt(0) == '-') {
 467                 String name = args[i];
 468                 Option option = getOption(name);
 469                 String param = null;
 470                 if (option.hasArg) {
 471                     if (name.startsWith("-") && name.indexOf('=') > 0) {
 472                         param = name.substring(name.indexOf('=') + 1, name.length());
 473                     } else if (i + 1 < args.length) {
 474                         param = args[++i];
 475                     }
 476                     if (param == null || param.isEmpty() || param.charAt(0) == '-') {
 477                         throw new BadArgs("err.missing.arg", name).showUsage(true);
 478                     }
 479                 }
 480                 option.process(this, name, param);
 481                 if (option.ignoreRest()) {
 482                     i = args.length;
 483                 }
 484             } else {
 485                 // process rest of the input arguments
 486                 for (; i < args.length; i++) {
 487                     String name = args[i];
 488                     if (name.charAt(0) == '-') {
 489                         throw new BadArgs("err.option.after.class", name).showUsage(true);
 490                     }
 491                     classes.add(name);
 492                 }
 493             }
 494         }
 495     }
 496 
 497     private Option getOption(String name) throws BadArgs {
 498         for (Option o : recognizedOptions) {
 499             if (o.matches(name)) {
 500                 return o;
 501             }
 502         }
 503         throw new BadArgs("err.unknown.option", name).showUsage(true);
 504     }
 505 
 506     private void reportError(String key, Object... args) {
 507         log.println(getMessage("error.prefix") + " " + getMessage(key, args));
 508     }
 509 
 510     private void warning(String key, Object... args) {
 511         log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
 512     }
 513 
 514     private void showHelp() {
 515         log.println(getMessage("main.usage", PROGNAME));
 516         for (Option o : recognizedOptions) {
 517             String name = o.aliases[0].substring(1); // there must always be at least one name
 518             name = name.charAt(0) == '-' ? name.substring(1) : name;
 519             if (o.isHidden() || name.equals("h")) {
 520                 continue;
 521             }
 522             log.println(getMessage("main.opt." + name));
 523         }
 524     }
 525 
 526     private void showVersion(boolean full) {
 527         log.println(version(full ? "full" : "release"));
 528     }
 529 
 530     private String version(String key) {
 531         // key=version:  mm.nn.oo[-milestone]
 532         // key=full:     mm.mm.oo[-milestone]-build
 533         if (ResourceBundleHelper.versionRB == null) {
 534             return System.getProperty("java.version");
 535         }
 536         try {
 537             return ResourceBundleHelper.versionRB.getString(key);
 538         } catch (MissingResourceException e) {
 539             return getMessage("version.unknown", System.getProperty("java.version"));
 540         }
 541     }
 542 
 543     static String getMessage(String key, Object... args) {
 544         try {
 545             return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
 546         } catch (MissingResourceException e) {
 547             throw new InternalError("Missing message: " + key);
 548         }
 549     }
 550 
 551     private static class Options {
 552         boolean help;
 553         boolean version;
 554         boolean fullVersion;
 555         boolean showProfile;
 556         boolean showSummary;
 557         boolean wildcard;
 558         boolean apiOnly;
 559         String dotOutputDir;
 560         String classpath = "";
 561         int depth = 1;
 562         Analyzer.Type verbose = Analyzer.Type.PACKAGE;
 563         Set<String> packageNames = new HashSet<>();
 564         String regex;             // apply to the dependences
 565         Pattern includePattern;   // apply to classes
 566     }
 567     private static class ResourceBundleHelper {
 568         static final ResourceBundle versionRB;
 569         static final ResourceBundle bundle;
 570 
 571         static {
 572             Locale locale = Locale.getDefault();
 573             try {
 574                 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
 575             } catch (MissingResourceException e) {
 576                 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
 577             }
 578             try {
 579                 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
 580             } catch (MissingResourceException e) {
 581                 throw new InternalError("version.resource.missing");
 582             }
 583         }
 584     }
 585 
 586     private List<Archive> getArchives(List<String> filenames) throws IOException {
 587         List<Archive> result = new ArrayList<Archive>();
 588         for (String s : filenames) {
 589             Path p = Paths.get(s);
 590             if (Files.exists(p)) {
 591                 result.add(new Archive(p, ClassFileReader.newInstance(p)));
 592             } else {
 593                 warning("warn.file.not.exist", s);
 594             }
 595         }
 596         return result;
 597     }
 598 
 599     private List<Archive> getClassPathArchives(String paths) throws IOException {
 600         List<Archive> result = new ArrayList<>();
 601         if (paths.isEmpty()) {
 602             return result;
 603         }
 604         for (String p : paths.split(File.pathSeparator)) {
 605             if (p.length() > 0) {
 606                 List<Path> files = new ArrayList<>();
 607                 // wildcard to parse all JAR files e.g. -classpath dir/*
 608                 int i = p.lastIndexOf(".*");
 609                 if (i > 0) {
 610                     Path dir = Paths.get(p.substring(0, i));
 611                     try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
 612                         for (Path entry : stream) {
 613                             files.add(entry);
 614                         }
 615                     }
 616                 } else {
 617                     files.add(Paths.get(p));
 618                 }
 619                 for (Path f : files) {
 620                     if (Files.exists(f)) {
 621                         result.add(new Archive(f, ClassFileReader.newInstance(f)));
 622                     }
 623                 }
 624             }
 625         }
 626         return result;
 627     }
 628 
 629 
 630     /**
 631      * Returns the file name of the archive for non-JRE class or
 632      * internal JRE classes.  It returns empty string for SE API.
 633      */
 634     private static String getArchiveName(Archive source, String profile) {
 635         String name = source.getFileName();
 636         if (PlatformClassPath.contains(source))
 637             return profile.isEmpty() ? "JDK internal API (" + name + ")" : "";
 638         return name;
 639     }
 640 
 641     class RawOutputFormatter implements Analyzer.Visitor {
 642         private final PrintWriter writer;
 643         RawOutputFormatter(PrintWriter writer) {
 644             this.writer = writer;
 645         }
 646 
 647         private String pkg = "";
 648         @Override
 649         public void visitDependence(String origin, Archive source,
 650                                     String target, Archive archive, String profile) {
 651             if (!origin.equals(pkg)) {
 652                 pkg = origin;
 653                 writer.format("   %s (%s)%n", origin, source.getFileName());
 654             }
 655             String name = (options.showProfile && !profile.isEmpty())
 656                                 ? profile
 657                                 : getArchiveName(archive, profile);
 658             writer.format("      -> %-50s %s%n", target, name);
 659         }
 660 
 661         @Override
 662         public void visitArchiveDependence(Archive origin, Archive target, String profile) {
 663             writer.format("%s -> %s", origin, target);
 664             if (options.showProfile && !profile.isEmpty()) {
 665                 writer.format(" (%s)%n", profile);
 666             } else {
 667                 writer.format("%n");
 668             }
 669         }
 670     }
 671 
 672     class DotFileFormatter implements Analyzer.Visitor, AutoCloseable {
 673         private final PrintWriter writer;
 674         private final String name;
 675         DotFileFormatter(PrintWriter writer, String name) {
 676             this.writer = writer;
 677             this.name = name;
 678             writer.format("digraph \"%s\" {%n", name);
 679         }
 680         DotFileFormatter(PrintWriter writer, Archive archive) {
 681             this.writer = writer;
 682             this.name = archive.getFileName();
 683             writer.format("digraph \"%s\" {%n", name);
 684             writer.format("    // Path: %s%n", archive.toString());
 685         }
 686 
 687         @Override
 688         public void close() {
 689             writer.println("}");
 690         }
 691 
 692         private final Set<String> edges = new HashSet<>();
 693         private String node = "";
 694         @Override
 695         public void visitDependence(String origin, Archive source,
 696                                     String target, Archive archive, String profile) {
 697             if (!node.equals(origin)) {
 698                 edges.clear();
 699                 node = origin;
 700             }
 701             // if -P option is specified, package name -> profile will
 702             // be shown and filter out multiple same edges.
 703             if (!edges.contains(target)) {
 704                 StringBuilder sb = new StringBuilder();
 705                 String name = options.showProfile && !profile.isEmpty()
 706                                   ? profile
 707                                   : getArchiveName(archive, profile);
 708                 writer.format("   %-50s -> %s;%n",
 709                                  String.format("\"%s\"", origin),
 710                                  name.isEmpty() ? String.format("\"%s\"", target)
 711                                                 :  String.format("\"%s (%s)\"", target, name));
 712                 edges.add(target);
 713             }
 714         }
 715 
 716         @Override
 717         public void visitArchiveDependence(Archive origin, Archive target, String profile) {
 718              String name = options.showProfile && !profile.isEmpty()
 719                                 ? profile : "";
 720              writer.format("   %-30s -> \"%s\";%n",
 721                            String.format("\"%s\"", origin.getFileName()),
 722                            name.isEmpty()
 723                                ? target.getFileName()
 724                                : String.format("%s (%s)", target.getFileName(), name));
 725         }
 726     }
 727 }