498 lines
17 KiB
Java
498 lines
17 KiB
Java
package net.woggioni.envelope.loader;
|
|
|
|
import lombok.SneakyThrows;
|
|
import net.woggioni.envelope.loader.JarFile;
|
|
import net.woggioni.envelope.loader.jar.Handler;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.lang.module.ModuleDescriptor;
|
|
import java.lang.module.ModuleFinder;
|
|
import java.lang.module.ModuleReader;
|
|
import java.lang.module.ModuleReference;
|
|
import java.net.URI;
|
|
import java.net.URL;
|
|
import java.net.URLStreamHandler;
|
|
import java.nio.file.Paths;
|
|
import java.util.AbstractMap;
|
|
import java.util.Arrays;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Collections;
|
|
import java.util.HashSet;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.TreeMap;
|
|
import java.util.jar.Attributes.Name;
|
|
import java.util.jar.JarEntry;
|
|
import java.util.jar.Manifest;
|
|
import java.util.jar.Attributes;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Stream;
|
|
import java.lang.module.FindException;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.lang.module.InvalidModuleDescriptorException;
|
|
|
|
public class JarFileModuleFinder implements ModuleFinder {
|
|
|
|
private static final String MODULE_DESCRIPTOR_ENTRY = "module-info.class";
|
|
private static final Name AUTOMATIC_MODULE_NAME_MANIFEST_ENTRY = new Name("Automatic-Module-Name");
|
|
private static final String SERVICES_PREFIX = "META-INF/services/";
|
|
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("\\.$");
|
|
}
|
|
|
|
|
|
// keywords, boolean and null literals, not allowed in identifiers
|
|
private static final Set<String> RESERVED = Set.of(
|
|
"abstract",
|
|
"assert",
|
|
"boolean",
|
|
"break",
|
|
"byte",
|
|
"case",
|
|
"catch",
|
|
"char",
|
|
"class",
|
|
"const",
|
|
"continue",
|
|
"default",
|
|
"do",
|
|
"double",
|
|
"else",
|
|
"enum",
|
|
"extends",
|
|
"final",
|
|
"finally",
|
|
"float",
|
|
"for",
|
|
"goto",
|
|
"if",
|
|
"implements",
|
|
"import",
|
|
"instanceof",
|
|
"int",
|
|
"interface",
|
|
"long",
|
|
"native",
|
|
"new",
|
|
"package",
|
|
"private",
|
|
"protected",
|
|
"public",
|
|
"return",
|
|
"short",
|
|
"static",
|
|
"strictfp",
|
|
"super",
|
|
"switch",
|
|
"synchronized",
|
|
"this",
|
|
"throw",
|
|
"throws",
|
|
"transient",
|
|
"try",
|
|
"void",
|
|
"volatile",
|
|
"while",
|
|
"true",
|
|
"false",
|
|
"null",
|
|
"_"
|
|
);
|
|
|
|
private final Map<String, Map.Entry<ModuleReference, Handler>> modules;
|
|
|
|
@SneakyThrows
|
|
private static final URI toURI(URL url) {
|
|
return url.toURI();
|
|
}
|
|
|
|
public final URLStreamHandler getStreamHandlerForModule(String moduleName) {
|
|
return modules.get(moduleName).getValue();
|
|
}
|
|
|
|
private static String cleanModuleName(String mn) {
|
|
// replace non-alphanumeric
|
|
mn = Patterns.NON_ALPHANUM.matcher(mn).replaceAll(".");
|
|
|
|
// collapse repeating dots
|
|
mn = Patterns.REPEATING_DOTS.matcher(mn).replaceAll(".");
|
|
|
|
// drop leading dots
|
|
if (!mn.isEmpty() && mn.charAt(0) == '.')
|
|
mn = Patterns.LEADING_DOTS.matcher(mn).replaceAll("");
|
|
|
|
// drop trailing dots
|
|
int len = mn.length();
|
|
if (len > 0 && mn.charAt(len-1) == '.')
|
|
mn = Patterns.TRAILING_DOTS.matcher(mn).replaceAll("");
|
|
|
|
return mn;
|
|
}
|
|
|
|
@SneakyThrows
|
|
private static String moduleNameFromURI(URI uri) {
|
|
String fn = null;
|
|
URI tmp = uri;
|
|
while(true) {
|
|
String schemeSpecificPart = tmp.getSchemeSpecificPart();
|
|
if(tmp.getPath() != null) {
|
|
String path = tmp.getPath();
|
|
int end = path.lastIndexOf("!");
|
|
if(end == -1) end = path.length();
|
|
int start = path.lastIndexOf("!", end - 1);
|
|
if(start == -1) start = 0;
|
|
fn = Paths.get(path.substring(start, end)).getFileName().toString();
|
|
break;
|
|
} else {
|
|
tmp = new URI(schemeSpecificPart);
|
|
}
|
|
}
|
|
// Derive the version, and the module name if needed, from JAR file name
|
|
int i = fn.lastIndexOf(File.separator);
|
|
if (i != -1)
|
|
fn = fn.substring(i + 1);
|
|
|
|
// drop ".jar"
|
|
String name = fn.substring(0, fn.length() - 4);
|
|
String vs = null;
|
|
|
|
// find first occurrence of -${NUMBER}. or -${NUMBER}$
|
|
Matcher matcher = Patterns.DASH_VERSION.matcher(name);
|
|
if (matcher.find()) {
|
|
int start = matcher.start();
|
|
|
|
// attempt to parse the tail as a version string
|
|
try {
|
|
String tail = name.substring(start + 1);
|
|
ModuleDescriptor.Version.parse(tail);
|
|
vs = tail;
|
|
} catch (IllegalArgumentException ignore) { }
|
|
|
|
name = name.substring(0, start);
|
|
}
|
|
return cleanModuleName(name);
|
|
}
|
|
|
|
public JarFileModuleFinder(JarFile ...jarFiles) {
|
|
this(Arrays.asList(jarFiles));
|
|
}
|
|
private static Set<String> collectPackageNames(JarFile jarFile) {
|
|
Set<String> result = jarFile
|
|
.versionedStream()
|
|
.filter(entry -> entry.getName().endsWith(".class"))
|
|
.map(entry -> {
|
|
String entryName = entry.getName();
|
|
int lastSlash = entryName.lastIndexOf('/');
|
|
if(lastSlash < 0) return null;
|
|
else return entryName.substring(0, lastSlash).replace('/', '.');
|
|
})
|
|
.filter(Objects::nonNull)
|
|
.collect(Collectors.toSet());
|
|
return Collections.unmodifiableSet(result);
|
|
}
|
|
@SneakyThrows
|
|
public JarFileModuleFinder(Iterable<JarFile> jarFiles) {
|
|
|
|
TreeMap<String, Map.Entry<ModuleReference, Handler>> modules = new TreeMap<>();
|
|
|
|
for(JarFile jarFile : jarFiles) {
|
|
URI uri = jarFile.getUrl().toURI();
|
|
String moduleName = null;
|
|
ModuleDescriptor moduleDescriptor;
|
|
JarEntry moduleDescriptorEntry = jarFile.getJarEntry(MODULE_DESCRIPTOR_ENTRY);
|
|
if (moduleDescriptorEntry != null) {
|
|
try(InputStream is = jarFile.getInputStream(moduleDescriptorEntry)) {
|
|
moduleDescriptor = ModuleDescriptor.read(is, () -> collectPackageNames(jarFile));
|
|
}
|
|
} else {
|
|
moduleDescriptor = deriveModuleDescriptor(jarFile);
|
|
}
|
|
|
|
modules.put(moduleDescriptor.name(),
|
|
new AbstractMap.SimpleEntry<>(new ModuleReference(moduleDescriptor, uri) {
|
|
@Override
|
|
public ModuleReader open() throws IOException {
|
|
return new ModuleReader() {
|
|
@Override
|
|
public Optional<URI> find(String name) throws IOException {
|
|
JarEntry jarEntry = jarFile.getJarEntry(name);
|
|
if(jarEntry == null) return Optional.empty();
|
|
return Optional.of(uri.resolve('!' + name));
|
|
}
|
|
|
|
@Override
|
|
public Optional<InputStream> open(String name) throws IOException {
|
|
JarEntry jarEntry = jarFile.getJarEntry(name);
|
|
if(jarEntry == null) return Optional.empty();
|
|
return Optional.of(jarFile.getInputStream(jarEntry));
|
|
}
|
|
|
|
@Override
|
|
public Stream<String> list() throws IOException {
|
|
return jarFile.stream().map(JarEntry::getName);
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {}
|
|
};
|
|
}
|
|
}, new Handler(jarFile)));
|
|
}
|
|
this.modules = Collections.unmodifiableMap(modules);
|
|
}
|
|
|
|
@Override
|
|
public Optional<ModuleReference> find(String name) {
|
|
return Optional.ofNullable(modules.get(name)).map(Map.Entry::getKey);
|
|
}
|
|
|
|
@Override
|
|
public Set<ModuleReference> findAll() {
|
|
return Collections.unmodifiableSet(modules.values()
|
|
.stream().map(Map.Entry::getKey)
|
|
.collect(Collectors.toSet()));
|
|
}
|
|
|
|
private ModuleDescriptor deriveModuleDescriptor(JarFile jf)
|
|
throws IOException
|
|
{
|
|
// Read Automatic-Module-Name attribute if present
|
|
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_MANIFEST_ENTRY);
|
|
}
|
|
}
|
|
|
|
// Derive the version, and the module name if needed, from JAR file name
|
|
String fn = jf.getName();
|
|
int i = fn.lastIndexOf(File.separator);
|
|
if (i != -1)
|
|
fn = fn.substring(i + 1);
|
|
|
|
// drop ".jar"
|
|
String name = fn.substring(0, fn.length() - 4);
|
|
String vs = null;
|
|
|
|
// find first occurrence of -${NUMBER}. or -${NUMBER}$
|
|
Matcher matcher = Patterns.DASH_VERSION.matcher(name);
|
|
if (matcher.find()) {
|
|
int start = matcher.start();
|
|
|
|
// attempt to parse the tail as a version string
|
|
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
|
|
// Automatic-Module-Name not present
|
|
ModuleDescriptor.Builder builder;
|
|
if (moduleName != null) {
|
|
try {
|
|
builder = ModuleDescriptor.newAutomaticModule(moduleName);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new FindException(AUTOMATIC_MODULE_NAME_MANIFEST_ENTRY + ": " + e.getMessage());
|
|
}
|
|
} else {
|
|
builder = ModuleDescriptor.newAutomaticModule(cleanModuleName(name));
|
|
}
|
|
|
|
// module version if present
|
|
if (vs != null)
|
|
builder.version(vs);
|
|
|
|
// scan the names of the entries in the JAR file
|
|
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);
|
|
|
|
// the packages containing class files
|
|
Set<String> packages = classFiles.stream()
|
|
.map(this::toPackageName)
|
|
.flatMap(Optional::stream)
|
|
.distinct()
|
|
.collect(Collectors.toSet());
|
|
|
|
// all packages are exported and open
|
|
builder.packages(packages);
|
|
|
|
// map names of service configuration files to service names
|
|
Set<String> serviceNames = configFiles.stream()
|
|
.map(this::toServiceName)
|
|
.flatMap(Optional::stream)
|
|
.collect(Collectors.toSet());
|
|
|
|
// parse each service configuration file
|
|
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, StandardCharsets.UTF_8));
|
|
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);
|
|
}
|
|
|
|
// Main-Class attribute if it exists
|
|
if (attrs != null) {
|
|
String mainClass = attrs.getValue(Attributes.Name.MAIN_CLASS);
|
|
if (mainClass != null) {
|
|
mainClass = mainClass.replace('/', '.');
|
|
if (isClassName(mainClass)) {
|
|
String pn = packageName(mainClass);
|
|
if (packages.contains(pn)) {
|
|
builder.mainClass(mainClass);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return builder.build();
|
|
}
|
|
|
|
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 (isClassName(sn))
|
|
return Optional.of(sn);
|
|
}
|
|
}
|
|
return Optional.empty();
|
|
}
|
|
|
|
private static String packageName(String cn) {
|
|
int index = cn.lastIndexOf('.');
|
|
return (index == -1) ? "" : cn.substring(0, index);
|
|
}
|
|
|
|
/**
|
|
* Maps the name of an entry in a JAR or ZIP file to a package name.
|
|
*
|
|
* @throws InvalidModuleDescriptorException if the name is a class file in
|
|
* the top-level directory of the JAR/ZIP file (and it's not
|
|
* module-info.class)
|
|
*/
|
|
private Optional<String> toPackageName(String name) {
|
|
assert !name.endsWith("/");
|
|
int index = name.lastIndexOf("/");
|
|
if (index == -1) {
|
|
if (name.endsWith(".class") && !name.equals(MODULE_DESCRIPTOR_ENTRY)) {
|
|
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 (isPackageName(pn)) {
|
|
return Optional.of(pn);
|
|
} else {
|
|
// not a valid package name
|
|
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;
|
|
}
|
|
|
|
private static boolean isClassName(String name) {
|
|
return isTypeName(name);
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if the given name is a legal type name.
|
|
*/ private static boolean isPackageName(String name) {
|
|
return isTypeName(name);
|
|
}
|
|
|
|
private static boolean isTypeName(String name) {
|
|
int next;
|
|
int off = 0;
|
|
while ((next = name.indexOf('.', off)) != -1) {
|
|
String id = name.substring(off, next);
|
|
if (!isJavaIdentifier(id))
|
|
return false;
|
|
off = next+1;
|
|
}
|
|
String last = name.substring(off);
|
|
return isJavaIdentifier(last);
|
|
}
|
|
|
|
private static boolean isJavaIdentifier(String str) {
|
|
if (str.isEmpty() || RESERVED.contains(str))
|
|
return false;
|
|
|
|
int first = Character.codePointAt(str, 0);
|
|
if (!Character.isJavaIdentifierStart(first))
|
|
return false;
|
|
|
|
int i = Character.charCount(first);
|
|
while (i < str.length()) {
|
|
int cp = Character.codePointAt(str, i);
|
|
if (!Character.isJavaIdentifierPart(cp))
|
|
return false;
|
|
i += Character.charCount(cp);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|