From f4abd161792e5bdc126891f46f570dc0871fe452 Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Sun, 12 Nov 2023 12:15:08 -0600 Subject: [PATCH] props: support placeholders in server properties --- .../curseforge/CurseForgeInstaller.java | 4 + .../itzg/helpers/env/SimplePlaceholders.java | 79 +++++++++++++++++++ .../itzg/helpers/files/ResultsFileWriter.java | 4 +- .../me/itzg/helpers/forge/ForgeInstaller.java | 7 +- .../me/itzg/helpers/modrinth/FetchedPack.java | 5 ++ .../InstallModrinthModpackCommand.java | 21 +++++ .../modrinth/ModrinthApiPackFetcher.java | 4 +- .../modrinth/ModrinthHttpPackFetcher.java | 8 +- .../properties/SetPropertiesCommand.java | 23 +++++- .../properties/SetPropertiesCommandTest.java | 45 +++++++++++ 10 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 src/main/java/me/itzg/helpers/env/SimplePlaceholders.java diff --git a/src/main/java/me/itzg/helpers/curseforge/CurseForgeInstaller.java b/src/main/java/me/itzg/helpers/curseforge/CurseForgeInstaller.java index 673f19a4..4a1c04e8 100644 --- a/src/main/java/me/itzg/helpers/curseforge/CurseForgeInstaller.java +++ b/src/main/java/me/itzg/helpers/curseforge/CurseForgeInstaller.java @@ -429,6 +429,8 @@ private void finalizeExistingInstallation(CurseForgeManifest prevManifest) throw if (prevManifest.getLevelName() != null) { resultsFileWriter.write("LEVEL", prevManifest.getLevelName()); } + resultsFileWriter.write(ResultsFileWriter.MODPACK_NAME, prevManifest.getModpackName()); + resultsFileWriter.write(ResultsFileWriter.MODPACK_VERSION, prevManifest.getModpackVersion()); } } } @@ -462,6 +464,8 @@ private void finalizeResults(InstallContext context, ModPackResults results, int try (ResultsFileWriter resultsFileWriter = new ResultsFileWriter(resultsFile, true)) { if (results.getLevelName() != null) { resultsFileWriter.write("LEVEL", results.getLevelName()); + resultsFileWriter.write(ResultsFileWriter.MODPACK_NAME, results.getName()); + resultsFileWriter.write(ResultsFileWriter.MODPACK_VERSION, results.getVersion()); } } } diff --git a/src/main/java/me/itzg/helpers/env/SimplePlaceholders.java b/src/main/java/me/itzg/helpers/env/SimplePlaceholders.java new file mode 100644 index 00000000..7f1c547c --- /dev/null +++ b/src/main/java/me/itzg/helpers/env/SimplePlaceholders.java @@ -0,0 +1,79 @@ +package me.itzg.helpers.env; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; + +/** + * Performs simple placeholder replacement of %VAR%s + */ +@Slf4j +public class SimplePlaceholders { + + /** + * Supports + * + */ + private static final Pattern PLACEHOLDERS_PATTERN = Pattern.compile("%((?\\w+):)?(?\\w+)%"); + + private final EnvironmentVariablesProvider environmentVariablesProvider; + private final Clock clock; + + public SimplePlaceholders(EnvironmentVariablesProvider environmentVariablesProvider, Clock clock) { + this.environmentVariablesProvider = environmentVariablesProvider; + this.clock = clock; + } + + public String processPlaceholders(String value) { + final Matcher m = PLACEHOLDERS_PATTERN.matcher(value); + if (m.find()) { + final StringBuffer sb = new StringBuffer(); + do { + final String type = Optional.ofNullable(m.group("type")) + .orElse("env"); + final String replacement = buildPlaceholderReplacement(type, m.group("var"), m.group()); + + m.appendReplacement(sb, replacement); + } while (m.find()); + + m.appendTail(sb); + return sb.toString(); + } + return value; + } + + private String buildPlaceholderReplacement(String type, String var, String fallback) { + switch (type) { + case "env": + final String result = environmentVariablesProvider.get(var); + if (result != null) { + return result; + } + else { + log.warn("Unable to resolve environment variable {}", var); + return fallback; + } + + case "date": + case "time": + try { + final DateTimeFormatter f = DateTimeFormatter.ofPattern(var); + return ZonedDateTime.now(clock).format(f); + } catch (IllegalArgumentException e) { + log.error("Invalid date/time format in {}", var, e); + return fallback; + } + } + + return fallback; + } + +} diff --git a/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java b/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java index b0ecf370..2480af44 100644 --- a/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java +++ b/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java @@ -18,6 +18,8 @@ public class ResultsFileWriter implements AutoCloseable { public static final String OPTION_DESCRIPTION = "A key=value file suitable for scripted environment variables. Currently includes" + "\n SERVER: the entry point jar or script"; + public static final String MODPACK_NAME = "MODPACK_NAME"; + public static final String MODPACK_VERSION = "MODPACK_VERSION"; private final BufferedWriter writer; public ResultsFileWriter(Path resultsFile) throws IOException { @@ -33,7 +35,7 @@ public ResultsFileWriter(Path resultsFile, boolean append) throws IOException { } public ResultsFileWriter write(String field, String value) throws IOException { - log.debug("Writing {}={} to results file", field, value); + log.debug("Writing {}=\"{}\" to results file", field, value); writer.write(String.format("%s=\"%s\"", field, value)); writer.newLine(); return this; diff --git a/src/main/java/me/itzg/helpers/forge/ForgeInstaller.java b/src/main/java/me/itzg/helpers/forge/ForgeInstaller.java index c23231d2..b56ce368 100644 --- a/src/main/java/me/itzg/helpers/forge/ForgeInstaller.java +++ b/src/main/java/me/itzg/helpers/forge/ForgeInstaller.java @@ -55,7 +55,7 @@ public void install( needsInstall = true; } else if (prevManifest != null) { - if (!Files.exists(Paths.get(prevManifest.getServerEntry()))) { + if (!serverEntryExists(outputDir, prevManifest.getServerEntry())) { log.warn("Server entry for Minecraft {} Forge {} is missing. Re-installing.", prevManifest.getMinecraftVersion(), prevManifest.getForgeVersion() ); @@ -110,6 +110,11 @@ else if ( } } + private boolean serverEntryExists(@NonNull Path outputDir, String serverEntry) { + return (serverEntry.startsWith("/") && Files.exists(Paths.get(serverEntry))) + || Files.exists(outputDir.resolve(serverEntry)); + } + private ForgeManifest loadManifest(Path outputDir, String variant) throws IOException { // First check for and retrofit legacy manifest format final Path legacyFile = outputDir.resolve(LegacyManifest.FILENAME); diff --git a/src/main/java/me/itzg/helpers/modrinth/FetchedPack.java b/src/main/java/me/itzg/helpers/modrinth/FetchedPack.java index 6f78c520..4b2811c9 100644 --- a/src/main/java/me/itzg/helpers/modrinth/FetchedPack.java +++ b/src/main/java/me/itzg/helpers/modrinth/FetchedPack.java @@ -10,4 +10,9 @@ public class FetchedPack { String projectSlug; String versionId; + + /** + * Human-readable version + */ + String versionNumber; } diff --git a/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java b/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java index 1e5839c6..bac993c1 100644 --- a/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java +++ b/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java @@ -15,6 +15,8 @@ import picocli.CommandLine; import picocli.CommandLine.ExitCode; import picocli.CommandLine.Option; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; @CommandLine.Command(name = "install-modrinth-modpack", description = "Supports installation of Modrinth modpacks along with the associated mod loader", @@ -103,6 +105,14 @@ public Integer call() throws IOException { this.forceModloaderReinstall ) .processModpack(sharedFetch) + .flatMap(installation -> { + if (resultsFile != null) { + return processResultsFile(fetchedPack, installation); + } + else { + return Mono.just(installation); + } + }) .map(installation -> ModrinthModpackManifest.builder() .files(Manifests.relativizeAll(this.outputDirectory, installation.files)) @@ -122,6 +132,17 @@ public Integer call() throws IOException { return ExitCode.OK; } + private Mono processResultsFile(FetchedPack fetchedPack, Installation installation) { + return Mono.fromCallable(() -> { + try (ResultsFileWriter results = new ResultsFileWriter(resultsFile, true)) { + results.write(ResultsFileWriter.MODPACK_NAME, installation.getIndex().getName()); + results.write(ResultsFileWriter.MODPACK_VERSION, fetchedPack.getVersionNumber()); + } + return installation; + }) + .subscribeOn(Schedulers.boundedElastic()); + } + private ModrinthPackFetcher buildModpackFetcher( ModrinthApiClient apiClient, ProjectRef projectRef) { diff --git a/src/main/java/me/itzg/helpers/modrinth/ModrinthApiPackFetcher.java b/src/main/java/me/itzg/helpers/modrinth/ModrinthApiPackFetcher.java index 905fe72e..4e5de9df 100644 --- a/src/main/java/me/itzg/helpers/modrinth/ModrinthApiPackFetcher.java +++ b/src/main/java/me/itzg/helpers/modrinth/ModrinthApiPackFetcher.java @@ -59,7 +59,9 @@ public Mono fetchModpack(ModrinthModpackManifest prevManifest) { .filter(version -> needsInstall(prevManifest, project.getSlug(), version)) .flatMap(version -> apiClient.downloadMrPack(ModrinthApiClient.pickVersionFile(version)) - .map(mrPackFile -> new FetchedPack(mrPackFile, project.getSlug(), version.getId())) + .map(mrPackFile -> new FetchedPack( + mrPackFile, project.getSlug(), version.getId(), version.getVersionNumber() + )) ) ); } diff --git a/src/main/java/me/itzg/helpers/modrinth/ModrinthHttpPackFetcher.java b/src/main/java/me/itzg/helpers/modrinth/ModrinthHttpPackFetcher.java index 015da4c7..347b0376 100644 --- a/src/main/java/me/itzg/helpers/modrinth/ModrinthHttpPackFetcher.java +++ b/src/main/java/me/itzg/helpers/modrinth/ModrinthHttpPackFetcher.java @@ -26,7 +26,13 @@ public Mono fetchModpack(ModrinthModpackManifest prevManifest) { (uri, file, contentSizeBytes) -> log.info("Downloaded {}", destFilePath) ) - .map(mrPackFile -> new FetchedPack(mrPackFile, "custom", deriveVersionId())); + .map(mrPackFile -> new FetchedPack(mrPackFile, "custom", deriveVersionId(), deriveVersionName())); + } + + private String deriveVersionName() { + final int lastSlash = modpackUri.getPath().lastIndexOf('/'); + return lastSlash > 0 ? modpackUri.getPath().substring(lastSlash + 1) + : "unknown"; } private String deriveVersionId() { diff --git a/src/main/java/me/itzg/helpers/properties/SetPropertiesCommand.java b/src/main/java/me/itzg/helpers/properties/SetPropertiesCommand.java index 999555b9..3cfd70c2 100644 --- a/src/main/java/me/itzg/helpers/properties/SetPropertiesCommand.java +++ b/src/main/java/me/itzg/helpers/properties/SetPropertiesCommand.java @@ -9,6 +9,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.time.Clock; import java.time.Instant; import java.util.Collections; import java.util.Map; @@ -21,6 +22,7 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import me.itzg.helpers.env.EnvironmentVariablesProvider; +import me.itzg.helpers.env.SimplePlaceholders; import me.itzg.helpers.env.StandardEnvironmentVariablesProvider; import me.itzg.helpers.errors.InvalidParameterException; import me.itzg.helpers.json.ObjectMappers; @@ -39,6 +41,8 @@ public class SetPropertiesCommand implements Callable { private static final TypeReference> PROPERTY_DEFINITIONS_TYPE = new TypeReference>() { }; + private static final Pattern UNICODE_ESCAPE = Pattern.compile("\\\\u([0-9a-fA-F]{4})"); + @Option(names = "--definitions", description = "JSON file of property names to PropertyDefinition mappings") Path propertyDefinitionsFile; @@ -56,7 +60,8 @@ public class SetPropertiesCommand implements Callable { @Setter private EnvironmentVariablesProvider environmentVariablesProvider = new StandardEnvironmentVariablesProvider(); - private static final Pattern UNICODE_ESCAPE = Pattern.compile("\\\\u([0-9a-fA-F]{4})"); + @Setter + private Clock clock = Clock.systemDefaultZone(); @Override public Integer call() throws Exception { @@ -131,6 +136,8 @@ private void loadProperties(Properties properties) throws IOException { private long processProperties(Map propertyDefinitions, Properties properties, Map customProperties ) { + final SimplePlaceholders simplePlaceholders = new SimplePlaceholders(environmentVariablesProvider, clock); + long modifiedViaDefinitions = 0; for (final Entry entry : propertyDefinitions.entrySet()) { final String name = entry.getKey(); @@ -146,7 +153,11 @@ private long processProperties(Map propertyDefinitio else { final String envValue = environmentVariablesProvider.get(definition.getEnv()); if (envValue != null) { - final String targetValue = mapAndValidateValue(definition, envValue); + final String targetValue = mapAndValidateValue(definition, + simplePlaceholders.processPlaceholders( + unescapeUnicode(envValue) + ) + ); final String propValue = properties.getProperty(name); @@ -163,7 +174,10 @@ private long processProperties(Map propertyDefinitio if (customProperties != null) { for (final Entry entry : customProperties.entrySet()) { final String name = entry.getKey(); - final String targetValue = entry.getValue(); + final String targetValue = + simplePlaceholders.processPlaceholders( + unescapeUnicode(entry.getValue()) + ); final String propValue = properties.getProperty(name); if (!Objects.equals(targetValue, propValue)) { log.debug("Setting property {} to new value '{}'", name, targetValue); @@ -176,6 +190,7 @@ private long processProperties(Map propertyDefinitio return modifiedViaDefinitions + modifiedViaCustom; } + private static boolean needsValueRedacted(String name) { return name.contains("password"); } @@ -209,7 +224,7 @@ private String mapAndValidateValue(PropertyDefinition definition, String value) } } - return unescapeUnicode(value); + return value; } @NotNull diff --git a/src/test/java/me/itzg/helpers/properties/SetPropertiesCommandTest.java b/src/test/java/me/itzg/helpers/properties/SetPropertiesCommandTest.java index 5f693434..358f4c90 100644 --- a/src/test/java/me/itzg/helpers/properties/SetPropertiesCommandTest.java +++ b/src/test/java/me/itzg/helpers/properties/SetPropertiesCommandTest.java @@ -2,6 +2,7 @@ import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemErr; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; import java.io.IOException; import java.io.InputStream; @@ -11,10 +12,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Properties; +import java.util.stream.Stream; import me.itzg.helpers.env.MappedEnvVarProvider; import org.apache.commons.lang3.RandomStringUtils; import org.jetbrains.annotations.NotNull; @@ -22,6 +27,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import picocli.CommandLine; import picocli.CommandLine.ExitCode; @@ -242,6 +249,44 @@ void encodesPreEscaped() { } + public static Stream processesPlaceholdersArgs() { + return Stream.of( + arguments("simple", "simple"), + arguments("Running %MODPACK_NAME%", "Running modpack"), + arguments("Running %env:MODPACK_NAME%", "Running modpack"), + arguments("Running %MODPACK_NAME% at %MODPACK_VERSION%", "Running modpack at version"), + arguments("Year month is %date:yyyy_MM%", "Year month is 2007_12"), + arguments("Stays %UNKNOWN%", "Stays %UNKNOWN%") + ); + } + + @ParameterizedTest + @MethodSource("processesPlaceholdersArgs") + void processesPlaceholders(String motd, String expected) { + final Path outputProperties = tempDir.resolve("out.properties"); + + final int exitCode = new CommandLine(new SetPropertiesCommand() + .setEnvironmentVariablesProvider(MappedEnvVarProvider.of( + "MODPACK_NAME", "modpack", + "MODPACK_VERSION", "version", + "MOTD", motd + )) + .setClock(Clock.fixed(Instant.parse("2007-12-03T10:15:30.00Z"), ZoneId.of("UTC"))) + ) + .execute( + "--definitions", definitionsFile.toString(), + outputProperties.toString() + ); + + assertThat(exitCode).isEqualTo(ExitCode.OK); + + //noinspection UnnecessaryUnicodeEscape + assertThat(outputProperties) + .content(StandardCharsets.UTF_8) + .containsIgnoringNewLines("motd=" + expected); + + } + private void assertPropertiesEqualExcept(Properties properties, String... propertiesToIgnore) { final HashSet actualKeys = new HashSet<>(properties.keySet()); Arrays.asList(propertiesToIgnore).forEach(actualKeys::remove);