Skip to content

Instantly share code, notes, and snippets.

@ccjmne
Created February 19, 2025 09:45
Show Gist options
  • Save ccjmne/5920d65bf6d715a3a41ca300e58587dd to your computer and use it in GitHub Desktop.
Save ccjmne/5920d65bf6d715a3a41ca300e58587dd to your computer and use it in GitHub Desktop.
Configuration reader
package com.mercateo.creditcardx.service;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Props {
private static final Comparator<String> KEY_MATCHER = Comparator.comparing(a -> a.toLowerCase().replaceAll("[_-]|!$", ""));
// Both of these will perform fuzzy matching on keys, so that e.g. "foo-bar", "foo_bar" and "fooBar" are considered equal
private final TreeMap<String, String> source;
private final TreeSet<String> guaranteed;
private Props(final TreeMap<String, String> source, final Map<String, String> defaults, final Set<String> required) {
this.source = source;
this.guaranteed = Stream.concat(defaults.keySet().stream(), required.stream())
.collect(Collectors.toCollection(() -> new TreeSet<>(KEY_MATCHER)));
}
public static PropsLoader load(final String... keys) {
return new PropsLoader("", Collections.emptyMap(), Arrays.stream(keys)
.filter(k -> k.endsWith("!")).collect(Collectors.toSet()));
}
public static Props from(final String filename) {
return load().from(filename);
}
public static String getFrom(final String filename, final String key) {
return load(key.replaceAll("!?$", "!")).from(filename).get(key);
}
public static String getFrom(final String filename, final String key, final String defaultValue) {
return load(key).from(filename).get(key, defaultValue);
}
public static Integer getIntFrom(final String filename, final String key) {
return Integer.parseInt(getFrom(filename, key));
}
public static Integer getIntFrom(final String filename, final String key, final int defaultValue) {
return Integer.parseInt(getFrom(filename, key, String.valueOf(defaultValue)));
}
public String get(final String key) {
if (!guaranteed.contains(key)) {
throw new IllegalArgumentException("The presence of key '" + key + "' is not guaranteed; you must either:\n"
+ "- mark it required at load-time by suffixing it with '!',\n"
+ "- provide a default value with withDefaults(), or\n"
+ "- use get(String, String) instead of get(String)");
}
return source.get(key);
}
public String get(final String key, final String defaultValue) {
if (guaranteed.contains(key)) {
throw new IllegalArgumentException("The presence of key '" + key + "' is guaranteed; you should use get(String) instead of get(String, String)");
}
return source.getOrDefault(key, defaultValue);
}
public Integer getInt(final String key) {
return Integer.parseInt(get(key));
}
public Integer getInt(final String key, final int defaultValue) {
return Integer.parseInt(get(key, String.valueOf(defaultValue)));
}
public static class PropsLoader {
private static final String SCONF = "/opt/mercateo/sconf";
private static final String CONF = "/opt/mercateo/conf";
private static final Pattern LINE_PATTERN = Pattern.compile(
"^(?<key>[a-z0-9_-]+(\\.[a-z0-9_-]+)*)=(?<value>.*)$", Pattern.CASE_INSENSITIVE);
private final String scope;
private final Map<String, String> defaults;
private final Set<String> required;
private PropsLoader(final String scope, final Map<String, String> defaults, final Set<String> required) {
this.scope = scope.replaceAll("\\.?$", "."); // Ensure the scope matches a full sub-path
this.defaults = defaults;
this.required = required;
}
public Props from(final String filename) {
try {
final String sanitised = filename.replaceFirst("^/?", "/");
return from(Stream.of(
getClass().getResource(sanitised),
Paths.get(SCONF, sanitised).toUri().toURL(),
Paths.get(CONF, sanitised).toUri().toURL()
).filter(Objects::nonNull).toArray(URL[]::new));
} catch (final MalformedURLException e) {
throw new RuntimeException(e); // The most obvious triggering case was handled through sanitization
}
}
public Props from(final URL... sources) {
final Map.Entry<URL, TreeMap<String, String>> parsed = parse(sources);
final TreeMap<String, String> map = parsed.getValue();
if (required.stream().allMatch(map::containsKey)) {
return new Props(map, defaults, required);
}
throw new IllegalStateException("Missing required properties: "
+ Arrays.toString(required.stream().filter(k -> !map.containsKey(k)).toArray())
+ " (from " + parsed.getKey() + ")");
}
public PropsLoader withScope(final String scope) {
return new PropsLoader(scope, defaults, required);
}
public PropsLoader withDefaults(final Map<String, String> defaults) {
return new PropsLoader(scope, defaults, required);
}
private Map.Entry<URL, TreeMap<String, String>> parse(final URL... sources) {
return Arrays.stream(sources).map(this::parse).filter(Optional::isPresent).map(Optional::get).findFirst()
.orElseThrow(() -> new IllegalStateException("Couldn't locate any of " + Arrays.toString(sources)));
}
private Optional<Map.Entry<URL, TreeMap<String, String>>> parse(final URL source) {
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(source.openStream()))) {
return Optional.of(new AbstractMap.SimpleEntry<>(source, reader.lines()
.map(LINE_PATTERN::matcher)
.filter(Matcher::matches)
.sorted(Comparator.comparing(m -> m.group("key").startsWith(scope))) // Lower priority to global properties
.map(match -> new AbstractMap.SimpleImmutableEntry<>(
match.group("key").replaceFirst("^" + Pattern.quote(scope), ""), // TODO: Should the scope be able to be fuzzily matched?
match.group("value").trim()))
.filter(entry -> !entry.getValue().isEmpty())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(a, b) -> b, // Latest one remains
() -> new TreeMap<>(KEY_MATCHER)))));
} catch (final IOException e) {
return Optional.empty();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment