Skip to content

Commit

Permalink
props: support placeholders in server properties
Browse files Browse the repository at this point in the history
  • Loading branch information
itzg committed Nov 12, 2023
1 parent b66ea70 commit f4abd16
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}
Expand Down Expand Up @@ -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());
}
}
}
Expand Down
79 changes: 79 additions & 0 deletions src/main/java/me/itzg/helpers/env/SimplePlaceholders.java
Original file line number Diff line number Diff line change
@@ -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 <code>%VAR%</code>s
*/
@Slf4j
public class SimplePlaceholders {

/**
* Supports
* <ul>
* <li>%VERSION%</li>
* <li>%env:VERSION%</li>
* <li>%date:YYYY%</li>
* </ul>
*/
private static final Pattern PLACEHOLDERS_PATTERN = Pattern.compile("%((?<type>\\w+):)?(?<var>\\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;
}

}
4 changes: 3 additions & 1 deletion src/main/java/me/itzg/helpers/files/ResultsFileWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/me/itzg/helpers/forge/ForgeInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/me/itzg/helpers/modrinth/FetchedPack.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ public class FetchedPack {
String projectSlug;

String versionId;

/**
* Human-readable version
*/
String versionNumber;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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))
Expand All @@ -122,6 +132,17 @@ public Integer call() throws IOException {
return ExitCode.OK;
}

private Mono<Installation> 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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ public Mono<FetchedPack> 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()
))
)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ public Mono<FetchedPack> 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() {
Expand Down
23 changes: 19 additions & 4 deletions src/main/java/me/itzg/helpers/properties/SetPropertiesCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -39,6 +41,8 @@ public class SetPropertiesCommand implements Callable<Integer> {
private static final TypeReference<Map<String, PropertyDefinition>> PROPERTY_DEFINITIONS_TYPE = new TypeReference<Map<String, PropertyDefinition>>() {
};

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;

Expand All @@ -56,7 +60,8 @@ public class SetPropertiesCommand implements Callable<Integer> {
@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 {
Expand Down Expand Up @@ -131,6 +136,8 @@ private void loadProperties(Properties properties) throws IOException {
private long processProperties(Map<String, PropertyDefinition> propertyDefinitions, Properties properties,
Map<String, String> customProperties
) {
final SimplePlaceholders simplePlaceholders = new SimplePlaceholders(environmentVariablesProvider, clock);

long modifiedViaDefinitions = 0;
for (final Entry<String, PropertyDefinition> entry : propertyDefinitions.entrySet()) {
final String name = entry.getKey();
Expand All @@ -146,7 +153,11 @@ private long processProperties(Map<String, PropertyDefinition> 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);

Expand All @@ -163,7 +174,10 @@ private long processProperties(Map<String, PropertyDefinition> propertyDefinitio
if (customProperties != null) {
for (final Entry<String, String> 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);
Expand All @@ -176,6 +190,7 @@ private long processProperties(Map<String, PropertyDefinition> propertyDefinitio
return modifiedViaDefinitions + modifiedViaCustom;
}


private static boolean needsValueRedacted(String name) {
return name.contains("password");
}
Expand Down Expand Up @@ -209,7 +224,7 @@ private String mapAndValidateValue(PropertyDefinition definition, String value)
}
}

return unescapeUnicode(value);
return value;
}

@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -11,17 +12,23 @@
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;
import org.junit.jupiter.api.BeforeEach;
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;
Expand Down Expand Up @@ -242,6 +249,44 @@ void encodesPreEscaped() {

}

public static Stream<Arguments> 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<Object> actualKeys = new HashSet<>(properties.keySet());
Arrays.asList(propertiesToIgnore).forEach(actualKeys::remove);
Expand Down

0 comments on commit f4abd16

Please sign in to comment.