diff --git a/common/src/main/java/net/woggioni/envelope/Common.java b/common/src/main/java/net/woggioni/envelope/Common.java
index ac95ce0..11dbaac 100644
--- a/common/src/main/java/net/woggioni/envelope/Common.java
+++ b/common/src/main/java/net/woggioni/envelope/Common.java
@@ -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.
+ * Variables follow the format ${variable_name}.
+ * Example:
+ * "This template was created by ${author}."
+ * @param valuesMap A hashmap with the values of the variables to be replaced.
+ * The key is the variable name and the value is the value to be replaced in the template.
+ * Example:
+ * {"author" => "John Doe"}
+ * @return The template text (String) with the variable names replaced by the values passed in the map.
+ * If any of the variable names is not contained in the map it will be replaced by an empty string.
+ * Example:
+ * "This template was created by John Doe."
+ */
+ public static String renderTemplate(String template, Map 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 valuesMap,
+ Map> 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 Stream opt2Stream(Optional opt) {
+ return opt.map(Stream::of).orElse(Stream.empty());
+ }
}
diff --git a/common/src/main/java/net/woggioni/envelope/Constants.java b/common/src/main/java/net/woggioni/envelope/Constants.java
index ff35a1b..345c169 100644
--- a/common/src/main/java/net/woggioni/envelope/Constants.java
+++ b/common/src/main/java/net/woggioni/envelope/Constants.java
@@ -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";
}
diff --git a/common/src/main/java/net/woggioni/envelope/TokenScanner.java b/common/src/main/java/net/woggioni/envelope/TokenScanner.java
new file mode 100644
index 0000000..34ddab3
--- /dev/null
+++ b/common/src/main/java/net/woggioni/envelope/TokenScanner.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 47ab112..4327f61 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -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
diff --git a/launcher/src/main/java/net/woggioni/envelope/Launcher.java b/launcher/src/main/java/net/woggioni/envelope/Launcher.java
index 5d1088d..e35f1b6 100644
--- a/launcher/src/main/java/net/woggioni/envelope/Launcher.java
+++ b/launcher/src/main/java/net/woggioni/envelope/Launcher.java
@@ -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> createContextMap() {
+ Map> 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) entry)
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
+ return Collections.unmodifiableMap(dictMap);
+ }
+
+ private static Stream getExtraClasspath(Attributes mainAttributes) {
+ return Common.opt2Stream(Optional.ofNullable(mainAttributes.getValue(Constants.ManifestAttributes.EXTRA_CLASSPATH))
+ .map(manifestAttribute -> {
+ Map> dictMap = createContextMap();
+ return Common.renderTemplate(manifestAttribute, Collections.emptyMap(), dictMap);
+ }).map(extraClasspathString -> {
+ List 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>() {
+ @Override
+ @SneakyThrows
+ public Stream 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() {
+ @Override
+ @SneakyThrows
+ public JarFile apply(File file) {
+ return new JarFile(file);
+ }
+ });
+ }
+
@SneakyThrows
public static void main(String[] args) {
Enumeration 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> runner = new Consumer>() {
@Override
@SneakyThrows
diff --git a/src/main/java/net/woggioni/gradle/envelope/EnvelopeJarTask.java b/src/main/java/net/woggioni/gradle/envelope/EnvelopeJarTask.java
index ebdb856..41c6165 100644
--- a/src/main/java/net/woggioni/gradle/envelope/EnvelopeJarTask.java
+++ b/src/main/java/net/woggioni/gradle/envelope/EnvelopeJarTask.java
@@ -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 systemProperties = new TreeMap<>();
+ @Getter(onMethod_ = {@Input})
+ private final ListProperty 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());
}