8156680: jdeps implementation refresh

Reviewed-by: dfuchs
This commit is contained in:
Mandy Chung 2016-05-19 10:55:33 -07:00
parent d027cbbffb
commit bbc75367c7
47 changed files with 4474 additions and 2286 deletions

View file

@ -25,23 +25,24 @@
package com.sun.tools.jdeps; package com.sun.tools.jdeps;
import java.io.PrintStream; import static com.sun.tools.jdeps.JdepsConfiguration.*;
import java.util.ArrayList;
import com.sun.tools.classfile.Dependency.Location;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.MissingResourceException;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.sun.tools.classfile.Dependency.Location;
/** /**
* Dependency Analyzer. * Dependency Analyzer.
*/ */
@ -52,6 +53,7 @@ public class Analyzer {
*/ */
public enum Type { public enum Type {
SUMMARY, SUMMARY,
MODULE, // equivalent to summary in addition, print module descriptor
PACKAGE, PACKAGE,
CLASS, CLASS,
VERBOSE VERBOSE
@ -62,9 +64,11 @@ public class Analyzer {
* Only the accepted dependencies are recorded. * Only the accepted dependencies are recorded.
*/ */
interface Filter { interface Filter {
boolean accepts(Location origin, Archive originArchive, Location target, Archive targetArchive); boolean accepts(Location origin, Archive originArchive,
Location target, Archive targetArchive);
} }
protected final JdepsConfiguration configuration;
protected final Type type; protected final Type type;
protected final Filter filter; protected final Filter filter;
protected final Map<Archive, Dependences> results = new HashMap<>(); protected final Map<Archive, Dependences> results = new HashMap<>();
@ -78,7 +82,8 @@ public class Analyzer {
* @param type Type of the dependency analysis * @param type Type of the dependency analysis
* @param filter * @param filter
*/ */
public Analyzer(Type type, Filter filter) { Analyzer(JdepsConfiguration config, Type type, Filter filter) {
this.configuration = config;
this.type = type; this.type = type;
this.filter = filter; this.filter = filter;
} }
@ -86,16 +91,10 @@ public class Analyzer {
/** /**
* Performs the dependency analysis on the given archives. * Performs the dependency analysis on the given archives.
*/ */
public boolean run(Stream<? extends Archive> archives) { boolean run(Iterable<? extends Archive> archives,
return run(archives.collect(Collectors.toList())); Map<Location, Archive> locationMap)
} {
this.locationToArchive.putAll(locationMap);
/**
* Performs the dependency analysis on the given archives.
*/
public boolean run(Iterable<? extends Archive> archives) {
// build a map from Location to Archive
buildLocationArchiveMap(archives);
// traverse and analyze all dependencies // traverse and analyze all dependencies
for (Archive archive : archives) { for (Archive archive : archives) {
@ -106,40 +105,50 @@ public class Analyzer {
return true; return true;
} }
protected void buildLocationArchiveMap(Iterable<? extends Archive> archives) { /**
// build a map from Location to Archive * Returns the analyzed archives
for (Archive archive: archives) { */
archive.getClasses() Set<Archive> archives() {
.forEach(l -> locationToArchive.putIfAbsent(l, archive)); return results.keySet();
}
} }
public boolean hasDependences(Archive archive) { /**
* Returns true if the given archive has dependences.
*/
boolean hasDependences(Archive archive) {
if (results.containsKey(archive)) { if (results.containsKey(archive)) {
return results.get(archive).dependencies().size() > 0; return results.get(archive).dependencies().size() > 0;
} }
return false; return false;
} }
public Set<String> dependences(Archive source) { /**
* Returns the dependences, either class name or package name
* as specified in the given verbose level, from the given source.
*/
Set<String> dependences(Archive source) {
if (!results.containsKey(source)) { if (!results.containsKey(source)) {
return Collections.emptySet(); return Collections.emptySet();
} }
Dependences result = results.get(source);
return result.dependencies().stream() return results.get(source).dependencies()
.stream()
.map(Dep::target) .map(Dep::target)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
public Stream<Archive> requires(Archive source) { /**
* Returns the direct dependences of the given source
*/
Stream<Archive> requires(Archive source) {
if (!results.containsKey(source)) { if (!results.containsKey(source)) {
return Stream.empty(); return Stream.empty();
} }
Dependences result = results.get(source); return results.get(source).requires()
return result.requires().stream().filter(a -> !a.isEmpty()); .stream();
} }
public interface Visitor { interface Visitor {
/** /**
* Visits a recorded dependency from origin to target which can be * Visits a recorded dependency from origin to target which can be
* a fully-qualified classname, a package name, a module or * a fully-qualified classname, a package name, a module or
@ -153,7 +162,7 @@ public class Analyzer {
* Visit the dependencies of the given source. * Visit the dependencies of the given source.
* If the requested level is SUMMARY, it will visit the required archives list. * If the requested level is SUMMARY, it will visit the required archives list.
*/ */
public void visitDependences(Archive source, Visitor v, Type level) { void visitDependences(Archive source, Visitor v, Type level) {
if (level == Type.SUMMARY) { if (level == Type.SUMMARY) {
final Dependences result = results.get(source); final Dependences result = results.get(source);
final Set<Archive> reqs = result.requires(); final Set<Archive> reqs = result.requires();
@ -187,7 +196,7 @@ public class Analyzer {
} }
} }
public void visitDependences(Archive source, Visitor v) { void visitDependences(Archive source, Visitor v) {
visitDependences(source, v, type); visitDependences(source, v, type);
} }
@ -224,14 +233,28 @@ public class Analyzer {
} }
} }
/*
* Returns the archive that contains the given location.
*/
Archive findArchive(Location t) { Archive findArchive(Location t) {
// local in this archive
if (archive.getClasses().contains(t)) if (archive.getClasses().contains(t))
return archive; return archive;
return locationToArchive.computeIfAbsent(t, _k -> NOT_FOUND); Archive target;
if (locationToArchive.containsKey(t)) {
target = locationToArchive.get(t);
} else {
// special case JDK removed API
target = configuration.findClass(t)
.orElseGet(() -> REMOVED_JDK_INTERNALS.contains(t)
? REMOVED_JDK_INTERNALS
: NOT_FOUND);
}
return locationToArchive.computeIfAbsent(t, _k -> target);
} }
// return classname or package name depedning on the level // return classname or package name depending on the level
private String getLocationName(Location o) { private String getLocationName(Location o) {
if (level == Type.CLASS || level == Type.VERBOSE) { if (level == Type.CLASS || level == Type.VERBOSE) {
return o.getClassName(); return o.getClassName();
@ -345,4 +368,66 @@ public class Analyzer {
target, targetArchive.getName()); target, targetArchive.getName());
} }
} }
private static final JdkInternals REMOVED_JDK_INTERNALS = new JdkInternals();
private static class JdkInternals extends Module {
private final String BUNDLE = "com.sun.tools.jdeps.resources.jdkinternals";
private final Set<String> jdkinternals;
private final Set<String> jdkUnsupportedClasses;
private JdkInternals() {
super("JDK removed internal API");
try {
ResourceBundle rb = ResourceBundle.getBundle(BUNDLE);
this.jdkinternals = rb.keySet();
} catch (MissingResourceException e) {
throw new InternalError("Cannot find jdkinternals resource bundle");
}
this.jdkUnsupportedClasses = getUnsupportedClasses();
}
public boolean contains(Location location) {
if (jdkUnsupportedClasses.contains(location.getName() + ".class")) {
return false;
}
String cn = location.getClassName();
int i = cn.lastIndexOf('.');
String pn = i > 0 ? cn.substring(0, i) : "";
return jdkinternals.contains(cn) || jdkinternals.contains(pn);
}
@Override
public String name() {
return getName();
}
@Override
public boolean isExported(String pn) {
return false;
}
private Set<String> getUnsupportedClasses() {
// jdk.unsupported may not be observable
Optional<Module> om = Profile.FULL_JRE.findModule(JDK_UNSUPPORTED);
if (om.isPresent()) {
return om.get().reader().entries();
}
// find from local run-time image
SystemModuleFinder system = new SystemModuleFinder();
if (system.find(JDK_UNSUPPORTED).isPresent()) {
try {
return system.getClassReader(JDK_UNSUPPORTED).entries();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
return Collections.emptySet();
}
}
} }

View file

@ -38,6 +38,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
/** /**
* Represents the source of the class files. * Represents the source of the class files.
@ -86,6 +87,10 @@ public class Archive {
return Module.UNNAMED_MODULE; return Module.UNNAMED_MODULE;
} }
public boolean contains(String entry) {
return reader.entries().contains(entry);
}
public void addClass(Location origin) { public void addClass(Location origin) {
deps.computeIfAbsent(origin, _k -> new HashSet<>()); deps.computeIfAbsent(origin, _k -> new HashSet<>());
} }
@ -98,6 +103,15 @@ public class Archive {
return deps.keySet(); return deps.keySet();
} }
public Stream<Location> getDependencies() {
return deps.values().stream()
.flatMap(Set::stream);
}
public boolean hasDependences() {
return getDependencies().count() > 0;
}
public void visitDependences(Visitor v) { public void visitDependences(Visitor v) {
for (Map.Entry<Location,Set<Location>> e: deps.entrySet()) { for (Map.Entry<Location,Set<Location>> e: deps.entrySet()) {
for (Location target : e.getValue()) { for (Location target : e.getValue()) {

View file

@ -173,7 +173,7 @@ public class ClassFileReader {
static boolean isClass(Path file) { static boolean isClass(Path file) {
String fn = file.getFileName().toString(); String fn = file.getFileName().toString();
return fn.endsWith(".class") && !fn.equals(MODULE_INFO); return fn.endsWith(".class");
} }
class FileIterator implements Iterator<ClassFile> { class FileIterator implements Iterator<ClassFile> {
@ -306,7 +306,7 @@ public class ClassFileReader {
protected Set<String> scan() { protected Set<String> scan() {
try (JarFile jf = new JarFile(path.toFile())) { try (JarFile jf = new JarFile(path.toFile())) {
return jf.stream().map(JarEntry::getName) return jf.stream().map(JarEntry::getName)
.filter(n -> n.endsWith(".class") && !n.endsWith(MODULE_INFO)) .filter(n -> n.endsWith(".class"))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} catch (IOException e) { } catch (IOException e) {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
@ -409,7 +409,7 @@ public class ClassFileReader {
while (entries.hasMoreElements()) { while (entries.hasMoreElements()) {
JarEntry e = entries.nextElement(); JarEntry e = entries.nextElement();
String name = e.getName(); String name = e.getName();
if (name.endsWith(".class") && !name.equals(MODULE_INFO)) { if (name.endsWith(".class")) {
return e; return e;
} }
} }

View file

@ -24,268 +24,156 @@
*/ */
package com.sun.tools.jdeps; package com.sun.tools.jdeps;
import static com.sun.tools.jdeps.Module.*;
import static com.sun.tools.jdeps.Analyzer.NOT_FOUND;
import static java.util.stream.Collectors.*;
import com.sun.tools.classfile.AccessFlags; import com.sun.tools.classfile.AccessFlags;
import com.sun.tools.classfile.ClassFile; import com.sun.tools.classfile.ClassFile;
import com.sun.tools.classfile.ConstantPoolException; import com.sun.tools.classfile.ConstantPoolException;
import com.sun.tools.classfile.Dependencies; import com.sun.tools.classfile.Dependencies;
import com.sun.tools.classfile.Dependency; import com.sun.tools.classfile.Dependency;
import com.sun.tools.classfile.Dependency.Location;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Deque; import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashSet; import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask; import java.util.concurrent.FutureTask;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static com.sun.tools.jdeps.Module.*; /**
import static com.sun.tools.jdeps.ModulePaths.SystemModulePath.JAVA_BASE; * Parses class files and finds dependences
public class DependencyFinder {
private final List<Archive> roots = new ArrayList<>();
private final List<Archive> classpaths = new ArrayList<>();
private final List<Module> modulepaths = new ArrayList<>();
private final List<String> classes = new ArrayList<>();
private final boolean compileTimeView;
DependencyFinder(boolean compileTimeView) {
this.compileTimeView = compileTimeView;
}
/*
* Adds a class name to the root set
*/ */
void addClassName(String cn) { class DependencyFinder {
classes.add(cn); private static Finder API_FINDER = new Finder(true);
} private static Finder CLASS_FINDER = new Finder(false);
/* private final JdepsConfiguration configuration;
* Adds the archive of the given path to the root set private final JdepsFilter filter;
*/
void addRoot(Path path) {
addRoot(Archive.getInstance(path));
}
/* private final Map<Finder, Deque<Archive>> parsedArchives = new ConcurrentHashMap<>();
* Adds the given archive to the root set private final Map<Location, Archive> parsedClasses = new ConcurrentHashMap<>();
*/
void addRoot(Archive archive) {
Objects.requireNonNull(archive);
if (!roots.contains(archive))
roots.add(archive);
}
/** private final ExecutorService pool = Executors.newFixedThreadPool(2);
* Add an archive specified in the classpath. private final Deque<FutureTask<Set<Location>>> tasks = new ConcurrentLinkedDeque<>();
*/
void addClassPathArchive(Path path) {
addClassPathArchive(Archive.getInstance(path));
}
/** DependencyFinder(JdepsConfiguration configuration,
* Add an archive specified in the classpath. JdepsFilter filter) {
*/ this.configuration = configuration;
void addClassPathArchive(Archive archive) {
Objects.requireNonNull(archive);
classpaths.add(archive);
}
/**
* Add an archive specified in the modulepath.
*/
void addModule(Module m) {
Objects.requireNonNull(m);
modulepaths.add(m);
}
/**
* Returns the root set.
*/
List<Archive> roots() {
return roots;
}
/**
* Returns a stream of all archives including the root set, module paths,
* and classpath.
*
* This only returns the archives with classes parsed.
*/
Stream<Archive> archives() {
Stream<Archive> archives = Stream.concat(roots.stream(), modulepaths.stream());
archives = Stream.concat(archives, classpaths.stream());
return archives.filter(a -> !a.isEmpty())
.distinct();
}
/**
* Finds dependencies
*
* @param apiOnly API only
* @param maxDepth depth of transitive dependency analysis; zero indicates
* @throws IOException
*/
void findDependencies(JdepsFilter filter, boolean apiOnly, int maxDepth)
throws IOException
{
Dependency.Finder finder =
apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
: Dependencies.getClassDependencyFinder();
// list of archives to be analyzed
Set<Archive> roots = new LinkedHashSet<>(this.roots);
// include java.base in root set
roots.add(JAVA_BASE);
// If -include pattern specified, classes may be in module path or class path.
// To get compile time view analysis, all classes are analyzed.
// add all modules except JDK modules to root set
modulepaths.stream()
.filter(filter::matches)
.forEach(roots::add);
// add classpath to the root set
classpaths.stream()
.filter(filter::matches)
.forEach(roots::add);
// transitive dependency
int depth = maxDepth > 0 ? maxDepth : Integer.MAX_VALUE;
// Work queue of names of classfiles to be searched.
// Entries will be unique, and for classes that do not yet have
// dependencies in the results map.
ConcurrentLinkedDeque<String> deque = new ConcurrentLinkedDeque<>();
ConcurrentSkipListSet<String> doneClasses = new ConcurrentSkipListSet<>();
TaskExecutor executor = new TaskExecutor(finder, filter, apiOnly, deque, doneClasses);
try {
// get the immediate dependencies of the input files
for (Archive source : roots) {
executor.task(source, deque);
}
executor.waitForTasksCompleted();
List<Archive> archives = Stream.concat(Stream.concat(roots.stream(),
modulepaths.stream()),
classpaths.stream())
.collect(Collectors.toList());
// Additional pass to find archive where dependences are identified
// and also any specified classes, if any.
// If -R is specified, perform transitive dependency analysis.
Deque<String> unresolved = new LinkedList<>(classes);
do {
String name;
while ((name = unresolved.poll()) != null) {
if (doneClasses.contains(name)) {
continue;
}
if (compileTimeView) {
final String cn = name + ".class";
// parse all classes in the source archive
Optional<Archive> source = archives.stream()
.filter(a -> a.reader().entries().contains(cn))
.findFirst();
trace("%s compile time view %s%n", name, source.map(Archive::getName).orElse(" not found"));
if (source.isPresent()) {
executor.runTask(source.get(), deque);
}
}
ClassFile cf = null;
for (Archive archive : archives) {
cf = archive.reader().getClassFile(name);
if (cf != null) {
String classFileName;
try {
classFileName = cf.getName();
} catch (ConstantPoolException e) {
throw new Dependencies.ClassFileError(e);
}
if (!doneClasses.contains(classFileName)) {
// if name is a fully-qualified class name specified
// from command-line, this class might already be parsed
doneClasses.add(classFileName);
for (Dependency d : finder.findDependencies(cf)) {
if (depth == 0) {
// ignore the dependency
archive.addClass(d.getOrigin());
break;
} else if (filter.accepts(d) && filter.accept(archive)) {
// continue analysis on non-JDK classes
archive.addClass(d.getOrigin(), d.getTarget());
String cn = d.getTarget().getName();
if (!doneClasses.contains(cn) && !deque.contains(cn)) {
deque.add(cn);
}
} else {
// ensure that the parsed class is added the archive
archive.addClass(d.getOrigin());
}
}
}
break;
}
}
if (cf == null) {
doneClasses.add(name);
}
}
unresolved = deque;
deque = new ConcurrentLinkedDeque<>();
} while (!unresolved.isEmpty() && depth-- > 0);
} finally {
executor.shutdown();
}
}
/**
* TaskExecutor creates FutureTask to analyze all classes in a given archive
*/
private class TaskExecutor {
final ExecutorService pool;
final Dependency.Finder finder;
final JdepsFilter filter;
final boolean apiOnly;
final Set<String> doneClasses;
final Map<Archive, FutureTask<Void>> tasks = new HashMap<>();
TaskExecutor(Dependency.Finder finder,
JdepsFilter filter,
boolean apiOnly,
ConcurrentLinkedDeque<String> deque,
Set<String> doneClasses) {
this.pool = Executors.newFixedThreadPool(2);
this.finder = finder;
this.filter = filter; this.filter = filter;
this.apiOnly = apiOnly; this.parsedArchives.put(API_FINDER, new ConcurrentLinkedDeque<>());
this.doneClasses = doneClasses; this.parsedArchives.put(CLASS_FINDER, new ConcurrentLinkedDeque<>());
}
Map<Location, Archive> locationToArchive() {
return parsedClasses;
} }
/** /**
* Creates a new task to analyze class files in the given archive. * Returns the modules of all dependencies found
* The dependences are added to the given deque for analysis.
*/ */
FutureTask<Void> task(Archive archive, final ConcurrentLinkedDeque<String> deque) { Stream<Archive> getDependences(Archive source) {
return source.getDependencies()
.map(this::locationToArchive)
.filter(a -> a != source);
}
/**
* Returns the location to archive map; or NOT_FOUND.
*
* Location represents a parsed class.
*/
Archive locationToArchive(Location location) {
return parsedClasses.containsKey(location)
? parsedClasses.get(location)
: configuration.findClass(location).orElse(NOT_FOUND);
}
/**
* Returns a map from an archive to its required archives
*/
Map<Archive, Set<Archive>> dependences() {
Map<Archive, Set<Archive>> map = new HashMap<>();
parsedArchives.values().stream()
.flatMap(Deque::stream)
.filter(a -> !a.isEmpty())
.forEach(source -> {
Set<Archive> deps = getDependences(source).collect(toSet());
if (!deps.isEmpty()) {
map.put(source, deps);
}
});
return map;
}
boolean isParsed(Location location) {
return parsedClasses.containsKey(location);
}
/**
* Parses all class files from the given archive stream and returns
* all target locations.
*/
public Set<Location> parse(Stream<? extends Archive> archiveStream) {
archiveStream.forEach(archive -> parse(archive, CLASS_FINDER));
return waitForTasksCompleted();
}
/**
* Parses the exported API class files from the given archive stream and
* returns all target locations.
*/
public Set<Location> parseExportedAPIs(Stream<? extends Archive> archiveStream) {
archiveStream.forEach(archive -> parse(archive, API_FINDER));
return waitForTasksCompleted();
}
/**
* Parses the named class from the given archive and
* returns all target locations the named class references.
*/
public Set<Location> parse(Archive archive, String name) {
try {
return parse(archive, CLASS_FINDER, name);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Parses the exported API of the named class from the given archive and
* returns all target locations the named class references.
*/
public Set<Location> parseExportedAPIs(Archive archive, String name)
{
try {
return parse(archive, API_FINDER, name);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private Optional<FutureTask<Set<Location>>> parse(Archive archive, Finder finder) {
if (parsedArchives.get(finder).contains(archive))
return Optional.empty();
parsedArchives.get(finder).add(archive);
trace("parsing %s %s%n", archive.getName(), archive.path()); trace("parsing %s %s%n", archive.getName(), archive.path());
FutureTask<Void> task = new FutureTask<Void>(new Callable<Void>() { FutureTask<Set<Location>> task = new FutureTask<>(new Callable<>() {
public Void call() throws Exception { public Set<Location> call() throws Exception {
Set<Location> targets = new HashSet<>();
for (ClassFile cf : archive.reader().getClassFiles()) { for (ClassFile cf : archive.reader().getClassFiles()) {
String classFileName; String classFileName;
try { try {
@ -294,69 +182,85 @@ public class DependencyFinder {
throw new Dependencies.ClassFileError(e); throw new Dependencies.ClassFileError(e);
} }
// tests if this class matches the -include // filter source class/archive
String cn = classFileName.replace('/', '.'); String cn = classFileName.replace('/', '.');
if (!finder.accept(archive, cn, cf.access_flags))
continue;
// tests if this class matches the -include
if (!filter.matches(cn)) if (!filter.matches(cn))
continue; continue;
// if -apionly is specified, analyze only exported and public types
if (apiOnly && !(isExported(archive, cn) && cf.access_flags.is(AccessFlags.ACC_PUBLIC)))
continue;
if (!doneClasses.contains(classFileName)) {
doneClasses.add(classFileName);
}
for (Dependency d : finder.findDependencies(cf)) { for (Dependency d : finder.findDependencies(cf)) {
if (filter.accepts(d) && filter.accept(archive)) { if (filter.accepts(d)) {
String name = d.getTarget().getName(); archive.addClass(d.getOrigin(), d.getTarget());
if (!doneClasses.contains(name) && !deque.contains(name)) { targets.add(d.getTarget());
deque.add(name); } else {
// ensure that the parsed class is added the archive
archive.addClass(d.getOrigin());
} }
parsedClasses.putIfAbsent(d.getOrigin(), archive);
}
}
return targets;
}
});
tasks.add(task);
pool.submit(task);
return Optional.of(task);
}
private Set<Location> parse(Archive archive, Finder finder, String name)
throws IOException
{
ClassFile cf = archive.reader().getClassFile(name);
if (cf == null) {
throw new IllegalArgumentException(archive.getName() + " does not contain " + name);
}
Set<Location> targets = new HashSet<>();
String cn;
try {
cn = cf.getName().replace('/', '.');
} catch (ConstantPoolException e) {
throw new Dependencies.ClassFileError(e);
}
if (!finder.accept(archive, cn, cf.access_flags))
return targets;
// tests if this class matches the -include
if (!filter.matches(cn))
return targets;
// skip checking filter.matches
for (Dependency d : finder.findDependencies(cf)) {
if (filter.accepts(d)) {
targets.add(d.getTarget());
archive.addClass(d.getOrigin(), d.getTarget()); archive.addClass(d.getOrigin(), d.getTarget());
} else { } else {
// ensure that the parsed class is added the archive // ensure that the parsed class is added the archive
archive.addClass(d.getOrigin()); archive.addClass(d.getOrigin());
} }
parsedClasses.putIfAbsent(d.getOrigin(), archive);
} }
} return targets;
return null;
}
});
tasks.put(archive, task);
pool.submit(task);
return task;
}
/*
* This task will parse all class files of the given archive, if it's a new task.
* This method waits until the task is completed.
*/
void runTask(Archive archive, final ConcurrentLinkedDeque<String> deque) {
if (tasks.containsKey(archive))
return;
FutureTask<Void> task = task(archive, deque);
try {
// wait for completion
task.get();
} catch (InterruptedException|ExecutionException e) {
throw new Error(e);
}
} }
/* /*
* Waits until all submitted tasks are completed. * Waits until all submitted tasks are completed.
*/ */
void waitForTasksCompleted() { private Set<Location> waitForTasksCompleted() {
try { try {
for (FutureTask<Void> t : tasks.values()) { Set<Location> targets = new HashSet<>();
if (t.isDone()) FutureTask<Set<Location>> task;
continue; while ((task = tasks.poll()) != null) {
// wait for completion // wait for completion
t.get(); if (!task.isDone())
targets.addAll(task.get());
} }
return targets;
} catch (InterruptedException|ExecutionException e) { } catch (InterruptedException|ExecutionException e) {
throw new Error(e); throw new Error(e);
} }
@ -369,15 +273,36 @@ public class DependencyFinder {
pool.shutdown(); pool.shutdown();
} }
/** private interface SourceFilter {
* Tests if the given class name is exported by the given archive. boolean accept(Archive archive, String cn, AccessFlags accessFlags);
* }
* All packages are exported in unnamed module.
*/ private static class Finder implements Dependency.Finder, SourceFilter {
private boolean isExported(Archive archive, String classname) { private final Dependency.Finder finder;
int i = classname.lastIndexOf('.'); private final boolean apiOnly;
String pn = i > 0 ? classname.substring(0, i) : ""; Finder(boolean apiOnly) {
return archive.getModule().isExported(pn); this.apiOnly = apiOnly;
this.finder = apiOnly
? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
: Dependencies.getClassDependencyFinder();
}
@Override
public boolean accept(Archive archive, String cn, AccessFlags accessFlags) {
int i = cn.lastIndexOf('.');
String pn = i > 0 ? cn.substring(0, i) : "";
// if -apionly is specified, analyze only exported and public types
// All packages are exported in unnamed module.
return apiOnly ? archive.getModule().isExported(pn) &&
accessFlags.is(AccessFlags.ACC_PUBLIC)
: true;
}
@Override
public Iterable<? extends Dependency> findDependencies(ClassFile classfile) {
return finder.findDependencies(classfile);
} }
} }
} }

View file

@ -0,0 +1,390 @@
/*
* Copyright (c) 2016, 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 com.sun.tools.classfile.Dependency.Location;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.sun.tools.jdeps.Analyzer.Type.CLASS;
import static com.sun.tools.jdeps.Analyzer.Type.VERBOSE;
import static com.sun.tools.jdeps.Module.trace;
import static java.util.stream.Collectors.*;
/**
* Dependency Analyzer.
*
* Type of filters:
* source filter: -include <pattern>
* target filter: -package, -regex, -requires
*
* The initial archive set for analysis includes
* 1. archives specified in the command line arguments
* 2. observable modules matching the source filter
* 3. classpath archives matching the source filter or target filter
* 4. -addmods and -m root modules
*/
public class DepsAnalyzer {
final JdepsConfiguration configuration;
final JdepsFilter filter;
final JdepsWriter writer;
final Analyzer.Type verbose;
final boolean apiOnly;
final DependencyFinder finder;
final Analyzer analyzer;
final List<Archive> rootArchives = new ArrayList<>();
// parsed archives
final Set<Archive> archives = new LinkedHashSet<>();
public DepsAnalyzer(JdepsConfiguration config,
JdepsFilter filter,
JdepsWriter writer,
Analyzer.Type verbose,
boolean apiOnly) {
this.configuration = config;
this.filter = filter;
this.writer = writer;
this.verbose = verbose;
this.apiOnly = apiOnly;
this.finder = new DependencyFinder(config, filter);
this.analyzer = new Analyzer(configuration, verbose, filter);
// determine initial archives to be analyzed
this.rootArchives.addAll(configuration.initialArchives());
// if -include pattern is specified, add the matching archives on
// classpath to the root archives
if (filter.hasIncludePattern() || filter.hasTargetFilter()) {
configuration.getModules().values().stream()
.filter(source -> filter.include(source) && filter.matches(source))
.forEach(this.rootArchives::add);
}
// class path archives
configuration.classPathArchives().stream()
.filter(filter::matches)
.forEach(this.rootArchives::add);
// Include the root modules for analysis
this.rootArchives.addAll(configuration.rootModules());
trace("analyze root archives: %s%n", this.rootArchives);
}
/*
* Perform runtime dependency analysis
*/
public boolean run() throws IOException {
return run(false, 1);
}
/**
* Perform compile-time view or run-time view dependency analysis.
*
* @param compileTimeView
* @param maxDepth depth of recursive analysis. depth == 0 if -R is set
*/
public boolean run(boolean compileTimeView, int maxDepth) throws IOException {
try {
// parse each packaged module or classpath archive
if (apiOnly) {
finder.parseExportedAPIs(rootArchives.stream());
} else {
finder.parse(rootArchives.stream());
}
archives.addAll(rootArchives);
int depth = maxDepth > 0 ? maxDepth : Integer.MAX_VALUE;
// transitive analysis
if (depth > 1) {
if (compileTimeView)
transitiveArchiveDeps(depth-1);
else
transitiveDeps(depth-1);
}
Set<Archive> archives = archives();
// analyze the dependencies collected
analyzer.run(archives, finder.locationToArchive());
writer.generateOutput(archives, analyzer);
} finally {
finder.shutdown();
}
return true;
}
/**
* Returns the archives for reporting that has matching dependences.
*
* If -requires is set, they should be excluded.
*/
Set<Archive> archives() {
if (filter.requiresFilter().isEmpty()) {
return archives.stream()
.filter(filter::include)
.filter(Archive::hasDependences)
.collect(Collectors.toSet());
} else {
// use the archives that have dependences and not specified in -requires
return archives.stream()
.filter(filter::include)
.filter(source -> !filter.requiresFilter().contains(source))
.filter(source ->
source.getDependencies()
.map(finder::locationToArchive)
.anyMatch(a -> a != source))
.collect(Collectors.toSet());
}
}
/**
* Returns the dependences, either class name or package name
* as specified in the given verbose level.
*/
Stream<String> dependences() {
return analyzer.archives().stream()
.map(analyzer::dependences)
.flatMap(Set::stream)
.distinct();
}
/**
* Returns the archives that contains the given locations and
* not parsed and analyzed.
*/
private Set<Archive> unresolvedArchives(Stream<Location> locations) {
return locations.filter(l -> !finder.isParsed(l))
.distinct()
.map(configuration::findClass)
.flatMap(Optional::stream)
.filter(filter::include)
.collect(toSet());
}
/*
* Recursively analyzes entire module/archives.
*/
private void transitiveArchiveDeps(int depth) throws IOException {
Stream<Location> deps = archives.stream()
.flatMap(Archive::getDependencies);
// start with the unresolved archives
Set<Archive> unresolved = unresolvedArchives(deps);
do {
// parse all unresolved archives
Set<Location> targets = apiOnly
? finder.parseExportedAPIs(unresolved.stream())
: finder.parse(unresolved.stream());
archives.addAll(unresolved);
// Add dependencies to the next batch for analysis
unresolved = unresolvedArchives(targets.stream());
} while (!unresolved.isEmpty() && depth-- > 0);
}
/*
* Recursively analyze the class dependences
*/
private void transitiveDeps(int depth) throws IOException {
Stream<Location> deps = archives.stream()
.flatMap(Archive::getDependencies);
Deque<Location> unresolved = deps.collect(Collectors.toCollection(LinkedList::new));
ConcurrentLinkedDeque<Location> deque = new ConcurrentLinkedDeque<>();
do {
Location target;
while ((target = unresolved.poll()) != null) {
if (finder.isParsed(target))
continue;
Archive archive = configuration.findClass(target).orElse(null);
if (archive != null && filter.include(archive)) {
archives.add(archive);
String name = target.getName();
Set<Location> targets = apiOnly
? finder.parseExportedAPIs(archive, name)
: finder.parse(archive, name);
// build unresolved dependencies
targets.stream()
.filter(t -> !finder.isParsed(t))
.forEach(deque::add);
}
}
unresolved = deque;
deque = new ConcurrentLinkedDeque<>();
} while (!unresolved.isEmpty() && depth-- > 0);
}
// ----- for testing purpose -----
public static enum Info {
REQUIRES,
REQUIRES_PUBLIC,
EXPORTED_API,
MODULE_PRIVATE,
QUALIFIED_EXPORTED_API,
INTERNAL_API,
JDK_INTERNAL_API
}
public static class Node {
public final String name;
public final String source;
public final Info info;
Node(String name, Info info) {
this(name, name, info);
}
Node(String name, String source, Info info) {
this.name = name;
this.source = source;
this.info = info;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (info != Info.REQUIRES && info != Info.REQUIRES_PUBLIC)
sb.append(source).append("/");
sb.append(name);
if (info == Info.QUALIFIED_EXPORTED_API)
sb.append(" (qualified)");
else if (info == Info.JDK_INTERNAL_API)
sb.append(" (JDK internal)");
else if (info == Info.INTERNAL_API)
sb.append(" (internal)");
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Node))
return false;
Node other = (Node)o;
return this.name.equals(other.name) &&
this.source.equals(other.source) &&
this.info.equals(other.info);
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + source.hashCode();
result = 31 * result + info.hashCode();
return result;
}
}
/**
* Returns a graph of module dependences.
*
* Each Node represents a module and each edge is a dependence.
* No analysis on "requires public".
*/
public Graph<Node> moduleGraph() {
Graph.Builder<Node> builder = new Graph.Builder<>();
archives().stream()
.forEach(m -> {
Node u = new Node(m.getName(), Info.REQUIRES);
builder.addNode(u);
analyzer.requires(m)
.map(req -> new Node(req.getName(), Info.REQUIRES))
.forEach(v -> builder.addEdge(u, v));
});
return builder.build();
}
/**
* Returns a graph of dependences.
*
* Each Node represents a class or package per the specified verbose level.
* Each edge indicates
*/
public Graph<Node> dependenceGraph() {
Graph.Builder<Node> builder = new Graph.Builder<>();
archives().stream()
.map(analyzer.results::get)
.filter(deps -> !deps.dependencies().isEmpty())
.flatMap(deps -> deps.dependencies().stream())
.forEach(d -> addEdge(builder, d));
return builder.build();
}
private void addEdge(Graph.Builder<Node> builder, Analyzer.Dep dep) {
Archive source = dep.originArchive();
Archive target = dep.targetArchive();
String pn = dep.target();
if ((verbose == CLASS || verbose == VERBOSE)) {
int i = dep.target().lastIndexOf('.');
pn = i > 0 ? dep.target().substring(0, i) : "";
}
final Info info;
if (source == target) {
info = Info.MODULE_PRIVATE;
} else if (!target.getModule().isNamed()) {
info = Info.EXPORTED_API;
} else if (target.getModule().isExported(pn)) {
info = Info.EXPORTED_API;
} else {
Module module = target.getModule();
if (!source.getModule().isJDK() && module.isJDK())
info = Info.JDK_INTERNAL_API;
// qualified exports or inaccessible
else if (module.isExported(pn, source.getModule().name()))
info = Info.QUALIFIED_EXPORTED_API;
else
info = Info.INTERNAL_API;
}
Node u = new Node(dep.origin(), source.getName(), info);
Node v = new Node(dep.target(), target.getName(), info);
builder.addEdge(u, v);
}
}

View file

@ -0,0 +1,401 @@
/*
* Copyright (c) 2016, 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 java.io.PrintWriter;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class Graph<T> {
private final Set<T> nodes;
private final Map<T, Set<T>> edges;
public Graph(Set<T> nodes, Map<T, Set<T>> edges) {
this.nodes = Collections.unmodifiableSet(nodes);
this.edges = Collections.unmodifiableMap(edges);
}
public Set<T> nodes() {
return nodes;
}
public Map<T, Set<T>> edges() {
return edges;
}
public Set<T> adjacentNodes(T u) {
return edges.get(u);
}
public boolean contains(T u) {
return nodes.contains(u);
}
public Set<Edge<T>> edgesFrom(T u) {
return edges.get(u).stream()
.map(v -> new Edge<T>(u, v))
.collect(Collectors.toSet());
}
/**
* Returns a new Graph after transitive reduction
*/
public Graph<T> reduce() {
Builder<T> builder = new Builder<>();
nodes.stream()
.forEach(u -> {
builder.addNode(u);
edges.get(u).stream()
.filter(v -> !pathExists(u, v, false))
.forEach(v -> builder.addEdge(u, v));
});
return builder.build();
}
/**
* Returns a new Graph after transitive reduction. All edges in
* the given g takes precedence over this graph.
*
* @throw IllegalArgumentException g must be a subgraph this graph
*/
public Graph<T> reduce(Graph<T> g) {
boolean subgraph = nodes.containsAll(g.nodes) &&
g.edges.keySet().stream()
.allMatch(u -> adjacentNodes(u).containsAll(g.adjacentNodes(u)));
if (!subgraph) {
throw new IllegalArgumentException(g + " is not a subgraph of " + this);
}
Builder<T> builder = new Builder<>();
nodes.stream()
.forEach(u -> {
builder.addNode(u);
// filter the edge if there exists a path from u to v in the given g
// or there exists another path from u to v in this graph
edges.get(u).stream()
.filter(v -> !g.pathExists(u, v) && !pathExists(u, v, false))
.forEach(v -> builder.addEdge(u, v));
});
// add the overlapped edges from this graph and the given g
g.edges().keySet().stream()
.forEach(u -> g.adjacentNodes(u).stream()
.filter(v -> isAdjacent(u, v))
.forEach(v -> builder.addEdge(u, v)));
return builder.build();
}
/**
* Returns nodes sorted in topological order.
*/
public Stream<T> orderedNodes() {
TopoSorter<T> sorter = new TopoSorter<>(this);
return sorter.result.stream();
}
/**
* Traverse this graph and performs the given action in topological order
*/
public void ordered(Consumer<T> action) {
TopoSorter<T> sorter = new TopoSorter<>(this);
sorter.ordered(action);
}
/**
* Traverses this graph and performs the given action in reverse topological order
*/
public void reverse(Consumer<T> action) {
TopoSorter<T> sorter = new TopoSorter<>(this);
sorter.reverse(action);
}
/**
* Returns a transposed graph from this graph
*/
public Graph<T> transpose() {
Builder<T> builder = new Builder<>();
builder.addNodes(nodes);
// reverse edges
edges.keySet().forEach(u -> {
edges.get(u).stream()
.forEach(v -> builder.addEdge(v, u));
});
return builder.build();
}
/**
* Returns all nodes reachable from the given set of roots.
*/
public Set<T> dfs(Set<T> roots) {
Deque<T> deque = new LinkedList<>(roots);
Set<T> visited = new HashSet<>();
while (!deque.isEmpty()) {
T u = deque.pop();
if (!visited.contains(u)) {
visited.add(u);
if (contains(u)) {
adjacentNodes(u).stream()
.filter(v -> !visited.contains(v))
.forEach(deque::push);
}
}
}
return visited;
}
private boolean isAdjacent(T u, T v) {
return edges.containsKey(u) && edges.get(u).contains(v);
}
private boolean pathExists(T u, T v) {
return pathExists(u, v, true);
}
/**
* Returns true if there exists a path from u to v in this graph.
* If includeAdjacent is false, it returns true if there exists
* another path from u to v of distance > 1
*/
private boolean pathExists(T u, T v, boolean includeAdjacent) {
if (!nodes.contains(u) || !nodes.contains(v)) {
return false;
}
if (includeAdjacent && isAdjacent(u, v)) {
return true;
}
Deque<T> stack = new LinkedList<>();
Set<T> visited = new HashSet<>();
stack.push(u);
while (!stack.isEmpty()) {
T node = stack.pop();
if (node.equals(v)) {
return true;
}
if (!visited.contains(node)) {
visited.add(node);
edges.get(node).stream()
.filter(e -> includeAdjacent || !node.equals(u) || !e.equals(v))
.forEach(e -> stack.push(e));
}
}
assert !visited.contains(v);
return false;
}
public void printGraph(PrintWriter out) {
out.println("graph for " + nodes);
nodes.stream()
.forEach(u -> adjacentNodes(u).stream()
.forEach(v -> out.format(" %s -> %s%n", u, v)));
}
@Override
public String toString() {
return nodes.toString();
}
static class Edge<T> {
final T u;
final T v;
Edge(T u, T v) {
this.u = u;
this.v = v;
}
@Override
public String toString() {
return String.format("%s -> %s", u, v);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof Edge))
return false;
@SuppressWarnings("unchecked")
Edge<T> edge = (Edge<T>) o;
return u.equals(edge.u) && v.equals(edge.v);
}
@Override
public int hashCode() {
int result = u.hashCode();
result = 31 * result + v.hashCode();
return result;
}
}
static class Builder<T> {
final Set<T> nodes = new HashSet<>();
final Map<T, Set<T>> edges = new HashMap<>();
public void addNode(T node) {
if (nodes.contains(node)) {
return;
}
nodes.add(node);
edges.computeIfAbsent(node, _e -> new HashSet<>());
}
public void addNodes(Set<T> nodes) {
nodes.addAll(nodes);
}
public void addEdge(T u, T v) {
addNode(u);
addNode(v);
edges.get(u).add(v);
}
public Graph<T> build() {
return new Graph<T>(nodes, edges);
}
}
/**
* Topological sort
*/
static class TopoSorter<T> {
final Deque<T> result = new LinkedList<>();
final Deque<T> nodes;
final Graph<T> graph;
TopoSorter(Graph<T> graph) {
this.graph = graph;
this.nodes = new LinkedList<>(graph.nodes);
sort();
}
public void ordered(Consumer<T> action) {
result.iterator().forEachRemaining(action);
}
public void reverse(Consumer<T> action) {
result.descendingIterator().forEachRemaining(action);
}
private void sort() {
Deque<T> visited = new LinkedList<>();
Deque<T> done = new LinkedList<>();
T node;
while ((node = nodes.poll()) != null) {
if (!visited.contains(node)) {
visit(node, visited, done);
}
}
}
private void visit(T node, Deque<T> visited, Deque<T> done) {
if (visited.contains(node)) {
if (!done.contains(node)) {
throw new IllegalArgumentException("Cyclic detected: " +
node + " " + graph.edges().get(node));
}
return;
}
visited.add(node);
graph.edges().get(node).stream()
.forEach(x -> visit(x, visited, done));
done.add(node);
result.addLast(node);
}
}
public static class DotGraph {
static final String ORANGE = "#e76f00";
static final String BLUE = "#437291";
static final String GRAY = "#dddddd";
static final String REEXPORTS = "";
static final String REQUIRES = "style=\"dashed\"";
static final String REQUIRES_BASE = "color=\"" + GRAY + "\"";
static final Set<String> javaModules = modules(name ->
(name.startsWith("java.") && !name.equals("java.smartcardio")));
static final Set<String> jdkModules = modules(name ->
(name.startsWith("java.") ||
name.startsWith("jdk.") ||
name.startsWith("javafx.")) && !javaModules.contains(name));
private static Set<String> modules(Predicate<String> predicate) {
return ModuleFinder.ofSystem().findAll()
.stream()
.map(ModuleReference::descriptor)
.map(ModuleDescriptor::name)
.filter(predicate)
.collect(Collectors.toSet());
}
static void printAttributes(PrintWriter out) {
out.format(" size=\"25,25\";%n");
out.format(" nodesep=.5;%n");
out.format(" ranksep=1.5;%n");
out.format(" pencolor=transparent;%n");
out.format(" node [shape=plaintext, fontname=\"DejaVuSans\", fontsize=36, margin=\".2,.2\"];%n");
out.format(" edge [penwidth=4, color=\"#999999\", arrowhead=open, arrowsize=2];%n");
}
static void printNodes(PrintWriter out, Graph<String> graph) {
out.format(" subgraph se {%n");
graph.nodes().stream()
.filter(javaModules::contains)
.forEach(mn -> out.format(" \"%s\" [fontcolor=\"%s\", group=%s];%n",
mn, ORANGE, "java"));
out.format(" }%n");
graph.nodes().stream()
.filter(jdkModules::contains)
.forEach(mn -> out.format(" \"%s\" [fontcolor=\"%s\", group=%s];%n",
mn, BLUE, "jdk"));
graph.nodes().stream()
.filter(mn -> !javaModules.contains(mn) && !jdkModules.contains(mn))
.forEach(mn -> out.format(" \"%s\";%n", mn));
}
static void printEdges(PrintWriter out, Graph<String> graph,
String node, Set<String> requiresPublic) {
graph.adjacentNodes(node).forEach(dn -> {
String attr = dn.equals("java.base") ? REQUIRES_BASE
: (requiresPublic.contains(dn) ? REEXPORTS : REQUIRES);
out.format(" \"%s\" -> \"%s\" [%s];%n", node, dn, attr);
});
}
}
}

View file

@ -0,0 +1,586 @@
/*
* Copyright (c) 2012, 2016, 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.Stream;
public class JdepsConfiguration {
private 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 JdepsConfiguration(SystemModuleFinder systemModulePath,
ModuleFinder finder,
Set<String> roots,
List<Path> classpaths,
List<Archive> initialArchives)
throws IOException
{
trace("root: %s%n", roots);
this.system = systemModulePath;
this.finder = finder;
// build root set
Set<String> mods = new HashSet<>();
if (initialArchives.isEmpty() && classpaths.isEmpty() && !roots.isEmpty()) {
// named module as root. No initial unnamed module
mods.addAll(roots);
} else {
// unnamed module
mods.addAll(roots);
mods.addAll(systemModulePath.defaultSystemRoots());
}
this.configuration = Configuration.empty()
.resolveRequires(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);
addPackagesInUnnamedModule(archive);
classpathArchives.add(archive);
}
}
// all roots specified in -addmods 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 isSystem(Module m) {
return system.find(m.name()).isPresent();
}
/**
* Returns the modules that the given module can read
*/
public Stream<Module> reads(Module module) {
return configuration.findModule(module.name()).get()
.reads().stream()
.map(ResolvedModule::name)
.map(nameToModule::get);
}
/**
* 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;
}
public Stream<Module> resolve(Set<String> roots) {
if (roots.isEmpty()) {
return nameToModule.values().stream();
} else {
return Configuration.empty()
.resolveRequires(finder, ModuleFinder.of(), roots)
.modules().stream()
.map(ResolvedModule::name)
.map(nameToModule::get);
}
}
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));
}
builder.classes(reader);
builder.location(location);
return builder.build();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
static class SystemModuleFinder implements ModuleFinder {
private static final String JAVA_SE = "java.se";
private final FileSystem fileSystem;
private final Path root;
private final Map<String, ModuleReference> systemModules;
SystemModuleFinder() {
this(System.getProperty("java.home"));
}
SystemModuleFinder(String javaHome) {
final FileSystem fs;
final Path root;
final Map<String, ModuleReference> systemModules;
if (javaHome != null) {
if (Files.isRegularFile(Paths.get(javaHome, "lib", "modules"))) {
try {
// jrt file system
fs = FileSystems.getFileSystem(URI.create("jrt:/"));
root = fs.getPath("/modules");
systemModules = Files.walk(root, 1)
.filter(path -> !path.equals(root))
.map(this::toModuleReference)
.collect(toMap(mref -> mref.descriptor().name(),
Function.identity()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} else {
// exploded image
fs = FileSystems.getDefault();
root = Paths.get(javaHome, "modules");
systemModules = ModuleFinder.ofSystem().findAll().stream()
.collect(toMap(mref -> mref.descriptor().name(), Function.identity()));
}
} else {
fs = null;
root = null;
systemModules = Collections.emptyMap();
}
this.fileSystem = fs;
this.root = root;
this.systemModules = systemModules;
}
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 Supplier<>() {
@Override
public ModuleReader get() {
return new ModuleReader() {
@Override
public Optional<URI> find(String name) throws IOException {
return name.equals(mn)
? Optional.of(uri) : Optional.empty();
}
@Override
public void close() throws IOException {
}
};
}
};
return new ModuleReference(descriptor, uri, readerSupplier);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private ModuleDescriptor dropHashes(ModuleDescriptor md) {
ModuleDescriptor.Builder builder = new ModuleDescriptor.Builder(md.name());
md.requires().forEach(builder::requires);
md.exports().forEach(builder::exports);
md.provides().values().stream().forEach(builder::provides);
md.uses().stream().forEach(builder::uses);
builder.conceals(md.conceals());
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 {
// the token for "all modules on the module path"
private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
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;
public Builder() {
this(System.getProperty("java.home"));
}
public Builder(String systemModulePath) {
this.systemModulePath = new SystemModuleFinder(systemModulePath);;
}
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;
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() {
systemModulePath.moduleNames()
.forEach(this.rootModules::add);
this.addAllApplicationModules = true;
return this;
}
public Builder addRoot(Path path) {
Archive archive = Archive.getInstance(path);
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 && appModulePath != null) {
appModulePath.findAll().stream()
.map(mref -> mref.descriptor().name())
.forEach(rootModules::add);
}
return new JdepsConfiguration(systemModulePath,
finder,
rootModules,
classPaths,
initialArchives);
}
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;
}
}
}

View file

@ -41,22 +41,27 @@ import java.util.stream.Stream;
* 2. -filter:package to filter out same-package dependencies * 2. -filter:package to filter out same-package dependencies
* This filter is applied when jdeps parses the class files * This filter is applied when jdeps parses the class files
* and filtered dependencies are not stored in the Analyzer. * and filtered dependencies are not stored in the Analyzer.
* 3. -module specifies to match target dependencies from the given module * 3. -requires specifies to match target dependence from the given module
* This gets expanded into package lists to be filtered. * This gets expanded into package lists to be filtered.
* 4. -filter:archive to filter out same-archive dependencies * 4. -filter:archive to filter out same-archive dependencies
* This filter is applied later in the Analyzer as the * This filter is applied later in the Analyzer as the
* containing archive of a target class may not be known until * containing archive of a target class may not be known until
* the entire archive * the entire archive
*/ */
class JdepsFilter implements Dependency.Filter, Analyzer.Filter { public class JdepsFilter implements Dependency.Filter, Analyzer.Filter {
public static final JdepsFilter DEFAULT_FILTER =
new JdepsFilter.Builder().filter(true, true).build();
private final Dependency.Filter filter; private final Dependency.Filter filter;
private final Pattern filterPattern; private final Pattern filterPattern;
private final boolean filterSamePackage; private final boolean filterSamePackage;
private final boolean filterSameArchive; private final boolean filterSameArchive;
private final boolean findJDKInternals; private final boolean findJDKInternals;
private final Pattern includePattern; private final Pattern includePattern;
private final Set<String> includePackages; private final Pattern includeSystemModules;
private final Set<String> excludeModules;
private final Set<String> requires;
private JdepsFilter(Dependency.Filter filter, private JdepsFilter(Dependency.Filter filter,
Pattern filterPattern, Pattern filterPattern,
@ -64,16 +69,16 @@ class JdepsFilter implements Dependency.Filter, Analyzer.Filter {
boolean filterSameArchive, boolean filterSameArchive,
boolean findJDKInternals, boolean findJDKInternals,
Pattern includePattern, Pattern includePattern,
Set<String> includePackages, Pattern includeSystemModules,
Set<String> excludeModules) { Set<String> requires) {
this.filter = filter; this.filter = filter;
this.filterPattern = filterPattern; this.filterPattern = filterPattern;
this.filterSamePackage = filterSamePackage; this.filterSamePackage = filterSamePackage;
this.filterSameArchive = filterSameArchive; this.filterSameArchive = filterSameArchive;
this.findJDKInternals = findJDKInternals; this.findJDKInternals = findJDKInternals;
this.includePattern = includePattern; this.includePattern = includePattern;
this.includePackages = includePackages; this.includeSystemModules = includeSystemModules;
this.excludeModules = excludeModules; this.requires = requires;
} }
/** /**
@ -82,12 +87,7 @@ class JdepsFilter implements Dependency.Filter, Analyzer.Filter {
* @param cn fully-qualified name * @param cn fully-qualified name
*/ */
public boolean matches(String cn) { public boolean matches(String cn) {
if (includePackages.isEmpty() && includePattern == null) if (includePattern == null)
return true;
int i = cn.lastIndexOf('.');
String pn = i > 0 ? cn.substring(0, i) : "";
if (includePackages.contains(pn))
return true; return true;
if (includePattern != null) if (includePattern != null)
@ -97,29 +97,39 @@ class JdepsFilter implements Dependency.Filter, Analyzer.Filter {
} }
/** /**
* Tests if the given source includes classes specified in includePattern * Tests if the given source includes classes specified in -include option
* or includePackages filters.
* *
* This method can be used to determine if the given source should eagerly * This method can be used to determine if the given source should eagerly
* be processed. * be processed.
*/ */
public boolean matches(Archive source) { public boolean matches(Archive source) {
if (!includePackages.isEmpty() && source.getModule().isNamed()) { if (includePattern != null) {
boolean found = source.getModule().packages() return source.reader().entries().stream()
.stream()
.filter(pn -> includePackages.contains(pn))
.findAny().isPresent();
if (found)
return true;
}
if (!includePackages.isEmpty() || includePattern != null) {
return source.reader().entries()
.stream()
.map(name -> name.replace('/', '.')) .map(name -> name.replace('/', '.'))
.filter(this::matches) .filter(name -> !name.equals("module-info.class"))
.findAny().isPresent(); .anyMatch(this::matches);
} }
return false; return hasTargetFilter();
}
public boolean include(Archive source) {
Module module = source.getModule();
// skip system module by default; or if includeSystemModules is set
// only include the ones matching the pattern
return !module.isSystem() || (includeSystemModules != null &&
includeSystemModules.matcher(module.name()).matches());
}
public boolean hasIncludePattern() {
return includePattern != null || includeSystemModules != null;
}
public boolean hasTargetFilter() {
return filter != null;
}
public Set<String> requiresFilter() {
return requires;
} }
// ----- Dependency.Filter ----- // ----- Dependency.Filter -----
@ -164,42 +174,35 @@ class JdepsFilter implements Dependency.Filter, Analyzer.Filter {
return true; return true;
} }
/**
* Returns true if dependency should be recorded for the given source.
*/
public boolean accept(Archive source) {
return !excludeModules.contains(source.getName());
}
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("exclude modules: ") sb.append("include pattern: ").append(includePattern).append("\n");
.append(excludeModules.stream().sorted().collect(Collectors.joining(",")))
.append("\n");
sb.append("filter same archive: ").append(filterSameArchive).append("\n"); sb.append("filter same archive: ").append(filterSameArchive).append("\n");
sb.append("filter same package: ").append(filterSamePackage).append("\n"); sb.append("filter same package: ").append(filterSamePackage).append("\n");
sb.append("requires: ").append(requires).append("\n");
return sb.toString(); return sb.toString();
} }
static class Builder { public static class Builder {
Dependency.Filter filter; static Pattern SYSTEM_MODULE_PATTERN = Pattern.compile("java\\..*|jdk\\..*|javafx\\..*");
Pattern filterPattern; Pattern filterPattern;
Pattern regex;
boolean filterSamePackage; boolean filterSamePackage;
boolean filterSameArchive; boolean filterSameArchive;
boolean findJDKInterals; boolean findJDKInterals;
// source filters // source filters
Pattern includePattern; Pattern includePattern;
Set<String> includePackages = new HashSet<>(); Pattern includeSystemModules;
Set<String> includeModules = new HashSet<>(); Set<String> requires = new HashSet<>();
Set<String> excludeModules = new HashSet<>(); Set<String> targetPackages = new HashSet<>();
public Builder packages(Set<String> packageNames) { public Builder packages(Set<String> packageNames) {
this.filter = Dependencies.getPackageFilter(packageNames, false); this.targetPackages.addAll(packageNames);
return this; return this;
} }
public Builder regex(Pattern regex) { public Builder regex(Pattern regex) {
this.filter = Dependencies.getRegexFilter(regex); this.regex = regex;
return this; return this;
} }
public Builder filter(Pattern regex) { public Builder filter(Pattern regex) {
@ -211,6 +214,13 @@ class JdepsFilter implements Dependency.Filter, Analyzer.Filter {
this.filterSameArchive = sameArchive; this.filterSameArchive = sameArchive;
return this; return this;
} }
public Builder requires(String name, Set<String> packageNames) {
this.requires.add(name);
this.targetPackages.addAll(packageNames);
includeIfSystemModule(name);
return this;
}
public Builder findJDKInternals(boolean value) { public Builder findJDKInternals(boolean value) {
this.findJDKInterals = value; this.findJDKInterals = value;
return this; return this;
@ -219,30 +229,33 @@ class JdepsFilter implements Dependency.Filter, Analyzer.Filter {
this.includePattern = regex; this.includePattern = regex;
return this; return this;
} }
public Builder includePackage(String pn) { public Builder includeSystemModules(Pattern regex) {
this.includePackages.add(pn); this.includeSystemModules = regex;
return this; return this;
} }
public Builder includeModules(Set<String> includes) { public Builder includeIfSystemModule(String name) {
this.includeModules.addAll(includes); if (includeSystemModules == null &&
return this; SYSTEM_MODULE_PATTERN.matcher(name).matches()) {
this.includeSystemModules = SYSTEM_MODULE_PATTERN;
} }
public Builder excludeModules(Set<String> excludes) {
this.excludeModules.addAll(excludes);
return this; return this;
} }
JdepsFilter build() { public JdepsFilter build() {
Dependency.Filter filter = null;
if (regex != null)
filter = Dependencies.getRegexFilter(regex);
else if (!targetPackages.isEmpty()) {
filter = Dependencies.getPackageFilter(targetPackages, false);
}
return new JdepsFilter(filter, return new JdepsFilter(filter,
filterPattern, filterPattern,
filterSamePackage, filterSamePackage,
filterSameArchive, filterSameArchive,
findJDKInterals, findJDKInterals,
includePattern, includePattern,
includePackages, includeSystemModules,
excludeModules.stream() requires);
.filter(mn -> !includeModules.contains(mn))
.collect(Collectors.toSet()));
} }
} }

View file

@ -25,31 +25,26 @@
package com.sun.tools.jdeps; package com.sun.tools.jdeps;
import static com.sun.tools.jdeps.Analyzer.NOT_FOUND;
import static com.sun.tools.jdeps.Analyzer.Type.*; import static com.sun.tools.jdeps.Analyzer.Type.*;
import static com.sun.tools.jdeps.JdepsWriter.*; import static com.sun.tools.jdeps.JdepsWriter.*;
import static com.sun.tools.jdeps.ModuleAnalyzer.Graph;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.module.ResolutionException; import java.lang.module.ResolutionException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.MissingResourceException; import java.util.MissingResourceException;
import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.function.Function;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -58,7 +53,12 @@ import java.util.stream.Stream;
* Implementation for the jdeps tool for static class dependency analysis. * Implementation for the jdeps tool for static class dependency analysis.
*/ */
class JdepsTask { class JdepsTask {
static class BadArgs extends Exception { static interface BadArguments {
String getKey();
Object[] getArgs();
boolean showUsage();
}
static class BadArgs extends Exception implements BadArguments {
static final long serialVersionUID = 8765093759964640721L; static final long serialVersionUID = 8765093759964640721L;
BadArgs(String key, Object... args) { BadArgs(String key, Object... args) {
super(JdepsTask.getMessage(key, args)); super(JdepsTask.getMessage(key, args));
@ -73,6 +73,44 @@ class JdepsTask {
final String key; final String key;
final Object[] args; final Object[] args;
boolean showUsage; boolean showUsage;
@Override
public String getKey() {
return key;
}
@Override
public Object[] getArgs() {
return args;
}
@Override
public boolean showUsage() {
return showUsage;
}
}
static class UncheckedBadArgs extends RuntimeException implements BadArguments {
static final long serialVersionUID = -1L;
final BadArgs cause;
UncheckedBadArgs(BadArgs cause) {
super(cause);
this.cause = cause;
}
@Override
public String getKey() {
return cause.key;
}
@Override
public Object[] getArgs() {
return cause.args;
}
@Override
public boolean showUsage() {
return cause.showUsage;
}
} }
static abstract class Option { static abstract class Option {
@ -126,7 +164,7 @@ class JdepsTask {
if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) { if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) {
throw new BadArgs("err.invalid.path", arg); throw new BadArgs("err.invalid.path", arg);
} }
task.options.dotOutputDir = arg; task.options.dotOutputDir = Paths.get(arg);;
} }
}, },
new Option(false, "-s", "-summary") { new Option(false, "-s", "-summary") {
@ -136,6 +174,7 @@ class JdepsTask {
} }
}, },
new Option(false, "-v", "-verbose", new Option(false, "-v", "-verbose",
"-verbose:module",
"-verbose:package", "-verbose:package",
"-verbose:class") { "-verbose:class") {
void process(JdepsTask task, String opt, String arg) throws BadArgs { void process(JdepsTask task, String opt, String arg) throws BadArgs {
@ -146,6 +185,9 @@ class JdepsTask {
task.options.filterSameArchive = false; task.options.filterSameArchive = false;
task.options.filterSamePackage = false; task.options.filterSamePackage = false;
break; break;
case "-verbose:module":
task.options.verbose = MODULE;
break;
case "-verbose:package": case "-verbose:package":
task.options.verbose = PACKAGE; task.options.verbose = PACKAGE;
break; break;
@ -157,6 +199,61 @@ class JdepsTask {
} }
} }
}, },
new Option(false, "-apionly") {
void process(JdepsTask task, String opt, String arg) {
task.options.apiOnly = true;
}
},
new Option(true, "-check") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
Set<String> mods = Set.of(arg.split(","));
task.options.checkModuleDeps = mods;
task.options.addmods.addAll(mods);
}
},
new Option(true, "-genmoduleinfo") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
Path p = Paths.get(arg);
if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) {
throw new BadArgs("err.invalid.path", arg);
}
task.options.genModuleInfo = Paths.get(arg);
}
},
new Option(false, "-jdkinternals") {
void process(JdepsTask task, String opt, String arg) {
task.options.findJDKInternals = true;
task.options.verbose = CLASS;
if (task.options.includePattern == null) {
task.options.includePattern = Pattern.compile(".*");
}
}
},
// ---- paths option ----
new Option(true, "-cp", "-classpath") {
void process(JdepsTask task, String opt, String arg) {
task.options.classpath = arg;
}
},
new Option(true, "-mp", "-modulepath") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.modulePath = arg;
}
},
new Option(true, "-upgrademodulepath") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.upgradeModulePath = arg;
}
},
new Option(true, "-m") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.rootModule = arg;
task.options.addmods.add(arg);
}
},
// ---- Target filtering options ----
new Option(true, "-p", "-package") { new Option(true, "-p", "-package") {
void process(JdepsTask task, String opt, String arg) { void process(JdepsTask task, String opt, String arg) {
task.options.packageNames.add(arg); task.options.packageNames.add(arg);
@ -198,26 +295,24 @@ class JdepsTask {
} }
} }
}, },
// ---- Source filtering options ----
new Option(true, "-include") { new Option(true, "-include") {
void process(JdepsTask task, String opt, String arg) throws BadArgs { void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.includePattern = Pattern.compile(arg); task.options.includePattern = Pattern.compile(arg);
} }
}, },
// Another alternative to list modules in -addmods option
new HiddenOption(true, "-include-system-modules") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.includeSystemModulePattern = Pattern.compile(arg);
}
},
new Option(false, "-P", "-profile") { new Option(false, "-P", "-profile") {
void process(JdepsTask task, String opt, String arg) throws BadArgs { void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.showProfile = true; task.options.showProfile = true;
task.options.showModule = false;
}
},
new Option(false, "-M") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.showModule = true;
task.options.showProfile = false;
}
},
new Option(false, "-apionly") {
void process(JdepsTask task, String opt, String arg) {
task.options.apiOnly = true;
} }
}, },
new Option(false, "-R", "-recursive") { new Option(false, "-R", "-recursive") {
@ -228,15 +323,6 @@ class JdepsTask {
task.options.filterSamePackage = false; task.options.filterSamePackage = false;
} }
}, },
new Option(true, "-genmoduleinfo") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
Path p = Paths.get(arg);
if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) {
throw new BadArgs("err.invalid.path", arg);
}
task.options.genModuleInfo = arg;
}
},
new Option(false, "-ct", "-compile-time") { new Option(false, "-ct", "-compile-time") {
void process(JdepsTask task, String opt, String arg) { void process(JdepsTask task, String opt, String arg) {
task.options.compileTimeView = true; task.options.compileTimeView = true;
@ -245,58 +331,7 @@ class JdepsTask {
task.options.depth = 0; task.options.depth = 0;
} }
}, },
new Option(false, "-jdkinternals") {
void process(JdepsTask task, String opt, String arg) {
task.options.findJDKInternals = true;
task.options.verbose = CLASS;
if (task.options.includePattern == null) {
task.options.includePattern = Pattern.compile(".*");
}
}
},
new Option(true, "-cp", "-classpath") {
void process(JdepsTask task, String opt, String arg) {
task.options.classpath = arg;
}
},
new Option(true, "-mp", "-modulepath") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.modulePath = arg;
task.options.showModule = true;
}
},
new Option(true, "-upgrademodulepath") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.upgradeModulePath = arg;
task.options.showModule = true;
}
},
new Option(true, "-m") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.rootModule = arg;
task.options.includes.add(arg);
task.options.showModule = true;
}
},
new Option(false, "-check") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.checkModuleDeps = true;
}
},
new HiddenOption(true, "-include-modules") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
Arrays.stream(arg.split(","))
.forEach(task.options.includes::add);
task.options.showModule = true;
}
},
new HiddenOption(true, "-exclude-modules") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
Arrays.stream(arg.split(","))
.forEach(task.options.excludes::add);
task.options.showModule = true;
}
},
new Option(false, "-q", "-quiet") { new Option(false, "-q", "-quiet") {
void process(JdepsTask task, String opt, String arg) { void process(JdepsTask task, String opt, String arg) {
task.options.nowarning = true; task.options.nowarning = true;
@ -318,7 +353,11 @@ class JdepsTask {
task.options.showLabel = true; task.options.showLabel = true;
} }
}, },
new HiddenOption(false, "-hide-module") {
void process(JdepsTask task, String opt, String arg) {
task.options.showModule = false;
}
},
new HiddenOption(true, "-depth") { new HiddenOption(true, "-depth") {
void process(JdepsTask task, String opt, String arg) throws BadArgs { void process(JdepsTask task, String opt, String arg) throws BadArgs {
try { try {
@ -332,7 +371,7 @@ class JdepsTask {
private static final String PROGNAME = "jdeps"; private static final String PROGNAME = "jdeps";
private final Options options = new Options(); private final Options options = new Options();
private final List<String> classes = new ArrayList<>(); private final List<String> inputArgs = new ArrayList<>();
private PrintWriter log; private PrintWriter log;
void setLog(PrintWriter out) { void setLog(PrintWriter out) {
@ -346,9 +385,9 @@ class JdepsTask {
EXIT_ERROR = 1, // Completed but reported errors. EXIT_ERROR = 1, // Completed but reported errors.
EXIT_CMDERR = 2, // Bad command-line arguments EXIT_CMDERR = 2, // Bad command-line arguments
EXIT_SYSERR = 3, // System error or resource exhaustion. EXIT_SYSERR = 3, // System error or resource exhaustion.
EXIT_ABNORMAL = 4;// terminated abnormally EXIT_ABNORMAL = 4; // terminated abnormally
int run(String[] args) { int run(String... args) {
if (log == null) { if (log == null) {
log = new PrintWriter(System.out); log = new PrintWriter(System.out);
} }
@ -360,15 +399,11 @@ class JdepsTask {
if (options.version || options.fullVersion) { if (options.version || options.fullVersion) {
showVersion(options.fullVersion); showVersion(options.fullVersion);
} }
if (options.rootModule != null && !classes.isEmpty()) { if (!inputArgs.isEmpty() && options.rootModule != null) {
reportError("err.invalid.module.option", options.rootModule, classes); reportError("err.invalid.arg.for.option", "-m");
return EXIT_CMDERR;
} }
if (options.checkModuleDeps && options.rootModule == null) { if (inputArgs.isEmpty() && options.addmods.isEmpty() && options.includePattern == null
reportError("err.root.module.not.set"); && options.includeSystemModulePattern == null && options.checkModuleDeps == null) {
return EXIT_CMDERR;
}
if (classes.isEmpty() && options.rootModule == null && options.includePattern == null) {
if (options.help || options.version || options.fullVersion) { if (options.help || options.version || options.fullVersion) {
return EXIT_OK; return EXIT_OK;
} else { } else {
@ -377,19 +412,10 @@ class JdepsTask {
} }
} }
if (options.genModuleInfo != null) { if (options.genModuleInfo != null) {
if (options.dotOutputDir != null || !options.classpath.isEmpty() || options.hasFilter()) { if (options.dotOutputDir != null || options.classpath != null || options.hasFilter()) {
showHelp(); showHelp();
return EXIT_CMDERR; return EXIT_CMDERR;
} }
// default to compile time view analysis
options.compileTimeView = true;
for (String fn : classes) {
Path p = Paths.get(fn);
if (!Files.exists(p) || !fn.endsWith(".jar")) {
reportError("err.genmoduleinfo.not.jarfile", fn);
return EXIT_CMDERR;
}
}
} }
if (options.numFilters() > 1) { if (options.numFilters() > 1) {
@ -405,12 +431,15 @@ class JdepsTask {
showHelp(); showHelp();
return EXIT_CMDERR; return EXIT_CMDERR;
} }
if (options.checkModuleDeps != null && !inputArgs.isEmpty()) {
reportError("err.invalid.module.option", inputArgs, "-check");
}
boolean ok = run(); boolean ok = run();
return ok ? EXIT_OK : EXIT_ERROR; return ok ? EXIT_OK : EXIT_ERROR;
} catch (BadArgs e) { } catch (BadArgs|UncheckedBadArgs e) {
reportError(e.key, e.args); reportError(e.getKey(), e.getArgs());
if (e.showUsage) { if (e.showUsage()) {
log.println(getMessage("main.usage.summary", PROGNAME)); log.println(getMessage("main.usage.summary", PROGNAME));
} }
return EXIT_CMDERR; return EXIT_CMDERR;
@ -419,176 +448,178 @@ class JdepsTask {
return EXIT_CMDERR; return EXIT_CMDERR;
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
return EXIT_ABNORMAL; return EXIT_CMDERR;
} finally { } finally {
log.flush(); log.flush();
} }
} }
private ModulePaths modulePaths; boolean run() throws IOException {
private boolean run() throws BadArgs, IOException { JdepsConfiguration config = buildConfig();
DependencyFinder dependencyFinder =
new DependencyFinder(options.compileTimeView);
buildArchive(dependencyFinder); // detect split packages
config.splitPackages().entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> System.out.format("split package: %s %s%n", e.getKey(),
e.getValue().toString()));
if (options.rootModule != null && // check if any module specified in -module is missing
(options.checkModuleDeps || (options.dotOutputDir != null && Stream.concat(options.addmods.stream(), options.requires.stream())
options.verbose == SUMMARY))) { .forEach(mn -> config.findModule(mn).orElseThrow(() ->
// -dotfile -s prints the configuration of the given root new UncheckedBadArgs(new BadArgs("err.module.not.found", mn))));
// -checkModuleDeps prints the suggested module-info.java
return analyzeModules(dependencyFinder);
}
// otherwise analyze the dependencies // -genmoduleinfo
if (options.genModuleInfo != null) { if (options.genModuleInfo != null) {
return genModuleInfo(dependencyFinder); return genModuleInfo(config);
} else {
return analyzeDeps(dependencyFinder);
}
} }
private void buildArchive(DependencyFinder dependencyFinder) // -check
throws BadArgs, IOException if (options.checkModuleDeps != null) {
{ return new ModuleAnalyzer(config, log, options.checkModuleDeps).run();
// If -genmoduleinfo is specified, the input arguments must be JAR files
// Treat them as automatic modules for analysis
List<Path> jarfiles = options.genModuleInfo != null
? classes.stream().map(Paths::get)
.collect(Collectors.toList())
: Collections.emptyList();
// Set module paths
this.modulePaths = new ModulePaths(options.upgradeModulePath, options.modulePath, jarfiles);
// add modules to dependency finder for analysis
Map<String, Module> modules = modulePaths.getModules();
modules.values().stream()
.forEach(dependencyFinder::addModule);
// If -m option is set, add the specified module and its transitive dependences
// to the root set
if (options.rootModule != null) {
modulePaths.dependences(options.rootModule)
.forEach(dependencyFinder::addRoot);
} }
// check if any module specified in -requires is missing if (options.dotOutputDir != null &&
Optional<String> req = options.requires.stream() (options.verbose == SUMMARY || options.verbose == MODULE) &&
.filter(mn -> !modules.containsKey(mn)) !options.addmods.isEmpty() && inputArgs.isEmpty()) {
.findFirst(); return new ModuleAnalyzer(config, log).genDotFiles(options.dotOutputDir);
if (req.isPresent()) {
throw new BadArgs("err.module.not.found", req.get());
} }
// classpath return analyzeDeps(config);
for (Path p : getClassPaths(options.classpath)) {
if (Files.exists(p)) {
dependencyFinder.addClassPathArchive(p);
}
} }
// if -genmoduleinfo is not set, the input arguments are considered as private JdepsConfiguration buildConfig() throws IOException {
// unnamed module. Add them to the root set JdepsConfiguration.Builder builder =
if (options.genModuleInfo == null) { new JdepsConfiguration.Builder(options.systemModulePath);
// add root set
for (String s : classes) { builder.upgradeModulePath(options.upgradeModulePath)
.appModulePath(options.modulePath)
.addmods(options.addmods);
if (options.checkModuleDeps != null) {
// check all system modules in the image
builder.allModules();
}
if (options.classpath != null)
builder.addClassPath(options.classpath);
// build the root set of archives to be analyzed
for (String s : inputArgs) {
Path p = Paths.get(s); Path p = Paths.get(s);
if (Files.exists(p)) { if (Files.exists(p)) {
// add to the initial root set builder.addRoot(p);
dependencyFinder.addRoot(p);
} else {
if (isValidClassName(s)) {
dependencyFinder.addClassName(s);
} else {
warning("warn.invalid.arg", s);
}
}
}
} }
} }
private boolean analyzeDeps(DependencyFinder dependencyFinder) throws IOException { return builder.build();
JdepsFilter filter = dependencyFilter(); }
// parse classfiles and find all dependencies
findDependencies(dependencyFinder, filter, options.apiOnly);
// analyze the dependencies collected
Analyzer analyzer = new Analyzer(options.verbose, filter);
analyzer.run(dependencyFinder.archives());
private boolean analyzeDeps(JdepsConfiguration config) throws IOException {
// output result // output result
final JdepsWriter writer; final JdepsWriter writer;
if (options.dotOutputDir != null) { if (options.dotOutputDir != null) {
Path dir = Paths.get(options.dotOutputDir); writer = new DotFileWriter(options.dotOutputDir,
Files.createDirectories(dir); options.verbose,
writer = new DotFileWriter(dir, options.verbose,
options.showProfile, options.showProfile,
options.showModule, options.showModule,
options.showLabel); options.showLabel);
} else { } else {
writer = new SimpleWriter(log, options.verbose, writer = new SimpleWriter(log,
options.verbose,
options.showProfile, options.showProfile,
options.showModule); options.showModule);
} }
// Targets for reporting - include the root sets and other analyzed archives // analyze the dependencies
final List<Archive> targets; DepsAnalyzer analyzer = new DepsAnalyzer(config,
if (options.rootModule == null) { dependencyFilter(config),
// no module as the root set writer,
targets = dependencyFinder.archives() options.verbose,
.filter(filter::accept) options.apiOnly);
.filter(a -> !a.getModule().isNamed())
.collect(Collectors.toList());
} else {
// named modules in topological order
Stream<Module> modules = dependencyFinder.archives()
.filter(a -> a.getModule().isNamed())
.map(Archive::getModule);
Graph<Module> graph = ModuleAnalyzer.graph(modulePaths, modules.toArray(Module[]::new));
// then add unnamed module
targets = graph.orderedNodes()
.filter(filter::accept)
.collect(Collectors.toList());
// in case any reference not found boolean ok = analyzer.run(options.compileTimeView, options.depth);
dependencyFinder.archives()
.filter(a -> !a.getModule().isNamed()) // print skipped entries, if any
.forEach(targets::add); analyzer.analyzer.archives()
} .forEach(archive -> archive.reader()
.skippedEntries().stream()
.forEach(name -> warning("warn.skipped.entry",
name, archive.getPathName())));
writer.generateOutput(targets, analyzer);
if (options.findJDKInternals && !options.nowarning) { if (options.findJDKInternals && !options.nowarning) {
showReplacements(targets, analyzer); Map<String, String> jdkInternals = analyzer.dependences()
.collect(Collectors.toMap(Function.identity(), this::replacementFor));
if (!jdkInternals.isEmpty()) {
log.println();
warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url"));
if (jdkInternals.values().stream().anyMatch(repl -> repl != null)) {
log.println();
log.format("%-40s %s%n", "JDK Internal API", "Suggested Replacement");
log.format("%-40s %s%n", "----------------", "---------------------");
jdkInternals.entrySet().stream()
.filter(e -> e.getValue() != null)
.sorted(Map.Entry.comparingByKey())
.forEach(e -> log.format("%-40s %s%n", e.getKey(), e.getValue()));
} }
return true;
} }
private JdepsFilter dependencyFilter() { }
return ok;
}
private boolean genModuleInfo(JdepsConfiguration config) throws IOException {
ModuleInfoBuilder builder
= new ModuleInfoBuilder(config, inputArgs, options.genModuleInfo);
boolean ok = builder.run();
builder.modules().forEach(module -> {
if (module.packages().contains("")) {
reportError("ERROR: %s contains unnamed package. " +
"module-info.java not generated%n", module.getPathName());
}
});
if (!ok && !options.nowarning) {
log.println("Missing dependencies");
builder.visitMissingDeps(
new Analyzer.Visitor() {
@Override
public void visitDependence(String origin, Archive originArchive,
String target, Archive targetArchive) {
if (targetArchive == NOT_FOUND)
log.format(" %-50s -> %-50s %s%n",
origin, target, targetArchive.getName());
}
});
log.println("ERROR: missing dependencies (check \"requires NOT_FOUND;\")");
}
return ok;
}
/**
* Returns a filter used during dependency analysis
*/
private JdepsFilter dependencyFilter(JdepsConfiguration config) {
// Filter specified by -filter, -package, -regex, and -module options // Filter specified by -filter, -package, -regex, and -module options
JdepsFilter.Builder builder = new JdepsFilter.Builder(); JdepsFilter.Builder builder = new JdepsFilter.Builder();
// Exclude JDK modules from analysis and reporting if -m specified.
modulePaths.getModules().values().stream()
.filter(m -> m.isJDK())
.map(Module::name)
.forEach(options.excludes::add);
// source filters // source filters
builder.includePattern(options.includePattern); builder.includePattern(options.includePattern);
builder.includeModules(options.includes); builder.includeSystemModules(options.includeSystemModulePattern);
builder.excludeModules(options.excludes);
builder.filter(options.filterSamePackage, options.filterSameArchive); builder.filter(options.filterSamePackage, options.filterSameArchive);
builder.findJDKInternals(options.findJDKInternals); builder.findJDKInternals(options.findJDKInternals);
// -module // -module
if (!options.requires.isEmpty()) { if (!options.requires.isEmpty()) {
Map<String, Module> modules = modulePaths.getModules(); options.requires.stream()
builder.packages(options.requires.stream() .forEach(mn -> {
.map(modules::get) Module m = config.findModule(mn).get();
.flatMap(m -> m.packages().stream()) builder.requires(mn, m.packages());
.collect(Collectors.toSet())); });
} }
// -regex // -regex
if (options.regex != null) if (options.regex != null)
@ -600,62 +631,14 @@ class JdepsTask {
if (options.filterRegex != null) if (options.filterRegex != null)
builder.filter(options.filterRegex); builder.filter(options.filterRegex);
// check if system module is set
config.rootModules().stream()
.map(Module::name)
.forEach(builder::includeIfSystemModule);
return builder.build(); return builder.build();
} }
private void findDependencies(DependencyFinder dependencyFinder,
JdepsFilter filter,
boolean apiOnly)
throws IOException
{
dependencyFinder.findDependencies(filter, apiOnly, options.depth);
// print skipped entries, if any
for (Archive a : dependencyFinder.roots()) {
for (String name : a.reader().skippedEntries()) {
warning("warn.skipped.entry", name, a.getPathName());
}
}
}
private boolean genModuleInfo(DependencyFinder dependencyFinder) throws IOException {
ModuleInfoBuilder builder = new ModuleInfoBuilder(modulePaths, dependencyFinder);
boolean result = builder.run(options.verbose, options.nowarning);
builder.build(Paths.get(options.genModuleInfo));
return result;
}
private boolean analyzeModules(DependencyFinder dependencyFinder)
throws IOException
{
ModuleAnalyzer analyzer = new ModuleAnalyzer(modulePaths,
dependencyFinder,
options.rootModule);
if (options.checkModuleDeps) {
return analyzer.run();
}
if (options.dotOutputDir != null && options.verbose == SUMMARY) {
Path dir = Paths.get(options.dotOutputDir);
Files.createDirectories(dir);
analyzer.genDotFile(dir);
return true;
}
return false;
}
private boolean isValidClassName(String name) {
if (!Character.isJavaIdentifierStart(name.charAt(0))) {
return false;
}
for (int i=1; i < name.length(); i++) {
char c = name.charAt(i);
if (c != '.' && !Character.isJavaIdentifierPart(c)) {
return false;
}
}
return true;
}
public void handleOptions(String[] args) throws BadArgs { public void handleOptions(String[] args) throws BadArgs {
// process options // process options
for (int i=0; i < args.length; i++) { for (int i=0; i < args.length; i++) {
@ -684,7 +667,7 @@ class JdepsTask {
if (name.charAt(0) == '-') { if (name.charAt(0) == '-') {
throw new BadArgs("err.option.after.class", name).showUsage(true); throw new BadArgs("err.option.after.class", name).showUsage(true);
} }
classes.add(name); inputArgs.add(name);
} }
} }
} }
@ -703,7 +686,7 @@ class JdepsTask {
log.println(getMessage("error.prefix") + " " + getMessage(key, args)); log.println(getMessage("error.prefix") + " " + getMessage(key, args));
} }
private void warning(String key, Object... args) { void warning(String key, Object... args) {
log.println(getMessage("warn.prefix") + " " + getMessage(key, args)); log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
} }
@ -749,7 +732,7 @@ class JdepsTask {
boolean version; boolean version;
boolean fullVersion; boolean fullVersion;
boolean showProfile; boolean showProfile;
boolean showModule; boolean showModule = true;
boolean showSummary; boolean showSummary;
boolean apiOnly; boolean apiOnly;
boolean showLabel; boolean showLabel;
@ -761,22 +744,22 @@ class JdepsTask {
boolean filterSamePackage = true; boolean filterSamePackage = true;
boolean filterSameArchive = false; boolean filterSameArchive = false;
Pattern filterRegex; Pattern filterRegex;
String dotOutputDir; Path dotOutputDir;
String genModuleInfo; Path genModuleInfo;
String classpath = ""; String classpath;
int depth = 1; int depth = 1;
Set<String> requires = new HashSet<>(); Set<String> requires = new HashSet<>();
Set<String> packageNames = new HashSet<>(); Set<String> packageNames = new HashSet<>();
Pattern regex; // apply to the dependences Pattern regex; // apply to the dependences
Pattern includePattern; // apply to classes Pattern includePattern;
Pattern includeSystemModulePattern;
boolean compileTimeView = false; boolean compileTimeView = false;
boolean checkModuleDeps = false; Set<String> checkModuleDeps;
String systemModulePath = System.getProperty("java.home");
String upgradeModulePath; String upgradeModulePath;
String modulePath; String modulePath;
String rootModule; String rootModule;
// modules to be included or excluded Set<String> addmods = new HashSet<>();
Set<String> includes = new HashSet<>();
Set<String> excludes = new HashSet<>();
boolean hasFilter() { boolean hasFilter() {
return numFilters() > 0; return numFilters() > 0;
@ -789,11 +772,8 @@ class JdepsTask {
if (packageNames.size() > 0) count++; if (packageNames.size() > 0) count++;
return count; return count;
} }
}
boolean isRootModule() {
return rootModule != null;
}
}
private static class ResourceBundleHelper { private static class ResourceBundleHelper {
static final ResourceBundle versionRB; static final ResourceBundle versionRB;
static final ResourceBundle bundle; static final ResourceBundle bundle;
@ -819,35 +799,6 @@ class JdepsTask {
} }
} }
/*
* Returns the list of Archive specified in cpaths and not included
* initialArchives
*/
private List<Path> getClassPaths(String cpaths) throws IOException
{
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);
}
}
} else {
paths.add(Paths.get(p));
}
}
}
return paths;
}
/** /**
* Returns the recommended replacement API for the given classname; * Returns the recommended replacement API for the given classname;
* or return null if replacement API is not known. * or return null if replacement API is not known.
@ -865,32 +816,5 @@ class JdepsTask {
} }
} }
return value; return value;
};
private void showReplacements(List<Archive> archives, Analyzer analyzer) {
Map<String,String> jdkinternals = new TreeMap<>();
boolean useInternals = false;
for (Archive source : archives) {
useInternals = useInternals || analyzer.hasDependences(source);
for (String cn : analyzer.dependences(source)) {
String repl = replacementFor(cn);
if (repl != null) {
jdkinternals.putIfAbsent(cn, repl);
} }
}
}
if (useInternals) {
log.println();
warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url"));
}
if (!jdkinternals.isEmpty()) {
log.println();
log.format("%-40s %s%n", "JDK Internal API", "Suggested Replacement");
log.format("%-40s %s%n", "----------------", "---------------------");
for (Map.Entry<String,String> e : jdkinternals.entrySet()) {
log.format("%-40s %s%n", e.getKey(), e.getValue());
}
}
}
} }

View file

@ -24,23 +24,33 @@
*/ */
package com.sun.tools.jdeps; package com.sun.tools.jdeps;
import static com.sun.tools.jdeps.Analyzer.Type.*;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.lang.module.ModuleDescriptor.Requires;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import static com.sun.tools.jdeps.Analyzer.Type.*;
public abstract class JdepsWriter { public abstract class JdepsWriter {
public static JdepsWriter newDotWriter(Path outputdir, Analyzer.Type type) {
return new DotFileWriter(outputdir, type, false, true, false);
}
public static JdepsWriter newSimpleWriter(PrintWriter writer, Analyzer.Type type) {
return new SimpleWriter(writer, type, false, true);
}
final Analyzer.Type type; final Analyzer.Type type;
final boolean showProfile; final boolean showProfile;
final boolean showModule; final boolean showModule;
JdepsWriter(Analyzer.Type type, boolean showProfile, boolean showModule) { private JdepsWriter(Analyzer.Type type, boolean showProfile, boolean showModule) {
this.type = type; this.type = type;
this.showProfile = showProfile; this.showProfile = showProfile;
this.showModule = showModule; this.showModule = showModule;
@ -48,7 +58,7 @@ public abstract class JdepsWriter {
abstract void generateOutput(Collection<Archive> archives, Analyzer analyzer) throws IOException; abstract void generateOutput(Collection<Archive> archives, Analyzer analyzer) throws IOException;
public static class DotFileWriter extends JdepsWriter { static class DotFileWriter extends JdepsWriter {
final boolean showLabel; final boolean showLabel;
final Path outputDir; final Path outputDir;
DotFileWriter(Path dir, Analyzer.Type type, DotFileWriter(Path dir, Analyzer.Type type,
@ -62,8 +72,10 @@ public abstract class JdepsWriter {
void generateOutput(Collection<Archive> archives, Analyzer analyzer) void generateOutput(Collection<Archive> archives, Analyzer analyzer)
throws IOException throws IOException
{ {
Files.createDirectories(outputDir);
// output individual .dot file for each archive // output individual .dot file for each archive
if (type != SUMMARY) { if (type != SUMMARY && type != MODULE) {
archives.stream() archives.stream()
.filter(analyzer::hasDependences) .filter(analyzer::hasDependences)
.forEach(archive -> { .forEach(archive -> {
@ -85,13 +97,13 @@ public abstract class JdepsWriter {
{ {
// If verbose mode (-v or -verbose option), // If verbose mode (-v or -verbose option),
// the summary.dot file shows package-level dependencies. // the summary.dot file shows package-level dependencies.
Analyzer.Type summaryType = boolean isSummary = type == PACKAGE || type == SUMMARY || type == MODULE;
(type == PACKAGE || type == SUMMARY) ? SUMMARY : PACKAGE; Analyzer.Type summaryType = isSummary ? SUMMARY : PACKAGE;
Path summary = outputDir.resolve("summary.dot"); Path summary = outputDir.resolve("summary.dot");
try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary)); try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary));
SummaryDotFile dotfile = new SummaryDotFile(sw, summaryType)) { SummaryDotFile dotfile = new SummaryDotFile(sw, summaryType)) {
for (Archive archive : archives) { for (Archive archive : archives) {
if (type == PACKAGE || type == SUMMARY) { if (isSummary) {
if (showLabel) { if (showLabel) {
// build labels listing package-level dependencies // build labels listing package-level dependencies
analyzer.visitDependences(archive, dotfile.labelBuilder(), PACKAGE); analyzer.visitDependences(archive, dotfile.labelBuilder(), PACKAGE);
@ -208,19 +220,22 @@ public abstract class JdepsWriter {
void generateOutput(Collection<Archive> archives, Analyzer analyzer) { void generateOutput(Collection<Archive> archives, Analyzer analyzer) {
RawOutputFormatter depFormatter = new RawOutputFormatter(writer); RawOutputFormatter depFormatter = new RawOutputFormatter(writer);
RawSummaryFormatter summaryFormatter = new RawSummaryFormatter(writer); RawSummaryFormatter summaryFormatter = new RawSummaryFormatter(writer);
for (Archive archive : archives) { archives.stream()
// print summary .filter(analyzer::hasDependences)
if (showModule && archive.getModule().isNamed()) { .sorted(Comparator.comparing(Archive::getName))
summaryFormatter.showModuleRequires(archive.getModule()); .forEach(archive -> {
} else { if (showModule && archive.getModule().isNamed() && type != SUMMARY) {
analyzer.visitDependences(archive, summaryFormatter, SUMMARY); // print module-info except -summary
summaryFormatter.printModuleDescriptor(archive.getModule());
} }
// print summary
analyzer.visitDependences(archive, summaryFormatter, SUMMARY);
if (analyzer.hasDependences(archive) && type != SUMMARY) { if (analyzer.hasDependences(archive) && type != SUMMARY) {
// print the class-level or package-level dependences // print the class-level or package-level dependences
analyzer.visitDependences(archive, depFormatter); analyzer.visitDependences(archive, depFormatter);
} }
} });
} }
class RawOutputFormatter implements Analyzer.Visitor { class RawOutputFormatter implements Analyzer.Visitor {
@ -269,20 +284,16 @@ public abstract class JdepsWriter {
writer.format("%n"); writer.format("%n");
} }
public void showModuleRequires(Module module) { public void printModuleDescriptor(Module module) {
if (!module.isNamed()) if (!module.isNamed())
return; return;
writer.format("module %s", module.name()); writer.format("%s%s%n", module.name(), module.isAutomatic() ? " automatic" : "");
if (module.isAutomatic()) writer.format(" [%s]%n", module.location());
writer.format(" (automatic)"); module.descriptor().requires()
writer.println();
module.requires().keySet()
.stream() .stream()
.sorted() .sorted(Comparator.comparing(Requires::name))
.forEach(req -> writer.format(" requires %s%s%n", .forEach(req -> writer.format(" requires %s%n", req));
module.requires.get(req) ? "public " : "",
req));
} }
} }
} }
@ -307,7 +318,7 @@ public abstract class JdepsWriter {
} }
// exported API // exported API
boolean jdkunsupported = Module.isJDKUnsupported(module, pn); boolean jdkunsupported = Module.JDK_UNSUPPORTED.equals(module.name());
if (module.isExported(pn) && !jdkunsupported) { if (module.isExported(pn) && !jdkunsupported) {
return showProfileOrModule(module); return showProfileOrModule(module);
} }

View file

@ -25,67 +25,56 @@
package com.sun.tools.jdeps; package com.sun.tools.jdeps;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.lang.module.ModuleDescriptor; import java.lang.module.ModuleDescriptor;
import java.net.URI; import java.net.URI;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
/** /**
* JDeps internal representation of module for dependency analysis. * Jdeps internal representation of module for dependency analysis.
*/ */
class Module extends Archive { class Module extends Archive {
static final boolean traceOn = Boolean.getBoolean("jdeps.debug"); static final Module UNNAMED_MODULE = new UnnamedModule();
static final String JDK_UNSUPPORTED = "jdk.unsupported";
static final boolean DEBUG = Boolean.getBoolean("jdeps.debug");
static void trace(String fmt, Object... args) { static void trace(String fmt, Object... args) {
trace(DEBUG, fmt, args);
}
static void trace(boolean traceOn, String fmt, Object... args) {
if (traceOn) { if (traceOn) {
System.err.format(fmt, args); System.err.format(fmt, args);
} }
} }
/* private final ModuleDescriptor descriptor;
* Returns true if the given package name is JDK critical internal API private final Map<String, Set<String>> exports;
* in jdk.unsupported module private final boolean isSystem;
*/ private final URI location;
static boolean isJDKUnsupported(Module m, String pn) {
return JDK_UNSUPPORTED.equals(m.name()) || unsupported.contains(pn);
};
protected final ModuleDescriptor descriptor; protected Module(String name) {
protected final Map<String, Boolean> requires; super(name);
protected final Map<String, Set<String>> exports; this.descriptor = null;
protected final Set<String> packages; this.location = null;
protected final boolean isJDK; this.exports = Collections.emptyMap();
protected final URI location; this.isSystem = true;
}
private Module(String name, private Module(String name,
URI location, URI location,
ModuleDescriptor descriptor, ModuleDescriptor descriptor,
Map<String, Boolean> requires,
Map<String, Set<String>> exports, Map<String, Set<String>> exports,
Set<String> packages, boolean isSystem,
boolean isJDK,
ClassFileReader reader) { ClassFileReader reader) {
super(name, location, reader); super(name, location, reader);
this.descriptor = descriptor; this.descriptor = descriptor;
this.location = location; this.location = location;
this.requires = Collections.unmodifiableMap(requires);
this.exports = Collections.unmodifiableMap(exports); this.exports = Collections.unmodifiableMap(exports);
this.packages = Collections.unmodifiableSet(packages); this.isSystem = isSystem;
this.isJDK = isJDK;
} }
/** /**
@ -111,31 +100,35 @@ class Module extends Archive {
return descriptor; return descriptor;
} }
public boolean isJDK() { public URI location() {
return isJDK; return location;
} }
public Map<String, Boolean> requires() { public boolean isJDK() {
return requires; String mn = name();
return isSystem &&
(mn.startsWith("java.") || mn.startsWith("jdk.") || mn.startsWith("javafx."));
}
public boolean isSystem() {
return isSystem;
} }
public Map<String, Set<String>> exports() { public Map<String, Set<String>> exports() {
return exports; return exports;
} }
public Map<String, Set<String>> provides() {
return descriptor.provides().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().providers()));
}
public Set<String> packages() { public Set<String> packages() {
return packages; return descriptor.packages();
} }
/** /**
* Tests if the package of the given name is exported. * Tests if the package of the given name is exported.
*/ */
public boolean isExported(String pn) { public boolean isExported(String pn) {
if (JDK_UNSUPPORTED.equals(this.name())) {
return false;
}
return exports.containsKey(pn) ? exports.get(pn).isEmpty() : false; return exports.containsKey(pn) ? exports.get(pn).isEmpty() : false;
} }
@ -159,11 +152,6 @@ class Module extends Archive {
return isExported(pn) || exports.containsKey(pn) && exports.get(pn).contains(target); return isExported(pn) || exports.containsKey(pn) && exports.get(pn).contains(target);
} }
private final static String JDK_UNSUPPORTED = "jdk.unsupported";
// temporary until jdk.unsupported module
private final static List<String> unsupported = Arrays.asList("sun.misc", "sun.reflect");
@Override @Override
public String toString() { public String toString() {
return name(); return name();
@ -171,21 +159,19 @@ class Module extends Archive {
public final static class Builder { public final static class Builder {
final String name; final String name;
final Map<String, Boolean> requires = new HashMap<>(); final ModuleDescriptor descriptor;
final Map<String, Set<String>> exports = new HashMap<>(); final boolean isSystem;
final Set<String> packages = new HashSet<>();
final boolean isJDK;
ClassFileReader reader; ClassFileReader reader;
ModuleDescriptor descriptor;
URI location; URI location;
public Builder(String name) { public Builder(ModuleDescriptor md) {
this(name, false); this(md, false);
} }
public Builder(String name, boolean isJDK) { public Builder(ModuleDescriptor md, boolean isSystem) {
this.name = name; this.name = md.name();
this.isJDK = isJDK; this.descriptor = md;
this.isSystem = isSystem;
} }
public Builder location(URI location) { public Builder location(URI location) {
@ -193,48 +179,30 @@ class Module extends Archive {
return this; return this;
} }
public Builder descriptor(ModuleDescriptor md) {
this.descriptor = md;
return this;
}
public Builder require(String d, boolean reexport) {
requires.put(d, reexport);
return this;
}
public Builder packages(Set<String> pkgs) {
packages.addAll(pkgs);
return this;
}
public Builder export(String p, Set<String> ms) {
Objects.requireNonNull(p);
Objects.requireNonNull(ms);
exports.put(p, new HashSet<>(ms));
return this;
}
public Builder classes(ClassFileReader reader) { public Builder classes(ClassFileReader reader) {
this.reader = reader; this.reader = reader;
return this; return this;
} }
public Module build() { public Module build() {
if (descriptor.isAutomatic() && isJDK) { if (descriptor.isAutomatic() && isSystem) {
throw new InternalError("JDK module: " + name + " can't be automatic module"); throw new InternalError("JDK module: " + name + " can't be automatic module");
} }
return new Module(name, location, descriptor, requires, exports, packages, isJDK, reader); Map<String, Set<String>> exports = new HashMap<>();
descriptor.exports().stream()
.forEach(exp -> exports.computeIfAbsent(exp.source(), _k -> new HashSet<>())
.addAll(exp.targets()));
return new Module(name, location, descriptor, exports, isSystem, reader);
} }
} }
final static Module UNNAMED_MODULE = new UnnamedModule();
private static class UnnamedModule extends Module { private static class UnnamedModule extends Module {
private UnnamedModule() { private UnnamedModule() {
super("unnamed", null, null, super("unnamed", null, null,
Collections.emptyMap(), Collections.emptyMap(),
Collections.emptyMap(),
Collections.emptySet(),
false, null); false, null);
} }
@ -260,10 +228,7 @@ class Module extends Archive {
} }
private static class StrictModule extends Module { private static class StrictModule extends Module {
private static final String SERVICES_PREFIX = "META-INF/services/"; private final ModuleDescriptor md;
private final Map<String, Set<String>> provides;
private final Module module;
private final JarFile jarfile;
/** /**
* Converts the given automatic module to a strict module. * Converts the given automatic module to a strict module.
@ -272,114 +237,26 @@ class Module extends Archive {
* declare service providers, if specified in META-INF/services configuration file * declare service providers, if specified in META-INF/services configuration file
*/ */
private StrictModule(Module m, Map<String, Boolean> requires) { private StrictModule(Module m, Map<String, Boolean> requires) {
super(m.name(), m.location, m.descriptor, requires, m.exports, m.packages, m.isJDK, m.reader()); super(m.name(), m.location, m.descriptor, m.exports, m.isSystem, m.reader());
this.module = m;
try { ModuleDescriptor.Builder builder = new ModuleDescriptor.Builder(m.name());
this.jarfile = new JarFile(m.path().toFile(), false); requires.keySet().forEach(mn -> {
} catch (IOException e) { if (requires.get(mn).equals(Boolean.TRUE)) {
throw new UncheckedIOException(e); builder.requires(ModuleDescriptor.Requires.Modifier.PUBLIC, mn);
} else {
builder.requires(mn);
} }
this.provides = providers(jarfile); });
m.descriptor.exports().forEach(e -> builder.exports(e));
m.descriptor.uses().forEach(s -> builder.uses(s));
m.descriptor.provides().values().forEach(p -> builder.provides(p));
builder.conceals(m.descriptor.conceals());
this.md = builder.build();
} }
@Override @Override
public Map<String, Set<String>> provides() { public ModuleDescriptor descriptor() {
return provides; return md;
}
private Map<String, Set<String>> providers(JarFile jf) {
Map<String, Set<String>> provides = new HashMap<>();
// map names of service configuration files to service names
Set<String> serviceNames = jf.stream()
.map(e -> e.getName())
.filter(e -> e.startsWith(SERVICES_PREFIX))
.distinct()
.map(this::toServiceName)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toSet());
// parse each service configuration file
for (String sn : serviceNames) {
JarEntry entry = jf.getJarEntry(SERVICES_PREFIX + sn);
Set<String> providerClasses = new HashSet<>();
try (InputStream in = jf.getInputStream(entry)) {
BufferedReader reader
= new BufferedReader(new InputStreamReader(in, "UTF-8"));
String cn;
while ((cn = nextLine(reader)) != null) {
if (isJavaIdentifier(cn)) {
providerClasses.add(cn);
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (!providerClasses.isEmpty())
provides.put(sn, providerClasses);
}
return provides;
}
/**
* Returns a container with the service type corresponding to the name of
* a services configuration file.
*
* For example, if called with "META-INF/services/p.S" then this method
* returns a container with the value "p.S".
*/
private Optional<String> toServiceName(String cf) {
assert cf.startsWith(SERVICES_PREFIX);
int index = cf.lastIndexOf("/") + 1;
if (index < cf.length()) {
String prefix = cf.substring(0, index);
if (prefix.equals(SERVICES_PREFIX)) {
String sn = cf.substring(index);
if (isJavaIdentifier(sn))
return Optional.of(sn);
}
}
return Optional.empty();
}
/**
* Reads the next line from the given reader and trims it of comments and
* leading/trailing white space.
*
* Returns null if the reader is at EOF.
*/
private String nextLine(BufferedReader reader) throws IOException {
String ln = reader.readLine();
if (ln != null) {
int ci = ln.indexOf('#');
if (ci >= 0)
ln = ln.substring(0, ci);
ln = ln.trim();
}
return ln;
}
/**
* Returns {@code true} if the given identifier is a legal Java identifier.
*/
private static boolean isJavaIdentifier(String id) {
int n = id.length();
if (n == 0)
return false;
if (!Character.isJavaIdentifierStart(id.codePointAt(0)))
return false;
int cp = id.codePointAt(0);
int i = Character.charCount(cp);
for (; i < n; i += Character.charCount(cp)) {
cp = id.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && id.charAt(i) != '.')
return false;
}
if (cp == '.')
return false;
return true;
} }
} }
} }

View file

@ -24,312 +24,273 @@
*/ */
package com.sun.tools.jdeps; package com.sun.tools.jdeps;
import java.io.IOException; import static com.sun.tools.jdeps.Graph.*;
import java.io.PrintStream; import static com.sun.tools.jdeps.JdepsFilter.DEFAULT_FILTER;
import java.io.UncheckedIOException; import static com.sun.tools.jdeps.Module.*;
import static java.lang.module.ModuleDescriptor.Requires.Modifier.*; import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
import static java.util.stream.Collectors.*;
import java.lang.module.Configuration; import com.sun.tools.classfile.Dependency;
import com.sun.tools.jdeps.JdepsTask.BadArgs;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.module.ModuleDescriptor; import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.lang.module.ResolvedModule;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Deque; import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static com.sun.tools.jdeps.Module.*;
import static com.sun.tools.jdeps.ModulePaths.SystemModulePath.JAVA_BASE;
/** /**
* Analyze module dependences and compare with module descriptor. * Analyze module dependences and compare with module descriptor.
* Also identify any qualified exports not used by the target module. * Also identify any qualified exports not used by the target module.
*/ */
class ModuleAnalyzer { public class ModuleAnalyzer {
private final ModulePaths modulePaths; private static final String JAVA_BASE = "java.base";
private final JdepsConfiguration configuration;
private final PrintWriter log;
private final DependencyFinder dependencyFinder; private final DependencyFinder dependencyFinder;
private final Module root; private final Map<Module, ModuleDeps> modules;
private final Set<Module> modules;
private final Set<String> requiresPublic = new HashSet<>();
private final Set<String> requires = new HashSet<>();
private final Set<Module> exportTargets = new HashSet<>();
private final JdepsFilter filter;
private Graph<Module> graph;
ModuleAnalyzer(ModulePaths modulePaths, DependencyFinder finder,
String moduleName) {
this.modulePaths = modulePaths;
this.dependencyFinder = finder;
this.root = modulePaths.getModules().get(moduleName);
this.modules = modulePaths.dependences(moduleName);
root.exports().values().stream() public ModuleAnalyzer(JdepsConfiguration config,
.flatMap(Set::stream) PrintWriter log) {
.map(target -> modulePaths.getModules().get(target)) this(config, log, Collections.emptySet());
.forEach(this.exportTargets::add); }
public ModuleAnalyzer(JdepsConfiguration config,
PrintWriter log,
Set<String> names) {
this.filter = new JdepsFilter.Builder().filter(true, true).build(); if (!config.initialArchives().isEmpty()) {
String list = config.initialArchives().stream()
.map(Archive::getPathName).collect(joining(" "));
throw new JdepsTask.UncheckedBadArgs(new BadArgs("err.invalid.module.option",
list, "-check"));
} }
/** this.configuration = config;
* Returns a graph of transitive closure of the given modules. this.log = log;
*
* This method does not add the implicit read edges and is intended to this.dependencyFinder = new DependencyFinder(config, DEFAULT_FILTER);
* get all transitive closures in (reverse) topological sort. if (names.isEmpty()) {
*/ this.modules = configuration.rootModules().stream()
public static Graph<Module> graph(ModulePaths modulePaths, Module... modules) { .collect(toMap(Function.identity(), ModuleDeps::new));
Graph.Builder<Module> gb = new Graph.Builder<>(); } else {
for (Module module : modules) { this.modules = names.stream()
module.descriptor().requires().stream() .map(configuration::findModule)
.map(ModuleDescriptor.Requires::name) .flatMap(Optional::stream)
.map(mn -> modulePaths.getModules().get(mn)) .collect(toMap(Function.identity(), ModuleDeps::new));
.forEach(m -> {
gb.addNode(m);
gb.addEdge(module, m);
});
} }
return gb.build();
} }
/** public boolean run() throws IOException {
* Do the analysis
*/
public boolean run() {
try { try {
computeRequiresPublic(); // compute "requires public" dependences
computeRequires(); modules.values().forEach(ModuleDeps::computeRequiresPublic);
modules.values().forEach(md -> {
// compute "requires" dependences
md.computeRequires();
// apply transitive reduction and reports recommended requires. // apply transitive reduction and reports recommended requires.
analyzeDeps(); md.analyzeDeps();
// detect any qualiifed exports not used by the target module });
checkQualifiedExports(); } finally {
} catch (IOException e) { dependencyFinder.shutdown();
throw new UncheckedIOException(e);
} }
return true; return true;
} }
class ModuleDeps {
final Module root;
Set<Module> requiresPublic;
Set<Module> requires;
Map<String, Set<String>> unusedQualifiedExports;
ModuleDeps(Module root) {
this.root = root;
}
/** /**
* Compute 'requires public' dependences by analyzing API dependencies * Compute 'requires public' dependences by analyzing API dependencies
*/ */
private void computeRequiresPublic() throws IOException { private void computeRequiresPublic() {
JdepsFilter.Builder builder = new JdepsFilter.Builder();
// only analyze exported API
root.descriptor.exports().stream()
.filter(exp -> !exp.isQualified())
.map(ModuleDescriptor.Exports::source)
.forEach(builder::includePackage);
JdepsFilter filter = builder.filter(true, true).build();
// analyze dependences for exported packages
dependencyFinder.findDependencies(filter, true /* api only */, 1);
Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter);
analyzer.run(modules);
// record requires public // record requires public
analyzer.requires(root) this.requiresPublic = computeRequires(true)
.filter(m -> m != JAVA_BASE && m != root) .filter(m -> !m.name().equals(JAVA_BASE))
.map(Archive::getName) .collect(toSet());
.forEach(requiresPublic::add);
trace("requires public: %s%n", requiresPublic); trace("requires public: %s%n", requiresPublic);
} }
private void computeRequires() throws IOException { private void computeRequires() {
// add the exportTargets of the qualified exports to the root set this.requires = computeRequires(false).collect(toSet());
exportTargets.stream() trace("requires: %s%n", requires);
.peek(target -> trace("add root: %s%n", target)) }
.forEach(dependencyFinder::addRoot);
private Stream<Module> computeRequires(boolean apionly) {
// analyze all classes // analyze all classes
dependencyFinder.findDependencies(filter, false /* all classes */, 1);
Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter);
analyzer.run(modules);
// record requires if (apionly) {
analyzer.requires(root) dependencyFinder.parseExportedAPIs(Stream.of(root));
.filter(m -> m != JAVA_BASE && m != root) } else {
.map(Archive::getName) dependencyFinder.parse(Stream.of(root));
.forEach(requires::add); }
this.graph = buildGraph(analyzer, root); // find the modules of all the dependencies found
if (traceOn) { return dependencyFinder.getDependences(root)
trace("dependences: %s%n", graph.nodes()); .map(Archive::getModule);
graph.printGraph(System.out);
} }
ModuleDescriptor descriptor() {
return descriptor(requiresPublic, requires);
} }
private ModuleDescriptor descriptor(Set<Module> requiresPublic,
Set<Module> requires) {
ModuleDescriptor.Builder builder = new ModuleDescriptor.Builder(root.name());
if (!root.name().equals(JAVA_BASE))
builder.requires(MANDATED, JAVA_BASE);
requiresPublic.stream()
.filter(m -> !m.name().equals(JAVA_BASE))
.map(Module::name)
.forEach(mn -> builder.requires(PUBLIC, mn));
requires.stream()
.filter(m -> !requiresPublic.contains(m))
.filter(m -> !m.name().equals(JAVA_BASE))
.map(Module::name)
.forEach(mn -> builder.requires(mn));
return builder.build();
}
ModuleDescriptor reduced() {
Graph.Builder<Module> bd = new Graph.Builder<>();
requiresPublic.stream()
.forEach(m -> {
bd.addNode(m);
bd.addEdge(root, m);
});
// requires public graph
Graph<Module> rbg = bd.build().reduce();
// transitive reduction
Graph<Module> newGraph = buildGraph(requires).reduce(rbg);
if (DEBUG) {
System.err.println("after transitive reduction: ");
newGraph.printGraph(log);
}
return descriptor(requiresPublic, newGraph.adjacentNodes(root));
}
/** /**
* Apply transitive reduction on the resulting graph and reports * Apply transitive reduction on the resulting graph and reports
* recommended requires. * recommended requires.
*/ */
private void analyzeDeps() { private void analyzeDeps() {
String moduleName = root.name(); Graph.Builder<Module> builder = new Graph.Builder<>();
Graph.Builder<String> builder = new Graph.Builder<>();
requiresPublic.stream() requiresPublic.stream()
.forEach(mn -> {
builder.addNode(mn);
builder.addEdge(moduleName, mn);
});
// requires public graph
Graph<String> rbg = builder.build().reduce();
// convert the dependence graph from Module to name
Set<String> nodes = this.graph.nodes().stream()
.map(Module::name)
.collect(Collectors.toSet());
Map<String, Set<String>> edges = new HashMap<>();
this.graph.edges().keySet().stream()
.forEach(m -> { .forEach(m -> {
String mn = m.name(); builder.addNode(m);
Set<String> es = edges.computeIfAbsent(mn, _k -> new HashSet<String>()); builder.addEdge(root, m);
this.graph.edges().get(m).stream()
.map(Module::name)
.forEach(es::add);
}); });
// requires public graph
Graph<Module> rbg = buildGraph(requiresPublic).reduce();
// transitive reduction // transitive reduction
Graph<String> newGraph = new Graph<>(nodes, edges).reduce(rbg); Graph<Module> newGraph = buildGraph(requires).reduce(builder.build().reduce());
if (traceOn) { if (DEBUG) {
System.out.println("after transitive reduction"); System.err.println("after transitive reduction: ");
newGraph.printGraph(System.out); newGraph.printGraph(log);
}; }
Set<String> reducedRequires = newGraph.adjacentNodes(moduleName); printModuleDescriptor(log, root);
if (matches(root.descriptor(), requires, requiresPublic) &&
matches(root.descriptor(), reducedRequires, requiresPublic)) { ModuleDescriptor analyzedDescriptor = descriptor();
System.out.println("--- Analysis result: no change for " + root.name()); if (!matches(root.descriptor(), analyzedDescriptor)) {
} else { log.format(" [Suggested module descriptor for %s]%n", root.name());
System.out.println("--- Analysis result: suggested requires for " + root.name()); analyzedDescriptor.requires()
System.out.format("module %s%n", root.name());
requires.stream()
.sorted()
.forEach(dn -> System.out.format(" requires %s%s;%n",
requiresPublic.contains(dn) ? "public " : "", dn));
if (!requires.equals(reducedRequires) && !reducedRequires.isEmpty()) {
System.out.format("%nmodule %s (reduced)%n", root.name());
newGraph.adjacentNodes(moduleName)
.stream() .stream()
.sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
.forEach(req -> log.format(" requires %s;%n", req));
}
ModuleDescriptor reduced = reduced();
if (!matches(root.descriptor(), reduced)) {
log.format(" [Transitive reduced graph for %s]%n", root.name());
reduced.requires()
.stream()
.sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
.forEach(req -> log.format(" requires %s;%n", req));
}
checkQualifiedExports();
log.println();
}
private void checkQualifiedExports() {
// detect any qualified exports not used by the target module
unusedQualifiedExports = unusedQualifiedExports();
if (!unusedQualifiedExports.isEmpty())
log.format(" [Unused qualified exports in %s]%n", root.name());
unusedQualifiedExports.keySet().stream()
.sorted() .sorted()
.forEach(dn -> System.out.format(" requires %s%s;%n", .forEach(pn -> log.format(" exports %s to %s%n", pn,
requiresPublic.contains(dn) ? "public " : "", dn)); unusedQualifiedExports.get(pn).stream()
} .sorted()
System.out.println("\n--- Module descriptor"); .collect(joining(","))));
Graph<Module> mdGraph = graph(modulePaths, root);
mdGraph.reverse(m -> printModuleDescriptor(System.out, m.descriptor()));
}
} }
/** private void printModuleDescriptor(PrintWriter out, Module module) {
* Detects any qualified exports not used by the target module. ModuleDescriptor descriptor = module.descriptor();
*/ out.format("%s (%s)%n", descriptor.name(), module.location());
private void checkQualifiedExports() throws IOException {
Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter);
analyzer.run(dependencyFinder.roots());
// build the qualified exports map if (descriptor.name().equals(JAVA_BASE))
Map<String, Set<String>> qualifiedExports =
root.exports().entrySet().stream()
.filter(e -> !e.getValue().isEmpty())
.map(Map.Entry::getKey)
.collect(Collectors.toMap(Function.identity(), _k -> new HashSet<>()));
// adds to the qualified exports map if a module references it
for (Module m : exportTargets) {
analyzer.dependences(m).stream()
.map(this::toPackageName)
.filter(qualifiedExports::containsKey)
.forEach(pn -> qualifiedExports.get(pn).add(m.name()));
}
// compare with the exports from ModuleDescriptor
Set<String> staleQualifiedExports =
qualifiedExports.keySet().stream()
.filter(pn -> !qualifiedExports.get(pn).equals(root.exports().get(pn)))
.collect(Collectors.toSet());
if (!staleQualifiedExports.isEmpty()) {
System.out.println("--- Unused qualified exports in " + root.name());
for (String pn : staleQualifiedExports) {
Set<String> unused = new HashSet<>(root.exports().get(pn));
unused.removeAll(qualifiedExports.get(pn));
System.out.format(" exports %s to %s%n", pn,
unused.stream().collect(Collectors.joining(",")));
}
}
}
private String toPackageName(String cn) {
int i = cn.lastIndexOf('.');
return i > 0 ? cn.substring(0, i) : "";
}
private boolean matches(ModuleDescriptor md, Set<String> requires, Set<String> requiresPublic) {
Set<String> reqPublic = md.requires().stream()
.filter(req -> req.modifiers().contains(PUBLIC))
.map(ModuleDescriptor.Requires::name)
.collect(Collectors.toSet());
if (!requiresPublic.equals(reqPublic)) {
trace("mismatch requires public: %s%n", reqPublic);
return false;
}
// java.base is not in requires
int javaBase = md.name().equals(JAVA_BASE.name()) ? 0 : 1;
if (requires.size()+javaBase != md.requires().size()) {
trace("mismatch requires: %d != %d%n", requires.size()+1, md.requires().size());
return false;
}
Set<String> unused = md.requires().stream()
.map(ModuleDescriptor.Requires::name)
.filter(req -> !requires.contains(req) && !req.equals(JAVA_BASE.name()))
.collect(Collectors.toSet());
if (!unused.isEmpty()) {
trace("mismatch requires: %s%n", unused);
return false;
}
return true;
}
private void printModuleDescriptor(PrintStream out, ModuleDescriptor descriptor) {
if (descriptor.name().equals("java.base"))
return; return;
out.format("module %s%n", descriptor.name()); out.println(" [Module descriptor]");
descriptor.requires() descriptor.requires()
.stream() .stream()
.sorted(Comparator.comparing(ModuleDescriptor.Requires::name)) .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
.forEach(req -> out.format(" requires %s;%n", req)); .forEach(req -> out.format(" requires %s;%n", req));
} }
/** /**
* Returns a graph of modules required by the specified module. * Returns a graph of modules required by the specified module.
* *
* Requires public edges of the dependences are added to the graph. * Requires public edges of the dependences are added to the graph.
*/ */
private Graph<Module> buildGraph(Analyzer analyzer, Module module) { private Graph<Module> buildGraph(Set<Module> deps) {
Graph.Builder<Module> builder = new Graph.Builder<>(); Graph.Builder<Module> builder = new Graph.Builder<>();
builder.addNode(module); builder.addNode(root);
Set<Module> visited = new HashSet<>(); Set<Module> visited = new HashSet<>();
visited.add(module); visited.add(root);
Deque<Module> deque = new LinkedList<>(); Deque<Module> deque = new LinkedList<>();
analyzer.requires(module) deps.stream()
.map(Archive::getModule)
.filter(m -> m != JAVA_BASE)
.forEach(m -> { .forEach(m -> {
deque.add(m); deque.add(m);
builder.addEdge(module, m); builder.addEdge(root, m);
}); });
// read requires public from ModuleDescription // read requires public from ModuleDescription
@ -337,14 +298,15 @@ class ModuleAnalyzer {
while ((source = deque.poll()) != null) { while ((source = deque.poll()) != null) {
if (visited.contains(source)) if (visited.contains(source))
continue; continue;
visited.add(source); visited.add(source);
builder.addNode(source); builder.addNode(source);
Module from = source; Module from = source;
source.descriptor().requires().stream() source.descriptor().requires().stream()
.filter(req -> req.modifiers().contains(PUBLIC)) .filter(req -> req.modifiers().contains(PUBLIC))
.map(ModuleDescriptor.Requires::name) .map(ModuleDescriptor.Requires::name)
.map(req -> modulePaths.getModules().get(req)) .map(configuration::findModule)
.filter(m -> m != JAVA_BASE) .flatMap(Optional::stream)
.forEach(m -> { .forEach(m -> {
deque.add(m); deque.add(m);
builder.addEdge(from, m); builder.addEdge(from, m);
@ -353,311 +315,116 @@ class ModuleAnalyzer {
return builder.build(); return builder.build();
} }
static class Graph<T> {
private final Set<T> nodes;
private final Map<T, Set<T>> edges;
private Graph(Set<T> nodes, Map<T, Set<T>> edges) {
this.nodes = nodes;
this.edges = edges;
}
public Set<T> nodes() {
return nodes;
}
public Map<T, Set<T>> edges() {
return edges;
}
public Set<T> adjacentNodes(T u) {
return edges.get(u);
}
/** /**
* Returns a new Graph after transitive reduction * Detects any qualified exports not used by the target module.
*/ */
public Graph<T> reduce() { private Map<String, Set<String>> unusedQualifiedExports() {
Graph.Builder<T> builder = new Builder<>(); Map<String, Set<String>> unused = new HashMap<>();
nodes.stream()
.forEach(u -> {
builder.addNode(u);
edges.get(u).stream()
.filter(v -> !pathExists(u, v, false))
.forEach(v -> builder.addEdge(u, v));
});
return builder.build();
}
/** // build the qualified exports map
* Returns a new Graph after transitive reduction. All edges in Map<String, Set<String>> qualifiedExports =
* the given g takes precedence over this graph. root.exports().entrySet().stream()
* .filter(e -> !e.getValue().isEmpty())
* @throw IllegalArgumentException g must be a subgraph this graph .map(Map.Entry::getKey)
*/ .collect(toMap(Function.identity(), _k -> new HashSet<>()));
public Graph<T> reduce(Graph<T> g) {
boolean subgraph = nodes.containsAll(g.nodes) && g.edges.keySet().stream()
.allMatch(u -> adjacentNodes(u).containsAll(g.adjacentNodes(u)));
if (!subgraph) {
throw new IllegalArgumentException(g + " is not a subgraph of " + this);
}
Graph.Builder<T> builder = new Builder<>(); Set<Module> mods = new HashSet<>();
nodes.stream() root.exports().values()
.forEach(u -> {
builder.addNode(u);
// filter the edge if there exists a path from u to v in the given g
// or there exists another path from u to v in this graph
edges.get(u).stream()
.filter(v -> !g.pathExists(u, v) && !pathExists(u, v, false))
.forEach(v -> builder.addEdge(u, v));
});
// add the overlapped edges from this graph and the given g
g.edges().keySet().stream()
.forEach(u -> g.adjacentNodes(u).stream()
.filter(v -> isAdjacent(u, v))
.forEach(v -> builder.addEdge(u, v)));
return builder.build();
}
/**
* Returns nodes sorted in topological order.
*/
public Stream<T> orderedNodes() {
TopoSorter<T> sorter = new TopoSorter<>(this);
return sorter.result.stream();
}
/**
* Iterates the nodes sorted in topological order and performs the
* given action.
*/
public void ordered(Consumer<T> action) {
TopoSorter<T> sorter = new TopoSorter<>(this);
sorter.ordered(action);
}
/**
* Iterates the nodes sorted in reverse topological order and
* performs the given action.
*/
public void reverse(Consumer<T> action) {
TopoSorter<T> sorter = new TopoSorter<>(this);
sorter.reverse(action);
}
private boolean isAdjacent(T u, T v) {
return edges.containsKey(u) && edges.get(u).contains(v);
}
private boolean pathExists(T u, T v) {
return pathExists(u, v, true);
}
/**
* Returns true if there exists a path from u to v in this graph.
* If includeAdjacent is false, it returns true if there exists
* another path from u to v of distance > 1
*/
private boolean pathExists(T u, T v, boolean includeAdjacent) {
if (!nodes.contains(u) || !nodes.contains(v)) {
return false;
}
if (includeAdjacent && isAdjacent(u, v)) {
return true;
}
Deque<T> stack = new LinkedList<>();
Set<T> visited = new HashSet<>();
stack.push(u);
while (!stack.isEmpty()) {
T node = stack.pop();
if (node.equals(v)) {
return true;
}
if (!visited.contains(node)) {
visited.add(node);
edges.get(node).stream()
.filter(e -> includeAdjacent || !node.equals(u) || !e.equals(v))
.forEach(e -> stack.push(e));
}
}
assert !visited.contains(v);
return false;
}
void printGraph(PrintStream out) {
out.println("graph for " + nodes);
nodes.stream()
.forEach(u -> adjacentNodes(u).stream()
.forEach(v -> out.format("%s -> %s%n", u, v)));
}
@Override
public String toString() {
return nodes.toString();
}
static class Builder<T> {
final Set<T> nodes = new HashSet<>();
final Map<T, Set<T>> edges = new HashMap<>();
public void addNode(T node) {
if (nodes.contains(node)) {
return;
}
nodes.add(node);
edges.computeIfAbsent(node, _e -> new HashSet<>());
}
public void addEdge(T u, T v) {
addNode(u);
addNode(v);
edges.get(u).add(v);
}
public Graph<T> build() {
return new Graph<>(nodes, edges);
}
void print(PrintStream out) {
out.println(nodes);
nodes.stream()
.forEach(u -> edges.get(u).stream()
.forEach(v -> out.format("%s -> %s%n", u, v)));
}
}
}
static class TopoSorter<T> {
final Deque<T> result = new LinkedList<>();
final Deque<T> nodes;
final Graph<T> graph;
TopoSorter(Graph<T> graph) {
this.graph = graph;
this.nodes = new LinkedList<>(graph.nodes);
sort();
}
public void ordered(Consumer<T> action) {
result.iterator().forEachRemaining(action);
}
public void reverse(Consumer<T> action) {
result.descendingIterator().forEachRemaining(action);
}
private void sort() {
Deque<T> visited = new LinkedList<>();
Deque<T> done = new LinkedList<>();
T node;
while ((node = nodes.poll()) != null) {
if (!visited.contains(node)) {
visit(node, visited, done);
}
}
}
private void visit(T node, Deque<T> visited, Deque<T> done) {
if (visited.contains(node)) {
if (!done.contains(node)) {
throw new IllegalArgumentException("Cyclic detected: " +
node + " " + graph.edges().get(node));
}
return;
}
visited.add(node);
graph.edges().get(node).stream()
.forEach(x -> visit(x, visited, done));
done.add(node);
result.addLast(node);
}
}
static class DotGraph {
static final String ORANGE = "#e76f00";
static final String BLUE = "#437291";
static final String GRAY = "#dddddd";
static final String REEXPORTS = "";
static final String REQUIRES = "style=\"dashed\"";
static final String REQUIRES_BASE = "color=\"" + GRAY + "\"";
static final Set<String> javaModules = modules(name ->
(name.startsWith("java.") && !name.equals("java.smartcardio")));
static final Set<String> jdkModules = modules(name ->
(name.startsWith("java.") ||
name.startsWith("jdk.") ||
name.startsWith("javafx.")) && !javaModules.contains(name));
private static Set<String> modules(Predicate<String> predicate) {
return ModuleFinder.ofSystem().findAll()
.stream() .stream()
.map(ModuleReference::descriptor) .flatMap(Set::stream)
.map(ModuleDescriptor::name) .forEach(target -> configuration.findModule(target)
.filter(predicate) .ifPresentOrElse(mods::add,
() -> log.format("Warning: %s not found%n", target))
);
// parse all target modules
dependencyFinder.parse(mods.stream());
// adds to the qualified exports map if a module references it
mods.stream().forEach(m ->
m.getDependencies()
.map(Dependency.Location::getPackageName)
.filter(qualifiedExports::containsKey)
.forEach(pn -> qualifiedExports.get(pn).add(m.name())));
// compare with the exports from ModuleDescriptor
Set<String> staleQualifiedExports =
qualifiedExports.keySet().stream()
.filter(pn -> !qualifiedExports.get(pn).equals(root.exports().get(pn)))
.collect(toSet());
if (!staleQualifiedExports.isEmpty()) {
for (String pn : staleQualifiedExports) {
Set<String> targets = new HashSet<>(root.exports().get(pn));
targets.removeAll(qualifiedExports.get(pn));
unused.put(pn, targets);
}
}
return unused;
}
}
private boolean matches(ModuleDescriptor md, ModuleDescriptor other) {
// build requires public from ModuleDescriptor
Set<ModuleDescriptor.Requires> reqPublic = md.requires().stream()
.filter(req -> req.modifiers().contains(PUBLIC))
.collect(toSet());
Set<ModuleDescriptor.Requires> otherReqPublic = other.requires().stream()
.filter(req -> req.modifiers().contains(PUBLIC))
.collect(toSet());
if (!reqPublic.equals(otherReqPublic)) {
trace("mismatch requires public: %s%n", reqPublic);
return false;
}
Set<ModuleDescriptor.Requires> unused = md.requires().stream()
.filter(req -> !other.requires().contains(req))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
if (!unused.isEmpty()) {
trace("mismatch requires: %s%n", unused);
return false;
}
return true;
} }
static void printAttributes(PrintStream out) { /**
out.format(" size=\"25,25\";%n"); * Generate dotfile from module descriptor
out.format(" nodesep=.5;%n"); *
out.format(" ranksep=1.5;%n"); * @param dir output directory
out.format(" pencolor=transparent;%n"); */
out.format(" node [shape=plaintext, fontname=\"DejaVuSans\", fontsize=36, margin=\".2,.2\"];%n"); public boolean genDotFiles(Path dir) throws IOException {
out.format(" edge [penwidth=4, color=\"#999999\", arrowhead=open, arrowsize=2];%n"); Files.createDirectories(dir);
for (Module m : modules.keySet()) {
genDotFile(dir, m.name());
}
return true;
} }
static void printNodes(PrintStream out, Graph<String> graph) {
out.format(" subgraph se {%n");
graph.nodes().stream()
.filter(javaModules::contains)
.forEach(mn -> out.format(" \"%s\" [fontcolor=\"%s\", group=%s];%n",
mn, ORANGE, "java"));
out.format(" }%n");
graph.nodes().stream()
.filter(jdkModules::contains)
.forEach(mn -> out.format(" \"%s\" [fontcolor=\"%s\", group=%s];%n",
mn, BLUE, "jdk"));
graph.nodes().stream() private void genDotFile(Path dir, String name) throws IOException {
.filter(mn -> !javaModules.contains(mn) && !jdkModules.contains(mn)) try (OutputStream os = Files.newOutputStream(dir.resolve(name + ".dot"));
.forEach(mn -> out.format(" \"%s\";%n", mn)); PrintWriter out = new PrintWriter(os)) {
} Set<Module> modules = configuration.resolve(Set.of(name))
.collect(Collectors.toSet());
static void printEdges(PrintStream out, Graph<String> graph,
String node, Set<String> requiresPublic) {
graph.adjacentNodes(node).forEach(dn -> {
String attr = dn.equals("java.base") ? REQUIRES_BASE
: (requiresPublic.contains(dn) ? REEXPORTS : REQUIRES);
out.format(" \"%s\" -> \"%s\" [%s];%n", node, dn, attr);
});
}
}
public void genDotFile(Path dir) throws IOException {
String name = root.name();
try (PrintStream out
= new PrintStream(Files.newOutputStream(dir.resolve(name + ".dot")))) {
Configuration cf = modulePaths.configuration(name);
// transitive reduction // transitive reduction
Graph<String> graph = gengraph(cf); Graph<String> graph = gengraph(modules);
out.format("digraph \"%s\" {%n", name); out.format("digraph \"%s\" {%n", name);
DotGraph.printAttributes(out); DotGraph.printAttributes(out);
DotGraph.printNodes(out, graph); DotGraph.printNodes(out, graph);
cf.modules().stream() modules.stream()
.map(ResolvedModule::reference) .map(Module::descriptor)
.map(ModuleReference::descriptor)
.sorted(Comparator.comparing(ModuleDescriptor::name)) .sorted(Comparator.comparing(ModuleDescriptor::name))
.forEach(md -> { .forEach(md -> {
String mn = md.name(); String mn = md.name();
Set<String> requiresPublic = md.requires().stream() Set<String> requiresPublic = md.requires().stream()
.filter(d -> d.modifiers().contains(PUBLIC)) .filter(d -> d.modifiers().contains(PUBLIC))
.map(d -> d.name()) .map(d -> d.name())
.collect(Collectors.toSet()); .collect(toSet());
DotGraph.printEdges(out, graph, mn, requiresPublic); DotGraph.printEdges(out, graph, mn, requiresPublic);
}); });
@ -666,7 +433,6 @@ class ModuleAnalyzer {
} }
} }
/** /**
* Returns a Graph of the given Configuration after transitive reduction. * Returns a Graph of the given Configuration after transitive reduction.
* *
@ -675,12 +441,12 @@ class ModuleAnalyzer {
* (e.g. U -> V) from being reduced by a path (U -> X -> Y -> V) * (e.g. U -> V) from being reduced by a path (U -> X -> Y -> V)
* in which V would not be re-exported from U. * in which V would not be re-exported from U.
*/ */
private Graph<String> gengraph(Configuration cf) { private Graph<String> gengraph(Set<Module> modules) {
// build a Graph containing only requires public edges // build a Graph containing only requires public edges
// with transitive reduction. // with transitive reduction.
Graph.Builder<String> rpgbuilder = new Graph.Builder<>(); Graph.Builder<String> rpgbuilder = new Graph.Builder<>();
for (ResolvedModule resolvedModule : cf.modules()) { for (Module module : modules) {
ModuleDescriptor md = resolvedModule.reference().descriptor(); ModuleDescriptor md = module.descriptor();
String mn = md.name(); String mn = md.name();
md.requires().stream() md.requires().stream()
.filter(d -> d.modifiers().contains(PUBLIC)) .filter(d -> d.modifiers().contains(PUBLIC))
@ -692,12 +458,12 @@ class ModuleAnalyzer {
// build the readability graph // build the readability graph
Graph.Builder<String> builder = new Graph.Builder<>(); Graph.Builder<String> builder = new Graph.Builder<>();
for (ResolvedModule resolvedModule : cf.modules()) { for (Module module : modules) {
ModuleDescriptor md = resolvedModule.reference().descriptor(); ModuleDescriptor md = module.descriptor();
String mn = md.name(); String mn = md.name();
builder.addNode(mn); builder.addNode(mn);
resolvedModule.reads().stream() configuration.reads(module)
.map(ResolvedModule::name) .map(Module::name)
.forEach(d -> builder.addEdge(mn, d)); .forEach(d -> builder.addEdge(mn, d));
} }
@ -705,4 +471,25 @@ class ModuleAnalyzer {
return builder.build().reduce(rpg); return builder.build().reduce(rpg);
} }
// ---- for testing purpose
public ModuleDescriptor[] descriptors(String name) {
ModuleDeps moduleDeps = modules.keySet().stream()
.filter(m -> m.name().equals(name))
.map(modules::get)
.findFirst().get();
ModuleDescriptor[] descriptors = new ModuleDescriptor[3];
descriptors[0] = moduleDeps.root.descriptor();
descriptors[1] = moduleDeps.descriptor();
descriptors[2] = moduleDeps.reduced();
return descriptors;
}
public Map<String, Set<String>> unusedQualifiedExports(String name) {
ModuleDeps moduleDeps = modules.keySet().stream()
.filter(m -> m.name().equals(name))
.map(modules::get)
.findFirst().get();
return moduleDeps.unusedQualifiedExports;
}
} }

View file

@ -24,15 +24,24 @@
*/ */
package com.sun.tools.jdeps; package com.sun.tools.jdeps;
import static com.sun.tools.jdeps.Analyzer.Type.CLASS; import static com.sun.tools.jdeps.JdepsTask.*;
import static com.sun.tools.jdeps.Analyzer.NOT_FOUND; import static com.sun.tools.jdeps.Analyzer.NOT_FOUND;
import static com.sun.tools.jdeps.Module.trace; import static com.sun.tools.jdeps.JdepsFilter.DEFAULT_FILTER;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.Exports;
import java.lang.module.ModuleDescriptor.Provides;
import java.lang.module.ModuleDescriptor.Requires;
import java.lang.module.ModuleFinder;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -40,168 +49,159 @@ import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
public class ModuleInfoBuilder { public class ModuleInfoBuilder {
final ModulePaths modulePaths; final JdepsConfiguration configuration;
final Path outputdir;
final DependencyFinder dependencyFinder; final DependencyFinder dependencyFinder;
final JdepsFilter filter;
final Analyzer analyzer; final Analyzer analyzer;
final Map<Module, Module> strictModules = new HashMap<>(); final Map<Module, Module> strictModules;
ModuleInfoBuilder(ModulePaths modulePaths, DependencyFinder finder) { public ModuleInfoBuilder(JdepsConfiguration configuration,
this.modulePaths = modulePaths; List<String> args,
this.dependencyFinder = finder; Path outputdir) {
this.filter = new JdepsFilter.Builder().filter(true, true).build(); this.configuration = configuration;
this.analyzer = new Analyzer(CLASS, filter); this.outputdir = outputdir;
this.dependencyFinder = new DependencyFinder(configuration, DEFAULT_FILTER);
this.analyzer = new Analyzer(configuration, Analyzer.Type.CLASS, DEFAULT_FILTER);
// add targets to modulepath if it has module-info.class
List<Path> paths = args.stream()
.map(fn -> Paths.get(fn))
.collect(Collectors.toList());
// automatic module to convert to strict module
this.strictModules = ModuleFinder.of(paths.toArray(new Path[0]))
.findAll().stream()
.map(configuration::toModule)
.collect(Collectors.toMap(Function.identity(), Function.identity()));
Optional<Module> om = strictModules.keySet().stream()
.filter(m -> !m.descriptor().isAutomatic())
.findAny();
if (om.isPresent()) {
throw new UncheckedBadArgs(new BadArgs("err.genmoduleinfo.not.jarfile",
om.get().getPathName()));
}
if (strictModules.isEmpty()) {
throw new UncheckedBadArgs(new BadArgs("err.invalid.path", args));
}
} }
private Stream<Module> automaticModules() { public boolean run() throws IOException {
return modulePaths.getModules().values() try {
.stream()
.filter(Module::isAutomatic);
}
/**
* Compute 'requires public' dependences by analyzing API dependencies
*/
Map<Module, Set<Module>> computeRequiresPublic() throws IOException {
dependencyFinder.findDependencies(filter, true /* api only */, 1);
Analyzer pass1 = new Analyzer(Analyzer.Type.CLASS, filter);
pass1.run(dependencyFinder.archives());
return automaticModules().collect(Collectors.toMap(Function.identity(),
source -> pass1.requires(source)
.map(Archive::getModule)
.collect(Collectors.toSet())));
}
boolean run(Analyzer.Type verbose, boolean quiet) throws IOException {
// add all automatic modules to the root set
automaticModules().forEach(dependencyFinder::addRoot);
// pass 1: find API dependencies // pass 1: find API dependencies
Map<Module, Set<Module>> requiresPublic = computeRequiresPublic(); Map<Archive, Set<Archive>> requiresPublic = computeRequiresPublic();
// pass 2: analyze all class dependences // pass 2: analyze all class dependences
dependencyFinder.findDependencies(filter, false /* all classes */, 1); dependencyFinder.parse(automaticModules().stream());
analyzer.run(dependencyFinder.archives());
analyzer.run(automaticModules(), dependencyFinder.locationToArchive());
// computes requires and requires public // computes requires and requires public
automaticModules().forEach(m -> { automaticModules().forEach(m -> {
Map<String, Boolean> requires; Map<String, Boolean> requires;
if (requiresPublic.containsKey(m)) { if (requiresPublic.containsKey(m)) {
requires = requiresPublic.get(m) requires = requiresPublic.get(m).stream()
.stream() .map(Archive::getModule)
.collect(Collectors.toMap(Archive::getName, (v) -> Boolean.TRUE)); .collect(Collectors.toMap(Module::name, (v) -> Boolean.TRUE));
} else { } else {
requires = new HashMap<>(); requires = new HashMap<>();
} }
analyzer.requires(m) analyzer.requires(m)
.forEach(d -> requires.putIfAbsent(d.getName(), Boolean.FALSE)); .map(Archive::getModule)
.forEach(d -> requires.putIfAbsent(d.name(), Boolean.FALSE));
trace("strict module %s requires %s%n", m.name(), requires);
strictModules.put(m, m.toStrictModule(requires)); strictModules.put(m, m.toStrictModule(requires));
}); });
// generate module-info.java
descriptors().forEach(md -> writeModuleInfo(outputdir, md));
// find any missing dependences // find any missing dependences
Optional<Module> missingDeps = automaticModules() return automaticModules().stream()
.filter(this::missingDep) .flatMap(analyzer::requires)
.findAny(); .allMatch(m -> !m.equals(NOT_FOUND));
if (missingDeps.isPresent()) { } finally {
automaticModules() dependencyFinder.shutdown();
.filter(this::missingDep) }
}
/**
* Returns the stream of resulting modules
*/
Stream<Module> modules() {
return strictModules.values().stream();
}
/**
* Returns the stream of resulting ModuleDescriptors
*/
public Stream<ModuleDescriptor> descriptors() {
return strictModules.values().stream().map(Module::descriptor);
}
void visitMissingDeps(Analyzer.Visitor visitor) {
automaticModules().stream()
.filter(m -> analyzer.requires(m).anyMatch(d -> d.equals(NOT_FOUND)))
.forEach(m -> { .forEach(m -> {
System.err.format("Missing dependencies from %s%n", m.name()); analyzer.visitDependences(m, visitor, Analyzer.Type.VERBOSE);
analyzer.visitDependences(m,
new Analyzer.Visitor() {
@Override
public void visitDependence(String origin, Archive originArchive,
String target, Archive targetArchive) {
if (targetArchive == NOT_FOUND)
System.err.format(" %-50s -> %-50s %s%n",
origin, target, targetArchive.getName());
}
}, verbose);
System.err.println();
}); });
System.err.println("ERROR: missing dependencies (check \"requires NOT_FOUND;\")");
} }
return missingDeps.isPresent() ? false : true; void writeModuleInfo(Path dir, ModuleDescriptor descriptor) {
} String mn = descriptor.name();
Path srcFile = dir.resolve(mn).resolve("module-info.java");
private boolean missingDep(Archive m) { try {
return analyzer.requires(m).filter(a -> a.equals(NOT_FOUND))
.findAny().isPresent();
}
void build(Path dir) throws IOException {
ModuleInfoWriter writer = new ModuleInfoWriter(dir);
writer.generateOutput(strictModules.values(), analyzer);
}
private class ModuleInfoWriter {
private final Path outputDir;
ModuleInfoWriter(Path dir) {
this.outputDir = dir;
}
void generateOutput(Iterable<Module> modules, Analyzer analyzer) throws IOException {
// generate module-info.java file for each archive
for (Module m : modules) {
if (m.packages().contains("")) {
System.err.format("ERROR: %s contains unnamed package. " +
"module-info.java not generated%n", m.getPathName());
continue;
}
String mn = m.getName();
Path srcFile = outputDir.resolve(mn).resolve("module-info.java");
Files.createDirectories(srcFile.getParent()); Files.createDirectories(srcFile.getParent());
System.out.println("writing to " + srcFile); System.out.println("writing to " + srcFile);
try (PrintWriter pw = new PrintWriter(Files.newOutputStream(srcFile))) { try (PrintWriter pw = new PrintWriter(Files.newOutputStream(srcFile))) {
printModuleInfo(pw, m); printModuleInfo(pw, descriptor);
} }
} catch (IOException e) {
throw new UncheckedIOException(e);
} }
} }
private void printModuleInfo(PrintWriter writer, Module m) { private void printModuleInfo(PrintWriter writer, ModuleDescriptor descriptor) {
writer.format("module %s {%n", m.name()); writer.format("module %s {%n", descriptor.name());
Map<String, Module> modules = modulePaths.getModules(); Map<String, Module> modules = configuration.getModules();
Map<String, Boolean> requires = m.requires();
// first print the JDK modules // first print the JDK modules
requires.keySet().stream() descriptor.requires().stream()
.filter(mn -> !mn.equals("java.base")) // implicit requires .filter(req -> !req.name().equals("java.base")) // implicit requires
.filter(mn -> modules.containsKey(mn) && modules.get(mn).isJDK()) .sorted(Comparator.comparing(Requires::name))
.sorted() .forEach(req -> writer.format(" requires %s;%n", req));
.forEach(mn -> {
String modifier = requires.get(mn) ? "public " : "";
writer.format(" requires %s%s;%n", modifier, mn);
});
// print requires non-JDK modules descriptor.exports().stream()
requires.keySet().stream() .peek(exp -> {
.filter(mn -> !modules.containsKey(mn) || !modules.get(mn).isJDK()) if (exp.targets().size() > 0)
.sorted() throw new InternalError(descriptor.name() + " qualified exports: " + exp);
.forEach(mn -> { })
String modifier = requires.get(mn) ? "public " : ""; .sorted(Comparator.comparing(Exports::source))
writer.format(" requires %s%s;%n", modifier, mn); .forEach(exp -> writer.format(" exports %s;%n", exp.source()));
});
m.packages().stream() descriptor.provides().values().stream()
.sorted(Comparator.comparing(Provides::service))
.forEach(p -> p.providers().stream()
.sorted() .sorted()
.forEach(pn -> writer.format(" exports %s;%n", pn)); .forEach(impl -> writer.format(" provides %s with %s;%n", p.service(), impl)));
m.provides().entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> {
String service = e.getKey();
e.getValue().stream()
.sorted()
.forEach(impl -> writer.format(" provides %s with %s;%n", service, impl));
});
writer.println("}"); writer.println("}");
} }
private Set<Module> automaticModules() {
return strictModules.keySet();
}
/**
* Compute 'requires public' dependences by analyzing API dependencies
*/
private Map<Archive, Set<Archive>> computeRequiresPublic() throws IOException {
// parse the input modules
dependencyFinder.parseExportedAPIs(automaticModules().stream());
return dependencyFinder.dependences();
} }
} }

View file

@ -1,206 +0,0 @@
/*
* Copyright (c) 2012, 2016, 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 java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.module.Configuration;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.lang.module.ResolvedModule;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.ProviderNotFoundException;
import java.util.*;
import java.util.stream.Collectors;
import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
public class ModulePaths {
final ModuleFinder finder;
final Map<String, Module> modules = new LinkedHashMap<>();
public ModulePaths(String upgradeModulePath, String modulePath) {
this(upgradeModulePath, modulePath, Collections.emptyList());
}
public ModulePaths(String upgradeModulePath, String modulePath, List<Path> jars) {
ModuleFinder finder = ModuleFinder.ofSystem();
if (upgradeModulePath != null) {
finder = ModuleFinder.compose(createModulePathFinder(upgradeModulePath), finder);
}
if (jars.size() > 0) {
finder = ModuleFinder.compose(finder, ModuleFinder.of(jars.toArray(new Path[0])));
}
if (modulePath != null) {
finder = ModuleFinder.compose(finder, createModulePathFinder(modulePath));
}
this.finder = finder;
// add modules from modulepaths
finder.findAll().stream().forEach(mref ->
modules.computeIfAbsent(mref.descriptor().name(), mn -> toModule(mn, mref))
);
}
/**
* Returns the list of Modules that can be found in the specified
* module paths.
*/
Map<String, Module> getModules() {
return modules;
}
Set<Module> dependences(String... roots) {
Configuration cf = configuration(roots);
return cf.modules().stream()
.map(ResolvedModule::name)
.map(modules::get)
.collect(Collectors.toSet());
}
Configuration configuration(String... roots) {
return Configuration.empty().resolveRequires(finder, ModuleFinder.empty(), Set.of(roots));
}
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);
}
}
private static Module toModule(String mn, ModuleReference mref) {
return SystemModulePath.find(mn)
.orElse(toModule(new Module.Builder(mn), mref));
}
private static Module toModule(Module.Builder builder, ModuleReference mref) {
ModuleDescriptor md = mref.descriptor();
builder.descriptor(md);
for (ModuleDescriptor.Requires req : md.requires()) {
builder.require(req.name(), req.modifiers().contains(PUBLIC));
}
for (ModuleDescriptor.Exports exp : md.exports()) {
builder.export(exp.source(), exp.targets());
}
builder.packages(md.packages());
try {
URI location = mref.location()
.orElseThrow(FileNotFoundException::new);
builder.location(location);
builder.classes(getClassReader(location, md.name()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return builder.build();
}
static class SystemModulePath {
final static Module JAVA_BASE;
private final static FileSystem fs;
private final static Path root;
private final static Map<String, Module> installed = new HashMap<>();
static {
if (isJrtAvailable()) {
// jrt file system
fs = FileSystems.getFileSystem(URI.create("jrt:/"));
root = fs.getPath("/modules");
} else {
// exploded image
String javahome = System.getProperty("java.home");
fs = FileSystems.getDefault();
root = Paths.get(javahome, "modules");
}
ModuleFinder.ofSystem().findAll().stream()
.forEach(mref ->
installed.computeIfAbsent(mref.descriptor().name(),
mn -> toModule(new Module.Builder(mn, true), mref))
);
JAVA_BASE = installed.get("java.base");
Profile.init(installed);
}
private static boolean isJrtAvailable() {
try {
FileSystems.getFileSystem(URI.create("jrt:/"));
return true;
} catch (ProviderNotFoundException | FileSystemNotFoundException e) {
return false;
}
}
public static Optional<Module> find(String mn) {
return installed.containsKey(mn) ? Optional.of(installed.get(mn))
: Optional.empty();
}
public static boolean contains(Module m) {
return installed.containsValue(m);
}
public static ClassFileReader getClassReader(String modulename) throws IOException {
Path mp = root.resolve(modulename);
if (Files.exists(mp) && Files.isDirectory(mp)) {
return ClassFileReader.newInstance(fs, mp);
} else {
throw new FileNotFoundException(mp.toString());
}
}
}
/**
* Returns a ModuleClassReader that only reads classes for the given modulename.
*/
public static ClassFileReader getClassReader(URI location, String modulename)
throws IOException {
if (location.getScheme().equals("jrt")) {
return SystemModulePath.getClassReader(modulename);
} else {
Path path = Paths.get(location);
return ClassFileReader.newInstance(path);
}
}
}

View file

@ -26,9 +26,13 @@
package com.sun.tools.jdeps; package com.sun.tools.jdeps;
import java.io.IOException; import java.io.IOException;
import java.lang.module.ModuleDescriptor;
import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
/** /**
@ -44,17 +48,18 @@ enum Profile {
// need a way to determine JRE modules // need a way to determine JRE modules
SE_JRE("Java SE JRE", 4, "java.se", "jdk.charsets", SE_JRE("Java SE JRE", 4, "java.se", "jdk.charsets",
"jdk.crypto.ec", "jdk.crypto.pkcs11", "jdk.crypto.ec", "jdk.crypto.pkcs11",
"jdk.crypto.mscapi", "jdk.crypto.ucrypto", "jdk.jvmstat", "jdk.crypto.mscapi", "jdk.crypto.ucrypto",
"jdk.localedata", "jdk.scripting.nashorn", "jdk.zipfs"), "jdk.localedata", "jdk.scripting.nashorn", "jdk.zipfs"),
FULL_JRE("Full JRE", 5, "java.se.ee", "jdk.charsets", FULL_JRE("Full JRE", 5, "java.se.ee", "jdk.charsets",
"jdk.crypto.ec", "jdk.crypto.pkcs11", "jdk.crypto.ec", "jdk.crypto.pkcs11",
"jdk.crypto.mscapi", "jdk.crypto.ucrypto", "jdk.jvmstat", "jdk.crypto.mscapi", "jdk.crypto.ucrypto", "jdk.jvmstat",
"jdk.localedata", "jdk.scripting.nashorn", "jdk.zipfs"); "jdk.localedata", "jdk.scripting.nashorn",
"jdk.unsupported", "jdk.zipfs");
final String name; final String name;
final int profile; final int profile;
final String[] mnames; final String[] mnames;
final Set<Module> modules = new HashSet<>(); final Map<String, Module> modules = new HashMap<>();
Profile(String name, int profile, String... mnames) { Profile(String name, int profile, String... mnames) {
this.name = name; this.name = name;
@ -75,12 +80,18 @@ enum Profile {
return JDK.isEmpty() ? 0 : Profile.values().length; return JDK.isEmpty() ? 0 : Profile.values().length;
} }
Optional<Module> findModule(String name) {
return modules.containsKey(name)
? Optional.of(modules.get(name))
: Optional.empty();
}
/** /**
* Returns the Profile for the given package name; null if not found. * Returns the Profile for the given package name; null if not found.
*/ */
public static Profile getProfile(String pn) { public static Profile getProfile(String pn) {
for (Profile p : Profile.values()) { for (Profile p : Profile.values()) {
for (Module m : p.modules) { for (Module m : p.modules.values()) {
if (m.packages().contains(pn)) { if (m.packages().contains(pn)) {
return p; return p;
} }
@ -94,7 +105,7 @@ enum Profile {
*/ */
public static Profile getProfile(Module m) { public static Profile getProfile(Module m) {
for (Profile p : Profile.values()) { for (Profile p : Profile.values()) {
if (p.modules.contains(m)) { if (p.modules.containsValue(m)) {
return p; return p;
} }
} }
@ -102,34 +113,28 @@ enum Profile {
} }
private final static Set<Module> JDK = new HashSet<>(); private final static Set<Module> JDK = new HashSet<>();
static synchronized void init(Map<String, Module> installed) { static synchronized void init(Map<String, Module> systemModules) {
for (Profile p : Profile.values()) { Arrays.stream(Profile.values()).forEach(p ->
for (String mn : p.mnames) {
// this includes platform-dependent module that may not exist // this includes platform-dependent module that may not exist
Module m = installed.get(mn); Arrays.stream(p.mnames)
if (m != null) { .filter(systemModules::containsKey)
p.addModule(installed, m); .map(systemModules::get)
} .forEach(m -> p.addModule(systemModules, m)));
}
}
// JDK modules should include full JRE plus other jdk.* modules // JDK modules should include full JRE plus other jdk.* modules
// Just include all installed modules. Assume jdeps is running // Just include all installed modules. Assume jdeps is running
// in JDK image // in JDK image
JDK.addAll(installed.values()); JDK.addAll(systemModules.values());
} }
private void addModule(Map<String, Module> installed, Module m) { private void addModule(Map<String, Module> systemModules, Module module) {
modules.add(m); modules.put(module.name(), module);
for (String n : m.requires().keySet()) { module.descriptor().requires().stream()
Module d = installed.get(n); .map(ModuleDescriptor.Requires::name)
if (d == null) { .map(systemModules::get)
throw new InternalError("module " + n + " required by " + .forEach(m -> modules.put(m.name(), m));
m.name() + " doesn't exist");
}
modules.add(d);
}
} }
// for debugging // for debugging
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
// find platform modules // find platform modules
@ -139,14 +144,6 @@ enum Profile {
for (Profile p : Profile.values()) { for (Profile p : Profile.values()) {
String profileName = p.name; String profileName = p.name;
System.out.format("%2d: %-10s %s%n", p.profile, profileName, p.modules); System.out.format("%2d: %-10s %s%n", p.profile, profileName, p.modules);
for (Module m: p.modules) {
System.out.format("module %s%n", m.name());
System.out.format(" requires %s%n", m.requires());
for (Map.Entry<String,Set<String>> e: m.exports().entrySet()) {
System.out.format(" exports %s %s%n", e.getKey(),
e.getValue().isEmpty() ? "" : "to " + e.getValue());
}
}
} }
System.out.println("All JDK modules:-"); System.out.println("All JDK modules:-");
JDK.stream().sorted(Comparator.comparing(Module::name)) JDK.stream().sorted(Comparator.comparing(Module::name))

View file

@ -1,12 +1,10 @@
main.usage.summary=\ main.usage.summary=\
Usage: {0} <options> [-m <module-name> | <classes...>]\n\ Usage: {0} <options> <path ...>]\n\
use -h, -? or -help for a list of possible options use -h, -? or -help for a list of possible options
main.usage=\ main.usage=\
Usage: {0} <options> [-m <module-name> | <classes...>]\n\ Usage: {0} <options> <path ...>]\n\
If -m <module-name> is specified, the specified module will be analyzed\n\ <path> can be a pathname to a .class file, a directory, a JAR file.\n\
otherwise, <classes> can be a pathname to a .class file, a directory,\n\
a JAR file, or a fully-qualified class name.\n\
\n\ \n\
Possible options include: Possible options include:
@ -27,34 +25,36 @@ main.opt.v=\
\ -verbose:class Print class-level dependencies excluding\n\ \ -verbose:class Print class-level dependencies excluding\n\
\ dependencies within the same package by default \ dependencies within the same package by default
main.opt.f=\
\ -f <regex> -filter <regex> Filter dependences matching the given pattern\n\
\ If given multiple times, the last one will be used.\n\
\ -filter:package Filter dependences within the same package (default)\n\
\ -filter:module Filter dependences within the same module\n\
\ -filter:archive Filter dependences within the same archive\n\
\ -filter:none No -filter:package and -filter:archive filtering\n\
\ Filtering specified via the -filter option still applies.
main.opt.s=\ main.opt.s=\
\ -s -summary Print dependency summary only.\n\ \ -s -summary Print dependency summary only.
\ If -s option is used with -m, the module descriptor of\n\
\ the given module will be read to generate the graph.
main.opt.p=\ main.opt.f=\
\ -p <pkgname> Finds dependences matching the given package name\n\ \ -f <regex> -filter <regex> Filter dependences matching the given\n\
\ -package <pkgname> (may be given multiple times). \ pattern. If given multiple times, the last\n\
\ one will be used.\n\
\ -filter:package Filter dependences within the same package.\n\
\ This is the default.\n\
\ -filter:archive Filter dependences within the same archive.\n\
\ -filter:module Filter dependences within the same module.\n\
\ -filter:none No -filter:package and -filter:archive\n\
\ filtering. Filtering specified via the\n\
\ -filter option still applies.\n\
main.opt.p=\n\
\Options to filter dependencies:\n\
\ -p <pkgname> -package <pkgname> Finds dependences matching the given package\n\
\ name (may be given multiple times).
main.opt.e=\ main.opt.e=\
\ -e <regex>\n\ \ -e <regex> -regex <regex> Finds dependences matching the given pattern.
\ -regex <regex> Finds dependences matching the given pattern.
main.opt.module=\ main.opt.module=\
\ -module <module-name> Finds dependences matching the given module name\n\ \ -module <module-name> Finds dependences matching the given module\n\
\ (may be given multiple times).\n\ \ name (may be given multiple times).\n\
\ -package, -regex, -requires are mutual exclusive. \ -package, -regex, -module are mutual exclusive.
main.opt.include=\ main.opt.include=\n\
\Options to filter classes to be analyzed:\n\
\ -include <regex> Restrict analysis to classes matching pattern\n\ \ -include <regex> Restrict analysis to classes matching pattern\n\
\ This option filters the list of classes to\n\ \ This option filters the list of classes to\n\
\ be analyzed. It can be used together with\n\ \ be analyzed. It can be used together with\n\
@ -63,9 +63,6 @@ main.opt.include=\
main.opt.P=\ main.opt.P=\
\ -P -profile Show profile containing a package \ -P -profile Show profile containing a package
main.opt.M=\
\ -M Show module containing a package
main.opt.cp=\ main.opt.cp=\
\ -cp <path> -classpath <path> Specify where to find class files \ -cp <path> -classpath <path> Specify where to find class files
@ -77,71 +74,76 @@ main.opt.upgrademodulepath=\
\ -upgrademodulepath <module path>... Specify upgrade module path \ -upgrademodulepath <module path>... Specify upgrade module path
main.opt.m=\ main.opt.m=\
\ -m <module-name> Specify the name of the module and its transitive\n\ \ -m <module-name> Specify the root module for analysis
\ dependences to be analyzed.
main.opt.R=\ main.opt.R=\
\ -R -recursive Recursively traverse all run-time dependencies.\n\ \ -R -recursive Recursively traverse all run-time dependencies.\n\
\ The -R option implies -filter:none. If -p, -e, -f\n\ \ The -R option implies -filter:none. If -p,\n\
\ option is specified, only the matching dependences\n\ \ -e, -foption is specified, only the matching\n\
\ are analyzed. \ dependences are analyzed.
main.opt.ct=\ main.opt.ct=\
\ -ct -compile-time Compile-time view of transitive dependencies\n\ \ -ct -compile-time Compile-time view of transitive dependencies\n\
\ i.e. compile-time view of -R option. If a dependence\n\ \ i.e. compile-time view of -R option.\n\
\ is found from a directory, a JAR file or a module,\n\ \ Analyzes the dependences per other given options\n\
\ all class files in that containing archive are analyzed. \ If a dependence is found from a directory,\n\
\ a JAR file or a module, all classes in that \n\
\ containing archive are analyzed.
main.opt.apionly=\ main.opt.apionly=\
\ -apionly Restrict analysis to APIs i.e. dependences\n\ \ -apionly Restrict analysis to APIs i.e. dependences\n\
\ from the signature of public and protected\n\ \ from the signature of public and protected\n\
\ members of public classes including field\n\ \ members of public classes including field\n\
\ type, method parameter types, returned type,\n\ \ type, method parameter types, returned type,\n\
\ checked exception types etc \ checked exception types etc.
main.opt.genmoduleinfo=\ main.opt.genmoduleinfo=\
\ -genmoduleinfo <dir> Generate module-info.java under the specified directory.\n\ \ -genmoduleinfo <dir> Generate module-info.java under the specified\n\
\ The specified JAR files will be analyzed.\n\ \ directory. The specified JAR files will be\n\
\ This option cannot be used with -dotoutput or -cp. \ analyzed. This option cannot be used with\n\
\ -dotoutput or -cp.
main.opt.check=\ main.opt.check=\
\ -check Analyze the dependence of a given module specified via\n\ \ -check <module-name>[,<module-name>...\n\
\ -m option. It prints out the resulting module dependency\n\ \ Analyze the dependence of the specified modules\n\
\ graph after transition reduction and also identifies any\n\ \ It prints the module descriptor, the resulting\n\
\ unused qualified exports. \ module dependences after analysis and the\n\
\ graph after transition reduction. It also\n\
\ identifies any unused qualified exports.
main.opt.dotoutput=\ main.opt.dotoutput=\
\ -dotoutput <dir> Destination directory for DOT file output \ -dotoutput <dir> Destination directory for DOT file output
main.opt.jdkinternals=\ main.opt.jdkinternals=\
\ -jdkinternals Finds class-level dependences on JDK internal APIs.\n\ \ -jdkinternals Finds class-level dependences on JDK internal\n\
\ By default, it analyzes all classes on -classpath\n\ \ APIs. By default, it analyzes all classes\n\
\ and input files unless -include option is specified.\n\ \ on -classpath and input files unless -include\n\
\ This option cannot be used with -p, -e and -s options.\n\ \ option is specified. This option cannot be\n\
\ WARNING: JDK internal APIs may not be accessible in\n\ \ used with -p, -e and -s options.\n\
\ the next release. \ WARNING: JDK internal APIs are inaccessible.
main.opt.depth=\ main.opt.depth=\
\ -depth=<depth> Specify the depth of the transitive\n\ \ -depth=<depth> Specify the depth of the transitive\n\
\ dependency analysis \ dependency analysis
main.opt.q=\ main.opt.q=\
\ -q -quiet Do not show missing dependencies from -genmoduleinfo output. \ -q -quiet Do not show missing dependencies from \n\
\ -genmoduleinfo output.
err.unknown.option=unknown option: {0} err.unknown.option=unknown option: {0}
err.missing.arg=no value given for {0} err.missing.arg=no value given for {0}
err.invalid.arg.for.option=invalid argument for option: {0} err.invalid.arg.for.option=invalid argument for option: {0}
err.option.after.class=option must be specified before classes: {0} err.option.after.class=option must be specified before classes: {0}
err.genmoduleinfo.not.jarfile={0} not valid for -genmoduleinfo option (must be JAR file) err.genmoduleinfo.not.jarfile={0} not valid for -genmoduleinfo option (must be non-modular JAR file)
err.profiles.msg=No profile information err.profiles.msg=No profile information
err.exception.message={0} err.exception.message={0}
err.invalid.path=invalid path: {0} err.invalid.path=invalid path: {0}
err.invalid.module.option=-m {0} is set but {1} is specified. err.invalid.module.option=Cannot set {0} with {1} option.
err.invalid.filters=Only one of -package (-p), -regex (-e), -requires option can be set err.invalid.filters=Only one of -package (-p), -regex (-e), -module option can be set
err.module.not.found=module not found: {0} err.module.not.found=module not found: {0}
err.root.module.not.set=-m is not set err.root.module.not.set=root module set empty
warn.invalid.arg=Invalid classname or pathname not exist: {0} warn.invalid.arg=Path not exist: {0}
warn.split.package=package {0} defined in {1} {2} warn.split.package=package {0} defined in {1} {2}
warn.replace.useJDKInternals=\ warn.replace.useJDKInternals=\
JDK internal APIs are unsupported and private to JDK implementation that are\n\ JDK internal APIs are unsupported and private to JDK implementation that are\n\

View file

@ -1,6 +1,5 @@
// No translation needed # No translation needed
com.sun.crypto.provider.SunJCE=Use java.security.Security.getProvider(provider-name) @since 1.3 com.sun.crypto.provider.SunJCE=Use java.security.Security.getProvider(provider-name) @since 1.3
com.sun.image.codec=Use javax.imageio @since 1.4
com.sun.org.apache.xml.internal.security=Use java.xml.crypto @since 1.6 com.sun.org.apache.xml.internal.security=Use java.xml.crypto @since 1.6
com.sun.org.apache.xml.internal.security.utils.Base64=Use java.util.Base64 @since 1.8 com.sun.org.apache.xml.internal.security.utils.Base64=Use java.util.Base64 @since 1.8
com.sun.org.apache.xml.internal.resolver=Use javax.xml.catalog @since 9 com.sun.org.apache.xml.internal.resolver=Use javax.xml.catalog @since 9
@ -9,17 +8,34 @@ com.sun.net.ssl.internal.ssl.Provider=Use java.security.Security.getProvider(pro
com.sun.rowset=Use javax.sql.rowset.RowSetProvider @since 1.7 com.sun.rowset=Use javax.sql.rowset.RowSetProvider @since 1.7
com.sun.tools.javac.tree=Use com.sun.source @since 1.6 com.sun.tools.javac.tree=Use com.sun.source @since 1.6
com.sun.tools.javac=Use javax.tools and javax.lang.model @since 1.6 com.sun.tools.javac=Use javax.tools and javax.lang.model @since 1.6
java.awt.peer=Should not use. See https://bugs.openjdk.java.net/browse/JDK-8037739
java.awt.dnd.peer=Should not use. See https://bugs.openjdk.java.net/browse/JDK-8037739
jdk.internal.ref.Cleaner=Use java.lang.ref.PhantomReference @since 1.2 or java.lang.ref.Cleaner @since 9
sun.awt.image.codec=Use javax.imageio @since 1.4 sun.awt.image.codec=Use javax.imageio @since 1.4
sun.misc.BASE64Encoder=Use java.util.Base64 @since 1.8 sun.awt.CausedFocusEvent=Use java.awt.event.FocusEvent::getCause @since 9
sun.misc.BASE64Decoder=Use java.util.Base64 @since 1.8 sun.font.FontUtilities=See java.awt.Font.textRequiresLayout @since 9
sun.misc.Cleaner=Use java.lang.ref.PhantomReference @since 1.2 or java.lang.ref.Cleaner @since 9 sun.reflect.Reflection=See StackWalker API @since 9
sun.misc.Service=Use java.util.ServiceLoader @since 1.6 sun.reflect.ReflectionFactory=See http://openjdk.java.net/jeps/260
sun.misc.Unsafe=See http://openjdk.java.net/jeps/260
sun.misc.Signal=See http://openjdk.java.net/jeps/260
sun.misc.SignalHandler=See http://openjdk.java.net/jeps/260
sun.security.action=Use java.security.PrivilegedAction @since 1.1 sun.security.action=Use java.security.PrivilegedAction @since 1.1
sun.security.krb5=Use com.sun.security.jgss sun.security.krb5=Use com.sun.security.jgss
sun.security.provider.PolicyFile=Use java.security.Policy.getInstance("JavaPolicy", new URIParameter(uri)) @since 1.6 sun.security.provider.PolicyFile=Use java.security.Policy.getInstance("JavaPolicy", new URIParameter(uri)) @since 1.6
sun.security.provider.Sun=Use java.security.Security.getProvider(provider-name) @since 1.3 sun.security.provider.Sun=Use java.security.Security.getProvider(provider-name) @since 1.3
sun.security.util.SecurityConstants=Use appropriate java.security.Permission subclass @since 1.1 sun.security.util.SecurityConstants=Use appropriate java.security.Permission subclass @since 1.1
sun.security.x509.X500Name=Use javax.security.auth.x500.X500Principal @since 1.4 sun.security.x509.X500Name=Use javax.security.auth.x500.X500Principal @since 1.4
sun.tools.jar=Use java.util.jar or jar tool @since 1.2 sun.tools.jar=Use java.util.jar or jar tool @since 1.2\
jdk.internal.ref.Cleaner=Use java.lang.ref.PhantomReference @since 1.2 or java.lang.ref.Cleaner @since 9 # Internal APIs removed in JDK 9
com.apple.eawt=Use java.awt.desktop and JEP 272 @since 9
com.apple.concurrent=Removed. See https://bugs.openjdk.java.net/browse/JDK-8148187
com.sun.image.codec=Use javax.imageio @since 1.4
sun.misc.BASE64Encoder=Use java.util.Base64 @since 1.8
sun.misc.BASE64Decoder=Use java.util.Base64 @since 1.8
sun.misc.Cleaner=Use java.lang.ref.PhantomReference @since 1.2 or java.lang.ref.Cleaner @since 9
sun.misc.Service=Use java.util.ServiceLoader @since 1.6
sun.misc=Removed. See http://openjdk.java.net/jeps/260
sun.reflect=Removed. See http://openjdk.java.net/jeps/260

View file

@ -25,6 +25,7 @@
* @test * @test
* @bug 8015912 8029216 8048063 8050804 * @bug 8015912 8029216 8048063 8050804
* @summary Test -apionly and -jdkinternals options * @summary Test -apionly and -jdkinternals options
* @library lib
* @modules java.base/sun.security.x509 * @modules java.base/sun.security.x509
* java.management * java.management
* jdk.jdeps/com.sun.tools.classfile * jdk.jdeps/com.sun.tools.classfile
@ -154,7 +155,8 @@ public class APIDeps {
Map<String,String> jdeps(String... args) { Map<String,String> jdeps(String... args) {
StringWriter sw = new StringWriter(); StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw); PrintWriter pw = new PrintWriter(sw);
System.err.println("jdeps " + Arrays.toString(args)); System.err.println("jdeps " + Arrays.stream(args)
.collect(Collectors.joining(" ")));
int rc = com.sun.tools.jdeps.Main.run(args, pw); int rc = com.sun.tools.jdeps.Main.run(args, pw);
pw.close(); pw.close();
String out = sw.toString(); String out = sw.toString();

View file

@ -39,6 +39,8 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.regex.*; import java.util.regex.*;
import java.util.stream.Collectors;
import static java.nio.file.StandardCopyOption.*; import static java.nio.file.StandardCopyOption.*;
public class Basic { public class Basic {
@ -157,7 +159,7 @@ public class Basic {
Map<String,String> jdeps(String... args) { Map<String,String> jdeps(String... args) {
StringWriter sw = new StringWriter(); StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw); PrintWriter pw = new PrintWriter(sw);
System.err.println("jdeps " + Arrays.toString(args)); System.err.println("jdeps " + Arrays.stream(args).collect(Collectors.joining(" ")));
int rc = com.sun.tools.jdeps.Main.run(args, pw); int rc = com.sun.tools.jdeps.Main.run(args, pw);
pw.close(); pw.close();
String out = sw.toString(); String out = sw.toString();

View file

@ -41,6 +41,7 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.*; import java.util.*;
import java.util.regex.*; import java.util.regex.*;
import java.util.stream.Collectors;
public class DotFileTest { public class DotFileTest {
public static void main(String... args) throws Exception { public static void main(String... args) throws Exception {
@ -182,7 +183,7 @@ public class DotFileTest {
// invoke jdeps // invoke jdeps
StringWriter sw = new StringWriter(); StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw); PrintWriter pw = new PrintWriter(sw);
System.err.println("jdeps " + args); System.err.println("jdeps " + args.stream().collect(Collectors.joining(" ")));
int rc = com.sun.tools.jdeps.Main.run(args.toArray(new String[0]), pw); int rc = com.sun.tools.jdeps.Main.run(args.toArray(new String[0]), pw);
pw.close(); pw.close();
String out = sw.toString(); String out = sw.toString();

View file

@ -0,0 +1,234 @@
/*
* Copyright (c) 2016, 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.
*
* 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.
*/
import com.sun.tools.jdeps.Analyzer;
import com.sun.tools.jdeps.DepsAnalyzer;
import com.sun.tools.jdeps.JdepsConfiguration;
import com.sun.tools.jdeps.JdepsFilter;
import com.sun.tools.jdeps.JdepsWriter;
import com.sun.tools.jdeps.ModuleAnalyzer;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.lang.module.ModuleDescriptor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Utilities to run jdeps command
*/
public final class JdepsUtil {
/*
* Runs jdeps with the given arguments
*/
public static String[] jdeps(String... args) {
String lineSep = System.getProperty("line.separator");
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
System.err.println("jdeps " + Arrays.stream(args).collect(Collectors.joining(" ")));
int rc = com.sun.tools.jdeps.Main.run(args, pw);
pw.close();
String out = sw.toString();
if (!out.isEmpty())
System.err.println(out);
if (rc != 0)
throw new Error("jdeps failed: rc=" + rc);
return out.split(lineSep);
}
public static Command newCommand(String cmd) {
return new Command(cmd);
}
public static class Command {
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw);
final JdepsFilter.Builder filter = new JdepsFilter.Builder().filter(true, true);
final JdepsConfiguration.Builder builder = new JdepsConfiguration.Builder();
final Set<String> requires = new HashSet<>();
Analyzer.Type verbose = Analyzer.Type.PACKAGE;
boolean apiOnly = false;
public Command(String cmd) {
System.err.println("============ ");
System.err.println(cmd);
}
public Command verbose(String verbose) {
switch (verbose) {
case "-verbose":
this.verbose = Analyzer.Type.VERBOSE;
filter.filter(false, false);
break;
case "-verbose:package":
this.verbose = Analyzer.Type.PACKAGE;
break;
case "-verbose:class":
this.verbose = Analyzer.Type.CLASS;
break;
case "-summary":
this.verbose = Analyzer.Type.SUMMARY;
break;
default:
throw new IllegalArgumentException(verbose);
}
return this;
}
public Command filter(String value) {
switch (value) {
case "-filter:package":
filter.filter(true, false);
break;
case "-filter:archive":
case "-filter:module":
filter.filter(false, true);
break;
default:
throw new IllegalArgumentException(value);
}
return this;
}
public Command addClassPath(String classpath) {
builder.addClassPath(classpath);
return this;
}
public Command addRoot(Path path) {
builder.addRoot(path);
return this;
}
public Command appModulePath(String modulePath) {
builder.appModulePath(modulePath);
return this;
}
public Command addmods(Set<String> mods) {
builder.addmods(mods);
return this;
}
public Command requires(Set<String> mods) {
requires.addAll(mods);
return this;
}
public Command matchPackages(Set<String> pkgs) {
filter.packages(pkgs);
return this;
}
public Command regex(String regex) {
filter.regex(Pattern.compile(regex));
return this;
}
public Command include(String regex) {
filter.includePattern(Pattern.compile(regex));
return this;
}
public Command includeSystemMoudles(String regex) {
filter.includeSystemModules(Pattern.compile(regex));
return this;
}
public Command apiOnly() {
this.apiOnly = true;
return this;
}
public JdepsConfiguration configuration() throws IOException {
JdepsConfiguration config = builder.build();
requires.forEach(name -> {
ModuleDescriptor md = config.findModuleDescriptor(name).get();
filter.requires(name, md.packages());
});
return config;
}
private JdepsWriter writer() {
return JdepsWriter.newSimpleWriter(pw, verbose);
}
public DepsAnalyzer getDepsAnalyzer() throws IOException {
return new DepsAnalyzer(configuration(), filter.build(), writer(),
verbose, apiOnly);
}
public ModuleAnalyzer getModuleAnalyzer(Set<String> mods) throws IOException {
// if -check is set, add to the root set and all modules are observable
addmods(mods);
builder.allModules();
return new ModuleAnalyzer(configuration(), pw, mods);
}
public void dumpOutput(PrintStream out) {
out.println(sw.toString());
}
}
/**
* Create a jar file using the list of files provided.
*/
public static void createJar(Path jarfile, Path root, Stream<Path> files)
throws IOException {
Path dir = jarfile.getParent();
if (dir != null && Files.notExists(dir)) {
Files.createDirectories(dir);
}
try (JarOutputStream target = new JarOutputStream(
Files.newOutputStream(jarfile))) {
files.forEach(file -> add(root.relativize(file), file, target));
}
}
private static void add(Path path, Path source, JarOutputStream target) {
try {
String name = path.toString().replace(File.separatorChar, '/');
JarEntry entry = new JarEntry(name);
entry.setTime(source.toFile().lastModified());
target.putNextEntry(entry);
Files.copy(source, target);
target.closeEntry();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

View file

@ -30,4 +30,3 @@ public class Foo extends Bar implements c.I {
setF(new f.F()); setF(new f.F());
} }
} }

View file

@ -0,0 +1,162 @@
/*
* Copyright (c) 2016, 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.
*
* 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.
*/
/*
* @test
* @summary Tests split packages
* @library ../lib
* @build CompilerUtils JdepsUtil
* @modules jdk.jdeps/com.sun.tools.jdeps
* @run testng CheckModuleTest
*/
import java.lang.module.ModuleDescriptor;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Set;
import com.sun.tools.jdeps.ModuleAnalyzer;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.assertEquals;
public class CheckModuleTest {
private static final String TEST_SRC = System.getProperty("test.src");
private static final String TEST_CLASSES = System.getProperty("test.classes");
private static final Path SRC_DIR = Paths.get(TEST_SRC, "src");
private static final Path MODS_DIR = Paths.get("mods");
// m4 and m5 are analyzed. Others are compiled to make sure they are present
// on the module path for analysis
private static final Set<String> modules = Set.of("unsafe", "m4", "m5", "m6", "m7", "m8");
private static final String JAVA_BASE = "java.base";
/**
* Compiles classes used by the test
*/
@BeforeTest
public void compileAll() throws Exception {
CompilerUtils.cleanDir(MODS_DIR);
modules.forEach(mn ->
assertTrue(CompilerUtils.compileModule(SRC_DIR, MODS_DIR, mn)));
}
@DataProvider(name = "javaBase")
public Object[][] base() {
return new Object[][] {
{ JAVA_BASE, new ModuleMetaData(JAVA_BASE)
},
};
};
@Test(dataProvider = "javaBase")
public void testJavaBase(String name, ModuleMetaData data) throws Exception {
JdepsUtil.Command jdeps = JdepsUtil.newCommand(
String.format("jdeps -check %s -mp %s%n", name, MODS_DIR)
);
jdeps.appModulePath(MODS_DIR.toString());
ModuleAnalyzer analyzer = jdeps.getModuleAnalyzer(Set.of(name));
assertTrue(analyzer.run());
jdeps.dumpOutput(System.err);
ModuleDescriptor[] descriptors = analyzer.descriptors(name);
for (int i=0; i < 3; i++) {
descriptors[i].requires().stream()
.forEach(req -> data.checkRequires(req));
}
}
@DataProvider(name = "modules")
public Object[][] unnamed() {
return new Object[][]{
{ "m4", new ModuleMetaData[] {
// original
new ModuleMetaData("m4")
.requiresPublic("java.compiler")
.requires("java.logging")
// unnused exports
.exports("p4.internal", Set.of("m6", "m7")),
// suggested version
new ModuleMetaData("m4")
.requires("java.compiler"),
// reduced version
new ModuleMetaData("m4")
.requires("java.compiler")
}
},
{ "m5", new ModuleMetaData[] {
// original
new ModuleMetaData("m5")
.requiresPublic("java.compiler")
.requiresPublic("java.logging")
.requires("java.sql")
.requiresPublic("m4"),
// suggested version
new ModuleMetaData("m5")
.requiresPublic("java.compiler")
.requires("java.logging")
.requiresPublic("java.sql")
.requiresPublic("m4"),
// reduced version
new ModuleMetaData("m5")
.requiresPublic("java.compiler")
.requiresPublic("java.sql")
.requiresPublic("m4"),
}
},
};
}
@Test(dataProvider = "modules")
public void modularTest(String name, ModuleMetaData[] data) throws Exception {
JdepsUtil.Command jdeps = JdepsUtil.newCommand(
String.format("jdeps -check %s -mp %s%n", name, MODS_DIR)
);
jdeps.appModulePath(MODS_DIR.toString());
ModuleAnalyzer analyzer = jdeps.getModuleAnalyzer(Set.of(name));
assertTrue(analyzer.run());
jdeps.dumpOutput(System.err);
// compare the module descriptors and the suggested versions
ModuleDescriptor[] descriptors = analyzer.descriptors(name);
for (int i=0; i < 3; i++) {
ModuleMetaData metaData = data[i];
descriptors[i].requires().stream()
.forEach(req -> metaData.checkRequires(req));
}
Map<String, Set<String>> unused = analyzer.unusedQualifiedExports(name);
// verify unuused qualified exports
assertEquals(unused, data[0].exports);
}
}

View file

@ -24,8 +24,8 @@
/* /*
* @test * @test
* @summary Tests jdeps -genmoduleinfo option * @summary Tests jdeps -genmoduleinfo option
* @library .. * @library ../lib
* @build CompilerUtils * @build CompilerUtils JdepsUtil
* @modules jdk.jdeps/com.sun.tools.jdeps * @modules jdk.jdeps/com.sun.tools.jdeps
* @run testng GenModuleInfo * @run testng GenModuleInfo
*/ */
@ -39,16 +39,12 @@ import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.testng.annotations.DataProvider;
import org.testng.annotations.BeforeTest; import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertTrue;
@ -86,7 +82,7 @@ public class GenModuleInfo {
for (String mn : modules) { for (String mn : modules) {
Path root = MODS_DIR.resolve(mn); Path root = MODS_DIR.resolve(mn);
createJar(LIBS_DIR.resolve(mn + ".jar"), root, JdepsUtil.createJar(LIBS_DIR.resolve(mn + ".jar"), root,
Files.walk(root, Integer.MAX_VALUE) Files.walk(root, Integer.MAX_VALUE)
.filter(f -> { .filter(f -> {
String fn = f.getFileName().toString(); String fn = f.getFileName().toString();
@ -100,7 +96,7 @@ public class GenModuleInfo {
Stream<String> files = Arrays.stream(modules) Stream<String> files = Arrays.stream(modules)
.map(mn -> LIBS_DIR.resolve(mn + ".jar")) .map(mn -> LIBS_DIR.resolve(mn + ".jar"))
.map(Path::toString); .map(Path::toString);
jdeps(Stream.concat(Stream.of("-cp"), files).toArray(String[]::new)); JdepsUtil.jdeps(Stream.concat(Stream.of("-cp"), files).toArray(String[]::new));
} }
@Test @Test
@ -109,7 +105,7 @@ public class GenModuleInfo {
.map(mn -> LIBS_DIR.resolve(mn + ".jar")) .map(mn -> LIBS_DIR.resolve(mn + ".jar"))
.map(Path::toString); .map(Path::toString);
jdeps(Stream.concat(Stream.of("-genmoduleinfo", DEST_DIR.toString()), JdepsUtil.jdeps(Stream.concat(Stream.of("-genmoduleinfo", DEST_DIR.toString()),
files) files)
.toArray(String[]::new)); .toArray(String[]::new));
@ -188,46 +184,4 @@ public class GenModuleInfo {
} }
} }
/*
* Runs jdeps with the given arguments
*/
public static String[] jdeps(String... args) {
String lineSep = System.getProperty("line.separator");
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
System.err.println("jdeps " + Arrays.toString(args));
int rc = com.sun.tools.jdeps.Main.run(args, pw);
pw.close();
String out = sw.toString();
if (!out.isEmpty())
System.err.println(out);
if (rc != 0)
throw new Error("jdeps failed: rc=" + rc);
return out.split(lineSep);
}
/**
* Create a jar file using the list of files provided.
*/
public static void createJar(Path jarfile, Path root, Stream<Path> files)
throws IOException {
try (JarOutputStream target = new JarOutputStream(
Files.newOutputStream(jarfile))) {
files.forEach(file -> add(root.relativize(file), file, target));
}
}
private static void add(Path path, Path source, JarOutputStream target) {
try {
String name = path.toString().replace(File.separatorChar, '/');
JarEntry entry = new JarEntry(name);
entry.setTime(source.toFile().lastModified());
target.putNextEntry(entry);
Files.copy(source, target);
target.closeEntry();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
} }

View file

@ -0,0 +1,209 @@
/*
* Copyright (c) 2016, 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.
*
* 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.
*/
import com.sun.tools.jdeps.DepsAnalyzer;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import static com.sun.tools.jdeps.DepsAnalyzer.Info.*;
import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
import static java.lang.module.ModuleDescriptor.*;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
public class ModuleMetaData {
public static final String JAVA_BASE = "java.base";
static final String INTERNAL = "(internal)";
static final String QUALIFIED = "(qualified)";
static final String JDK_INTERNAL = "JDK internal API";
final String moduleName;
final boolean isNamed;
final Map<String, ModuleRequires> requires = new LinkedHashMap<>();
final Map<String, Dependence> references = new LinkedHashMap<>();
final Map<String, Set<String>> exports = new LinkedHashMap<>();
ModuleMetaData(String name) {
this(name, true);
}
ModuleMetaData(String name, boolean isNamed) {
this.moduleName = name;
this.isNamed = isNamed;
requires(JAVA_BASE); // implicit requires
}
String name() {
return moduleName;
}
ModuleMetaData requires(String name) {
requires.put(name, new ModuleRequires(name));
return this;
}
ModuleMetaData requiresPublic(String name) {
requires.put(name, new ModuleRequires(name, PUBLIC));
return this;
}
// for unnamed module
ModuleMetaData depends(String name) {
requires.put(name, new ModuleRequires(name));
return this;
}
ModuleMetaData reference(String origin, String target, String module) {
return dependence(origin, target, module, "");
}
ModuleMetaData internal(String origin, String target, String module) {
return dependence(origin, target, module, INTERNAL);
}
ModuleMetaData qualified(String origin, String target, String module) {
return dependence(origin, target, module, QUALIFIED);
}
ModuleMetaData jdkInternal(String origin, String target, String module) {
return dependence(origin, target, module, JDK_INTERNAL);
}
ModuleMetaData exports(String pn, Set<String> targets) {
exports.put(pn, targets);
return this;
}
private ModuleMetaData dependence(String origin, String target, String module, String access) {
references.put(key(origin, target), new Dependence(origin, target, module, access));
return this;
}
String key(String origin, String target) {
return origin + ":" + target;
}
void checkRequires(String name, Set<DepsAnalyzer.Node> adjacentNodes) {
// System.err.format("%s: Expected %s Found %s %n", name, requires, adjacentNodes);
adjacentNodes.stream()
.forEach(v -> checkRequires(v.name));
assertEquals(adjacentNodes.size(), requires.size());
}
void checkRequires(String name) {
ModuleRequires req = requires.get(name);
if (req == null)
System.err.println(moduleName + ": unexpected requires " + name);
assertTrue(requires.containsKey(name));
}
void checkRequires(Requires require) {
String name = require.name();
if (name.equals(JAVA_BASE))
return;
ModuleRequires req = requires.get(name);
if (req == null)
System.err.format("%s: unexpected dependence %s%n", moduleName, name);
assertTrue(requires.containsKey(name));
assertEquals(require.modifiers(), req.modifiers());
}
void checkDependences(String name, Set<DepsAnalyzer.Node> adjacentNodes) {
// System.err.format("%s: Expected %s Found %s %n", name, references, adjacentNodes);
adjacentNodes.stream()
.forEach(v -> checkDependence(name, v.name, v.source, v.info));
assertEquals(adjacentNodes.size(), references.size());
}
void checkDependence(String origin, String target, String module, DepsAnalyzer.Info info) {
String key = key(origin, target);
Dependence dep = references.get(key);
String access = "";
if (info == QUALIFIED_EXPORTED_API)
access = QUALIFIED;
else if (info == JDK_INTERNAL_API)
access = JDK_INTERNAL;
else if (info == INTERNAL_API)
access = INTERNAL;
assertTrue(references.containsKey(key));
assertEquals(dep.access, access);
assertEquals(dep.module, module);
}
public static class ModuleRequires {
final String name;
final Requires.Modifier mod;
ModuleRequires(String name) {
this.name = name;
this.mod = null;
}
ModuleRequires(String name, Requires.Modifier mod) {
this.name = name;
this.mod = mod;
}
Set<Requires.Modifier> modifiers() {
return mod != null ? Set.of(mod) : Collections.emptySet();
}
@Override
public String toString() {
return name;
}
}
public static class Dependence {
final String origin;
final String target;
final String module;
final String access;
Dependence(String origin, String target, String module, String access) {
this.origin = origin;
this.target = target;
this.module = module;
this.access = access;
}
@Override
public String toString() {
return String.format("%s -> %s (%s) %s", origin, target, module, access);
}
}
}

View file

@ -24,30 +24,28 @@
/* /*
* @test * @test
* @summary Tests jdeps -m and -mp options on named modules and unnamed modules * @summary Tests jdeps -m and -mp options on named modules and unnamed modules
* @library .. * @library ../lib
* @build CompilerUtils * @build CompilerUtils JdepsUtil
* @modules jdk.jdeps/com.sun.tools.jdeps * @modules jdk.jdeps/com.sun.tools.jdeps
* @run testng ModuleTest * @run testng ModuleTest
*/ */
import java.io.PrintWriter; import java.io.File;
import java.io.StringWriter; import java.io.IOException;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.Requires.Modifier;
import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.sun.tools.jdeps.DepsAnalyzer;
import com.sun.tools.jdeps.Graph;
import org.testng.annotations.DataProvider; import org.testng.annotations.DataProvider;
import org.testng.annotations.BeforeTest; import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertTrue;
public class ModuleTest { public class ModuleTest {
@ -56,6 +54,7 @@ public class ModuleTest {
private static final Path SRC_DIR = Paths.get(TEST_SRC, "src"); private static final Path SRC_DIR = Paths.get(TEST_SRC, "src");
private static final Path MODS_DIR = Paths.get("mods"); private static final Path MODS_DIR = Paths.get("mods");
private static final Path UNNAMED_DIR = Paths.get("unnamed");
// the names of the modules in this test // the names of the modules in this test
private static final String UNSUPPORTED = "unsupported"; private static final String UNSUPPORTED = "unsupported";
@ -66,17 +65,22 @@ public class ModuleTest {
@BeforeTest @BeforeTest
public void compileAll() throws Exception { public void compileAll() throws Exception {
CompilerUtils.cleanDir(MODS_DIR); CompilerUtils.cleanDir(MODS_DIR);
CompilerUtils.cleanDir(UNNAMED_DIR);
assertTrue(CompilerUtils.compileModule(SRC_DIR, MODS_DIR, UNSUPPORTED, assertTrue(CompilerUtils.compileModule(SRC_DIR, MODS_DIR, UNSUPPORTED,
"-XaddExports:java.base/jdk.internal.perf=" + UNSUPPORTED)); "-XaddExports:java.base/jdk.internal.perf=" + UNSUPPORTED));
// m4 is not referenced // m4 is not referenced
Arrays.asList("m1", "m2", "m3", "m4") Arrays.asList("m1", "m2", "m3", "m4")
.forEach(mn -> assertTrue(CompilerUtils.compileModule(SRC_DIR, MODS_DIR, mn))); .forEach(mn -> assertTrue(CompilerUtils.compileModule(SRC_DIR, MODS_DIR, mn)));
assertTrue(CompilerUtils.compile(SRC_DIR.resolve("m3"), UNNAMED_DIR, "-mp", MODS_DIR.toString()));
Files.delete(UNNAMED_DIR.resolve("module-info.class"));
} }
@DataProvider(name = "modules") @DataProvider(name = "modules")
public Object[][] expected() { public Object[][] expected() {
return new Object[][]{ return new Object[][]{
{ "m3", new Data("m3").requiresPublic("java.sql") { "m3", new ModuleMetaData("m3").requiresPublic("java.sql")
.requiresPublic("m2") .requiresPublic("m2")
.requires("java.logging") .requires("java.logging")
.requiresPublic("m1") .requiresPublic("m1")
@ -87,41 +91,42 @@ public class ModuleTest {
.reference("p3", "p2", "m2") .reference("p3", "p2", "m2")
.qualified("p3", "p2.internal", "m2") .qualified("p3", "p2.internal", "m2")
}, },
{ "m2", new Data("m2").requiresPublic("m1") { "m2", new ModuleMetaData("m2").requiresPublic("m1")
.reference("p2", "java.lang", "java.base") .reference("p2", "java.lang", "java.base")
.reference("p2", "p1", "m1") .reference("p2", "p1", "m1")
.reference("p2.internal", "java.lang", "java.base") .reference("p2.internal", "java.lang", "java.base")
.reference("p2.internal", "java.io", "java.base") .reference("p2.internal", "java.io", "java.base")
}, },
{ "m1", new Data("m1").requires("unsupported") { "m1", new ModuleMetaData("m1").requires("unsupported")
.reference("p1", "java.lang", "java.base") .reference("p1", "java.lang", "java.base")
.reference("p1.internal", "java.lang", "java.base") .reference("p1.internal", "java.lang", "java.base")
.reference("p1.internal", "p1", "m1") .reference("p1.internal", "p1", "m1")
.reference("p1.internal", "q", "unsupported") .reference("p1.internal", "q", "unsupported")
}, },
{ "unsupported", new Data("unsupported") { "unsupported", new ModuleMetaData("unsupported")
.reference("q", "java.lang", "java.base") .reference("q", "java.lang", "java.base")
.jdkInternal("q", "jdk.internal.perf", "(java.base)") .jdkInternal("q", "jdk.internal.perf", "java.base")
}, },
}; };
} }
@Test(dataProvider = "modules") @Test(dataProvider = "modules")
public void modularTest(String name, Data data) { public void modularTest(String name, ModuleMetaData data) throws IOException {
// print only the specified module // jdeps -modulepath mods -m <name>
String excludes = Arrays.stream(modules) runTest(data, MODS_DIR.toString(), Set.of(name));
.filter(mn -> !mn.endsWith(name))
.collect(Collectors.joining(",")); // jdeps -modulepath libs/m1.jar:.... -m <name>
String[] result = jdeps("-exclude-modules", excludes, String mp = Arrays.stream(modules)
"-mp", MODS_DIR.toString(), .filter(mn -> !mn.equals(name))
"-m", name); .map(mn -> MODS_DIR.resolve(mn).toString())
assertTrue(data.check(result)); .collect(Collectors.joining(File.pathSeparator));
runTest(data, mp, Collections.emptySet(), MODS_DIR.resolve(name));
} }
@DataProvider(name = "unnamed") @DataProvider(name = "unnamed")
public Object[][] unnamed() { public Object[][] unnamed() {
return new Object[][]{ return new Object[][]{
{ "m3", new Data("m3", false) { "unnamed", new ModuleMetaData("unnamed", false)
.depends("java.sql") .depends("java.sql")
.depends("java.logging") .depends("java.logging")
.depends("m1") .depends("m1")
@ -133,178 +138,43 @@ public class ModuleTest {
.reference("p3", "p2", "m2") .reference("p3", "p2", "m2")
.internal("p3", "p2.internal", "m2") .internal("p3", "p2.internal", "m2")
}, },
{ "unsupported", new Data("unsupported", false)
.reference("q", "java.lang", "java.base")
.jdkInternal("q", "jdk.internal.perf", "(java.base)")
},
}; };
} }
@Test(dataProvider = "unnamed") @Test(dataProvider = "unnamed")
public void unnamedTest(String name, Data data) { public void unnamedTest(String name, ModuleMetaData data) throws IOException {
String[] result = jdeps("-mp", MODS_DIR.toString(), MODS_DIR.resolve(name).toString()); runTest(data, MODS_DIR.toString(), Set.of("m1", "m2"), UNNAMED_DIR);
assertTrue(data.check(result));
} }
/* private void runTest(ModuleMetaData data, String modulepath,
* Runs jdeps with the given arguments Set<String> roots, Path... paths)
*/ throws IOException
public static String[] jdeps(String... args) { {
String lineSep = System.getProperty("line.separator"); // jdeps -modulepath <modulepath> -m root paths
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
System.err.println("jdeps " + Arrays.toString(args));
int rc = com.sun.tools.jdeps.Main.run(args, pw);
pw.close();
String out = sw.toString();
if (!out.isEmpty())
System.err.println(out);
if (rc != 0)
throw new Error("jdeps failed: rc=" + rc);
return out.split(lineSep);
}
static class Data { JdepsUtil.Command jdeps = JdepsUtil.newCommand(
static final String INTERNAL = "(internal)"; String.format("jdeps -modulepath %s -addmods %s %s%n", MODS_DIR,
static final String QUALIFIED = "(qualified)"; roots.stream().collect(Collectors.joining(",")), paths)
static final String JDK_INTERNAL = "JDK internal API"; );
jdeps.appModulePath(modulepath)
.addmods(roots);
Arrays.stream(paths).forEach(jdeps::addRoot);
final String moduleName; // run the analyzer
final boolean isNamed; DepsAnalyzer analyzer = jdeps.getDepsAnalyzer();
final Map<String, ModuleRequires> requires = new LinkedHashMap<>(); assertTrue(analyzer.run());
final Map<String, Dependence> references = new LinkedHashMap<>();
Data(String name) {
this(name, true);
}
Data(String name, boolean isNamed) {
this.moduleName = name;
this.isNamed = isNamed;
requires("java.base"); // implicit requires
}
Data requires(String name) { // analyze result
requires.put(name, new ModuleRequires(name)); Graph<DepsAnalyzer.Node> g1 = analyzer.moduleGraph();
return this; g1.nodes().stream()
} .filter(u -> u.name.equals(data.moduleName))
Data requiresPublic(String name) { .forEach(u -> data.checkRequires(u.name, g1.adjacentNodes(u)));
requires.put(name, new ModuleRequires(name, PUBLIC));
return this;
}
// for unnamed module
Data depends(String name) {
requires.put(name, new ModuleRequires(name));
return this;
}
Data reference(String origin, String target, String module) {
return dependence(origin, target, module, "");
}
Data internal(String origin, String target, String module) {
return dependence(origin, target, module, INTERNAL);
}
Data qualified(String origin, String target, String module) {
return dependence(origin, target, module, QUALIFIED);
}
Data jdkInternal(String origin, String target, String module) {
return dependence(origin, target, module, JDK_INTERNAL);
}
private Data dependence(String origin, String target, String module, String access) {
references.put(key(origin, target), new Dependence(origin, target, module, access));
return this;
}
String key(String origin, String target) { Graph<DepsAnalyzer.Node> g2 = analyzer.dependenceGraph();
return origin+":"+target; g2.nodes().stream()
} .filter(u -> u.name.equals(data.moduleName))
boolean check(String[] lines) { .forEach(u -> data.checkDependences(u.name, g2.adjacentNodes(u)));
System.out.format("verifying module %s%s%n", moduleName, isNamed ? "" : " (unnamed module)");
for (String l : lines) {
String[] tokens = l.trim().split("\\s+");
System.out.println(" " + Arrays.stream(tokens).collect(Collectors.joining(" ")));
switch (tokens[0]) {
case "module":
assertEquals(tokens.length, 2);
assertEquals(moduleName, tokens[1]);
break;
case "requires":
String name = tokens.length == 2 ? tokens[1] : tokens[2];
Modifier modifier = null;
if (tokens.length == 3) {
assertEquals("public", tokens[1]);
modifier = PUBLIC;
}
checkRequires(name, modifier);
break;
default:
if (tokens.length == 3) {
// unnamed module requires
assertFalse(isNamed);
assertEquals(moduleName, tokens[0]);
String mn = tokens[2];
checkRequires(mn, null);
} else {
checkDependence(tokens);
}
}
}
return true;
}
private void checkRequires(String name, Modifier modifier) { jdeps.dumpOutput(System.err);
assertTrue(requires.containsKey(name));
ModuleRequires req = requires.get(name);
assertEquals(req.mod, modifier);
}
private void checkDependence(String[] tokens) {
assertTrue(tokens.length >= 4);
String origin = tokens[0];
String target = tokens[2];
String module = tokens[3];
String key = key(origin, target);
assertTrue(references.containsKey(key));
Dependence dep = references.get(key);
if (tokens.length == 4) {
assertEquals(dep.access, "");
} else if (tokens.length == 5) {
assertEquals(dep.access, tokens[4]);
} else {
// JDK internal API
module = tokens[6];
assertEquals(tokens.length, 7);
assertEquals(tokens[3], "JDK");
assertEquals(tokens[4], "internal");
assertEquals(tokens[5], "API");
}
assertEquals(dep.module, module);
}
public static class ModuleRequires {
final String name;
final ModuleDescriptor.Requires.Modifier mod;
ModuleRequires(String name) {
this.name = name;
this.mod = null;
}
ModuleRequires(String name, ModuleDescriptor.Requires.Modifier mod) {
this.name = name;
this.mod = mod;
}
}
public static class Dependence {
final String origin;
final String target;
final String module;
final String access;
Dependence(String origin, String target, String module, String access) {
this.origin = origin;
this.target = target;
this.module = module;
this.access = access;
}
}
} }
} }

View file

@ -0,0 +1,105 @@
/*
* Copyright (c) 2016, 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.
*
* 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.
*/
/*
* @test
* @summary Tests split packages
* @library ../lib
* @build CompilerUtils
* @modules jdk.jdeps/com.sun.tools.jdeps
* @run testng SplitPackage
*/
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.sun.tools.jdeps.DepsAnalyzer;
import com.sun.tools.jdeps.JdepsConfiguration;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import static org.testng.Assert.assertTrue;
public class SplitPackage {
private static final String TEST_SRC = System.getProperty("test.src");
private static final Path CLASSES_DIR = Paths.get("classes");
private static final String SPLIT_PKG_NAME = "javax.annotation";
private static final String JAVA_ANNOTATIONS_COMMON = "java.annotations.common";
/**
* Compiles classes used by the test
*/
@BeforeTest
public void compileAll() throws Exception {
CompilerUtils.cleanDir(CLASSES_DIR);
assertTrue(CompilerUtils.compile(Paths.get(TEST_SRC, "patches"), CLASSES_DIR));
}
@Test
public void runTest() throws Exception {
// Test jdeps classes
runTest(null);
// Test jdeps -addmods
runTest(JAVA_ANNOTATIONS_COMMON, SPLIT_PKG_NAME);
}
private void runTest(String root, String... splitPackages) throws Exception {
JdepsUtil.Command jdeps = JdepsUtil.newCommand(
String.format("jdeps -verbose:class -addmods %s %s%n",
root, CLASSES_DIR)
);
jdeps.verbose("-verbose:class")
.addRoot(CLASSES_DIR);
if (root != null)
jdeps.addmods(Set.of(root));
JdepsConfiguration config = jdeps.configuration();
Map<String, Set<String>> pkgs = config.splitPackages();
final Set<String> expected;
if (splitPackages != null) {
expected = Arrays.stream(splitPackages).collect(Collectors.toSet());
} else {
expected = Collections.emptySet();
}
if (!pkgs.keySet().equals(expected)) {
throw new RuntimeException(splitPackages.toString());
}
// java.annotations.common is not observable
DepsAnalyzer analyzer = jdeps.getDepsAnalyzer();
assertTrue(analyzer.run());
jdeps.dumpOutput(System.err);
}
}

View file

@ -0,0 +1,325 @@
/*
* Copyright (c) 2016, 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.
*
* 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.
*/
/*
* @test
* @summary Tests jdeps -m and -mp options on named modules and unnamed modules
* @library ../lib
* @build CompilerUtils JdepsUtil
* @modules jdk.jdeps/com.sun.tools.jdeps
* @run testng TransitiveDeps
*/
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.sun.tools.jdeps.DepsAnalyzer;
import com.sun.tools.jdeps.Graph;
import org.testng.annotations.DataProvider;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import static org.testng.Assert.assertTrue;
public class TransitiveDeps {
private static final String TEST_SRC = System.getProperty("test.src");
private static final Path SRC_DIR = Paths.get(TEST_SRC, "src");
private static final Path MODS_DIR = Paths.get("mods");
private static final Path LIBS_DIR = Paths.get("libs");
// the names of the modules in this test
private static String[] modules = new String[] {"unsafe", "m6", "m7"};
/**
* Compiles all modules used by the test
*/
@BeforeTest
public void compileAll() throws Exception {
CompilerUtils.cleanDir(MODS_DIR);
CompilerUtils.cleanDir(LIBS_DIR);
for (String mn : modules) {
// compile a module
assertTrue(CompilerUtils.compileModule(SRC_DIR, MODS_DIR, mn));
// create JAR files with no module-info.class
Path root = MODS_DIR.resolve(mn);
JdepsUtil.createJar(LIBS_DIR.resolve(mn + ".jar"), root,
Files.walk(root, Integer.MAX_VALUE)
.filter(f -> {
String fn = f.getFileName().toString();
return fn.endsWith(".class") && !fn.equals("module-info.class");
}));
}
}
@DataProvider(name = "modules")
public Object[][] expected1() {
return new Object[][]{
{ "m7",
List.of(new ModuleMetaData("m7")
.requires("m6")
.requires("unsafe")
.reference("p7.Main", "java.lang.Object", "java.base")
.reference("p7.Main", "java.lang.String", "java.base")
.reference("p7.Main", "org.safe.Lib", "unsafe")
.reference("p7.Main", "p6.safe.Lib", "m6"),
new ModuleMetaData("m6")
.requires("unsafe")
.reference("p6.indirect.UnsafeRef", "java.lang.Object", "java.base")
.reference("p6.indirect.UnsafeRef", "org.unsafe.UseUnsafe ", "unsafe")
.reference("p6.safe.Lib", "java.io.PrintStream", "java.base")
.reference("p6.safe.Lib", "java.lang.Class", "java.base")
.reference("p6.safe.Lib", "java.lang.Object", "java.base")
.reference("p6.safe.Lib", "java.lang.String", "java.base")
.reference("p6.safe.Lib", "java.lang.System", "java.base")
.reference("p6.safe.Lib", "org.safe.Lib", "unsafe"),
new ModuleMetaData("unsafe")
.requires("jdk.unsupported")
.reference("org.indirect.UnsafeRef", "java.lang.Object", "java.base")
.reference("org.safe.Lib", "java.io.PrintStream", "java.base")
.reference("org.safe.Lib", "java.lang.Class", "java.base")
.reference("org.safe.Lib", "java.lang.Object", "java.base")
.reference("org.safe.Lib", "java.lang.String", "java.base")
.reference("org.safe.Lib", "java.lang.System", "java.base")
.reference("org.unsafe.UseUnsafe", "java.lang.Object", "java.base")
.jdkInternal("org.unsafe.UseUnsafe", "sun.misc.Unsafe", "java.base")
)
},
};
}
@Test(dataProvider = "modules")
public void testModulePath(String name, List<ModuleMetaData> data) throws IOException {
Set<String> roots = Set.of("m6", "unsafe");
JdepsUtil.Command jdeps = JdepsUtil.newCommand(
String.format("jdeps -modulepath %s -addmods %s -m %s%n", MODS_DIR,
roots.stream().collect(Collectors.joining(",")), name)
);
jdeps.verbose("-verbose:class")
.appModulePath(MODS_DIR.toString())
.addmods(roots)
.addmods(Set.of(name));
runJdeps(jdeps, data);
// run automatic modules
roots = Set.of("ALL-MODULE-PATH", "jdk.unsupported");
jdeps = JdepsUtil.newCommand(
String.format("jdeps -modulepath %s -addmods %s -m %s%n", LIBS_DIR,
roots.stream().collect(Collectors.joining(",")), name)
);
jdeps.verbose("-verbose:class")
.appModulePath(LIBS_DIR.toString())
.addmods(roots)
.addmods(Set.of(name));
runJdeps(jdeps, data);
}
@DataProvider(name = "jars")
public Object[][] expected2() {
return new Object[][]{
{ "m7", List.of(new ModuleMetaData("m7.jar")
.requires("m6.jar")
.requires("unsafe.jar")
.reference("p7.Main", "java.lang.Object", "java.base")
.reference("p7.Main", "java.lang.String", "java.base")
.reference("p7.Main", "org.safe.Lib", "unsafe.jar")
.reference("p7.Main", "p6.safe.Lib", "m6.jar"))
},
};
}
@Test(dataProvider = "jars")
public void testClassPath(String name, List<ModuleMetaData> data) throws IOException {
String cpath = Arrays.stream(modules)
.filter(mn -> !mn.equals(name))
.map(mn -> LIBS_DIR.resolve(mn + ".jar").toString())
.collect(Collectors.joining(File.pathSeparator));
Path jarfile = LIBS_DIR.resolve(name + ".jar");
JdepsUtil.Command jdeps = JdepsUtil.newCommand(
String.format("jdeps -classpath %s %s%n", cpath, jarfile)
);
jdeps.verbose("-verbose:class")
.addClassPath(cpath)
.addRoot(jarfile);
runJdeps(jdeps, data);
}
@DataProvider(name = "compileTimeView")
public Object[][] expected3() {
return new Object[][] {
{"m7",
List.of(new ModuleMetaData("m7.jar")
.requires("m6.jar")
.requires("unsafe.jar")
.reference("p7.Main", "java.lang.Object", "java.base")
.reference("p7.Main", "java.lang.String", "java.base")
.reference("p7.Main", "org.safe.Lib", "unsafe.jar")
.reference("p7.Main", "p6.safe.Lib", "m6.jar"),
new ModuleMetaData("m6.jar")
.requires("unsafe.jar")
.reference("p6.indirect.UnsafeRef", "java.lang.Object", "java.base")
.reference("p6.indirect.UnsafeRef", "org.unsafe.UseUnsafe ", "unsafe.jar")
.reference("p6.safe.Lib", "java.io.PrintStream", "java.base")
.reference("p6.safe.Lib", "java.lang.Class", "java.base")
.reference("p6.safe.Lib", "java.lang.Object", "java.base")
.reference("p6.safe.Lib", "java.lang.String", "java.base")
.reference("p6.safe.Lib", "java.lang.System", "java.base")
.reference("p6.safe.Lib", "org.safe.Lib", "unsafe.jar"),
new ModuleMetaData("unsafe.jar")
.requires("jdk.unsupported")
.reference("org.indirect.UnsafeRef", "java.lang.Object", "java.base")
.reference("org.safe.Lib", "java.io.PrintStream", "java.base")
.reference("org.safe.Lib", "java.lang.Class", "java.base")
.reference("org.safe.Lib", "java.lang.Object", "java.base")
.reference("org.safe.Lib", "java.lang.String", "java.base")
.reference("org.safe.Lib", "java.lang.System", "java.base")
.reference("org.unsafe.UseUnsafe", "java.lang.Object", "java.base")
.jdkInternal("org.unsafe.UseUnsafe", "sun.misc.Unsafe", "java.base")
)
},
};
}
@Test(dataProvider = "compileTimeView")
public void compileTimeView(String name, List<ModuleMetaData> data) throws IOException {
String cpath = Arrays.stream(modules)
.filter(mn -> !mn.equals(name))
.map(mn -> LIBS_DIR.resolve(mn + ".jar").toString())
.collect(Collectors.joining(File.pathSeparator));
Path jarfile = LIBS_DIR.resolve(name + ".jar");
JdepsUtil.Command jdeps = JdepsUtil.newCommand(
String.format("jdeps -ct -classpath %s %s%n", cpath, jarfile)
);
jdeps.verbose("-verbose:class")
.addClassPath(cpath)
.addRoot(jarfile);
runJdeps(jdeps, data, true, 0 /* -recursive */);
}
@DataProvider(name = "recursiveDeps")
public Object[][] expected4() {
return new Object[][] {
{"m7",
List.of(new ModuleMetaData("m7.jar")
.requires("m6.jar")
.requires("unsafe.jar")
.reference("p7.Main", "java.lang.Object", "java.base")
.reference("p7.Main", "java.lang.String", "java.base")
.reference("p7.Main", "org.safe.Lib", "unsafe.jar")
.reference("p7.Main", "p6.safe.Lib", "m6.jar"),
new ModuleMetaData("m6.jar")
.requires("unsafe.jar")
.reference("p6.safe.Lib", "java.io.PrintStream", "java.base")
.reference("p6.safe.Lib", "java.lang.Class", "java.base")
.reference("p6.safe.Lib", "java.lang.Object", "java.base")
.reference("p6.safe.Lib", "java.lang.String", "java.base")
.reference("p6.safe.Lib", "java.lang.System", "java.base")
.reference("p6.safe.Lib", "org.safe.Lib", "unsafe.jar"),
new ModuleMetaData("unsafe.jar")
.requires("jdk.unsupported")
.reference("org.indirect.UnsafeRef", "java.lang.Object", "java.base")
.reference("org.safe.Lib", "java.io.PrintStream", "java.base")
.reference("org.safe.Lib", "java.lang.Class", "java.base")
.reference("org.safe.Lib", "java.lang.Object", "java.base")
.reference("org.safe.Lib", "java.lang.String", "java.base")
.reference("org.safe.Lib", "java.lang.System", "java.base")
)
},
};
}
@Test(dataProvider = "recursiveDeps")
public void recursiveDeps(String name, List<ModuleMetaData> data) throws IOException {
String cpath = Arrays.stream(modules)
.filter(mn -> !mn.equals(name))
.map(mn -> LIBS_DIR.resolve(mn + ".jar").toString())
.collect(Collectors.joining(File.pathSeparator));
Path jarfile = LIBS_DIR.resolve(name + ".jar");
JdepsUtil.Command jdeps = JdepsUtil.newCommand(
String.format("jdeps -R -classpath %s %s%n", cpath, jarfile)
);
jdeps.verbose("-verbose:class").filter("-filter:archive")
.addClassPath(cpath)
.addRoot(jarfile);
runJdeps(jdeps, data, true, 0 /* -recursive */);
}
private void runJdeps(JdepsUtil.Command jdeps, List<ModuleMetaData> data)
throws IOException
{
runJdeps(jdeps, data, false, 1 /* depth */);
}
private void runJdeps(JdepsUtil.Command jdeps, List<ModuleMetaData> data,
boolean compileTimeView, int depth)
throws IOException
{
// run the analyzer
DepsAnalyzer analyzer = jdeps.getDepsAnalyzer();
assertTrue(analyzer.run(compileTimeView, depth));
jdeps.dumpOutput(System.err);
// analyze result
Graph<DepsAnalyzer.Node> g1 = analyzer.moduleGraph();
Map<String, ModuleMetaData> dataMap = data.stream()
.collect(Collectors.toMap(ModuleMetaData::name, Function.identity()));
// the returned graph contains all nodes such as java.base and jdk.unsupported
g1.nodes().stream()
.filter(u -> dataMap.containsKey(u.name))
.forEach(u -> {
ModuleMetaData md = dataMap.get(u.name);
md.checkRequires(u.name, g1.adjacentNodes(u));
});
Graph<DepsAnalyzer.Node> g2 = analyzer.dependenceGraph();
g2.nodes().stream()
.filter(u -> dataMap.containsKey(u.name))
.forEach(u -> {
ModuleMetaData md = dataMap.get(u.name);
md.checkDependences(u.name, g2.adjacentNodes(u));
});
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2016, 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.
*
* 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 javax.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
@Documented
@Target({FIELD, LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NonNull {
}

View file

@ -22,8 +22,14 @@
*/ */
module m4 { module m4 {
// not used in signature
requires public java.compiler; requires public java.compiler;
// unused dependence
requires java.logging; requires java.logging;
exports p4; exports p4;
exports p4.internal to m1,m2,m3;
// unuused qualified exports
exports p4.internal to m6,m7;
} }

View file

@ -23,9 +23,14 @@
package p4.internal; package p4.internal;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public class Impl { public class Impl {
private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
public String name() { public String name() {
return Impl.class.getName(); return Impl.class.getName();
} }
} }

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2016, 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.
*
* 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.
*/
module m5 {
// m4 requires public java.compilerr
requires public m4;
requires public java.compiler;
// java.sql should be requires public
requires java.sql;
// java.logging is used for implementation only
requires public java.logging;
exports p5;
// m8 is not in the resolved graph but used by m8
exports p5.internal to m8;
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2016, 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.
*
* 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 p5;
import java.sql.Driver;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public class Main {
public void run(Driver driver) throws Exception {
driver.getParentLogger().config("test");
}
public p4.Lib getLib() {
return new p4.Lib();
}
public JavaCompiler getCompiler() {
return ToolProvider.getSystemJavaCompiler();
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2016, 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.
*
* 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 p5.internal;
public class T {
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2016, 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.
*
* 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.
*/
module m6 {
requires unsafe;
// no dependency on sun.misc.Unsafe directly or indirectly
exports p6.safe;
// direct dependency on org.unsafe
// hence indirect dependency on sun.misc.Unsafe
exports p6.indirect;
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2016, 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.
*
* 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 p6.indirect;
// indirectly depend on sun.misc.Unsafe
public class UnsafeRef {
public static org.unsafe.UseUnsafe get() {
return new org.unsafe.UseUnsafe();
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2016, 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.
*
* 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 p6.safe;
// no direct or indirect dependency on sun.misc.Unsafe
public class Lib {
public static void doit() {
System.out.println(Lib.class.getName());
org.safe.Lib.doit();
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2016, 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.
*
* 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.
*/
module m7 {
// only use classes that have no direct or indirect dependency
// to sun.misc.Unsafe
requires unsafe;
requires m6;
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2016, 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.
*
* 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 p7;
// Only use classes in unsafe and m6 modules with no
// direct or indirect dependency on sun.misc.Unsafe
public class Main {
public static void main(String... args) {
p6.safe.Lib.doit();
org.safe.Lib.doit();
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2016, 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.
*
* 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.
*/
module m8 {
requires m5;
// use p5.internal
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2016, 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.
*
* 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 p8;
import p5.internal.T;
public class Main {
public static void main() {
T t = new T();
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2016, 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.
*
* 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.
*/
module unsafe {
requires jdk.unsupported;
// direct dependency on sun.misc.Unsafe
exports org.unsafe;
// no dependency on sun.misc.Unsafe directly or indirectly
exports org.safe;
// indirect dependency on sun.misc.Unsafe
exports org.indirect;
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2016, 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.
*
* 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 org.indirect;
// indirectly depend on sun.misc.Unsafe
public class UnsafeRef {
public static org.unsafe.UseUnsafe get() {
return new org.unsafe.UseUnsafe();
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2016, 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.
*
* 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 org.safe;
// no direct or indirect dependency on sun.misc.Unsafe
public class Lib {
public static void doit() {
System.out.println(Lib.class.getName());
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2016, 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.
*
* 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 org.unsafe;
import sun.misc.Unsafe;
public class UseUnsafe {
static Unsafe unsafe = Unsafe.getUnsafe();
}

View file

@ -62,7 +62,7 @@ public class JDKUnsupportedTest {
public void test(String filename, String[][] expected) { public void test(String filename, String[][] expected) {
Path path = Paths.get(TEST_CLASSES, filename); Path path = Paths.get(TEST_CLASSES, filename);
Map<String, String> result = jdeps("-M", path.toString()); Map<String, String> result = jdeps(path.toString());
for (String[] e : expected) { for (String[] e : expected) {
String pn = e[0]; String pn = e[0];
String module = e[1]; String module = e[1];