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) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Object absent = new Object();
|
||||
|
||||
int cursor = 0;
|
||||
TokenScanner tokenScanner = new TokenScanner(template, '$', '$');
|
||||
while (cursor < template.length()) {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
woggioniMavenRepositoryUrl=https://woggioni.net/mvn/
|
||||
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
|
||||
|
@@ -4,9 +4,11 @@ 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;
|
||||
@@ -17,6 +19,8 @@ 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;
|
||||
@@ -27,16 +31,20 @@ 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]");
|
||||
@@ -44,6 +52,66 @@ public class JarFileModuleFinder implements ModuleFinder {
|
||||
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
|
||||
@@ -150,20 +218,7 @@ public class JarFileModuleFinder implements ModuleFinder {
|
||||
moduleDescriptor = ModuleDescriptor.read(is, () -> collectPackageNames(jarFile));
|
||||
}
|
||||
} else {
|
||||
Manifest mf = jarFile.getManifest();
|
||||
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();
|
||||
moduleDescriptor = deriveModuleDescriptor(jarFile);
|
||||
}
|
||||
|
||||
modules.put(moduleDescriptor.name(),
|
||||
@@ -210,4 +265,233 @@ public class JarFileModuleFinder implements ModuleFinder {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user