JdepsConfiguration.java revision 3976:65d446c80cdf
1/*
2 * Copyright (c) 2012, 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.jdeps;
27
28import static com.sun.tools.jdeps.Module.trace;
29import static java.util.stream.Collectors.*;
30
31import com.sun.tools.classfile.Dependency;
32
33import java.io.BufferedInputStream;
34import java.io.File;
35import java.io.FileNotFoundException;
36import java.io.IOException;
37import java.io.InputStream;
38import java.io.UncheckedIOException;
39import java.lang.module.Configuration;
40import java.lang.module.ModuleDescriptor;
41import java.lang.module.ModuleDescriptor.Exports;
42import java.lang.module.ModuleDescriptor.Opens;
43import java.lang.module.ModuleFinder;
44import java.lang.module.ModuleReader;
45import java.lang.module.ModuleReference;
46import java.lang.module.ResolvedModule;
47import java.net.URI;
48import java.nio.file.DirectoryStream;
49import java.nio.file.FileSystem;
50import java.nio.file.FileSystems;
51import java.nio.file.Files;
52import java.nio.file.Path;
53import java.nio.file.Paths;
54import java.util.ArrayList;
55import java.util.Collections;
56import java.util.HashMap;
57import java.util.HashSet;
58import java.util.LinkedHashMap;
59import java.util.LinkedHashSet;
60import java.util.List;
61import java.util.Map;
62import java.util.Objects;
63import java.util.Optional;
64import java.util.Set;
65import java.util.function.Function;
66import java.util.function.Supplier;
67import java.util.stream.Stream;
68
69public class JdepsConfiguration implements AutoCloseable {
70    // the token for "all modules on the module path"
71    public static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
72    public static final String ALL_DEFAULT = "ALL-DEFAULT";
73    public static final String ALL_SYSTEM = "ALL-SYSTEM";
74    public static final String MODULE_INFO = "module-info.class";
75
76    private final SystemModuleFinder system;
77    private final ModuleFinder finder;
78
79    private final Map<String, Module> nameToModule = new LinkedHashMap<>();
80    private final Map<String, Module> packageToModule = new HashMap<>();
81    private final Map<String, List<Archive>> packageToUnnamedModule = new HashMap<>();
82
83    private final List<Archive> classpathArchives = new ArrayList<>();
84    private final List<Archive> initialArchives = new ArrayList<>();
85    private final Set<Module> rootModules = new HashSet<>();
86    private final Configuration configuration;
87    private final Runtime.Version version;
88
89    private JdepsConfiguration(SystemModuleFinder systemModulePath,
90                               ModuleFinder finder,
91                               Set<String> roots,
92                               List<Path> classpaths,
93                               List<Archive> initialArchives,
94                               boolean allDefaultModules,
95                               boolean allSystemModules,
96                               Runtime.Version version)
97        throws IOException
98    {
99        trace("root: %s%n", roots);
100
101        this.system = systemModulePath;
102        this.finder = finder;
103        this.version = version;
104
105        // build root set for resolution
106        Set<String> mods = new HashSet<>(roots);
107
108        // add all system modules to the root set for unnamed module or set explicitly
109        boolean unnamed = !initialArchives.isEmpty() || !classpaths.isEmpty();
110        if (allSystemModules || (unnamed && !allDefaultModules)) {
111            systemModulePath.findAll().stream()
112                .map(mref -> mref.descriptor().name())
113                .forEach(mods::add);
114        }
115
116        if (allDefaultModules) {
117            mods.addAll(systemModulePath.defaultSystemRoots());
118        }
119
120        this.configuration = Configuration.empty()
121                .resolve(finder, ModuleFinder.of(), mods);
122
123        this.configuration.modules().stream()
124                .map(ResolvedModule::reference)
125                .forEach(this::addModuleReference);
126
127        // packages in unnamed module
128        initialArchives.forEach(archive -> {
129            addPackagesInUnnamedModule(archive);
130            this.initialArchives.add(archive);
131        });
132
133        // classpath archives
134        for (Path p : classpaths) {
135            if (Files.exists(p)) {
136                Archive archive = Archive.getInstance(p, version);
137                addPackagesInUnnamedModule(archive);
138                classpathArchives.add(archive);
139            }
140        }
141
142        // all roots specified in --add-modules or -m are included
143        // as the initial set for analysis.
144        roots.stream()
145             .map(nameToModule::get)
146             .forEach(this.rootModules::add);
147
148        initProfiles();
149
150        trace("resolved modules: %s%n", nameToModule.keySet().stream()
151                .sorted().collect(joining("\n", "\n", "")));
152    }
153
154    private void initProfiles() {
155        // other system modules are not observed and not added in nameToModule map
156        Map<String, Module> systemModules =
157            system.moduleNames()
158                .collect(toMap(Function.identity(), (mn) -> {
159                    Module m = nameToModule.get(mn);
160                    if (m == null) {
161                        ModuleReference mref = finder.find(mn).get();
162                        m = toModule(mref);
163                    }
164                    return m;
165                }));
166        Profile.init(systemModules);
167    }
168
169    private void addModuleReference(ModuleReference mref) {
170        Module module = toModule(mref);
171        nameToModule.put(mref.descriptor().name(), module);
172        mref.descriptor().packages()
173            .forEach(pn -> packageToModule.putIfAbsent(pn, module));
174    }
175
176    private void addPackagesInUnnamedModule(Archive archive) {
177        archive.reader().entries().stream()
178               .filter(e -> e.endsWith(".class") && !e.equals(MODULE_INFO))
179               .map(this::toPackageName)
180               .distinct()
181               .forEach(pn -> packageToUnnamedModule
182                   .computeIfAbsent(pn, _n -> new ArrayList<>()).add(archive));
183    }
184
185    private String toPackageName(String name) {
186        int i = name.lastIndexOf('/');
187        return i > 0 ? name.replace('/', '.').substring(0, i) : "";
188    }
189
190    public Optional<Module> findModule(String name) {
191        Objects.requireNonNull(name);
192        Module m = nameToModule.get(name);
193        return m!= null ? Optional.of(m) : Optional.empty();
194
195    }
196
197    public Optional<ModuleDescriptor> findModuleDescriptor(String name) {
198        Objects.requireNonNull(name);
199        Module m = nameToModule.get(name);
200        return m!= null ? Optional.of(m.descriptor()) : Optional.empty();
201    }
202
203    boolean isValidToken(String name) {
204        return ALL_MODULE_PATH.equals(name) ||
205                ALL_DEFAULT.equals(name) ||
206                ALL_SYSTEM.equals(name);
207    }
208
209    /**
210     * Returns the modules that the given module can read
211     */
212    public Stream<Module> reads(Module module) {
213        return configuration.findModule(module.name()).get()
214            .reads().stream()
215            .map(ResolvedModule::name)
216            .map(nameToModule::get);
217    }
218
219    /**
220     * Returns the list of packages that split between resolved module and
221     * unnamed module
222     */
223    public Map<String, Set<String>> splitPackages() {
224        Set<String> splitPkgs = packageToModule.keySet().stream()
225                                       .filter(packageToUnnamedModule::containsKey)
226                                       .collect(toSet());
227        if (splitPkgs.isEmpty())
228            return Collections.emptyMap();
229
230        return splitPkgs.stream().collect(toMap(Function.identity(), (pn) -> {
231            Set<String> sources = new LinkedHashSet<>();
232            sources.add(packageToModule.get(pn).getModule().location().toString());
233            packageToUnnamedModule.get(pn).stream()
234                .map(Archive::getPathName)
235                .forEach(sources::add);
236            return sources;
237        }));
238    }
239
240    /**
241     * Returns an optional archive containing the given Location
242     */
243    public Optional<Archive> findClass(Dependency.Location location) {
244        String name = location.getName();
245        int i = name.lastIndexOf('/');
246        String pn = i > 0 ? name.substring(0, i).replace('/', '.') : "";
247        Archive archive = packageToModule.get(pn);
248        if (archive != null) {
249            return archive.contains(name + ".class")
250                        ? Optional.of(archive)
251                        : Optional.empty();
252        }
253
254        if (packageToUnnamedModule.containsKey(pn)) {
255            return packageToUnnamedModule.get(pn).stream()
256                    .filter(a -> a.contains(name + ".class"))
257                    .findFirst();
258        }
259        return Optional.empty();
260    }
261
262    /**
263     * Returns the list of Modules that can be found in the specified
264     * module paths.
265     */
266    public Map<String, Module> getModules() {
267        return nameToModule;
268    }
269
270    public Stream<Module> resolve(Set<String> roots) {
271        if (roots.isEmpty()) {
272            return nameToModule.values().stream();
273        } else {
274            return Configuration.empty()
275                    .resolve(finder, ModuleFinder.of(), roots)
276                    .modules().stream()
277                    .map(ResolvedModule::name)
278                    .map(nameToModule::get);
279        }
280    }
281
282    public List<Archive> classPathArchives() {
283        return classpathArchives;
284    }
285
286    public List<Archive> initialArchives() {
287        return initialArchives;
288    }
289
290    public Set<Module> rootModules() {
291        return rootModules;
292    }
293
294    public Module toModule(ModuleReference mref) {
295        try {
296            String mn = mref.descriptor().name();
297            URI location = mref.location().orElseThrow(FileNotFoundException::new);
298            ModuleDescriptor md = mref.descriptor();
299            Module.Builder builder = new Module.Builder(md, system.find(mn).isPresent());
300
301            final ClassFileReader reader;
302            if (location.getScheme().equals("jrt")) {
303                reader = system.getClassReader(mn);
304            } else {
305                reader = ClassFileReader.newInstance(Paths.get(location), version);
306            }
307
308            builder.classes(reader);
309            builder.location(location);
310
311            return builder.build();
312        } catch (IOException e) {
313            throw new UncheckedIOException(e);
314        }
315    }
316
317    public Runtime.Version getVersion() {
318        return version;
319    }
320
321    /*
322     * Close all archives e.g. JarFile
323     */
324    @Override
325    public void close() throws IOException {
326        for (Archive archive : initialArchives)
327            archive.close();
328        for (Archive archive : classpathArchives)
329            archive.close();
330        for (Module module : nameToModule.values())
331            module.close();
332    }
333
334    static class SystemModuleFinder implements ModuleFinder {
335        private static final String JAVA_HOME = System.getProperty("java.home");
336        private static final String JAVA_SE = "java.se";
337
338        private final FileSystem fileSystem;
339        private final Path root;
340        private final Map<String, ModuleReference> systemModules;
341
342        SystemModuleFinder() {
343            if (Files.isRegularFile(Paths.get(JAVA_HOME, "lib", "modules"))) {
344                // jrt file system
345                this.fileSystem = FileSystems.getFileSystem(URI.create("jrt:/"));
346                this.root = fileSystem.getPath("/modules");
347                this.systemModules = walk(root);
348            } else {
349                // exploded image
350                this.fileSystem = FileSystems.getDefault();
351                root = Paths.get(JAVA_HOME, "modules");
352                this.systemModules = ModuleFinder.ofSystem().findAll().stream()
353                    .collect(toMap(mref -> mref.descriptor().name(), Function.identity()));
354            }
355        }
356
357        SystemModuleFinder(String javaHome) throws IOException {
358            if (javaHome == null) {
359                // --system none
360                this.fileSystem = null;
361                this.root = null;
362                this.systemModules = Collections.emptyMap();
363            } else {
364                if (Files.isRegularFile(Paths.get(javaHome, "lib", "modules")))
365                    throw new IllegalArgumentException("Invalid java.home: " + javaHome);
366
367                // alternate java.home
368                Map<String, String> env = new HashMap<>();
369                env.put("java.home", javaHome);
370                // a remote run-time image
371                this.fileSystem = FileSystems.newFileSystem(URI.create("jrt:/"), env);
372                this.root = fileSystem.getPath("/modules");
373                this.systemModules = walk(root);
374            }
375        }
376
377        private Map<String, ModuleReference> walk(Path root) {
378            try (Stream<Path> stream = Files.walk(root, 1)) {
379                return stream.filter(path -> !path.equals(root))
380                             .map(this::toModuleReference)
381                             .collect(toMap(mref -> mref.descriptor().name(),
382                                            Function.identity()));
383            } catch (IOException e) {
384                throw new UncheckedIOException(e);
385            }
386        }
387
388        private ModuleReference toModuleReference(Path path) {
389            Path minfo = path.resolve(MODULE_INFO);
390            try (InputStream in = Files.newInputStream(minfo);
391                 BufferedInputStream bin = new BufferedInputStream(in)) {
392
393                ModuleDescriptor descriptor = dropHashes(ModuleDescriptor.read(bin));
394                String mn = descriptor.name();
395                URI uri = URI.create("jrt:/" + path.getFileName().toString());
396                Supplier<ModuleReader> readerSupplier = () -> new ModuleReader() {
397                    @Override
398                    public Optional<URI> find(String name) throws IOException {
399                        return name.equals(mn)
400                            ? Optional.of(uri) : Optional.empty();
401                    }
402
403                    @Override
404                    public Stream<String> list() {
405                        return Stream.empty();
406                    }
407
408                    @Override
409                    public void close() {
410                    }
411                };
412
413                return new ModuleReference(descriptor, uri) {
414                    @Override
415                    public ModuleReader open() {
416                        return readerSupplier.get();
417                    }
418                };
419            } catch (IOException e) {
420                throw new UncheckedIOException(e);
421            }
422        }
423
424        private ModuleDescriptor dropHashes(ModuleDescriptor md) {
425            ModuleDescriptor.Builder builder = ModuleDescriptor.newModule(md.name());
426            md.requires().forEach(builder::requires);
427            md.exports().forEach(builder::exports);
428            md.opens().forEach(builder::opens);
429            md.provides().stream().forEach(builder::provides);
430            md.uses().stream().forEach(builder::uses);
431            builder.packages(md.packages());
432            return builder.build();
433        }
434
435        @Override
436        public Set<ModuleReference> findAll() {
437            return systemModules.values().stream().collect(toSet());
438        }
439
440        @Override
441        public Optional<ModuleReference> find(String mn) {
442            return systemModules.containsKey(mn)
443                    ? Optional.of(systemModules.get(mn)) : Optional.empty();
444        }
445
446        public Stream<String> moduleNames() {
447            return systemModules.values().stream()
448                .map(mref -> mref.descriptor().name());
449        }
450
451        public ClassFileReader getClassReader(String modulename) throws IOException {
452            Path mp = root.resolve(modulename);
453            if (Files.exists(mp) && Files.isDirectory(mp)) {
454                return ClassFileReader.newInstance(fileSystem, mp);
455            } else {
456                throw new FileNotFoundException(mp.toString());
457            }
458        }
459
460        public Set<String> defaultSystemRoots() {
461            Set<String> roots = new HashSet<>();
462            boolean hasJava = false;
463            if (systemModules.containsKey(JAVA_SE)) {
464                // java.se is a system module
465                hasJava = true;
466                roots.add(JAVA_SE);
467            }
468
469            for (ModuleReference mref : systemModules.values()) {
470                String mn = mref.descriptor().name();
471                if (hasJava && mn.startsWith("java."))
472                    continue;
473
474                // add as root if observable and exports at least one package
475                ModuleDescriptor descriptor = mref.descriptor();
476                for (ModuleDescriptor.Exports e : descriptor.exports()) {
477                    if (!e.isQualified()) {
478                        roots.add(mn);
479                        break;
480                    }
481                }
482            }
483            return roots;
484        }
485    }
486
487    public static class Builder {
488
489        final SystemModuleFinder systemModulePath;
490        final Set<String> rootModules = new HashSet<>();
491        final List<Archive> initialArchives = new ArrayList<>();
492        final List<Path> paths = new ArrayList<>();
493        final List<Path> classPaths = new ArrayList<>();
494
495        ModuleFinder upgradeModulePath;
496        ModuleFinder appModulePath;
497        boolean addAllApplicationModules;
498        boolean addAllDefaultModules;
499        boolean addAllSystemModules;
500        boolean allModules;
501        Runtime.Version version;
502
503        public Builder() {
504            this.systemModulePath = new SystemModuleFinder();
505        }
506
507        public Builder(String javaHome) throws IOException {
508            this.systemModulePath = SystemModuleFinder.JAVA_HOME.equals(javaHome)
509                ? new SystemModuleFinder()
510                : new SystemModuleFinder(javaHome);
511        }
512
513        public Builder upgradeModulePath(String upgradeModulePath) {
514            this.upgradeModulePath = createModulePathFinder(upgradeModulePath);
515            return this;
516        }
517
518        public Builder appModulePath(String modulePath) {
519            this.appModulePath = createModulePathFinder(modulePath);
520            return this;
521        }
522
523        public Builder addmods(Set<String> addmods) {
524            for (String mn : addmods) {
525                switch (mn) {
526                    case ALL_MODULE_PATH:
527                        this.addAllApplicationModules = true;
528                        break;
529                    case ALL_DEFAULT:
530                        this.addAllDefaultModules = true;
531                        break;
532                    case ALL_SYSTEM:
533                        this.addAllSystemModules = true;
534                        break;
535                    default:
536                        this.rootModules.add(mn);
537                }
538            }
539            return this;
540        }
541
542        /*
543         * This method is for --check option to find all target modules specified
544         * in qualified exports.
545         *
546         * Include all system modules and modules found on modulepath
547         */
548        public Builder allModules() {
549            this.allModules = true;
550            return this;
551        }
552
553        public Builder multiRelease(Runtime.Version version) {
554            this.version = version;
555            return this;
556        }
557
558        public Builder addRoot(Path path) {
559            Archive archive = Archive.getInstance(path, version);
560            if (archive.contains(MODULE_INFO)) {
561                paths.add(path);
562            } else {
563                initialArchives.add(archive);
564            }
565            return this;
566        }
567
568        public Builder addClassPath(String classPath) {
569            this.classPaths.addAll(getClassPaths(classPath));
570            return this;
571        }
572
573        public JdepsConfiguration build() throws  IOException {
574            ModuleFinder finder = systemModulePath;
575            if (upgradeModulePath != null) {
576                finder = ModuleFinder.compose(upgradeModulePath, systemModulePath);
577            }
578            if (appModulePath != null) {
579                finder = ModuleFinder.compose(finder, appModulePath);
580            }
581            if (!paths.isEmpty()) {
582                ModuleFinder otherModulePath = ModuleFinder.of(paths.toArray(new Path[0]));
583
584                finder = ModuleFinder.compose(finder, otherModulePath);
585                // add modules specified on command-line (convenience) as root set
586                otherModulePath.findAll().stream()
587                        .map(mref -> mref.descriptor().name())
588                        .forEach(rootModules::add);
589            }
590
591            if ((addAllApplicationModules || allModules) && appModulePath != null) {
592                appModulePath.findAll().stream()
593                    .map(mref -> mref.descriptor().name())
594                    .forEach(rootModules::add);
595            }
596
597            // no archive is specified for analysis
598            // add all system modules as root if --add-modules ALL-SYSTEM is specified
599            if (addAllSystemModules && rootModules.isEmpty() &&
600                    initialArchives.isEmpty() && classPaths.isEmpty()) {
601                systemModulePath.findAll()
602                    .stream()
603                    .map(mref -> mref.descriptor().name())
604                    .forEach(rootModules::add);
605            }
606
607            return new JdepsConfiguration(systemModulePath,
608                                          finder,
609                                          rootModules,
610                                          classPaths,
611                                          initialArchives,
612                                          addAllDefaultModules,
613                                          allModules,
614                                          version);
615        }
616
617        private static ModuleFinder createModulePathFinder(String mpaths) {
618            if (mpaths == null) {
619                return null;
620            } else {
621                String[] dirs = mpaths.split(File.pathSeparator);
622                Path[] paths = new Path[dirs.length];
623                int i = 0;
624                for (String dir : dirs) {
625                    paths[i++] = Paths.get(dir);
626                }
627                return ModuleFinder.of(paths);
628            }
629        }
630
631        /*
632         * Returns the list of Archive specified in cpaths and not included
633         * initialArchives
634         */
635        private List<Path> getClassPaths(String cpaths) {
636            if (cpaths.isEmpty()) {
637                return Collections.emptyList();
638            }
639            List<Path> paths = new ArrayList<>();
640            for (String p : cpaths.split(File.pathSeparator)) {
641                if (p.length() > 0) {
642                    // wildcard to parse all JAR files e.g. -classpath dir/*
643                    int i = p.lastIndexOf(".*");
644                    if (i > 0) {
645                        Path dir = Paths.get(p.substring(0, i));
646                        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
647                            for (Path entry : stream) {
648                                paths.add(entry);
649                            }
650                        } catch (IOException e) {
651                            throw new UncheckedIOException(e);
652                        }
653                    } else {
654                        paths.add(Paths.get(p));
655                    }
656                }
657            }
658            return paths;
659        }
660    }
661
662}
663