|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
|
|
package jdk.internal.module; |
|
|
|
import java.io.BufferedInputStream; |
|
import java.io.BufferedReader; |
|
import java.io.File; |
|
import java.io.IOException; |
|
import java.io.InputStream; |
|
import java.io.InputStreamReader; |
|
import java.io.UncheckedIOException; |
|
import java.lang.module.FindException; |
|
import java.lang.module.InvalidModuleDescriptorException; |
|
import java.lang.module.ModuleDescriptor; |
|
import java.lang.module.ModuleDescriptor.Builder; |
|
import java.lang.module.ModuleFinder; |
|
import java.lang.module.ModuleReference; |
|
import java.net.URI; |
|
import java.nio.file.DirectoryStream; |
|
import java.nio.file.Files; |
|
import java.nio.file.NoSuchFileException; |
|
import java.nio.file.Path; |
|
import java.nio.file.attribute.BasicFileAttributes; |
|
import java.util.ArrayList; |
|
import java.util.HashMap; |
|
import java.util.List; |
|
import java.util.Map; |
|
import java.util.Objects; |
|
import java.util.Optional; |
|
import java.util.Set; |
|
import java.util.jar.Attributes; |
|
import java.util.jar.JarEntry; |
|
import java.util.jar.JarFile; |
|
import java.util.jar.Manifest; |
|
import java.util.regex.Matcher; |
|
import java.util.regex.Pattern; |
|
import java.util.stream.Collectors; |
|
import java.util.zip.ZipException; |
|
import java.util.zip.ZipFile; |
|
|
|
import sun.nio.cs.UTF_8; |
|
|
|
import jdk.internal.jmod.JmodFile; |
|
import jdk.internal.jmod.JmodFile.Section; |
|
import jdk.internal.perf.PerfCounter; |
|
|
|
/** |
|
* A {@code ModuleFinder} that locates modules on the file system by searching |
|
* a sequence of directories or packaged modules. The ModuleFinder can be |
|
* created to work in either the run-time or link-time phases. In both cases it |
|
* locates modular JAR and exploded modules. When created for link-time then it |
|
* additionally locates modules in JMOD files. The ModuleFinder can also |
|
* optionally patch any modules that it locates with a ModulePatcher. |
|
*/ |
|
|
|
public class ModulePath implements ModuleFinder { |
|
private static final String MODULE_INFO = "module-info.class"; |
|
|
|
|
|
private final Runtime.Version releaseVersion; |
|
|
|
|
|
private final boolean isLinkPhase; |
|
|
|
|
|
private final ModulePatcher patcher; |
|
|
|
|
|
private final Path[] entries; |
|
private int next; |
|
|
|
|
|
private final Map<String, ModuleReference> cachedModules = new HashMap<>(); |
|
|
|
|
|
private ModulePath(Runtime.Version version, |
|
boolean isLinkPhase, |
|
ModulePatcher patcher, |
|
Path... entries) { |
|
this.releaseVersion = version; |
|
this.isLinkPhase = isLinkPhase; |
|
this.patcher = patcher; |
|
this.entries = entries.clone(); |
|
for (Path entry : this.entries) { |
|
Objects.requireNonNull(entry); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public static ModuleFinder of(ModulePatcher patcher, Path... entries) { |
|
return new ModulePath(JarFile.runtimeVersion(), false, patcher, entries); |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
public static ModuleFinder of(Path... entries) { |
|
return of((ModulePatcher)null, entries); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public static ModuleFinder of(Runtime.Version version, |
|
boolean isLinkPhase, |
|
Path... entries) { |
|
return new ModulePath(version, isLinkPhase, null, entries); |
|
} |
|
|
|
|
|
@Override |
|
public Optional<ModuleReference> find(String name) { |
|
Objects.requireNonNull(name); |
|
|
|
|
|
ModuleReference m = cachedModules.get(name); |
|
if (m != null) |
|
return Optional.of(m); |
|
|
|
|
|
while (hasNextEntry()) { |
|
scanNextEntry(); |
|
m = cachedModules.get(name); |
|
if (m != null) |
|
return Optional.of(m); |
|
} |
|
return Optional.empty(); |
|
} |
|
|
|
@Override |
|
public Set<ModuleReference> findAll() { |
|
|
|
while (hasNextEntry()) { |
|
scanNextEntry(); |
|
} |
|
return cachedModules.values().stream().collect(Collectors.toSet()); |
|
} |
|
|
|
|
|
|
|
*/ |
|
private boolean hasNextEntry() { |
|
return next < entries.length; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private void scanNextEntry() { |
|
if (hasNextEntry()) { |
|
|
|
long t0 = System.nanoTime(); |
|
|
|
Path entry = entries[next]; |
|
Map<String, ModuleReference> modules = scan(entry); |
|
next++; |
|
|
|
|
|
int initialSize = cachedModules.size(); |
|
for (Map.Entry<String, ModuleReference> e : modules.entrySet()) { |
|
cachedModules.putIfAbsent(e.getKey(), e.getValue()); |
|
} |
|
|
|
|
|
int added = cachedModules.size() - initialSize; |
|
moduleCount.add(added); |
|
|
|
scanTime.addElapsedTimeFrom(t0); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private Map<String, ModuleReference> scan(Path entry) { |
|
|
|
BasicFileAttributes attrs; |
|
try { |
|
attrs = Files.readAttributes(entry, BasicFileAttributes.class); |
|
} catch (NoSuchFileException e) { |
|
return Map.of(); |
|
} catch (IOException ioe) { |
|
throw new FindException(ioe); |
|
} |
|
|
|
try { |
|
|
|
if (attrs.isDirectory()) { |
|
Path mi = entry.resolve(MODULE_INFO); |
|
if (!Files.exists(mi)) { |
|
|
|
return scanDirectory(entry); |
|
} |
|
} |
|
|
|
|
|
ModuleReference mref = readModule(entry, attrs); |
|
if (mref != null) { |
|
String name = mref.descriptor().name(); |
|
return Map.of(name, mref); |
|
} |
|
|
|
|
|
String msg; |
|
if (!isLinkPhase && entry.toString().endsWith(".jmod")) { |
|
msg = "JMOD format not supported at execution time"; |
|
} else { |
|
msg = "Module format not recognized"; |
|
} |
|
throw new FindException(msg + ": " + entry); |
|
|
|
} catch (IOException ioe) { |
|
throw new FindException(ioe); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private Map<String, ModuleReference> scanDirectory(Path dir) |
|
throws IOException |
|
{ |
|
|
|
Map<String, ModuleReference> nameToReference = new HashMap<>(); |
|
|
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { |
|
for (Path entry : stream) { |
|
BasicFileAttributes attrs; |
|
try { |
|
attrs = Files.readAttributes(entry, BasicFileAttributes.class); |
|
} catch (NoSuchFileException ignore) { |
|
|
|
continue; |
|
} |
|
|
|
ModuleReference mref = readModule(entry, attrs); |
|
|
|
|
|
if (mref != null) { |
|
|
|
String name = mref.descriptor().name(); |
|
ModuleReference previous = nameToReference.put(name, mref); |
|
if (previous != null) { |
|
String fn1 = fileName(mref); |
|
String fn2 = fileName(previous); |
|
throw new FindException("Two versions of module " |
|
+ name + " found in " + dir |
|
+ " (" + fn1 + " and " + fn2 + ")"); |
|
} |
|
} |
|
} |
|
} |
|
|
|
return nameToReference; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private ModuleReference readModule(Path entry, BasicFileAttributes attrs) |
|
throws IOException |
|
{ |
|
try { |
|
|
|
|
|
if (attrs.isDirectory()) { |
|
return readExplodedModule(entry); |
|
} |
|
|
|
|
|
if (attrs.isRegularFile()) { |
|
String fn = entry.getFileName().toString(); |
|
boolean isDefaultFileSystem = isDefaultFileSystem(entry); |
|
|
|
|
|
if (fn.endsWith(".jar")) { |
|
if (isDefaultFileSystem) { |
|
return readJar(entry); |
|
} else { |
|
// the JAR file is in a custom file system so |
|
|
|
Path tmpdir = Files.createTempDirectory("mlib"); |
|
Path target = Files.copy(entry, tmpdir.resolve(fn)); |
|
return readJar(target); |
|
} |
|
} |
|
|
|
|
|
if (isDefaultFileSystem && isLinkPhase && fn.endsWith(".jmod")) { |
|
return readJMod(entry); |
|
} |
|
} |
|
|
|
return null; |
|
|
|
} catch (InvalidModuleDescriptorException e) { |
|
throw new FindException("Error reading module: " + entry, e); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private String fileName(ModuleReference mref) { |
|
URI uri = mref.location().orElse(null); |
|
if (uri != null) { |
|
if (uri.getScheme().equalsIgnoreCase("file")) { |
|
Path file = Path.of(uri); |
|
return file.getFileName().toString(); |
|
} else { |
|
return uri.toString(); |
|
} |
|
} else { |
|
return "<unknown>"; |
|
} |
|
} |
|
|
|
// -- JMOD files -- |
|
|
|
private Set<String> jmodPackages(JmodFile jf) { |
|
return jf.stream() |
|
.filter(e -> e.section() == Section.CLASSES) |
|
.map(JmodFile.Entry::name) |
|
.map(this::toPackageName) |
|
.flatMap(Optional::stream) |
|
.collect(Collectors.toSet()); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private ModuleReference readJMod(Path file) throws IOException { |
|
try (JmodFile jf = new JmodFile(file)) { |
|
ModuleInfo.Attributes attrs; |
|
try (InputStream in = jf.getInputStream(Section.CLASSES, MODULE_INFO)) { |
|
attrs = ModuleInfo.read(in, () -> jmodPackages(jf)); |
|
} |
|
return ModuleReferences.newJModModule(attrs, file); |
|
} |
|
} |
|
|
|
|
|
// -- JAR files -- |
|
|
|
private static final String SERVICES_PREFIX = "META-INF/services/"; |
|
|
|
private static final Attributes.Name AUTOMATIC_MODULE_NAME |
|
= new Attributes.Name("Automatic-Module-Name"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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 (Checks.isClassName(sn)) |
|
return Optional.of(sn); |
|
} |
|
} |
|
return Optional.empty(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private ModuleDescriptor deriveModuleDescriptor(JarFile jf) |
|
throws IOException |
|
{ |
|
|
|
Manifest man = jf.getManifest(); |
|
Attributes attrs = null; |
|
String moduleName = null; |
|
if (man != null) { |
|
attrs = man.getMainAttributes(); |
|
if (attrs != null) { |
|
moduleName = attrs.getValue(AUTOMATIC_MODULE_NAME); |
|
} |
|
} |
|
|
|
|
|
String fn = jf.getName(); |
|
int i = fn.lastIndexOf(File.separator); |
|
if (i != -1) |
|
fn = fn.substring(i + 1); |
|
|
|
|
|
String name = fn.substring(0, fn.length() - 4); |
|
String vs = null; |
|
|
|
|
|
Matcher matcher = Patterns.DASH_VERSION.matcher(name); |
|
if (matcher.find()) { |
|
int start = matcher.start(); |
|
|
|
|
|
try { |
|
String tail = name.substring(start + 1); |
|
ModuleDescriptor.Version.parse(tail); |
|
vs = tail; |
|
} catch (IllegalArgumentException ignore) { } |
|
|
|
name = name.substring(0, start); |
|
} |
|
|
|
// Create builder, using the name derived from file name when |
|
|
|
Builder builder; |
|
if (moduleName != null) { |
|
try { |
|
builder = ModuleDescriptor.newAutomaticModule(moduleName); |
|
} catch (IllegalArgumentException e) { |
|
throw new FindException(AUTOMATIC_MODULE_NAME + ": " + e.getMessage()); |
|
} |
|
} else { |
|
builder = ModuleDescriptor.newAutomaticModule(cleanModuleName(name)); |
|
} |
|
|
|
|
|
if (vs != null) |
|
builder.version(vs); |
|
|
|
|
|
Map<Boolean, Set<String>> map = jf.versionedStream() |
|
.filter(e -> !e.isDirectory()) |
|
.map(JarEntry::getName) |
|
.filter(e -> (e.endsWith(".class") ^ e.startsWith(SERVICES_PREFIX))) |
|
.collect(Collectors.partitioningBy(e -> e.startsWith(SERVICES_PREFIX), |
|
Collectors.toSet())); |
|
|
|
Set<String> classFiles = map.get(Boolean.FALSE); |
|
Set<String> configFiles = map.get(Boolean.TRUE); |
|
|
|
|
|
Set<String> packages = classFiles.stream() |
|
.map(this::toPackageName) |
|
.flatMap(Optional::stream) |
|
.distinct() |
|
.collect(Collectors.toSet()); |
|
|
|
|
|
builder.packages(packages); |
|
|
|
|
|
Set<String> serviceNames = configFiles.stream() |
|
.map(this::toServiceName) |
|
.flatMap(Optional::stream) |
|
.collect(Collectors.toSet()); |
|
|
|
|
|
for (String sn : serviceNames) { |
|
JarEntry entry = jf.getJarEntry(SERVICES_PREFIX + sn); |
|
List<String> providerClasses = new ArrayList<>(); |
|
try (InputStream in = jf.getInputStream(entry)) { |
|
BufferedReader reader |
|
= new BufferedReader(new InputStreamReader(in, UTF_8.INSTANCE)); |
|
String cn; |
|
while ((cn = nextLine(reader)) != null) { |
|
if (!cn.isEmpty()) { |
|
String pn = packageName(cn); |
|
if (!packages.contains(pn)) { |
|
String msg = "Provider class " + cn + " not in module"; |
|
throw new InvalidModuleDescriptorException(msg); |
|
} |
|
providerClasses.add(cn); |
|
} |
|
} |
|
} |
|
if (!providerClasses.isEmpty()) |
|
builder.provides(sn, providerClasses); |
|
} |
|
|
|
|
|
if (attrs != null) { |
|
String mainClass = attrs.getValue(Attributes.Name.MAIN_CLASS); |
|
if (mainClass != null) { |
|
mainClass = mainClass.replace('/', '.'); |
|
if (Checks.isClassName(mainClass)) { |
|
String pn = packageName(mainClass); |
|
if (packages.contains(pn)) { |
|
builder.mainClass(mainClass); |
|
} |
|
} |
|
} |
|
} |
|
|
|
return builder.build(); |
|
} |
|
|
|
|
|
|
|
*/ |
|
private static class Patterns { |
|
static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))"); |
|
static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]"); |
|
static final Pattern REPEATING_DOTS = Pattern.compile("(\\.)(\\1)+"); |
|
static final Pattern LEADING_DOTS = Pattern.compile("^\\."); |
|
static final Pattern TRAILING_DOTS = Pattern.compile("\\.$"); |
|
} |
|
|
|
|
|
|
|
*/ |
|
private static String cleanModuleName(String mn) { |
|
|
|
mn = Patterns.NON_ALPHANUM.matcher(mn).replaceAll("."); |
|
|
|
|
|
mn = Patterns.REPEATING_DOTS.matcher(mn).replaceAll("."); |
|
|
|
|
|
if (!mn.isEmpty() && mn.charAt(0) == '.') |
|
mn = Patterns.LEADING_DOTS.matcher(mn).replaceAll(""); |
|
|
|
|
|
int len = mn.length(); |
|
if (len > 0 && mn.charAt(len-1) == '.') |
|
mn = Patterns.TRAILING_DOTS.matcher(mn).replaceAll(""); |
|
|
|
return mn; |
|
} |
|
|
|
private Set<String> jarPackages(JarFile jf) { |
|
return jf.versionedStream() |
|
.filter(e -> !e.isDirectory()) |
|
.map(JarEntry::getName) |
|
.map(this::toPackageName) |
|
.flatMap(Optional::stream) |
|
.collect(Collectors.toSet()); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private ModuleReference readJar(Path file) throws IOException { |
|
try (JarFile jf = new JarFile(file.toFile(), |
|
true, |
|
ZipFile.OPEN_READ, |
|
releaseVersion)) |
|
{ |
|
ModuleInfo.Attributes attrs; |
|
JarEntry entry = jf.getJarEntry(MODULE_INFO); |
|
if (entry == null) { |
|
|
|
|
|
try { |
|
ModuleDescriptor md = deriveModuleDescriptor(jf); |
|
attrs = new ModuleInfo.Attributes(md, null, null, null); |
|
} catch (RuntimeException e) { |
|
throw new FindException("Unable to derive module descriptor for " |
|
+ jf.getName(), e); |
|
} |
|
|
|
} else { |
|
attrs = ModuleInfo.read(jf.getInputStream(entry), |
|
() -> jarPackages(jf)); |
|
} |
|
|
|
return ModuleReferences.newJarModule(attrs, patcher, file); |
|
} catch (ZipException e) { |
|
throw new FindException("Error reading " + file, e); |
|
} |
|
} |
|
|
|
|
|
// -- exploded directories -- |
|
|
|
private Set<String> explodedPackages(Path dir) { |
|
try { |
|
return Files.find(dir, Integer.MAX_VALUE, |
|
((path, attrs) -> attrs.isRegularFile() && !isHidden(path))) |
|
.map(path -> dir.relativize(path)) |
|
.map(this::toPackageName) |
|
.flatMap(Optional::stream) |
|
.collect(Collectors.toSet()); |
|
} catch (IOException x) { |
|
throw new UncheckedIOException(x); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private ModuleReference readExplodedModule(Path dir) throws IOException { |
|
Path mi = dir.resolve(MODULE_INFO); |
|
ModuleInfo.Attributes attrs; |
|
try (InputStream in = Files.newInputStream(mi)) { |
|
attrs = ModuleInfo.read(new BufferedInputStream(in), |
|
() -> explodedPackages(dir)); |
|
} catch (NoSuchFileException e) { |
|
|
|
return null; |
|
} |
|
return ModuleReferences.newExplodedModule(attrs, patcher, dir); |
|
} |
|
|
|
|
|
|
|
*/ |
|
private static String packageName(String cn) { |
|
int index = cn.lastIndexOf('.'); |
|
return (index == -1) ? "" : cn.substring(0, index); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private Optional<String> toPackageName(String name) { |
|
assert !name.endsWith("/"); |
|
int index = name.lastIndexOf("/"); |
|
if (index == -1) { |
|
if (name.endsWith(".class") && !name.equals(MODULE_INFO)) { |
|
String msg = name + " found in top-level directory" |
|
+ " (unnamed package not allowed in module)"; |
|
throw new InvalidModuleDescriptorException(msg); |
|
} |
|
return Optional.empty(); |
|
} |
|
|
|
String pn = name.substring(0, index).replace('/', '.'); |
|
if (Checks.isPackageName(pn)) { |
|
return Optional.of(pn); |
|
} else { |
|
|
|
return Optional.empty(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private Optional<String> toPackageName(Path file) { |
|
assert file.getRoot() == null; |
|
|
|
Path parent = file.getParent(); |
|
if (parent == null) { |
|
String name = file.toString(); |
|
if (name.endsWith(".class") && !name.equals(MODULE_INFO)) { |
|
String msg = name + " found in top-level directory" |
|
+ " (unnamed package not allowed in module)"; |
|
throw new InvalidModuleDescriptorException(msg); |
|
} |
|
return Optional.empty(); |
|
} |
|
|
|
String pn = parent.toString().replace(File.separatorChar, '.'); |
|
if (Checks.isPackageName(pn)) { |
|
return Optional.of(pn); |
|
} else { |
|
|
|
return Optional.empty(); |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
private boolean isHidden(Path file) { |
|
try { |
|
return Files.isHidden(file); |
|
} catch (IOException ioe) { |
|
return false; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private boolean isDefaultFileSystem(Path path) { |
|
return path.getFileSystem().provider() |
|
.getScheme().equalsIgnoreCase("file"); |
|
} |
|
|
|
|
|
private static final PerfCounter scanTime |
|
= PerfCounter.newPerfCounter("jdk.module.finder.modulepath.scanTime"); |
|
private static final PerfCounter moduleCount |
|
= PerfCounter.newPerfCounter("jdk.module.finder.modulepath.modules"); |
|
} |