blob: 65294d7cd8691180f74ef4ad39c60c8497819b00 [file] [log] [blame]
/*
* Copyright (c) 2012, 2017, 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.ModuleDescriptor.Exports;
import java.lang.module.ModuleDescriptor.Opens;
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.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 Configuration configuration;
private final Runtime.Version version;
private JdepsConfiguration(SystemModuleFinder systemModulePath,
ModuleFinder finder,
Set<String> roots,
List<Path> classpaths,
List<Archive> initialArchives,
boolean allDefaultModules,
boolean allSystemModules,
Runtime.Version version)
throws IOException
{
trace("root: %s%n", roots);
this.system = systemModulePath;
this.finder = finder;
this.version = version;
// build root set for resolution
Set<String> mods = new HashSet<>(roots);
// add all system modules to the root set for unnamed module or set explicitly
boolean unnamed = !initialArchives.isEmpty() || !classpaths.isEmpty();
if (allSystemModules || (unnamed && !allDefaultModules)) {
systemModulePath.findAll().stream()
.map(mref -> mref.descriptor().name())
.forEach(mods::add);
}
if (allDefaultModules) {
mods.addAll(systemModulePath.defaultSystemRoots());
}
this.configuration = Configuration.empty()
.resolve(finder, ModuleFinder.of(), mods);
this.configuration.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();
}
boolean isValidToken(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();
Module.Builder builder = new Module.Builder(md, system.find(mn).isPresent());
final ClassFileReader reader;
if (location.getScheme().equals("jrt")) {
reader = system.getClassReader(mn);
} else {
reader = ClassFileReader.newInstance(Paths.get(location), version);
}
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 static final String JAVA_SE = "java.se";
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() {
Set<String> roots = new HashSet<>();
boolean hasJava = false;
if (systemModules.containsKey(JAVA_SE)) {
// java.se is a system module
hasJava = true;
roots.add(JAVA_SE);
}
for (ModuleReference mref : systemModules.values()) {
String mn = mref.descriptor().name();
if (hasJava && mn.startsWith("java."))
continue;
// add as root if observable and exports at least one package
ModuleDescriptor descriptor = mref.descriptor();
for (ModuleDescriptor.Exports e : descriptor.exports()) {
if (!e.isQualified()) {
roots.add(mn);
break;
}
}
}
return roots;
}
}
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<>();
ModuleFinder upgradeModulePath;
ModuleFinder appModulePath;
boolean addAllApplicationModules;
boolean addAllDefaultModules;
boolean addAllSystemModules;
boolean allModules;
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) {
switch (mn) {
case ALL_MODULE_PATH:
this.addAllApplicationModules = true;
break;
case ALL_DEFAULT:
this.addAllDefaultModules = true;
break;
case ALL_SYSTEM:
this.addAllSystemModules = true;
break;
default:
this.rootModules.add(mn);
}
}
return this;
}
/*
* This method is for --check option to find all target modules specified
* in qualified exports.
*
* Include all system modules and modules found on modulepath
*/
public Builder allModules() {
this.allModules = true;
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);
}
if ((addAllApplicationModules || allModules) && appModulePath != null) {
appModulePath.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 (addAllSystemModules && rootModules.isEmpty() &&
initialArchives.isEmpty() && classPaths.isEmpty()) {
systemModulePath.findAll()
.stream()
.map(mref -> mref.descriptor().name())
.forEach(rootModules::add);
}
return new JdepsConfiguration(systemModulePath,
finder,
rootModules,
classPaths,
initialArchives,
addAllDefaultModules,
allModules,
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;
}
}
}