1 /*
   2  * Copyright (c) 2016, 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 
  26 package com.sun.tools.jdeprscan;
  27 
  28 import java.io.File;
  29 import java.io.IOException;
  30 import java.io.PrintStream;
  31 import java.net.URI;
  32 import java.nio.charset.StandardCharsets;
  33 import java.nio.file.Files;
  34 import java.nio.file.FileSystems;
  35 import java.nio.file.Path;
  36 import java.nio.file.Paths;
  37 import java.util.ArrayDeque;
  38 import java.util.ArrayList;
  39 import java.util.Arrays;
  40 import java.util.Collection;
  41 import java.util.HashSet;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.NoSuchElementException;
  45 import java.util.Set;
  46 import java.util.Queue;
  47 import java.util.stream.Stream;
  48 import java.util.jar.JarEntry;
  49 import java.util.jar.JarFile;
  50 
  51 import javax.tools.Diagnostic;
  52 import javax.tools.DiagnosticListener;
  53 import javax.tools.JavaCompiler;
  54 import javax.tools.JavaFileObject;
  55 import javax.tools.StandardJavaFileManager;
  56 import javax.tools.StandardLocation;
  57 import javax.tools.ToolProvider;
  58 
  59 import com.sun.tools.javac.file.JavacFileManager;
  60 
  61 import com.sun.tools.jdeprscan.scan.Scan;
  62 
  63 import static java.util.stream.Collectors.*;
  64 
  65 import javax.lang.model.element.PackageElement;
  66 import javax.lang.model.element.TypeElement;
  67 
  68 /**
  69  * Deprecation Scanner tool. Loads API deprecation information from the
  70  * JDK image, or optionally, from a jar file or class hierarchy. Then scans
  71  * a class library for usages of those APIs.
  72  *
  73  * TODO:
  74  *  - audit error handling throughout, but mainly in scan package
  75  *  - handling of covariant overrides
  76  *  - handling of override of method found in multiple superinterfaces
  77  *  - convert type/method/field output to Java source like syntax, e.g.
  78  *      instead of java/lang/Runtime.runFinalizersOnExit(Z)V
  79  *      print void java.lang.Runtime.runFinalizersOnExit(boolean)
  80  *  - more example output in man page
  81  *  - more rigorous GNU style option parsing; use joptsimple?
  82  *
  83  * FUTURES:
  84  *  - add module support: -addmods, -modulepath, module arg
  85  *  - load deprecation declarations from a designated class library instead
  86  *    of the JDK
  87  *  - load deprecation declarations from a module
  88  *  - scan a module (but a modular jar can be treated just a like an ordinary jar)
  89  *  - multi-version jar
  90  */
  91 public class Main implements DiagnosticListener<JavaFileObject> {
  92     public static Main instance;
  93 
  94     final PrintStream out;
  95     final PrintStream err;
  96     final List<File> bootClassPath = new ArrayList<>();
  97     final List<File> classPath = new ArrayList<>();
  98     final List<File> systemModules = new ArrayList<>();
  99     final List<String> options = new ArrayList<>();
 100     final List<String> comments = new ArrayList<>();
 101 
 102     // Valid releases need to match what the compiler supports.
 103     // Keep these updated manually until there's a compiler API
 104     // that allows querying of supported releases.
 105     final Set<String> releasesWithoutForRemoval = Set.of("6", "7", "8");
 106     final Set<String> releasesWithForRemoval = Set.of("9");
 107 
 108     final Set<String> validReleases;
 109     {
 110         Set<String> temp = new HashSet<>(releasesWithoutForRemoval);
 111         temp.addAll(releasesWithForRemoval);
 112         validReleases = Set.of(temp.toArray(new String[0]));
 113     }
 114 
 115     boolean verbose = false;
 116     boolean forRemoval = false;
 117 
 118     final JavaCompiler compiler;
 119     final StandardJavaFileManager fm;
 120 
 121     List<DeprData> deprList; // non-null after successful load phase
 122 
 123     /**
 124      * Processes a collection of class names. Names should fully qualified
 125      * names in the form "pkg.pkg.pkg.classname".
 126      *
 127      * @param classNames collection of fully qualified classnames to process
 128      * @return true for success, false for failure
 129      * @throws IOException if an I/O error occurs
 130      */
 131     boolean doClassNames(Collection<String> classNames) throws IOException {
 132         if (verbose) {
 133             out.println("List of classes to process:");
 134             classNames.forEach(out::println);
 135             out.println("End of class list.");
 136         }
 137 
 138         // TODO: not sure this is necessary...
 139         if (fm instanceof JavacFileManager) {
 140             ((JavacFileManager)fm).setSymbolFileEnabled(false);
 141         }
 142 
 143         fm.setLocation(StandardLocation.CLASS_PATH, classPath);
 144         if (!bootClassPath.isEmpty()) {
 145             fm.setLocation(StandardLocation.PLATFORM_CLASS_PATH, bootClassPath);
 146         }
 147 
 148         if (!systemModules.isEmpty()) {
 149             fm.setLocation(StandardLocation.SYSTEM_MODULES, systemModules);
 150         }
 151 
 152         LoadProc proc = new LoadProc();
 153         JavaCompiler.CompilationTask task =
 154             compiler.getTask(null, fm, this, options, classNames, null);
 155         task.setProcessors(List.of(proc));
 156         boolean r = task.call();
 157         if (r) {
 158             if (forRemoval) {
 159                 deprList = proc.getDeprecations().stream()
 160                                .filter(DeprData::isForRemoval)
 161                                .collect(toList());
 162             } else {
 163                 deprList = proc.getDeprecations();
 164             }
 165         }
 166         return r;
 167     }
 168 
 169     /**
 170      * Processes a stream of filenames (strings). The strings are in the
 171      * form pkg/pkg/pkg/classname.class relative to the root of a package
 172      * hierarchy.
 173      *
 174      * @param filenames a Stream of filenames to process
 175      * @return true for success, false for failure
 176      * @throws IOException if an I/O error occurs
 177      */
 178     boolean doFileNames(Stream<String> filenames) throws IOException {
 179         return doClassNames(
 180             filenames.filter(name -> name.endsWith(".class"))
 181                      .filter(name -> !name.endsWith("package-info.class"))
 182                      .filter(name -> !name.endsWith("module-info.class"))
 183                      .map(s -> s.replaceAll("\\.class$", ""))
 184                      .map(s -> s.replace(File.separatorChar, '.'))
 185                      .collect(toList()));
 186     }
 187 
 188     /**
 189      * Replaces all but the first occurrence of '/' with '.'. Assumes
 190      * that the name is in the format module/pkg/pkg/classname.class.
 191      * That is, the name should contain at least one '/' character
 192      * separating the module name from the package-class name.
 193      *
 194      * @param filename the input filename
 195      * @return the modular classname
 196      */
 197     String convertModularFileName(String filename) {
 198         int slash = filename.indexOf('/');
 199         return filename.substring(0, slash)
 200                + "/"
 201                + filename.substring(slash+1).replace('/', '.');
 202     }
 203 
 204     /**
 205      * Processes a stream of filenames (strings) including a module prefix.
 206      * The strings are in the form module/pkg/pkg/pkg/classname.class relative
 207      * to the root of a directory containing modules. The strings are processed
 208      * into module-qualified class names of the form
 209      * "module/pkg.pkg.pkg.classname".
 210      *
 211      * @param filenames a Stream of filenames to process
 212      * @return true for success, false for failure
 213      * @throws IOException if an I/O error occurs
 214      */
 215     boolean doModularFileNames(Stream<String> filenames) throws IOException {
 216         return doClassNames(
 217             filenames.filter(name -> name.endsWith(".class"))
 218                      .filter(name -> !name.endsWith("package-info.class"))
 219                      .filter(name -> !name.endsWith("module-info.class"))
 220                      .map(s -> s.replaceAll("\\.class$", ""))
 221                      .map(this::convertModularFileName)
 222                      .collect(toList()));
 223     }
 224 
 225     /**
 226      * Processes named class files in the given directory. The directory
 227      * should be the root of a package hierarchy. If classNames is
 228      * empty, walks the directory hierarchy to find all classes.
 229      *
 230      * @param dirname the name of the directory to process
 231      * @param classNames the names of classes to process
 232      * @return true for success, false for failure
 233      * @throws IOException if an I/O error occurs
 234      */
 235     boolean processDirectory(String dirname, Collection<String> classNames) throws IOException {
 236         if (!Files.isDirectory(Paths.get(dirname))) {
 237             err.printf("%s: not a directory%n", dirname);
 238             return false;
 239         }
 240 
 241         classPath.add(0, new File(dirname));
 242 
 243         if (classNames.isEmpty()) {
 244             Path base = Paths.get(dirname);
 245             int baseCount = base.getNameCount();
 246             try (Stream<Path> paths = Files.walk(base)) {
 247                 Stream<String> files =
 248                     paths.filter(p -> p.getNameCount() > baseCount)
 249                          .map(p -> p.subpath(baseCount, p.getNameCount()))
 250                          .map(Path::toString);
 251                 return doFileNames(files);
 252             }
 253         } else {
 254             return doClassNames(classNames);
 255         }
 256     }
 257 
 258     /**
 259      * Processes all class files in the given jar file.
 260      *
 261      * @param jarname the name of the jar file to process
 262      * @return true for success, false for failure
 263      * @throws IOException if an I/O error occurs
 264      */
 265     boolean doJarFile(String jarname) throws IOException {
 266         try (JarFile jf = new JarFile(jarname)) {
 267             Stream<String> files =
 268                 jf.stream()
 269                   .map(JarEntry::getName);
 270             return doFileNames(files);
 271         }
 272     }
 273 
 274     /**
 275      * Processes named class files from the given jar file,
 276      * or all classes if classNames is empty.
 277      *
 278      * @param jarname the name of the jar file to process
 279      * @param classNames the names of classes to process
 280      * @return true for success, false for failure
 281      * @throws IOException if an I/O error occurs
 282      */
 283     boolean processJarFile(String jarname, Collection<String> classNames) throws IOException {
 284         classPath.add(0, new File(jarname));
 285 
 286         if (classNames.isEmpty()) {
 287             return doJarFile(jarname);
 288         } else {
 289             return doClassNames(classNames);
 290         }
 291     }
 292 
 293     /**
 294      * Processes named class files from rt.jar of a JDK version 7 or 8.
 295      * If classNames is empty, processes all classes.
 296      *
 297      * @param jdkHome the path to the "home" of the JDK to process
 298      * @param classNames the names of classes to process
 299      * @return true for success, false for failure
 300      * @throws IOException if an I/O error occurs
 301      */
 302     boolean processOldJdk(String jdkHome, Collection<String> classNames) throws IOException {
 303         String RTJAR = jdkHome + "/jre/lib/rt.jar";
 304         String CSJAR = jdkHome + "/jre/lib/charsets.jar";
 305 
 306         bootClassPath.add(0, new File(RTJAR));
 307         bootClassPath.add(1, new File(CSJAR));
 308         options.add("-source");
 309         options.add("8");
 310 
 311         if (classNames.isEmpty()) {
 312             return doJarFile(RTJAR);
 313         } else {
 314             return doClassNames(classNames);
 315         }
 316     }
 317 
 318     /**
 319      * Processes listed classes given a JDK 9 home.
 320      */
 321     boolean processJdk9(String jdkHome, Collection<String> classes) throws IOException {
 322         systemModules.add(new File(jdkHome));
 323         return doClassNames(classes);
 324     }
 325 
 326     /**
 327      * Processes the class files from the currently running JDK,
 328      * using the jrt: filesystem.
 329      *
 330      * @return true for success, false for failure
 331      * @throws IOException if an I/O error occurs
 332      */
 333     boolean processSelf(Collection<String> classes) throws IOException {
 334         options.add("-addmods");
 335         options.add("java.se.ee,jdk.xml.bind"); // TODO why jdk.xml.bind?
 336 
 337         if (classes.isEmpty()) {
 338             Path modules = FileSystems.getFileSystem(URI.create("jrt:/"))
 339                                       .getPath("/modules");
 340 
 341             // names are /modules/<modulename>/pkg/.../Classname.class
 342             try (Stream<Path> paths = Files.walk(modules)) {
 343                 Stream<String> files =
 344                     paths.filter(p -> p.getNameCount() > 2)
 345                          .map(p -> p.subpath(1, p.getNameCount()))
 346                          .map(Path::toString);
 347                 return doModularFileNames(files);
 348             }
 349         } else {
 350             return doClassNames(classes);
 351         }
 352     }
 353 
 354     /**
 355      * Process classes from a particular JDK release, using only information
 356      * in this JDK.
 357      *
 358      * @param release "6", "7", "8", or "9"
 359      * @param classes collection of classes to process, may be empty
 360      * @return success value
 361      */
 362     boolean processRelease(String release, Collection<String> classes) throws IOException {
 363         options.addAll(List.of("-release", release));
 364 
 365         if (release.equals("9")) {
 366             List<String> rootMods = List.of("java.se", "java.se.ee");
 367             TraverseProc proc = new TraverseProc(rootMods);
 368             JavaCompiler.CompilationTask task =
 369                 compiler.getTask(null, fm, this,
 370                                  // options
 371                                  List.of("-addmods", String.join(",", rootMods)),
 372                                  // classes
 373                                  List.of("java.lang.Object"),
 374                                  null);
 375             task.setProcessors(List.of(proc));
 376             if (!task.call()) {
 377                 return false;
 378             }
 379             Map<PackageElement, List<TypeElement>> types = proc.getPublicTypes();
 380             options.add("-addmods");
 381             options.add(String.join(",", rootMods));
 382             return doClassNames(
 383                 types.values().stream()
 384                      .flatMap(List::stream)
 385                      .map(TypeElement::toString)
 386                      .collect(toList()));
 387         } else {
 388             // TODO: kind of a hack...
 389             // Create a throwaway compilation task with options "-release N"
 390             // which has the side effect of setting the file manager's
 391             // PLATFORM_CLASS_PATH to the right value.
 392             JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
 393             StandardJavaFileManager fm =
 394                 compiler.getStandardFileManager(this, null, StandardCharsets.UTF_8);
 395             JavaCompiler.CompilationTask task =
 396                 compiler.getTask(null, fm, this, List.of("-release", release), null, null);
 397             List<Path> paths = new ArrayList<>();
 398             for (Path p : fm.getLocationAsPaths(StandardLocation.PLATFORM_CLASS_PATH)) {
 399                 try (Stream<Path> str = Files.walk(p)) {
 400                     str.forEachOrdered(paths::add);
 401                 }
 402             }
 403 
 404             options.add("-Xlint:-options");
 405 
 406             return doClassNames(
 407                 paths.stream()
 408                      .filter(path -> path.toString().endsWith(".sig"))
 409                      .map(path -> path.subpath(1, path.getNameCount()))
 410                      .map(Path::toString)
 411                      .map(s -> s.replaceAll("\\.sig$", ""))
 412                      .map(s -> s.replace('/', '.'))
 413                      .collect(toList()));
 414         }
 415     }
 416 
 417     /**
 418      * Prints a usage message to the err stream.
 419      */
 420     void usage() {
 421 
 422     }
 423 
 424     /**
 425      * An enum denoting the mode in which the tool is running.
 426      * Different modes correspond to the different process* methods.
 427      * The exception is UNKNOWN, which indicates that a mode wasn't
 428      * specified on the command line, which is an error.
 429      */
 430     static enum LoadMode {
 431         CLASSES, DIR, JAR, OLD_JDK, JDK9, SELF, RELEASE, LOAD_CSV
 432     }
 433 
 434     static enum ScanMode {
 435         ARGS, LIST, PRINT_CSV
 436     }
 437 
 438     /**
 439      * A checked exception that's thrown if a command-line syntax error
 440      * is detected.
 441      */
 442     static class UsageException extends Exception {
 443         private static final long serialVersionUID = 3611828659572908743L;
 444     }
 445 
 446     /**
 447      * Convenience method to throw UsageException if a condition is false.
 448      *
 449      * @param cond the condition that's required to be true
 450      * @throws UsageException
 451      */
 452     void require(boolean cond) throws UsageException {
 453         if (!cond) {
 454             throw new UsageException();
 455         }
 456     }
 457 
 458     /**
 459      * Constructs an instance of the finder tool.
 460      *
 461      * @param out the stream to which the tool's output is sent
 462      * @param err the stream to which error messages are sent
 463      */
 464     Main(PrintStream out, PrintStream err) {
 465         this.out = out;
 466         this.err = err;
 467         compiler = ToolProvider.getSystemJavaCompiler();
 468         fm = compiler.getStandardFileManager(this, null, StandardCharsets.UTF_8);
 469     }
 470 
 471     /**
 472      * Prints the diagnostic to the err stream.
 473      *
 474      * Specified by the DiagnosticListener interface.
 475      *
 476      * @param diagnostic the tool diagnostic to print
 477      */
 478     @Override
 479     public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
 480         err.println(diagnostic);
 481     }
 482 
 483     /**
 484      * Parses arguments and performs the requested processing.
 485      *
 486      * @param argArray command-line arguments
 487      * @return true on success, false on error
 488      */
 489     boolean run(String... argArray) {
 490         Queue<String> args = new ArrayDeque<>(Arrays.asList(argArray));
 491         LoadMode loadMode = LoadMode.RELEASE;
 492         ScanMode scanMode = ScanMode.ARGS;
 493         String dir = null;
 494         String jar = null;
 495         String jdkHome = null;
 496         String release = "9";
 497         List<String> loadClasses = new ArrayList<>();
 498         String csvFile = null;
 499 
 500         try {
 501             while (!args.isEmpty()) {
 502                 String a = args.element();
 503                 if (a.startsWith("-")) {
 504                     args.remove();
 505                     switch (a) {
 506                         case "--class-path":
 507                         case "-cp":
 508                             classPath.clear();
 509                             Arrays.stream(args.remove().split(File.pathSeparator))
 510                                   .map(File::new)
 511                                   .forEachOrdered(classPath::add);
 512                             break;
 513                         case "--for-removal":
 514                             forRemoval = true;
 515                             break;
 516                         case "--full-version":
 517                             out.println(System.getProperty("java.vm.version"));
 518                             return false;
 519                         case "--help":
 520                         case "-h":
 521                             out.println(Messages.get("main.usage"));
 522                             out.println();
 523                             out.println(Messages.get("main.help"));
 524                             return false;
 525                         case "-l":
 526                         case "--list":
 527                             require(scanMode == ScanMode.ARGS);
 528                             scanMode = ScanMode.LIST;
 529                             break;
 530                         case "--release":
 531                             loadMode = LoadMode.RELEASE;
 532                             release = args.remove();
 533                             if (!validReleases.contains(release)) {
 534                                 throw new UsageException();
 535                             }
 536                             break;
 537                         case "-v":
 538                         case "--verbose":
 539                             verbose = true;
 540                             break;
 541                         case "--version":
 542                             out.println(System.getProperty("java.version"));
 543                             return false;
 544                         case "--Xcompiler-arg":
 545                             options.add(args.remove());
 546                             break;
 547                         case "--Xcsv-comment":
 548                             comments.add(args.remove());
 549                             break;
 550                         case "--Xhelp":
 551                             out.println(Messages.get("main.xhelp"));
 552                             return false;
 553                         case "--Xload-class":
 554                             loadMode = LoadMode.CLASSES;
 555                             loadClasses.add(args.remove());
 556                             break;
 557                         case "--Xload-csv":
 558                             loadMode = LoadMode.LOAD_CSV;
 559                             csvFile = args.remove();
 560                             break;
 561                         case "--Xload-dir":
 562                             loadMode = LoadMode.DIR;
 563                             dir = args.remove();
 564                             break;
 565                         case "--Xload-jar":
 566                             loadMode = LoadMode.JAR;
 567                             jar = args.remove();
 568                             break;
 569                         case "--Xload-jdk9":
 570                             loadMode = LoadMode.JDK9;
 571                             jdkHome = args.remove();
 572                             break;
 573                         case "--Xload-old-jdk":
 574                             loadMode = LoadMode.OLD_JDK;
 575                             jdkHome = args.remove();
 576                             break;
 577                         case "--Xload-self":
 578                             loadMode = LoadMode.SELF;
 579                             break;
 580                         case "--Xprint-csv":
 581                             require(scanMode == ScanMode.ARGS);
 582                             scanMode = ScanMode.PRINT_CSV;
 583                             break;
 584                         default:
 585                             throw new UsageException();
 586                     }
 587                 } else {
 588                     break;
 589                 }
 590             }
 591 
 592             if ((scanMode == ScanMode.ARGS) == args.isEmpty()) {
 593                 throw new UsageException();
 594             }
 595 
 596             if (    forRemoval && loadMode == LoadMode.RELEASE &&
 597                     releasesWithoutForRemoval.contains(release)) {
 598                 throw new UsageException();
 599             }
 600 
 601             boolean success = false;
 602 
 603             switch (loadMode) {
 604                 case CLASSES:
 605                     success = doClassNames(loadClasses);
 606                     break;
 607                 case DIR:
 608                     success = processDirectory(dir, loadClasses);
 609                     break;
 610                 case JAR:
 611                     success = processJarFile(jar, loadClasses);
 612                     break;
 613                 case JDK9:
 614                     require(!args.isEmpty());
 615                     success = processJdk9(jdkHome, loadClasses);
 616                     break;
 617                 case LOAD_CSV:
 618                     deprList = DeprDB.loadFromFile(csvFile);
 619                     success = true;
 620                     break;
 621                 case OLD_JDK:
 622                     success = processOldJdk(jdkHome, loadClasses);
 623                     break;
 624                 case RELEASE:
 625                     success = processRelease(release, loadClasses);
 626                     break;
 627                 case SELF:
 628                     success = processSelf(loadClasses);
 629                     break;
 630                 default:
 631                     throw new UsageException();
 632             }
 633 
 634             if (!success) {
 635                 return false;
 636             }
 637         } catch (NoSuchElementException | UsageException ex) {
 638             err.println(Messages.get("main.usage"));
 639             return false;
 640         } catch (IOException ioe) {
 641             if (verbose) {
 642                 ioe.printStackTrace(err);
 643             } else {
 644                 err.println(ioe);
 645             }
 646             return false;
 647         }
 648 
 649         // now the scanning phase
 650 
 651         switch (scanMode) {
 652             case LIST:
 653                 for (DeprData dd : deprList) {
 654                     if (!forRemoval || dd.isForRemoval()) {
 655                         out.println(Pretty.print(dd));
 656                     }
 657                 }
 658                 break;
 659             case PRINT_CSV:
 660                 out.println("#jdepr1");
 661                 comments.forEach(s -> out.println("# " + s));
 662                 for (DeprData dd : deprList) {
 663                     CSV.write(out, dd.kind, dd.typeName, dd.nameSig, dd.since, dd.forRemoval);
 664                 }
 665                 break;
 666             case ARGS:
 667                 DeprDB db = DeprDB.loadFromList(deprList);
 668                 List<String> cp = classPath.stream()
 669                                            .map(File::toString)
 670                                            .collect(toList());
 671                 Scan scan = new Scan(out, err, cp, db, verbose);
 672 
 673                 for (String a : args) {
 674                     boolean success;
 675 
 676                     if (a.endsWith(".jar")) {
 677                         success = scan.scanJar(a);
 678                     } else if (Files.isDirectory(Paths.get(a))) {
 679                         success = scan.scanDir(a);
 680                     } else {
 681                         success = scan.processClassName(a.replace('.', '/'));
 682                     }
 683 
 684                     if (!success) {
 685                         return false;
 686                     }
 687                 }
 688                 break;
 689         }
 690 
 691         return true;
 692     }
 693 
 694     /**
 695      * Programmatic main entry point: initializes the tool instance to
 696      * use stdout and stderr; runs the tool, passing command-line args;
 697      * returns an exit status.
 698      *
 699      * @return true on success, false otherwise
 700      */
 701     public static boolean call(PrintStream out, PrintStream err, String... args) {
 702         try {
 703             instance = new Main(out, err);
 704             return instance.run(args);
 705         } finally {
 706             instance = null;
 707         }
 708     }
 709 
 710     /**
 711      * Calls the main entry point and exits the JVM with an exit
 712      * status determined by the return status.
 713      */
 714     public static void main(String[] args) {
 715         System.exit(call(System.out, System.err, args) ? 0 : 1);
 716     }
 717 }