added extra classpath manifest attribute

This commit is contained in:
2023-06-24 14:15:07 +08:00
parent d2040fb02a
commit a1139bdc1c
6 changed files with 310 additions and 8 deletions

View File

@@ -1,24 +1,27 @@
package net.woggioni.envelope;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.AbstractMap;
import java.util.Map;
import java.util.MissingFormatArgumentException;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import java.io.IOException;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Common {
@@ -114,4 +117,144 @@ public class Common {
OutputStream result = new FileOutputStream(file);
return buffered ? new BufferedOutputStream(result) : result;
}
/**
* @param template Template text containing the variables to be replaced by this method. <br>
* Variables follow the format ${variable_name}. <br>
* Example: <br>
* "This template was created by ${author}."
* @param valuesMap A hashmap with the values of the variables to be replaced. <br>
* The key is the variable name and the value is the value to be replaced in the template. <br>
* Example: <br>
* {"author" =&gt; "John Doe"}
* @return The template text (String) with the variable names replaced by the values passed in the map. <br>
* If any of the variable names is not contained in the map it will be replaced by an empty string. <br>
* Example: <br>
* "This template was created by John Doe."
*/
public static String renderTemplate(String template, Map<String, Object> valuesMap) {
return renderTemplate(template, valuesMap, null);
}
public static int indexOfWithEscape(String haystack, char needle, char escape, int begin, int end) {
int result = -1;
int cursor = begin;
if (end == 0) {
end = haystack.length();
}
int escapeCount = 0;
while (cursor < end) {
char c = haystack.charAt(cursor);
if (escapeCount > 0) {
--escapeCount;
if (c == escape) {
result = -1;
}
} else if (escapeCount == 0) {
if (c == escape) {
++escapeCount;
}
if (c == needle) {
result = cursor;
}
}
if (result >= 0 && escapeCount == 0) {
break;
}
++cursor;
}
return result;
}
public static String renderTemplate(
String template,
Map<String, Object> valuesMap,
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()) {
tokenScanner.next();
int nextPlaceHolder;
switch (tokenScanner.getTokenType()) {
case TOKEN: {
nextPlaceHolder = tokenScanner.getTokenIndex();
while (cursor < nextPlaceHolder) {
char ch = template.charAt(cursor++);
sb.append(ch);
}
if (cursor + 1 < template.length() && template.charAt(cursor + 1) == '{') {
String key;
String context = null;
String defaultValue = null;
Object value;
int end = template.indexOf('}', cursor + 1);
int colon;
if (dictMap == null)
colon = -1;
else {
colon = indexOfWithEscape(template, ':', '\\', cursor + 1, template.length());
if (colon >= end) colon = -1;
}
if (colon < 0) {
key = template.substring(cursor + 2, end);
value = valuesMap.getOrDefault(key, absent);
} else {
context = template.substring(cursor + 2, colon);
int secondColon = indexOfWithEscape(template, ':', '\\', colon + 1, end);
if (secondColon < 0) {
key = template.substring(colon + 1, end);
} else {
key = template.substring(colon + 1, secondColon);
defaultValue = template.substring(secondColon + 1, end);
}
value = Optional.ofNullable(dictMap.get(context))
.map(m -> m.get(key))
.orElse(absent);
}
if (value != absent) {
sb.append(value.toString());
} else {
if (defaultValue != null) {
sb.append(defaultValue);
} else {
throw new MissingFormatArgumentException(
String.format("Missing value for placeholder '%s'",
context == null ? key : context + ':' + key
)
);
}
}
cursor = end + 1;
}
break;
}
case ESCAPE:
nextPlaceHolder = tokenScanner.getTokenIndex();
while (cursor < nextPlaceHolder) {
char ch = template.charAt(cursor++);
sb.append(ch);
}
cursor = nextPlaceHolder + 1;
sb.append(template.charAt(cursor++));
break;
case END:
default:
nextPlaceHolder = template.length();
while (cursor < nextPlaceHolder) {
char ch = template.charAt(cursor++);
sb.append(ch);
}
break;
}
}
return sb.toString();
}
public static <T> Stream<T> opt2Stream(Optional<T> opt) {
return opt.map(Stream::of).orElse(Stream.empty());
}
}

View File

@@ -1,9 +1,10 @@
package net.woggioni.envelope;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.Calendar;
import java.util.GregorianCalendar;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Constants {
@@ -16,10 +17,12 @@ public class Constants {
public static final String SYSTEM_PROPERTIES_FILE = METADATA_FOLDER + "/system.properties";
public static final String LIBRARIES_TOC = METADATA_FOLDER + "/libraries.txt";
public static final char EXTRA_CLASSPATH_ENTRY_SEPARATOR = ';';
public static class ManifestAttributes {
public static final String MAIN_MODULE = "Executable-Jar-Main-Module";
public static final String MAIN_CLASS = "Executable-Jar-Main-Class";
public static final String EXTRA_CLASSPATH = "Executable-Jar-Extra-Classpath";
public static final String ENTRY_HASH = "SHA-256-Digest";
}

View File

@@ -0,0 +1,74 @@
package net.woggioni.envelope;
import lombok.Getter;
public class TokenScanner {
public enum TokenType {
ESCAPE, TOKEN, END;
}
private final String haystack;
private final char needle;
private final char escape;
private int begin;
private final int end;
@Getter
private int tokenIndex = -1;
@Getter
private TokenType tokenType = null;
public TokenScanner(String haystack, char needle, char escape, int begin, int end) {
this.haystack = haystack;
this.needle = needle;
this.escape = escape;
this.begin = begin;
this.end = end;
}
public TokenScanner(String haystack, char needle, char escape, int begin) {
this(haystack, needle, escape, begin, haystack.length());
}
public TokenScanner(String haystack, char needle, char escape) {
this(haystack, needle, escape, 0, haystack.length());
}
public void next() {
int result = -1;
int cursor = begin;
int escapeCount = 0;
while(true) {
if(cursor < end) {
char c = haystack.charAt(cursor);
if (escapeCount > 0) {
--escapeCount;
if(c == escape || c == needle) {
tokenIndex = cursor - 1;
tokenType = TokenType.ESCAPE;
break;
}
} else if (escapeCount == 0) {
if (c == escape) {
++escapeCount;
}
if (c == needle) {
result = cursor;
}
}
if (result >= 0 && escapeCount == 0) {
tokenIndex = result;
tokenType = TokenType.TOKEN;
break;
}
++cursor;
} else {
tokenIndex = result;
tokenType = result < 0 ? TokenType.END :TokenType.TOKEN;
break;
}
}
begin = cursor + 1;
}
}

View File

@@ -1,7 +1,7 @@
woggioniMavenRepositoryUrl=https://woggioni.net/mvn/
publishMavenRepositoryUrl=https://mvn.woggioni.net/
lys.version = 2023.06.13
lys.version = 2023.06.22
version.envelope=2023.06.13
version.envelope=2023.06.24
version.gradle=7.6

View File

@@ -10,16 +10,27 @@ import java.io.Reader;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static net.woggioni.envelope.Constants.EXTRA_CLASSPATH_ENTRY_SEPARATOR;
public class Launcher {
@@ -53,6 +64,65 @@ public class Launcher {
}
}
private static Map<String, Map<String, Object>> createContextMap() {
Map<String, Map<String, Object>> dictMap = new TreeMap<>();
dictMap.put("env", Collections.unmodifiableMap(System.getenv()
.entrySet()
.stream()
.map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), (Object) entry.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
dictMap.put("sys", Collections.unmodifiableMap(System.getProperties().entrySet().stream()
.map((Map.Entry<? super String, Object> entry) -> (Map.Entry<String, Object>) entry)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
return Collections.unmodifiableMap(dictMap);
}
private static Stream<JarFile> getExtraClasspath(Attributes mainAttributes) {
return Common.opt2Stream(Optional.ofNullable(mainAttributes.getValue(Constants.ManifestAttributes.EXTRA_CLASSPATH))
.map(manifestAttribute -> {
Map<String, Map<String, Object>> dictMap = createContextMap();
return Common.renderTemplate(manifestAttribute, Collections.emptyMap(), dictMap);
}).map(extraClasspathString -> {
List<String> paths = new ArrayList<>();
int cursor = 0;
while(true) {
int sep = Common.indexOfWithEscape(
extraClasspathString,
EXTRA_CLASSPATH_ENTRY_SEPARATOR,
EXTRA_CLASSPATH_ENTRY_SEPARATOR,
cursor,
extraClasspathString.length()
);
if(sep < 0) break;
paths.add(extraClasspathString.substring(cursor, sep));
cursor = sep + 1;
}
return paths;
}))
.flatMap(List::stream)
.map(Paths::get)
.flatMap(new Function<Path, Stream<Path>>() {
@Override
@SneakyThrows
public Stream<Path> apply(Path path) {
if(Files.isDirectory(path)) {
return Files.list(path).filter(childPath -> !Files.isDirectory(childPath));
} else {
return Stream.of(path);
}
}
})
.filter(path -> path.getFileName().toString().toLowerCase().endsWith(".jar"))
.map(Path::toFile)
.map(new Function<File, JarFile>() {
@Override
@SneakyThrows
public JarFile apply(File file) {
return new JarFile(file);
}
});
}
@SneakyThrows
public static void main(String[] args) {
Enumeration<URL> it = Launcher.class.getClassLoader().getResources(Constants.SYSTEM_PROPERTIES_FILE);
@@ -94,6 +164,8 @@ public class Launcher {
else sb.append((char) c);
}
}
getExtraClasspath(mainAttributes).forEach(classpath::add);
Consumer<Class<?>> runner = new Consumer<Class<?>>() {
@Override
@SneakyThrows

View File

@@ -20,6 +20,7 @@ import org.gradle.api.java.archives.internal.DefaultManifest;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.BasePluginExtension;
import org.gradle.api.plugins.JavaApplication;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
@@ -86,6 +87,9 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
@Getter(onMethod_ = {@Input})
private final Map<String, String> systemProperties = new TreeMap<>();
@Getter(onMethod_ = {@Input})
private final ListProperty<String> extraClasspath;
private final org.gradle.api.java.archives.Manifest manifest;
public org.gradle.api.java.archives.Manifest manifest() {
@@ -156,6 +160,7 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
manifest = new DefaultManifest(fileResolver);
mainClass = objects.property(String.class);
mainModule = objects.property(String.class);
extraClasspath = objects.listProperty(String.class);
JavaApplication javaApplication = getProject().getExtensions().findByType(JavaApplication.class);
if(!Objects.isNull(javaApplication)) {
mainClass.convention(javaApplication.getMainClass());
@@ -286,6 +291,11 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
mainAttributes.put(new Attributes.Name("Launcher-Agent-Class"), Constants.AGENT_LAUNCHER);
mainAttributes.put(new Attributes.Name("Can-Redefine-Classes"), "true");
mainAttributes.put(new Attributes.Name("Can-Retransform-Classes"), "true");
String separator = "" + Constants.EXTRA_CLASSPATH_ENTRY_SEPARATOR;
String extraClasspath = EnvelopeJarTask.this.extraClasspath.get().stream()
.map(it -> it.replace(separator, separator + separator)
).collect(Collectors.joining(separator));
mainAttributes.put(new Attributes.Name(Constants.ManifestAttributes.EXTRA_CLASSPATH), extraClasspath);
if(mainClass.isPresent()) {
mainAttributes.putValue(Constants.ManifestAttributes.MAIN_CLASS, mainClass.get());
}