added support for service loader in automatic modules

This commit is contained in:
2023-07-09 20:32:55 +08:00
parent a1139bdc1c
commit 995d937b25
3 changed files with 301 additions and 18 deletions

View File

@@ -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()) {

View File

@@ -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

View File

@@ -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;
}
}