added support for service loader in automatic modules
This commit is contained in:
@@ -173,7 +173,6 @@ public class Common {
|
|||||||
Map<String, Map<String, Object>> dictMap) {
|
Map<String, Map<String, Object>> dictMap) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
Object absent = new Object();
|
Object absent = new Object();
|
||||||
|
|
||||||
int cursor = 0;
|
int cursor = 0;
|
||||||
TokenScanner tokenScanner = new TokenScanner(template, '$', '$');
|
TokenScanner tokenScanner = new TokenScanner(template, '$', '$');
|
||||||
while (cursor < template.length()) {
|
while (cursor < template.length()) {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
woggioniMavenRepositoryUrl=https://woggioni.net/mvn/
|
woggioniMavenRepositoryUrl=https://woggioni.net/mvn/
|
||||||
publishMavenRepositoryUrl=https://mvn.woggioni.net/
|
publishMavenRepositoryUrl=https://mvn.woggioni.net/
|
||||||
|
|
||||||
lys.version = 2023.06.22
|
lys.version = 2023.07.09
|
||||||
|
|
||||||
version.envelope=2023.06.24
|
version.envelope=2023.07.09
|
||||||
version.gradle=7.6
|
version.gradle=7.6
|
||||||
|
@@ -4,9 +4,11 @@ import lombok.SneakyThrows;
|
|||||||
import net.woggioni.envelope.loader.JarFile;
|
import net.woggioni.envelope.loader.JarFile;
|
||||||
import net.woggioni.envelope.loader.jar.Handler;
|
import net.woggioni.envelope.loader.jar.Handler;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.lang.module.ModuleDescriptor;
|
import java.lang.module.ModuleDescriptor;
|
||||||
import java.lang.module.ModuleFinder;
|
import java.lang.module.ModuleFinder;
|
||||||
import java.lang.module.ModuleReader;
|
import java.lang.module.ModuleReader;
|
||||||
@@ -17,6 +19,8 @@ import java.net.URLStreamHandler;
|
|||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.AbstractMap;
|
import java.util.AbstractMap;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -27,16 +31,20 @@ import java.util.TreeMap;
|
|||||||
import java.util.jar.Attributes.Name;
|
import java.util.jar.Attributes.Name;
|
||||||
import java.util.jar.JarEntry;
|
import java.util.jar.JarEntry;
|
||||||
import java.util.jar.Manifest;
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
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;
|
||||||
|
import java.lang.module.FindException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.lang.module.InvalidModuleDescriptorException;
|
||||||
|
|
||||||
public class JarFileModuleFinder implements ModuleFinder {
|
public class JarFileModuleFinder implements ModuleFinder {
|
||||||
|
|
||||||
private static final String MODULE_DESCRIPTOR_ENTRY = "module-info.class";
|
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 Name AUTOMATIC_MODULE_NAME_MANIFEST_ENTRY = new Name("Automatic-Module-Name");
|
||||||
|
private static final String SERVICES_PREFIX = "META-INF/services/";
|
||||||
private static class Patterns {
|
private static class Patterns {
|
||||||
static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
|
static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
|
||||||
static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
|
static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
|
||||||
@@ -44,6 +52,66 @@ public class JarFileModuleFinder implements ModuleFinder {
|
|||||||
static final Pattern LEADING_DOTS = Pattern.compile("^\\.");
|
static final Pattern LEADING_DOTS = Pattern.compile("^\\.");
|
||||||
static final Pattern TRAILING_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;
|
private final Map<String, Map.Entry<ModuleReference, Handler>> modules;
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
@@ -150,20 +218,7 @@ public class JarFileModuleFinder implements ModuleFinder {
|
|||||||
moduleDescriptor = ModuleDescriptor.read(is, () -> collectPackageNames(jarFile));
|
moduleDescriptor = ModuleDescriptor.read(is, () -> collectPackageNames(jarFile));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Manifest mf = jarFile.getManifest();
|
moduleDescriptor = deriveModuleDescriptor(jarFile);
|
||||||
moduleName = mf.getMainAttributes().getValue(AUTOMATIC_MODULE_NAME_MANIFEST_ENTRY);
|
|
||||||
if(moduleName == null) {
|
|
||||||
moduleName = moduleNameFromURI(uri);
|
|
||||||
}
|
|
||||||
ModuleDescriptor.Builder mdb = ModuleDescriptor.newAutomaticModule(moduleName);
|
|
||||||
mdb.packages(collectPackageNames(jarFile));
|
|
||||||
|
|
||||||
// Main-Class attribute if it exists
|
|
||||||
String mainClass = mf.getMainAttributes().getValue(Name.MAIN_CLASS);
|
|
||||||
if (mainClass != null) {
|
|
||||||
mdb.mainClass(mainClass);
|
|
||||||
}
|
|
||||||
moduleDescriptor = mdb.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modules.put(moduleDescriptor.name(),
|
modules.put(moduleDescriptor.name(),
|
||||||
@@ -210,4 +265,233 @@ public class JarFileModuleFinder implements ModuleFinder {
|
|||||||
.stream().map(Map.Entry::getKey)
|
.stream().map(Map.Entry::getKey)
|
||||||
.collect(Collectors.toSet()));
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user