package com.onthegomap.planetiler.config; import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; import com.onthegomap.planetiler.geo.GeoUtils; import com.onthegomap.planetiler.stats.Stats; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeMap; import java.util.function.Supplier; import java.util.function.UnaryOperator; import java.util.regex.Pattern; import java.util.stream.Stream; import org.locationtech.jts.geom.Envelope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Lightweight abstraction over ways to provide key/value pair arguments to a program like jvm properties, environmental * variables, or a config file. *
* When looking up a key, tries to find a case-and-separator-insensitive match, for example {@code "CONFIG_OPTION"} will * match {@code "config-option"} and {@code "config_option"}. *
* If you replace an option with a new value, you can read a value from the new option and fall back to old one by using
* {@code "new_flag|old_flag"} as the key.
*/
public class Arguments {
private static final Logger LOGGER = LoggerFactory.getLogger(Arguments.class);
private final UnaryOperator
* For example to set {@code key=value}: {@code java -Dplanetiler.key=value -jar ...}
*/
public static Arguments fromJvmProperties() {
return fromJvmProperties(
System::getProperty,
() -> System.getProperties().stringPropertyNames()
);
}
static Arguments fromJvmProperties(UnaryOperator
* For example to set {@code key=value}: {@code PLANETILER_KEY=value java -jar ...}
*/
public static Arguments fromEnvironment() {
return fromEnvironment(
System::getenv,
() -> System.getenv().keySet()
);
}
static Arguments fromEnvironment(UnaryOperator
* For example to set {@code key=value}: {@code java -jar ... key=value} or {@code java -jar ... --key value}
*
* Or to set {@code key=true}: {@code java -jar ... --key}
*
* @param args arguments provided to main method
* @return arguments parsed from command-line arguments
*/
public static Arguments fromArgs(String... args) {
Map
* Priority order:
*
* Priority order:
*
* Format: {@code westLng,southLat,eastLng,northLat}
*
* @param key argument name
* @param description argument description
* @return An envelope parsed from {@code key} or null if missing
*/
public Envelope bounds(String key, String description) {
String input = getArg(key);
Envelope result = null;
if ("world".equalsIgnoreCase(input) || "planet".equalsIgnoreCase(input)) {
result = GeoUtils.WORLD_LAT_LON_BOUNDS;
} else if (input != null) {
double[] bounds = Stream.of(input.split("[\\s,]+")).mapToDouble(Double::parseDouble).toArray();
if (bounds.length != 4) {
throw new IllegalArgumentException("bounds must have 4 coordinates, got: " + input);
}
result = new Envelope(bounds[0], bounds[2], bounds[1], bounds[3]);
}
logArgValue(key, description, result);
return result;
}
protected void logArgValue(String key, String description, Object result) {
if (!silent && LOGGER.isDebugEnabled()) {
LOGGER.debug("argument: {}={} ({})", key.replaceFirst("\\|.*$", ""), result, description);
}
}
/** Stop logging argument values when they are read and return this instance. */
public Arguments silence() {
this.silent = true;
return this;
}
public String getString(String key, String description, String defaultValue) {
String value = getArg(key, defaultValue);
logArgValue(key, description, value);
return value;
}
public String getString(String key, String description) {
String value = getRequiredArg(key, description);
logArgValue(key, description, value);
return value;
}
/** Returns a {@link Path} parsed from {@code key} argument, or fall back to a default if the argument is not set. */
public Path file(String key, String description, Path defaultValue) {
String value = getArg(key);
Path file = value == null ? defaultValue : Path.of(value);
logArgValue(key, description, file);
return file;
}
/** Returns a {@link Path} parsed from {@code key} argument which may or may not exist. */
public Path file(String key, String description) {
String value = getRequiredArg(key, description);
Path file = Path.of(value);
logArgValue(key, description, file);
return file;
}
private String getRequiredArg(String key, String description) {
String value = getArg(key);
if (value == null) {
throw new IllegalArgumentException("Missing required parameter: " + key + " (" + description + ")");
}
return value;
}
/**
* Returns a {@link Path} parsed from {@code key} argument which must exist for the program to function.
*
* @throws IllegalArgumentException if the file does not exist
*/
public Path inputFile(String key, String description, Path defaultValue) {
Path path = file(key, description, defaultValue);
if (!Files.exists(path)) {
throw new IllegalArgumentException(path + " does not exist");
}
return path;
}
/**
* Returns a {@link Path} parsed from a required {@code key} argument which must exist for the program to function.
*
* @throws IllegalArgumentException if the file does not exist or if the parameter is not provided.
*/
public Path inputFile(String key, String description) {
Path path = file(key, description);
if (!Files.exists(path)) {
throw new IllegalArgumentException(path + " does not exist");
}
return path;
}
/** Returns a boolean parsed from {@code key} argument where {@code "true"} is true and anything else is false. */
public boolean getBoolean(String key, String description, boolean defaultValue) {
boolean value = "true".equalsIgnoreCase(getArg(key, Boolean.toString(defaultValue)));
logArgValue(key, description, value);
return value;
}
/** Returns a boolean parsed from {@code key} or {@code null} if not specified. */
public Boolean getBooleanObject(String key, String description) {
var arg = getArg(key);
Boolean value = arg == null ? null : "true".equalsIgnoreCase(arg);
logArgValue(key, description, value);
return value;
}
/** Returns a {@link List} parsed from {@code key} argument where values are separated by commas. */
public List
* If {@code pushgateway} is set then it uses a stats implementation that pushes to prometheus through a
* push gateway every {@code pushgateway.interval} seconds.
* Otherwise, uses an in-memory stats implementation.
*/
public Stats getStats() {
String prometheus = getArg("pushgateway");
if (prometheus != null && !prometheus.isBlank()) {
LOGGER.info("argument: stats=use prometheus push gateway stats");
String job = getString("pushgateway.job", "prometheus pushgateway job ID", "planetiler");
Duration interval = getDuration("pushgateway.interval", "how often to send stats to prometheus push gateway",
"15s");
return Stats.prometheusPushGateway(prometheus, job, interval);
} else {
LOGGER.info("argument: stats=use in-memory stats");
return Stats.inMemory();
}
}
/**
* Returns an argument as integer.
*
* @throws NumberFormatException if the argument cannot be parsed as an integer
*/
public int getInteger(String key, String description, int defaultValue) {
String value = getArg(key, Integer.toString(defaultValue));
int parsed = Integer.parseInt(value);
logArgValue(key, description, parsed);
return parsed;
}
/**
* Returns an argument as double.
*
* @throws NumberFormatException if the argument cannot be parsed as a double
*/
public double getDouble(String key, String description, double defaultValue) {
String value = getArg(key, Double.toString(defaultValue));
double parsed = Double.parseDouble(value);
logArgValue(key, description, parsed);
return parsed;
}
/**
* Returns an argument as a {@link Duration} (i.e. "10s", "90m", "1h30m").
*
* @throws DateTimeParseException if the argument cannot be parsed as a duration
*/
public Duration getDuration(String key, String description, String defaultValue) {
String value = getArg(key, defaultValue);
Duration parsed = Duration.parse("PT" + value);
logArgValue(key, description, parsed.get(ChronoUnit.SECONDS) + " seconds");
return parsed;
}
/**
* Returns an argument as long.
*
* @throws NumberFormatException if the argument cannot be parsed as a long
*/
public long getLong(String key, String description, long defaultValue) {
String value = getArg(key, Long.toString(defaultValue));
long parsed = Long.parseLong(value);
logArgValue(key, description, parsed);
return parsed;
}
/**
* Returns a map from all the arguments provided to their values.
*/
public Map
*
*
* @param args command-line args provide to main entrypoint method
* @return arguments parsed from those sources
*/
public static Arguments fromArgsOrConfigFile(String... args) {
Arguments fromArgsOrEnv = fromEnvOrArgs(args);
Path configFile = fromArgsOrEnv.file("config", "path to config file", null);
if (configFile != null) {
return fromArgsOrEnv.orElse(fromConfigFile(configFile));
} else {
return fromArgsOrEnv;
}
}
/**
* Returns arguments parsed from command-line arguments, JVM properties, environmental variables.
*
*
*
* @param args command-line args provide to main entrypoint method
* @return arguments parsed from those sources
*/
public static Arguments fromEnvOrArgs(String... args) {
return fromArgs(args)
.orElse(fromJvmProperties())
.orElse(fromEnvironment());
}
private static String normalize(String key, String separator, boolean upperCase) {
String result = key.replaceAll("[._-]", separator);
return upperCase ? result.toUpperCase(Locale.ROOT) : result.toLowerCase(Locale.ROOT);
}
private static String normalize(String key) {
return normalize(key, "_", false);
}
public static Arguments of(Map> keys = () -> rawKeys.get().stream().flatMap(key -> {
String reversed = reverse.apply(key);
return normalize(key).equals(normalize(reversed)) ? Stream.empty() : Stream.of(reversed);
}).toList();
return new Arguments(key -> provider.apply(forward.apply(key)), keys);
}
private static Arguments fromPrefixed(UnaryOperator