Main.java revision 3628:047d4d42b466
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 26package com.sun.tools.jdeprscan; 27 28import java.io.File; 29import java.io.IOException; 30import java.io.PrintStream; 31import java.net.URI; 32import java.nio.charset.StandardCharsets; 33import java.nio.file.Files; 34import java.nio.file.FileSystems; 35import java.nio.file.Path; 36import java.nio.file.Paths; 37import java.util.ArrayDeque; 38import java.util.ArrayList; 39import java.util.Arrays; 40import java.util.Collection; 41import java.util.HashSet; 42import java.util.List; 43import java.util.Map; 44import java.util.NoSuchElementException; 45import java.util.Set; 46import java.util.Queue; 47import java.util.stream.Stream; 48import java.util.jar.JarEntry; 49import java.util.jar.JarFile; 50 51import javax.tools.Diagnostic; 52import javax.tools.DiagnosticListener; 53import javax.tools.JavaCompiler; 54import javax.tools.JavaFileObject; 55import javax.tools.StandardJavaFileManager; 56import javax.tools.StandardLocation; 57import javax.tools.ToolProvider; 58 59import com.sun.tools.javac.file.JavacFileManager; 60 61import com.sun.tools.jdeprscan.scan.Scan; 62 63import static java.util.stream.Collectors.*; 64 65import javax.lang.model.element.PackageElement; 66import 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: --add-modules, --module-path, 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 */ 91public 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("--add-modules"); 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("--add-modules", 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("--add-modules"); 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} 718