added extra classpath manifest attribute
This commit is contained in:
@@ -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" => "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());
|
||||
}
|
||||
}
|
||||
|
@@ -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";
|
||||
}
|
||||
|
||||
|
74
common/src/main/java/net/woggioni/envelope/TokenScanner.java
Normal file
74
common/src/main/java/net/woggioni/envelope/TokenScanner.java
Normal 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;
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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());
|
||||
}
|
||||
|
Reference in New Issue
Block a user