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