src/jdk.jdeps/share/classes/com/sun/tools/jdeps/JdepsConfiguration.java
author mchung
Wed, 21 Nov 2018 22:34:01 -0800
changeset 52650 c16b6cc93272
parent 50738 6cc2dc161c64
permissions -rw-r--r--
8213909: jdeps --print-module-deps should report missing dependences 8168869: jdeps: localized messages don't use proper line breaks Reviewed-by: sundar

/*
 * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.tools.jdeps;

import static com.sun.tools.jdeps.Module.trace;
import static java.util.stream.Collectors.*;

import com.sun.tools.classfile.Dependency;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.module.Configuration;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReader;
import java.lang.module.ModuleReference;
import java.lang.module.ResolvedModule;
import java.net.URI;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class JdepsConfiguration implements AutoCloseable {
    // the token for "all modules on the module path"
    public static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
    public static final String ALL_DEFAULT = "ALL-DEFAULT";
    public static final String ALL_SYSTEM = "ALL-SYSTEM";

    public static final String MODULE_INFO = "module-info.class";

    private final SystemModuleFinder system;
    private final ModuleFinder finder;

    private final Map<String, Module> nameToModule = new LinkedHashMap<>();
    private final Map<String, Module> packageToModule = new HashMap<>();
    private final Map<String, List<Archive>> packageToUnnamedModule = new HashMap<>();

    private final List<Archive> classpathArchives = new ArrayList<>();
    private final List<Archive> initialArchives = new ArrayList<>();
    private final Set<Module> rootModules = new HashSet<>();
    private final Runtime.Version version;

    private JdepsConfiguration(Configuration config,
                               SystemModuleFinder systemModulePath,
                               ModuleFinder finder,
                               Set<String> roots,
                               List<Path> classpaths,
                               List<Archive> initialArchives,
                               Runtime.Version version)
        throws IOException
    {
        trace("root: %s%n", roots);
        trace("initial archives: %s%n", initialArchives);
        trace("class path: %s%n", classpaths);
        this.system = systemModulePath;
        this.finder = finder;
        this.version = version;

        config.modules().stream()
              .map(ResolvedModule::reference)
              .forEach(this::addModuleReference);

        // packages in unnamed module
        initialArchives.forEach(archive -> {
            addPackagesInUnnamedModule(archive);
            this.initialArchives.add(archive);
        });

        // classpath archives
        for (Path p : classpaths) {
            if (Files.exists(p)) {
                Archive archive = Archive.getInstance(p, version);
                addPackagesInUnnamedModule(archive);
                classpathArchives.add(archive);
            }
        }

        // all roots specified in --add-modules or -m are included
        // as the initial set for analysis.
        roots.stream()
             .map(nameToModule::get)
             .forEach(this.rootModules::add);

        initProfiles();

        trace("resolved modules: %s%n", nameToModule.keySet().stream()
                .sorted().collect(joining("\n", "\n", "")));
    }

    private void initProfiles() {
        // other system modules are not observed and not added in nameToModule map
        Map<String, Module> systemModules =
            system.moduleNames()
                .collect(toMap(Function.identity(), (mn) -> {
                    Module m = nameToModule.get(mn);
                    if (m == null) {
                        ModuleReference mref = finder.find(mn).get();
                        m = toModule(mref);
                    }
                    return m;
                }));
        Profile.init(systemModules);
    }

    private void addModuleReference(ModuleReference mref) {
        Module module = toModule(mref);
        nameToModule.put(mref.descriptor().name(), module);
        mref.descriptor().packages()
            .forEach(pn -> packageToModule.putIfAbsent(pn, module));
    }

    private void addPackagesInUnnamedModule(Archive archive) {
        archive.reader().entries().stream()
               .filter(e -> e.endsWith(".class") && !e.equals(MODULE_INFO))
               .map(this::toPackageName)
               .distinct()
               .forEach(pn -> packageToUnnamedModule
                   .computeIfAbsent(pn, _n -> new ArrayList<>()).add(archive));
    }

    private String toPackageName(String name) {
        int i = name.lastIndexOf('/');
        return i > 0 ? name.replace('/', '.').substring(0, i) : "";
    }

    public Optional<Module> findModule(String name) {
        Objects.requireNonNull(name);
        Module m = nameToModule.get(name);
        return m!= null ? Optional.of(m) : Optional.empty();

    }

    public Optional<ModuleDescriptor> findModuleDescriptor(String name) {
        Objects.requireNonNull(name);
        Module m = nameToModule.get(name);
        return m!= null ? Optional.of(m.descriptor()) : Optional.empty();
    }

    public static boolean isToken(String name) {
        return ALL_MODULE_PATH.equals(name) ||
               ALL_DEFAULT.equals(name) ||
               ALL_SYSTEM.equals(name);
    }

    /**
     * Returns the list of packages that split between resolved module and
     * unnamed module
     */
    public Map<String, Set<String>> splitPackages() {
        Set<String> splitPkgs = packageToModule.keySet().stream()
                                       .filter(packageToUnnamedModule::containsKey)
                                       .collect(toSet());
        if (splitPkgs.isEmpty())
            return Collections.emptyMap();

        return splitPkgs.stream().collect(toMap(Function.identity(), (pn) -> {
            Set<String> sources = new LinkedHashSet<>();
            sources.add(packageToModule.get(pn).getModule().location().toString());
            packageToUnnamedModule.get(pn).stream()
                .map(Archive::getPathName)
                .forEach(sources::add);
            return sources;
        }));
    }

    /**
     * Returns an optional archive containing the given Location
     */
    public Optional<Archive> findClass(Dependency.Location location) {
        String name = location.getName();
        int i = name.lastIndexOf('/');
        String pn = i > 0 ? name.substring(0, i).replace('/', '.') : "";
        Archive archive = packageToModule.get(pn);
        if (archive != null) {
            return archive.contains(name + ".class")
                        ? Optional.of(archive)
                        : Optional.empty();
        }

        if (packageToUnnamedModule.containsKey(pn)) {
            return packageToUnnamedModule.get(pn).stream()
                    .filter(a -> a.contains(name + ".class"))
                    .findFirst();
        }
        return Optional.empty();
    }

    /**
     * Returns the list of Modules that can be found in the specified
     * module paths.
     */
    public Map<String, Module> getModules() {
        return nameToModule;
    }

    /**
     * Returns Configuration with the given roots
     */
    public Configuration resolve(Set<String> roots) {
        if (roots.isEmpty())
            throw new IllegalArgumentException("empty roots");

        return Configuration.empty()
                    .resolve(finder, ModuleFinder.of(), roots);
    }

    public List<Archive> classPathArchives() {
        return classpathArchives;
    }

    public List<Archive> initialArchives() {
        return initialArchives;
    }

    public Set<Module> rootModules() {
        return rootModules;
    }

    public Module toModule(ModuleReference mref) {
        try {
            String mn = mref.descriptor().name();
            URI location = mref.location().orElseThrow(FileNotFoundException::new);
            ModuleDescriptor md = mref.descriptor();
            // is this module from the system module path?
            URI loc = system.find(mn).flatMap(ModuleReference::location).orElse(null);
            boolean isSystem = location.equals(loc);

            final ClassFileReader reader;
            if (location.getScheme().equals("jrt")) {
                reader = system.getClassReader(mn);
            } else {
                reader = ClassFileReader.newInstance(Paths.get(location), version);
            }
            Module.Builder builder = new Module.Builder(md, isSystem);
            builder.classes(reader);
            builder.location(location);

            return builder.build();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public Runtime.Version getVersion() {
        return version;
    }

    /*
     * Close all archives e.g. JarFile
     */
    @Override
    public void close() throws IOException {
        for (Archive archive : initialArchives)
            archive.close();
        for (Archive archive : classpathArchives)
            archive.close();
        for (Module module : nameToModule.values())
            module.close();
    }

    static class SystemModuleFinder implements ModuleFinder {
        private static final String JAVA_HOME = System.getProperty("java.home");

        private final FileSystem fileSystem;
        private final Path root;
        private final Map<String, ModuleReference> systemModules;

        SystemModuleFinder() {
            if (Files.isRegularFile(Paths.get(JAVA_HOME, "lib", "modules"))) {
                // jrt file system
                this.fileSystem = FileSystems.getFileSystem(URI.create("jrt:/"));
                this.root = fileSystem.getPath("/modules");
                this.systemModules = walk(root);
            } else {
                // exploded image
                this.fileSystem = FileSystems.getDefault();
                root = Paths.get(JAVA_HOME, "modules");
                this.systemModules = ModuleFinder.ofSystem().findAll().stream()
                    .collect(toMap(mref -> mref.descriptor().name(), Function.identity()));
            }
        }

        SystemModuleFinder(String javaHome) throws IOException {
            if (javaHome == null) {
                // --system none
                this.fileSystem = null;
                this.root = null;
                this.systemModules = Collections.emptyMap();
            } else {
                if (!Files.isRegularFile(Paths.get(javaHome, "lib", "modules")))
                    throw new IllegalArgumentException("Invalid java.home: " + javaHome);

                // alternate java.home
                Map<String, String> env = new HashMap<>();
                env.put("java.home", javaHome);
                // a remote run-time image
                this.fileSystem = FileSystems.newFileSystem(URI.create("jrt:/"), env);
                this.root = fileSystem.getPath("/modules");
                this.systemModules = walk(root);
            }
        }

        private Map<String, ModuleReference> walk(Path root) {
            try (Stream<Path> stream = Files.walk(root, 1)) {
                return stream.filter(path -> !path.equals(root))
                             .map(this::toModuleReference)
                             .collect(toMap(mref -> mref.descriptor().name(),
                                            Function.identity()));
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }

        private ModuleReference toModuleReference(Path path) {
            Path minfo = path.resolve(MODULE_INFO);
            try (InputStream in = Files.newInputStream(minfo);
                 BufferedInputStream bin = new BufferedInputStream(in)) {

                ModuleDescriptor descriptor = dropHashes(ModuleDescriptor.read(bin));
                String mn = descriptor.name();
                URI uri = URI.create("jrt:/" + path.getFileName().toString());
                Supplier<ModuleReader> readerSupplier = () -> new ModuleReader() {
                    @Override
                    public Optional<URI> find(String name) throws IOException {
                        return name.equals(mn)
                            ? Optional.of(uri) : Optional.empty();
                    }

                    @Override
                    public Stream<String> list() {
                        return Stream.empty();
                    }

                    @Override
                    public void close() {
                    }
                };

                return new ModuleReference(descriptor, uri) {
                    @Override
                    public ModuleReader open() {
                        return readerSupplier.get();
                    }
                };
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }

        private ModuleDescriptor dropHashes(ModuleDescriptor md) {
            ModuleDescriptor.Builder builder = ModuleDescriptor.newModule(md.name());
            md.requires().forEach(builder::requires);
            md.exports().forEach(builder::exports);
            md.opens().forEach(builder::opens);
            md.provides().stream().forEach(builder::provides);
            md.uses().stream().forEach(builder::uses);
            builder.packages(md.packages());
            return builder.build();
        }

        @Override
        public Set<ModuleReference> findAll() {
            return systemModules.values().stream().collect(toSet());
        }

        @Override
        public Optional<ModuleReference> find(String mn) {
            return systemModules.containsKey(mn)
                    ? Optional.of(systemModules.get(mn)) : Optional.empty();
        }

        public Stream<String> moduleNames() {
            return systemModules.values().stream()
                .map(mref -> mref.descriptor().name());
        }

        public ClassFileReader getClassReader(String modulename) throws IOException {
            Path mp = root.resolve(modulename);
            if (Files.exists(mp) && Files.isDirectory(mp)) {
                return ClassFileReader.newInstance(fileSystem, mp);
            } else {
                throw new FileNotFoundException(mp.toString());
            }
        }

        public Set<String> defaultSystemRoots() {
            return systemModules.values().stream()
                .map(ModuleReference::descriptor)
                .filter(descriptor -> descriptor.exports()
                        .stream()
                        .filter(e -> !e.isQualified())
                        .findAny()
                        .isPresent())
                .map(ModuleDescriptor::name)
                .collect(Collectors.toSet());
        }
    }

    public static class Builder {

        final SystemModuleFinder systemModulePath;
        final Set<String> rootModules = new HashSet<>();
        final List<Archive> initialArchives = new ArrayList<>();
        final List<Path> paths = new ArrayList<>();
        final List<Path> classPaths = new ArrayList<>();
        final Set<String> tokens = new HashSet<>();

        ModuleFinder upgradeModulePath;
        ModuleFinder appModulePath;
        Runtime.Version version;

        public Builder() {
            this.systemModulePath = new SystemModuleFinder();
        }

        public Builder(String javaHome) throws IOException {
            this.systemModulePath = SystemModuleFinder.JAVA_HOME.equals(javaHome)
                ? new SystemModuleFinder()
                : new SystemModuleFinder(javaHome);
        }

        public Builder upgradeModulePath(String upgradeModulePath) {
            this.upgradeModulePath = createModulePathFinder(upgradeModulePath);
            return this;
        }

        public Builder appModulePath(String modulePath) {
            this.appModulePath = createModulePathFinder(modulePath);
            return this;
        }

        public Builder addmods(Set<String> addmods) {
            for (String mn : addmods) {
                if (isToken(mn)) {
                    tokens.add(mn);
                } else {
                    rootModules.add(mn);
                }
            }
            return this;
        }

        public Builder multiRelease(Runtime.Version version) {
            this.version = version;
            return this;
        }

        public Builder addRoot(Path path) {
            Archive archive = Archive.getInstance(path, version);
            if (archive.contains(MODULE_INFO)) {
                paths.add(path);
            } else {
                initialArchives.add(archive);
            }
            return this;
        }

        public Builder addClassPath(String classPath) {
            this.classPaths.addAll(getClassPaths(classPath));
            return this;
        }

        public JdepsConfiguration build() throws  IOException {
            ModuleFinder finder = systemModulePath;
            if (upgradeModulePath != null) {
                finder = ModuleFinder.compose(upgradeModulePath, systemModulePath);
            }
            if (appModulePath != null) {
                finder = ModuleFinder.compose(finder, appModulePath);
            }
            if (!paths.isEmpty()) {
                ModuleFinder otherModulePath = ModuleFinder.of(paths.toArray(new Path[0]));

                finder = ModuleFinder.compose(finder, otherModulePath);
                // add modules specified on command-line (convenience) as root set
                otherModulePath.findAll().stream()
                        .map(mref -> mref.descriptor().name())
                        .forEach(rootModules::add);
            }

            // no archive is specified for analysis
            // add all system modules as root if --add-modules ALL-SYSTEM is specified
            if (tokens.contains(ALL_SYSTEM) && rootModules.isEmpty() &&
                    initialArchives.isEmpty() && classPaths.isEmpty()) {
                systemModulePath.findAll()
                    .stream()
                    .map(mref -> mref.descriptor().name())
                    .forEach(rootModules::add);
            }

            // add all modules on app module path as roots if ALL-MODULE-PATH is specified
            if ((tokens.contains(ALL_MODULE_PATH)) && appModulePath != null) {
                appModulePath.findAll().stream()
                    .map(mref -> mref.descriptor().name())
                    .forEach(rootModules::add);
            }


            // build root set for module resolution
            Set<String> mods = new HashSet<>(rootModules);
            // if archives are specified for analysis, then consider as unnamed module
            boolean unnamed = !initialArchives.isEmpty() || !classPaths.isEmpty();
            if (tokens.contains(ALL_DEFAULT)) {
                mods.addAll(systemModulePath.defaultSystemRoots());
            } else if (tokens.contains(ALL_SYSTEM) || unnamed) {
                // resolve all system modules as unnamed module may reference any class
                systemModulePath.findAll().stream()
                    .map(mref -> mref.descriptor().name())
                    .forEach(mods::add);
            }
            if (unnamed && appModulePath != null) {
                // resolve all modules on module path as unnamed module may reference any class
                appModulePath.findAll().stream()
                    .map(mref -> mref.descriptor().name())
                    .forEach(mods::add);
            }

            // resolve the module graph
            Configuration config = Configuration.empty().resolve(finder, ModuleFinder.of(), mods);
            return new JdepsConfiguration(config,
                                          systemModulePath,
                                          finder,
                                          rootModules,
                                          classPaths,
                                          initialArchives,
                                          version);
        }

        private static ModuleFinder createModulePathFinder(String mpaths) {
            if (mpaths == null) {
                return null;
            } else {
                String[] dirs = mpaths.split(File.pathSeparator);
                Path[] paths = new Path[dirs.length];
                int i = 0;
                for (String dir : dirs) {
                    paths[i++] = Paths.get(dir);
                }
                return ModuleFinder.of(paths);
            }
        }

        /*
         * Returns the list of Archive specified in cpaths and not included
         * initialArchives
         */
        private List<Path> getClassPaths(String cpaths) {
            if (cpaths.isEmpty()) {
                return Collections.emptyList();
            }
            List<Path> paths = new ArrayList<>();
            for (String p : cpaths.split(File.pathSeparator)) {
                if (p.length() > 0) {
                    // wildcard to parse all JAR files e.g. -classpath dir/*
                    int i = p.lastIndexOf(".*");
                    if (i > 0) {
                        Path dir = Paths.get(p.substring(0, i));
                        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
                            for (Path entry : stream) {
                                paths.add(entry);
                            }
                        } catch (IOException e) {
                            throw new UncheckedIOException(e);
                        }
                    } else {
                        paths.add(Paths.get(p));
                    }
                }
            }
            return paths;
        }
    }

}