1/*
2 * Copyright (c) 2012, 2017, 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 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    /**
261     * Returns Configuration with the given roots
262     */
263    public Configuration resolve(Set<String> roots) {
264        if (roots.isEmpty())
265            throw new IllegalArgumentException("empty roots");
266
267        return Configuration.empty()
268                    .resolve(finder, ModuleFinder.of(), roots);
269    }
270
271    public List<Archive> classPathArchives() {
272        return classpathArchives;
273    }
274
275    public List<Archive> initialArchives() {
276        return initialArchives;
277    }
278
279    public Set<Module> rootModules() {
280        return rootModules;
281    }
282
283    public Module toModule(ModuleReference mref) {
284        try {
285            String mn = mref.descriptor().name();
286            URI location = mref.location().orElseThrow(FileNotFoundException::new);
287            ModuleDescriptor md = mref.descriptor();
288            Module.Builder builder = new Module.Builder(md, system.find(mn).isPresent());
289
290            final ClassFileReader reader;
291            if (location.getScheme().equals("jrt")) {
292                reader = system.getClassReader(mn);
293            } else {
294                reader = ClassFileReader.newInstance(Paths.get(location), version);
295            }
296
297            builder.classes(reader);
298            builder.location(location);
299
300            return builder.build();
301        } catch (IOException e) {
302            throw new UncheckedIOException(e);
303        }
304    }
305
306    public Runtime.Version getVersion() {
307        return version;
308    }
309
310    /*
311     * Close all archives e.g. JarFile
312     */
313    @Override
314    public void close() throws IOException {
315        for (Archive archive : initialArchives)
316            archive.close();
317        for (Archive archive : classpathArchives)
318            archive.close();
319        for (Module module : nameToModule.values())
320            module.close();
321    }
322
323    static class SystemModuleFinder implements ModuleFinder {
324        private static final String JAVA_HOME = System.getProperty("java.home");
325        private static final String JAVA_SE = "java.se";
326
327        private final FileSystem fileSystem;
328        private final Path root;
329        private final Map<String, ModuleReference> systemModules;
330
331        SystemModuleFinder() {
332            if (Files.isRegularFile(Paths.get(JAVA_HOME, "lib", "modules"))) {
333                // jrt file system
334                this.fileSystem = FileSystems.getFileSystem(URI.create("jrt:/"));
335                this.root = fileSystem.getPath("/modules");
336                this.systemModules = walk(root);
337            } else {
338                // exploded image
339                this.fileSystem = FileSystems.getDefault();
340                root = Paths.get(JAVA_HOME, "modules");
341                this.systemModules = ModuleFinder.ofSystem().findAll().stream()
342                    .collect(toMap(mref -> mref.descriptor().name(), Function.identity()));
343            }
344        }
345
346        SystemModuleFinder(String javaHome) throws IOException {
347            if (javaHome == null) {
348                // --system none
349                this.fileSystem = null;
350                this.root = null;
351                this.systemModules = Collections.emptyMap();
352            } else {
353                if (Files.isRegularFile(Paths.get(javaHome, "lib", "modules")))
354                    throw new IllegalArgumentException("Invalid java.home: " + javaHome);
355
356                // alternate java.home
357                Map<String, String> env = new HashMap<>();
358                env.put("java.home", javaHome);
359                // a remote run-time image
360                this.fileSystem = FileSystems.newFileSystem(URI.create("jrt:/"), env);
361                this.root = fileSystem.getPath("/modules");
362                this.systemModules = walk(root);
363            }
364        }
365
366        private Map<String, ModuleReference> walk(Path root) {
367            try (Stream<Path> stream = Files.walk(root, 1)) {
368                return stream.filter(path -> !path.equals(root))
369                             .map(this::toModuleReference)
370                             .collect(toMap(mref -> mref.descriptor().name(),
371                                            Function.identity()));
372            } catch (IOException e) {
373                throw new UncheckedIOException(e);
374            }
375        }
376
377        private ModuleReference toModuleReference(Path path) {
378            Path minfo = path.resolve(MODULE_INFO);
379            try (InputStream in = Files.newInputStream(minfo);
380                 BufferedInputStream bin = new BufferedInputStream(in)) {
381
382                ModuleDescriptor descriptor = dropHashes(ModuleDescriptor.read(bin));
383                String mn = descriptor.name();
384                URI uri = URI.create("jrt:/" + path.getFileName().toString());
385                Supplier<ModuleReader> readerSupplier = () -> 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 Stream<String> list() {
394                        return Stream.empty();
395                    }
396
397                    @Override
398                    public void close() {
399                    }
400                };
401
402                return new ModuleReference(descriptor, uri) {
403                    @Override
404                    public ModuleReader open() {
405                        return readerSupplier.get();
406                    }
407                };
408            } catch (IOException e) {
409                throw new UncheckedIOException(e);
410            }
411        }
412
413        private ModuleDescriptor dropHashes(ModuleDescriptor md) {
414            ModuleDescriptor.Builder builder = ModuleDescriptor.newModule(md.name());
415            md.requires().forEach(builder::requires);
416            md.exports().forEach(builder::exports);
417            md.opens().forEach(builder::opens);
418            md.provides().stream().forEach(builder::provides);
419            md.uses().stream().forEach(builder::uses);
420            builder.packages(md.packages());
421            return builder.build();
422        }
423
424        @Override
425        public Set<ModuleReference> findAll() {
426            return systemModules.values().stream().collect(toSet());
427        }
428
429        @Override
430        public Optional<ModuleReference> find(String mn) {
431            return systemModules.containsKey(mn)
432                    ? Optional.of(systemModules.get(mn)) : Optional.empty();
433        }
434
435        public Stream<String> moduleNames() {
436            return systemModules.values().stream()
437                .map(mref -> mref.descriptor().name());
438        }
439
440        public ClassFileReader getClassReader(String modulename) throws IOException {
441            Path mp = root.resolve(modulename);
442            if (Files.exists(mp) && Files.isDirectory(mp)) {
443                return ClassFileReader.newInstance(fileSystem, mp);
444            } else {
445                throw new FileNotFoundException(mp.toString());
446            }
447        }
448
449        public Set<String> defaultSystemRoots() {
450            Set<String> roots = new HashSet<>();
451            boolean hasJava = false;
452            if (systemModules.containsKey(JAVA_SE)) {
453                // java.se is a system module
454                hasJava = true;
455                roots.add(JAVA_SE);
456            }
457
458            for (ModuleReference mref : systemModules.values()) {
459                String mn = mref.descriptor().name();
460                if (hasJava && mn.startsWith("java."))
461                    continue;
462
463                // add as root if observable and exports at least one package
464                ModuleDescriptor descriptor = mref.descriptor();
465                for (ModuleDescriptor.Exports e : descriptor.exports()) {
466                    if (!e.isQualified()) {
467                        roots.add(mn);
468                        break;
469                    }
470                }
471            }
472            return roots;
473        }
474    }
475
476    public static class Builder {
477
478        final SystemModuleFinder systemModulePath;
479        final Set<String> rootModules = new HashSet<>();
480        final List<Archive> initialArchives = new ArrayList<>();
481        final List<Path> paths = new ArrayList<>();
482        final List<Path> classPaths = new ArrayList<>();
483
484        ModuleFinder upgradeModulePath;
485        ModuleFinder appModulePath;
486        boolean addAllApplicationModules;
487        boolean addAllDefaultModules;
488        boolean addAllSystemModules;
489        boolean allModules;
490        Runtime.Version version;
491
492        public Builder() {
493            this.systemModulePath = new SystemModuleFinder();
494        }
495
496        public Builder(String javaHome) throws IOException {
497            this.systemModulePath = SystemModuleFinder.JAVA_HOME.equals(javaHome)
498                ? new SystemModuleFinder()
499                : new SystemModuleFinder(javaHome);
500        }
501
502        public Builder upgradeModulePath(String upgradeModulePath) {
503            this.upgradeModulePath = createModulePathFinder(upgradeModulePath);
504            return this;
505        }
506
507        public Builder appModulePath(String modulePath) {
508            this.appModulePath = createModulePathFinder(modulePath);
509            return this;
510        }
511
512        public Builder addmods(Set<String> addmods) {
513            for (String mn : addmods) {
514                switch (mn) {
515                    case ALL_MODULE_PATH:
516                        this.addAllApplicationModules = true;
517                        break;
518                    case ALL_DEFAULT:
519                        this.addAllDefaultModules = true;
520                        break;
521                    case ALL_SYSTEM:
522                        this.addAllSystemModules = true;
523                        break;
524                    default:
525                        this.rootModules.add(mn);
526                }
527            }
528            return this;
529        }
530
531        /*
532         * This method is for --check option to find all target modules specified
533         * in qualified exports.
534         *
535         * Include all system modules and modules found on modulepath
536         */
537        public Builder allModules() {
538            this.allModules = true;
539            return this;
540        }
541
542        public Builder multiRelease(Runtime.Version version) {
543            this.version = version;
544            return this;
545        }
546
547        public Builder addRoot(Path path) {
548            Archive archive = Archive.getInstance(path, version);
549            if (archive.contains(MODULE_INFO)) {
550                paths.add(path);
551            } else {
552                initialArchives.add(archive);
553            }
554            return this;
555        }
556
557        public Builder addClassPath(String classPath) {
558            this.classPaths.addAll(getClassPaths(classPath));
559            return this;
560        }
561
562        public JdepsConfiguration build() throws  IOException {
563            ModuleFinder finder = systemModulePath;
564            if (upgradeModulePath != null) {
565                finder = ModuleFinder.compose(upgradeModulePath, systemModulePath);
566            }
567            if (appModulePath != null) {
568                finder = ModuleFinder.compose(finder, appModulePath);
569            }
570            if (!paths.isEmpty()) {
571                ModuleFinder otherModulePath = ModuleFinder.of(paths.toArray(new Path[0]));
572
573                finder = ModuleFinder.compose(finder, otherModulePath);
574                // add modules specified on command-line (convenience) as root set
575                otherModulePath.findAll().stream()
576                        .map(mref -> mref.descriptor().name())
577                        .forEach(rootModules::add);
578            }
579
580            if ((addAllApplicationModules || allModules) && appModulePath != null) {
581                appModulePath.findAll().stream()
582                    .map(mref -> mref.descriptor().name())
583                    .forEach(rootModules::add);
584            }
585
586            // no archive is specified for analysis
587            // add all system modules as root if --add-modules ALL-SYSTEM is specified
588            if (addAllSystemModules && rootModules.isEmpty() &&
589                    initialArchives.isEmpty() && classPaths.isEmpty()) {
590                systemModulePath.findAll()
591                    .stream()
592                    .map(mref -> mref.descriptor().name())
593                    .forEach(rootModules::add);
594            }
595
596            return new JdepsConfiguration(systemModulePath,
597                                          finder,
598                                          rootModules,
599                                          classPaths,
600                                          initialArchives,
601                                          addAllDefaultModules,
602                                          allModules,
603                                          version);
604        }
605
606        private static ModuleFinder createModulePathFinder(String mpaths) {
607            if (mpaths == null) {
608                return null;
609            } else {
610                String[] dirs = mpaths.split(File.pathSeparator);
611                Path[] paths = new Path[dirs.length];
612                int i = 0;
613                for (String dir : dirs) {
614                    paths[i++] = Paths.get(dir);
615                }
616                return ModuleFinder.of(paths);
617            }
618        }
619
620        /*
621         * Returns the list of Archive specified in cpaths and not included
622         * initialArchives
623         */
624        private List<Path> getClassPaths(String cpaths) {
625            if (cpaths.isEmpty()) {
626                return Collections.emptyList();
627            }
628            List<Path> paths = new ArrayList<>();
629            for (String p : cpaths.split(File.pathSeparator)) {
630                if (p.length() > 0) {
631                    // wildcard to parse all JAR files e.g. -classpath dir/*
632                    int i = p.lastIndexOf(".*");
633                    if (i > 0) {
634                        Path dir = Paths.get(p.substring(0, i));
635                        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
636                            for (Path entry : stream) {
637                                paths.add(entry);
638                            }
639                        } catch (IOException e) {
640                            throw new UncheckedIOException(e);
641                        }
642                    } else {
643                        paths.add(Paths.get(p));
644                    }
645                }
646            }
647            return paths;
648        }
649    }
650
651}
652