diff --git a/avaje-config/src/main/java/io/avaje/config/ConfigServiceLoader.java b/avaje-config/src/main/java/io/avaje/config/ConfigServiceLoader.java index 495bb28d..0bc2caaa 100644 --- a/avaje-config/src/main/java/io/avaje/config/ConfigServiceLoader.java +++ b/avaje-config/src/main/java/io/avaje/config/ConfigServiceLoader.java @@ -5,8 +5,8 @@ import java.util.ServiceLoader; /** - * Load all the avaje-config extensions via ServiceLoader using the single - * common ConfigExtension interface. + * Load all the avaje-config extensions via ServiceLoader using the single common ConfigExtension + * interface. */ final class ConfigServiceLoader { @@ -21,13 +21,15 @@ static ConfigServiceLoader get() { private final ModificationEventRunner eventRunner; private final List sources = new ArrayList<>(); private final List plugins = new ArrayList<>(); + private final List uriLoaders; private final Parsers parsers; ConfigServiceLoader() { - ModificationEventRunner _eventRunner = null; - ConfigurationLog _log = null; - ResourceLoader _resourceLoader = null; + ModificationEventRunner spiEventRunner = null; + ConfigurationLog spiLog = null; + ResourceLoader spiResourceLoader = null; List otherParsers = new ArrayList<>(); + List loaders = new ArrayList<>(); for (var spi : ServiceLoader.load(ConfigExtension.class)) { if (spi instanceof ConfigurationSource) { @@ -36,25 +38,33 @@ static ConfigServiceLoader get() { plugins.add((ConfigurationPlugin) spi); } else if (spi instanceof ConfigParser) { otherParsers.add((ConfigParser) spi); + } else if (spi instanceof URIConfigLoader) { + loaders.add((URIConfigLoader) spi); } else if (spi instanceof ConfigurationLog) { - _log = (ConfigurationLog) spi; + spiLog = (ConfigurationLog) spi; } else if (spi instanceof ResourceLoader) { - _resourceLoader = (ResourceLoader) spi; + spiResourceLoader = (ResourceLoader) spi; } else if (spi instanceof ModificationEventRunner) { - _eventRunner = (ModificationEventRunner) spi; + spiEventRunner = (ModificationEventRunner) spi; } } - this.log = _log == null ? new DefaultConfigurationLog() : _log; - this.resourceLoader = _resourceLoader == null ? new DefaultResourceLoader() : _resourceLoader; - this.eventRunner = _eventRunner == null ? new CoreConfiguration.ForegroundEventRunner() : _eventRunner; + this.log = spiLog == null ? new DefaultConfigurationLog() : spiLog; + this.resourceLoader = spiResourceLoader == null ? new DefaultResourceLoader() : spiResourceLoader; + this.eventRunner = + spiEventRunner == null ? new CoreConfiguration.ForegroundEventRunner() : spiEventRunner; this.parsers = new Parsers(otherParsers); + this.uriLoaders = loaders; } Parsers parsers() { return parsers; } + public List uriLoaders() { + return uriLoaders; + } + ConfigurationLog log() { return log; } diff --git a/avaje-config/src/main/java/io/avaje/config/CoreComponents.java b/avaje-config/src/main/java/io/avaje/config/CoreComponents.java index 586106b5..8e9532ec 100644 --- a/avaje-config/src/main/java/io/avaje/config/CoreComponents.java +++ b/avaje-config/src/main/java/io/avaje/config/CoreComponents.java @@ -1,6 +1,5 @@ package io.avaje.config; -import java.util.Collections; import java.util.List; final class CoreComponents { @@ -8,13 +7,21 @@ final class CoreComponents { private final ModificationEventRunner runner; private final ConfigurationLog log; private final Parsers parsers; + private final List uriLoaders; private final List sources; private final List plugins; - CoreComponents(ModificationEventRunner runner, ConfigurationLog log, Parsers parsers, List sources, List plugins) { + CoreComponents( + ModificationEventRunner runner, + ConfigurationLog log, + Parsers parsers, + List uriLoaders, + List sources, + List plugins) { this.runner = runner; this.log = log; this.parsers = parsers; + this.uriLoaders = uriLoaders; this.sources = sources; this.plugins = plugins; } @@ -23,15 +30,20 @@ final class CoreComponents { CoreComponents() { this.runner = new CoreConfiguration.ForegroundEventRunner(); this.log = new DefaultConfigurationLog(); - this.parsers = new Parsers(Collections.emptyList()); - this.sources = Collections.emptyList(); - this.plugins = Collections.emptyList(); + this.parsers = ConfigServiceLoader.get().parsers(); + this.uriLoaders = ConfigServiceLoader.get().uriLoaders(); + this.sources = List.of(); + this.plugins = List.of(); } Parsers parsers() { return parsers; } + public List uriLoaders() { + return uriLoaders; + } + ConfigurationLog log() { return log; } diff --git a/avaje-config/src/main/java/io/avaje/config/CoreConfigurationBuilder.java b/avaje-config/src/main/java/io/avaje/config/CoreConfigurationBuilder.java index 45dd8284..b0840d6e 100644 --- a/avaje-config/src/main/java/io/avaje/config/CoreConfigurationBuilder.java +++ b/avaje-config/src/main/java/io/avaje/config/CoreConfigurationBuilder.java @@ -8,6 +8,7 @@ import java.io.FileReader; import java.io.IOException; import java.io.UncheckedIOException; +import java.util.List; import java.util.Map; import java.util.Properties; @@ -19,6 +20,7 @@ final class CoreConfigurationBuilder implements Configuration.Builder { private final CoreEntry.CoreMap sourceMap = CoreEntry.newMap(); private final ConfigServiceLoader serviceLoader = ConfigServiceLoader.get(); private final Parsers parsers = serviceLoader.parsers(); + private final List uriLoaders = serviceLoader.uriLoaders(); private ConfigurationLog log = serviceLoader.log(); private ResourceLoader resourceLoader = serviceLoader.resourceLoader(); private ModificationEventRunner eventRunner = serviceLoader.eventRunner(); @@ -130,7 +132,14 @@ public Configuration.Builder includeResourceLoading() { @Override public Configuration build() { - var components = new CoreComponents(eventRunner, log, parsers, serviceLoader.sources(), serviceLoader.plugins()); + var components = + new CoreComponents( + eventRunner, + log, + parsers, + uriLoaders, + serviceLoader.sources(), + serviceLoader.plugins()); if (includeResourceLoading) { log.preInitialisation(); initialLoader = new InitialLoader(components, resourceLoader); diff --git a/avaje-config/src/main/java/io/avaje/config/InitialLoadContext.java b/avaje-config/src/main/java/io/avaje/config/InitialLoadContext.java index 5c0fbd90..7ee4ebce 100644 --- a/avaje-config/src/main/java/io/avaje/config/InitialLoadContext.java +++ b/avaje-config/src/main/java/io/avaje/config/InitialLoadContext.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; +import java.util.Optional; import java.util.Set; /** @@ -109,6 +110,13 @@ private InputStream resourceStream(String resourcePath) { return resourceLoader.getResourceAsStream(resourcePath); } + /** Get a property */ + Optional get(String key) { + + return Optional.ofNullable(map.get(key)) + .map(e -> e.needsEvaluation() ? eval(e.value()) : e.value()); + } + /** * Add a property entry. */ diff --git a/avaje-config/src/main/java/io/avaje/config/InitialLoader.java b/avaje-config/src/main/java/io/avaje/config/InitialLoader.java index dbfa651d..c5dab275 100644 --- a/avaje-config/src/main/java/io/avaje/config/InitialLoader.java +++ b/avaje-config/src/main/java/io/avaje/config/InitialLoader.java @@ -3,11 +3,14 @@ import static io.avaje.config.InitialLoader.Source.FILE; import static io.avaje.config.InitialLoader.Source.RESOURCE; import static java.lang.System.Logger.Level.WARNING; +import static java.util.stream.Collectors.joining; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.*; import java.util.regex.Pattern; @@ -40,13 +43,17 @@ enum Source { private final ConfigurationLog log; private final InitialLoadContext loadContext; + private final DURILoadContext uriContext; private final Set profileResourceLoaded = new HashSet<>(); private final Parsers parsers; + private final List uriLoaders; InitialLoader(CoreComponents components, ResourceLoader resourceLoader) { this.parsers = components.parsers(); + this.uriLoaders = components.uriLoaders(); this.log = components.log(); this.loadContext = new InitialLoadContext(log, resourceLoader); + this.uriContext = new DURILoadContext(log, parsers, loadContext::get); } Set loadedFrom() { @@ -113,12 +120,12 @@ void loadLocalFiles() { loadViaProfiles(RESOURCE); loadViaProfiles(FILE); loadViaSystemProperty(); - loadViaIndirection(); // test configuration (if found) overrides main configuration // we should only find these resources when running tests if (!loadTest()) { loadLocalDev(); } + loadViaIndirection(); loadViaCommandLineArgs(); } @@ -252,23 +259,67 @@ private void loadViaSystemProperty() { } boolean loadWithExtensionCheck(String fileName) { + + if (loadURI(fileName)) return true; + var extension = fileName.substring(fileName.lastIndexOf(".") + 1); if ("properties".equals(extension)) { + return loadProperties(fileName, RESOURCE) | loadProperties(fileName, FILE); } else { var parser = parsers.get(extension); if (parser == null) { throw new IllegalArgumentException( - "Expecting only properties or " - + parsers.supportedExtensions() - + " file extensions but got [" - + fileName - + "]"); + "Expecting only properties or " + + parsers.supportedExtensions() + + " file extensions or " + + uriLoaders.stream().map(s -> s.getClass().getSimpleName()).collect(joining(",")) + + " compatible uri schemes but got [" + + fileName + + "]"); } return loadCustomExtension(fileName, parser, RESOURCE) - | loadCustomExtension(fileName, parser, FILE); + | loadCustomExtension(fileName, parser, FILE); + } + } + + private boolean loadURI(String fileName) { + URI uri; + try { + uri = new URI(fileName); + } catch (URISyntaxException e) { + return false; + } + + var scheme = uri.getScheme(); + + if (scheme == null || "classpath".equals(scheme) || "file".equals(scheme)) { + + return false; + } + + var loader = getLoader(uri); + + if (loader != null) { + final var source = loader.redact(uri); + loader.load(uri, uriContext).forEach((k, v) -> loadContext.put(k, v, source)); + return true; } + return false; + } + + @Nullable + private URIConfigLoader getLoader(URI uri) { + + for (var loader : uriLoaders) { + + if (loader.supports(uri)) { + return loader; + } + } + + return null; } /** diff --git a/avaje-config/src/main/java/io/avaje/config/Parsers.java b/avaje-config/src/main/java/io/avaje/config/Parsers.java index d86f7a02..0b2b50e6 100644 --- a/avaje-config/src/main/java/io/avaje/config/Parsers.java +++ b/avaje-config/src/main/java/io/avaje/config/Parsers.java @@ -5,9 +5,6 @@ import java.util.Map; import java.util.Set; -/** - * Holds the non-properties ConfigParsers. - */ final class Parsers { private final Map parserMap = new HashMap<>(); @@ -42,31 +39,19 @@ private void initParsers(List otherParsers) { } } - /** - * Return the extension ConfigParser pairs. - */ - Set> entrySet() { + public Set> entrySet() { return parserMap.entrySet(); } - /** - * Return the ConfigParser for the given extension. - */ - ConfigParser get(String extension) { + public ConfigParser get(String extension) { return parserMap.get(extension.toLowerCase()); } - /** - * Return true if the extension has a matching parser. - */ - boolean supportsExtension(String extension) { + public boolean supportsExtension(String extension) { return parserMap.containsKey(extension.toLowerCase()); } - /** - * Return the set of supported extensions. - */ - Set supportedExtensions() { + public Set supportedExtensions() { return parserMap.keySet(); } } diff --git a/avaje-config/src/main/java/io/avaje/config/URIConfigLoader.java b/avaje-config/src/main/java/io/avaje/config/URIConfigLoader.java new file mode 100644 index 00000000..f72f0a19 --- /dev/null +++ b/avaje-config/src/main/java/io/avaje/config/URIConfigLoader.java @@ -0,0 +1,25 @@ +package io.avaje.config; + +import java.net.URI; +import java.util.Map; + +/** Custom URI configuration parser for reading load.properties properties that use a URI. */ +public interface URIConfigLoader extends ConfigExtension { + + /** redact any sensitive information in the URI when displayed by logging */ + default String redact(URI uri) { + + return uri.toString(); + } + + /** Whether the URI is supported by this loader */ + boolean supports(URI uri); + + /** + * @param uri uri from which to load data + * @param parsers map of {@link ConfigParser} available to assist in parsing data, keyed by {@link + * ConfigParser#supportedExtensions()} + * @return key/value map of loaded properties + */ + Map load(URI uri, URILoadContext ctx); +} diff --git a/avaje-config/src/main/java/io/avaje/config/URILoadContext.java b/avaje-config/src/main/java/io/avaje/config/URILoadContext.java new file mode 100644 index 00000000..1af974b4 --- /dev/null +++ b/avaje-config/src/main/java/io/avaje/config/URILoadContext.java @@ -0,0 +1,74 @@ +package io.avaje.config; + +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +public interface URILoadContext { + + ConfigParser configParser(String extension); + + ConfigurationLog logger(); + + Optional getProperty(String key); + + default Map> splitQueryParams(URI uri) { + final Map> queryPairs = new LinkedHashMap<>(); + final String[] pairs = uri.getQuery().split("&"); + for (String pair : pairs) { + final int idx = pair.indexOf("="); + final String key = + idx > 0 ? URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8) : pair; + + final String value = + idx > 0 && pair.length() > idx + 1 + ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8) + : null; + queryPairs.computeIfAbsent(key, k -> new ArrayList<>()).add(value); + } + return queryPairs; + } +} + +class DURILoadContext implements URILoadContext { + ConfigurationLog logger; + Parsers parsers; + + Function> getProperty; + + DURILoadContext( + ConfigurationLog logger, + Parsers parsers, + Function> getPropertyFunction) { + this.parsers = parsers; + this.getProperty = getPropertyFunction; + } + + @Override + public ConfigurationLog logger() { + return logger; + } + + @Override + public ConfigParser configParser(String extension) { + var parser = parsers.get(extension); + if (parser == null) { + + throw new IllegalArgumentException("No ConfigParser found for file extension" + extension); + } + + return parser; + } + + @Override + public Optional getProperty(String key) { + + return getProperty.apply(key); + } +} diff --git a/avaje-config/src/test/java/io/avaje/config/FileWatchTest.java b/avaje-config/src/test/java/io/avaje/config/FileWatchTest.java index 1eeb5d0d..55100450 100644 --- a/avaje-config/src/test/java/io/avaje/config/FileWatchTest.java +++ b/avaje-config/src/test/java/io/avaje/config/FileWatchTest.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; @@ -137,7 +138,7 @@ void test_check_whenFileWritten() throws Exception { } private static FileWatch fileWatch(CoreConfiguration config, List files) { - return new FileWatch(config, files, new Parsers(Collections.emptyList())); + return new FileWatch(config, files, ConfigServiceLoader.get().parsers()); } private void writeContent(String content) throws IOException {