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