| /* |
| * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package com.sun.tools.jdeprscan; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.net.URI; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.FileSystems; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| import java.util.Queue; |
| import java.util.stream.Stream; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| |
| import javax.tools.Diagnostic; |
| import javax.tools.DiagnosticListener; |
| import javax.tools.JavaCompiler; |
| import javax.tools.JavaFileObject; |
| import javax.tools.StandardJavaFileManager; |
| import javax.tools.StandardLocation; |
| import javax.tools.ToolProvider; |
| |
| import com.sun.tools.javac.file.JavacFileManager; |
| |
| import com.sun.tools.jdeprscan.scan.Scan; |
| |
| import static java.util.stream.Collectors.*; |
| |
| import javax.lang.model.element.PackageElement; |
| import javax.lang.model.element.TypeElement; |
| |
| /** |
| * Deprecation Scanner tool. Loads API deprecation information from the |
| * JDK image, or optionally, from a jar file or class hierarchy. Then scans |
| * a class library for usages of those APIs. |
| * |
| * TODO: |
| * - audit error handling throughout, but mainly in scan package |
| * - handling of covariant overrides |
| * - handling of override of method found in multiple superinterfaces |
| * - convert type/method/field output to Java source like syntax, e.g. |
| * instead of java/lang/Runtime.runFinalizersOnExit(Z)V |
| * print void java.lang.Runtime.runFinalizersOnExit(boolean) |
| * - more example output in man page |
| * - more rigorous GNU style option parsing; use joptsimple? |
| * |
| * FUTURES: |
| * - add module support: --add-modules, --module-path, module arg |
| * - load deprecation declarations from a designated class library instead |
| * of the JDK |
| * - load deprecation declarations from a module |
| * - scan a module (but a modular jar can be treated just a like an ordinary jar) |
| * - multi-version jar |
| */ |
| public class Main implements DiagnosticListener<JavaFileObject> { |
| final PrintStream out; |
| final PrintStream err; |
| final List<File> bootClassPath = new ArrayList<>(); |
| final List<File> classPath = new ArrayList<>(); |
| final List<File> systemModules = new ArrayList<>(); |
| final List<String> options = new ArrayList<>(); |
| final List<String> comments = new ArrayList<>(); |
| |
| // Valid releases need to match what the compiler supports. |
| // Keep these updated manually until there's a compiler API |
| // that allows querying of supported releases. |
| final Set<String> releasesWithoutForRemoval = Set.of("6", "7", "8"); |
| final Set<String> releasesWithForRemoval = Set.of("9", "10"); |
| |
| final Set<String> validReleases; |
| { |
| Set<String> temp = new HashSet<>(releasesWithoutForRemoval); |
| temp.addAll(releasesWithForRemoval); |
| validReleases = Set.of(temp.toArray(new String[0])); |
| } |
| |
| boolean verbose = false; |
| boolean forRemoval = false; |
| |
| final JavaCompiler compiler; |
| final StandardJavaFileManager fm; |
| |
| List<DeprData> deprList; // non-null after successful load phase |
| |
| /** |
| * Processes a collection of class names. Names should fully qualified |
| * names in the form "pkg.pkg.pkg.classname". |
| * |
| * @param classNames collection of fully qualified classnames to process |
| * @return true for success, false for failure |
| * @throws IOException if an I/O error occurs |
| */ |
| boolean doClassNames(Collection<String> classNames) throws IOException { |
| if (verbose) { |
| out.println("List of classes to process:"); |
| classNames.forEach(out::println); |
| out.println("End of class list."); |
| } |
| |
| // TODO: not sure this is necessary... |
| if (fm instanceof JavacFileManager) { |
| ((JavacFileManager)fm).setSymbolFileEnabled(false); |
| } |
| |
| fm.setLocation(StandardLocation.CLASS_PATH, classPath); |
| if (!bootClassPath.isEmpty()) { |
| fm.setLocation(StandardLocation.PLATFORM_CLASS_PATH, bootClassPath); |
| } |
| |
| if (!systemModules.isEmpty()) { |
| fm.setLocation(StandardLocation.SYSTEM_MODULES, systemModules); |
| } |
| |
| LoadProc proc = new LoadProc(); |
| JavaCompiler.CompilationTask task = |
| compiler.getTask(null, fm, this, options, classNames, null); |
| task.setProcessors(List.of(proc)); |
| boolean r = task.call(); |
| if (r) { |
| if (forRemoval) { |
| deprList = proc.getDeprecations().stream() |
| .filter(DeprData::isForRemoval) |
| .collect(toList()); |
| } else { |
| deprList = proc.getDeprecations(); |
| } |
| } |
| return r; |
| } |
| |
| /** |
| * Processes a stream of filenames (strings). The strings are in the |
| * form pkg/pkg/pkg/classname.class relative to the root of a package |
| * hierarchy. |
| * |
| * @param filenames a Stream of filenames to process |
| * @return true for success, false for failure |
| * @throws IOException if an I/O error occurs |
| */ |
| boolean doFileNames(Stream<String> filenames) throws IOException { |
| return doClassNames( |
| filenames.filter(name -> name.endsWith(".class")) |
| .filter(name -> !name.endsWith("package-info.class")) |
| .filter(name -> !name.endsWith("module-info.class")) |
| .map(s -> s.replaceAll("\\.class$", "")) |
| .map(s -> s.replace(File.separatorChar, '.')) |
| .collect(toList())); |
| } |
| |
| /** |
| * Replaces all but the first occurrence of '/' with '.'. Assumes |
| * that the name is in the format module/pkg/pkg/classname.class. |
| * That is, the name should contain at least one '/' character |
| * separating the module name from the package-class name. |
| * |
| * @param filename the input filename |
| * @return the modular classname |
| */ |
| String convertModularFileName(String filename) { |
| int slash = filename.indexOf('/'); |
| return filename.substring(0, slash) |
| + "/" |
| + filename.substring(slash+1).replace('/', '.'); |
| } |
| |
| /** |
| * Processes a stream of filenames (strings) including a module prefix. |
| * The strings are in the form module/pkg/pkg/pkg/classname.class relative |
| * to the root of a directory containing modules. The strings are processed |
| * into module-qualified class names of the form |
| * "module/pkg.pkg.pkg.classname". |
| * |
| * @param filenames a Stream of filenames to process |
| * @return true for success, false for failure |
| * @throws IOException if an I/O error occurs |
| */ |
| boolean doModularFileNames(Stream<String> filenames) throws IOException { |
| return doClassNames( |
| filenames.filter(name -> name.endsWith(".class")) |
| .filter(name -> !name.endsWith("package-info.class")) |
| .filter(name -> !name.endsWith("module-info.class")) |
| .map(s -> s.replaceAll("\\.class$", "")) |
| .map(this::convertModularFileName) |
| .collect(toList())); |
| } |
| |
| /** |
| * Processes named class files in the given directory. The directory |
| * should be the root of a package hierarchy. If classNames is |
| * empty, walks the directory hierarchy to find all classes. |
| * |
| * @param dirname the name of the directory to process |
| * @param classNames the names of classes to process |
| * @return true for success, false for failure |
| * @throws IOException if an I/O error occurs |
| */ |
| boolean processDirectory(String dirname, Collection<String> classNames) throws IOException { |
| if (!Files.isDirectory(Paths.get(dirname))) { |
| err.printf("%s: not a directory%n", dirname); |
| return false; |
| } |
| |
| classPath.add(0, new File(dirname)); |
| |
| if (classNames.isEmpty()) { |
| Path base = Paths.get(dirname); |
| int baseCount = base.getNameCount(); |
| try (Stream<Path> paths = Files.walk(base)) { |
| Stream<String> files = |
| paths.filter(p -> p.getNameCount() > baseCount) |
| .map(p -> p.subpath(baseCount, p.getNameCount())) |
| .map(Path::toString); |
| return doFileNames(files); |
| } |
| } else { |
| return doClassNames(classNames); |
| } |
| } |
| |
| /** |
| * Processes all class files in the given jar file. |
| * |
| * @param jarname the name of the jar file to process |
| * @return true for success, false for failure |
| * @throws IOException if an I/O error occurs |
| */ |
| boolean doJarFile(String jarname) throws IOException { |
| try (JarFile jf = new JarFile(jarname)) { |
| Stream<String> files = |
| jf.stream() |
| .map(JarEntry::getName); |
| return doFileNames(files); |
| } |
| } |
| |
| /** |
| * Processes named class files from the given jar file, |
| * or all classes if classNames is empty. |
| * |
| * @param jarname the name of the jar file to process |
| * @param classNames the names of classes to process |
| * @return true for success, false for failure |
| * @throws IOException if an I/O error occurs |
| */ |
| boolean processJarFile(String jarname, Collection<String> classNames) throws IOException { |
| classPath.add(0, new File(jarname)); |
| |
| if (classNames.isEmpty()) { |
| return doJarFile(jarname); |
| } else { |
| return doClassNames(classNames); |
| } |
| } |
| |
| /** |
| * Processes named class files from rt.jar of a JDK version 7 or 8. |
| * If classNames is empty, processes all classes. |
| * |
| * @param jdkHome the path to the "home" of the JDK to process |
| * @param classNames the names of classes to process |
| * @return true for success, false for failure |
| * @throws IOException if an I/O error occurs |
| */ |
| boolean processOldJdk(String jdkHome, Collection<String> classNames) throws IOException { |
| String RTJAR = jdkHome + "/jre/lib/rt.jar"; |
| String CSJAR = jdkHome + "/jre/lib/charsets.jar"; |
| |
| bootClassPath.add(0, new File(RTJAR)); |
| bootClassPath.add(1, new File(CSJAR)); |
| options.add("-source"); |
| options.add("8"); |
| |
| if (classNames.isEmpty()) { |
| return doJarFile(RTJAR); |
| } else { |
| return doClassNames(classNames); |
| } |
| } |
| |
| /** |
| * Processes listed classes given a JDK 9 home. |
| */ |
| boolean processJdk9(String jdkHome, Collection<String> classes) throws IOException { |
| systemModules.add(new File(jdkHome)); |
| return doClassNames(classes); |
| } |
| |
| /** |
| * Processes the class files from the currently running JDK, |
| * using the jrt: filesystem. |
| * |
| * @return true for success, false for failure |
| * @throws IOException if an I/O error occurs |
| */ |
| boolean processSelf(Collection<String> classes) throws IOException { |
| options.add("--add-modules"); |
| options.add("java.se.ee,jdk.xml.bind"); // TODO why jdk.xml.bind? |
| |
| if (classes.isEmpty()) { |
| Path modules = FileSystems.getFileSystem(URI.create("jrt:/")) |
| .getPath("/modules"); |
| |
| // names are /modules/<modulename>/pkg/.../Classname.class |
| try (Stream<Path> paths = Files.walk(modules)) { |
| Stream<String> files = |
| paths.filter(p -> p.getNameCount() > 2) |
| .map(p -> p.subpath(1, p.getNameCount())) |
| .map(Path::toString); |
| return doModularFileNames(files); |
| } |
| } else { |
| return doClassNames(classes); |
| } |
| } |
| |
| /** |
| * Process classes from a particular JDK release, using only information |
| * in this JDK. |
| * |
| * @param release "6", "7", "8", "9", or "10" |
| * @param classes collection of classes to process, may be empty |
| * @return success value |
| */ |
| boolean processRelease(String release, Collection<String> classes) throws IOException { |
| options.addAll(List.of("--release", release)); |
| |
| if (release.equals("9") || release.equals("10")) { |
| List<String> rootMods = List.of("java.se", "java.se.ee"); |
| TraverseProc proc = new TraverseProc(rootMods); |
| JavaCompiler.CompilationTask task = |
| compiler.getTask(null, fm, this, |
| // options |
| List.of("--add-modules", String.join(",", rootMods)), |
| // classes |
| List.of("java.lang.Object"), |
| null); |
| task.setProcessors(List.of(proc)); |
| if (!task.call()) { |
| return false; |
| } |
| Map<PackageElement, List<TypeElement>> types = proc.getPublicTypes(); |
| options.add("--add-modules"); |
| options.add(String.join(",", rootMods)); |
| return doClassNames( |
| types.values().stream() |
| .flatMap(List::stream) |
| .map(TypeElement::toString) |
| .collect(toList())); |
| } else { |
| // TODO: kind of a hack... |
| // Create a throwaway compilation task with options "--release N" |
| // which has the side effect of setting the file manager's |
| // PLATFORM_CLASS_PATH to the right value. |
| JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); |
| StandardJavaFileManager fm = |
| compiler.getStandardFileManager(this, null, StandardCharsets.UTF_8); |
| JavaCompiler.CompilationTask task = |
| compiler.getTask(null, fm, this, List.of("--release", release), null, null); |
| List<Path> paths = new ArrayList<>(); |
| for (Path p : fm.getLocationAsPaths(StandardLocation.PLATFORM_CLASS_PATH)) { |
| try (Stream<Path> str = Files.walk(p)) { |
| str.forEachOrdered(paths::add); |
| } |
| } |
| |
| options.add("-Xlint:-options"); |
| |
| return doClassNames( |
| paths.stream() |
| .filter(path -> path.toString().endsWith(".sig")) |
| .map(path -> path.subpath(1, path.getNameCount())) |
| .map(Path::toString) |
| .map(s -> s.replaceAll("\\.sig$", "")) |
| .map(s -> s.replace('/', '.')) |
| .collect(toList())); |
| } |
| } |
| |
| /** |
| * An enum denoting the mode in which the tool is running. |
| * Different modes correspond to the different process* methods. |
| * The exception is UNKNOWN, which indicates that a mode wasn't |
| * specified on the command line, which is an error. |
| */ |
| static enum LoadMode { |
| CLASSES, DIR, JAR, OLD_JDK, JDK9, SELF, RELEASE, LOAD_CSV |
| } |
| |
| static enum ScanMode { |
| ARGS, LIST, PRINT_CSV |
| } |
| |
| /** |
| * A checked exception that's thrown if a command-line syntax error |
| * is detected. |
| */ |
| static class UsageException extends Exception { |
| private static final long serialVersionUID = 3611828659572908743L; |
| } |
| |
| /** |
| * Convenience method to throw UsageException if a condition is false. |
| * |
| * @param cond the condition that's required to be true |
| * @throws UsageException |
| */ |
| void require(boolean cond) throws UsageException { |
| if (!cond) { |
| throw new UsageException(); |
| } |
| } |
| |
| /** |
| * Constructs an instance of the finder tool. |
| * |
| * @param out the stream to which the tool's output is sent |
| * @param err the stream to which error messages are sent |
| */ |
| Main(PrintStream out, PrintStream err) { |
| this.out = out; |
| this.err = err; |
| compiler = ToolProvider.getSystemJavaCompiler(); |
| fm = compiler.getStandardFileManager(this, null, StandardCharsets.UTF_8); |
| } |
| |
| /** |
| * Prints the diagnostic to the err stream. |
| * |
| * Specified by the DiagnosticListener interface. |
| * |
| * @param diagnostic the tool diagnostic to print |
| */ |
| @Override |
| public void report(Diagnostic<? extends JavaFileObject> diagnostic) { |
| err.println(diagnostic); |
| } |
| |
| /** |
| * Parses arguments and performs the requested processing. |
| * |
| * @param argArray command-line arguments |
| * @return true on success, false on error |
| */ |
| boolean run(String... argArray) { |
| Queue<String> args = new ArrayDeque<>(Arrays.asList(argArray)); |
| LoadMode loadMode = LoadMode.RELEASE; |
| ScanMode scanMode = ScanMode.ARGS; |
| String dir = null; |
| String jar = null; |
| String jdkHome = null; |
| String release = "10"; |
| List<String> loadClasses = new ArrayList<>(); |
| String csvFile = null; |
| |
| try { |
| while (!args.isEmpty()) { |
| String a = args.element(); |
| if (a.startsWith("-")) { |
| args.remove(); |
| switch (a) { |
| case "--class-path": |
| classPath.clear(); |
| Arrays.stream(args.remove().split(File.pathSeparator)) |
| .map(File::new) |
| .forEachOrdered(classPath::add); |
| break; |
| case "--for-removal": |
| forRemoval = true; |
| break; |
| case "--full-version": |
| out.println(System.getProperty("java.vm.version")); |
| return false; |
| case "--help": |
| case "-h": |
| out.println(Messages.get("main.usage")); |
| out.println(); |
| out.println(Messages.get("main.help")); |
| return false; |
| case "-l": |
| case "--list": |
| require(scanMode == ScanMode.ARGS); |
| scanMode = ScanMode.LIST; |
| break; |
| case "--release": |
| loadMode = LoadMode.RELEASE; |
| release = args.remove(); |
| if (!validReleases.contains(release)) { |
| throw new UsageException(); |
| } |
| break; |
| case "-v": |
| case "--verbose": |
| verbose = true; |
| break; |
| case "--version": |
| out.println(System.getProperty("java.version")); |
| return false; |
| case "--Xcompiler-arg": |
| options.add(args.remove()); |
| break; |
| case "--Xcsv-comment": |
| comments.add(args.remove()); |
| break; |
| case "--Xhelp": |
| out.println(Messages.get("main.xhelp")); |
| return false; |
| case "--Xload-class": |
| loadMode = LoadMode.CLASSES; |
| loadClasses.add(args.remove()); |
| break; |
| case "--Xload-csv": |
| loadMode = LoadMode.LOAD_CSV; |
| csvFile = args.remove(); |
| break; |
| case "--Xload-dir": |
| loadMode = LoadMode.DIR; |
| dir = args.remove(); |
| break; |
| case "--Xload-jar": |
| loadMode = LoadMode.JAR; |
| jar = args.remove(); |
| break; |
| case "--Xload-jdk9": |
| loadMode = LoadMode.JDK9; |
| jdkHome = args.remove(); |
| break; |
| case "--Xload-old-jdk": |
| loadMode = LoadMode.OLD_JDK; |
| jdkHome = args.remove(); |
| break; |
| case "--Xload-self": |
| loadMode = LoadMode.SELF; |
| break; |
| case "--Xprint-csv": |
| require(scanMode == ScanMode.ARGS); |
| scanMode = ScanMode.PRINT_CSV; |
| break; |
| default: |
| throw new UsageException(); |
| } |
| } else { |
| break; |
| } |
| } |
| |
| if ((scanMode == ScanMode.ARGS) == args.isEmpty()) { |
| throw new UsageException(); |
| } |
| |
| if ( forRemoval && loadMode == LoadMode.RELEASE && |
| releasesWithoutForRemoval.contains(release)) { |
| throw new UsageException(); |
| } |
| |
| boolean success = false; |
| |
| switch (loadMode) { |
| case CLASSES: |
| success = doClassNames(loadClasses); |
| break; |
| case DIR: |
| success = processDirectory(dir, loadClasses); |
| break; |
| case JAR: |
| success = processJarFile(jar, loadClasses); |
| break; |
| case JDK9: |
| require(!args.isEmpty()); |
| success = processJdk9(jdkHome, loadClasses); |
| break; |
| case LOAD_CSV: |
| deprList = DeprDB.loadFromFile(csvFile); |
| success = true; |
| break; |
| case OLD_JDK: |
| success = processOldJdk(jdkHome, loadClasses); |
| break; |
| case RELEASE: |
| success = processRelease(release, loadClasses); |
| break; |
| case SELF: |
| success = processSelf(loadClasses); |
| break; |
| default: |
| throw new UsageException(); |
| } |
| |
| if (!success) { |
| return false; |
| } |
| } catch (NoSuchElementException | UsageException ex) { |
| err.println(Messages.get("main.usage")); |
| return false; |
| } catch (IOException ioe) { |
| if (verbose) { |
| ioe.printStackTrace(err); |
| } else { |
| err.println(ioe); |
| } |
| return false; |
| } |
| |
| // now the scanning phase |
| |
| boolean scanStatus = true; |
| |
| switch (scanMode) { |
| case LIST: |
| for (DeprData dd : deprList) { |
| if (!forRemoval || dd.isForRemoval()) { |
| out.println(Pretty.print(dd)); |
| } |
| } |
| break; |
| case PRINT_CSV: |
| out.println("#jdepr1"); |
| comments.forEach(s -> out.println("# " + s)); |
| for (DeprData dd : deprList) { |
| CSV.write(out, dd.kind, dd.typeName, dd.nameSig, dd.since, dd.forRemoval); |
| } |
| break; |
| case ARGS: |
| DeprDB db = DeprDB.loadFromList(deprList); |
| List<String> cp = classPath.stream() |
| .map(File::toString) |
| .collect(toList()); |
| Scan scan = new Scan(out, err, cp, db, verbose); |
| |
| for (String a : args) { |
| boolean s; |
| if (a.endsWith(".jar")) { |
| s = scan.scanJar(a); |
| } else if (a.endsWith(".class")) { |
| s = scan.processClassFile(a); |
| } else if (Files.isDirectory(Paths.get(a))) { |
| s = scan.scanDir(a); |
| } else { |
| s = scan.processClassName(a.replace('.', '/')); |
| } |
| scanStatus = scanStatus && s; |
| } |
| break; |
| } |
| |
| return scanStatus; |
| } |
| |
| /** |
| * Programmatic main entry point: initializes the tool instance to |
| * use stdout and stderr; runs the tool, passing command-line args; |
| * returns an exit status. |
| * |
| * @return true on success, false otherwise |
| */ |
| public static boolean call(PrintStream out, PrintStream err, String... args) { |
| return new Main(out, err).run(args); |
| } |
| |
| /** |
| * Calls the main entry point and exits the JVM with an exit |
| * status determined by the return status. |
| */ |
| public static void main(String[] args) { |
| System.exit(call(System.out, System.err, args) ? 0 : 1); |
| } |
| } |