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