getConfigSources(final ConfigSourceContext context) {
+ final ConfigValue value = context.getValue(SMALLRYE_LOCATIONS);
+ if (value.getValue() == null) {
+ return Collections.emptyList();
+ }
+
+ return loadConfigSources(newArrayConverter(STRING_CONVERTER, String[].class).convert(value.getValue()));
+ }
+}
diff --git a/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceLoader.java b/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceLoader.java
new file mode 100644
index 000000000..16e998f6e
--- /dev/null
+++ b/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceLoader.java
@@ -0,0 +1,312 @@
+package io.smallrye.config;
+
+import static io.smallrye.common.classloader.ClassPathUtils.consumeAsPath;
+import static io.smallrye.common.classloader.ClassPathUtils.consumeAsPaths;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+import org.eclipse.microprofile.config.spi.Converter;
+
+import io.smallrye.common.annotation.Experimental;
+
+/**
+ * This {@link AbstractLocationConfigSourceLoader} loads {@link ConfigSource}s from a list of specific
+ * locations.
+ *
+ *
+ * The locations comprise a list of valid {@link URI}s which are loaded in order. The following URI schemes are
+ * supported:
+ *
+ *
+ * - file or directory
+ * - classpath resource
+ * - jar resource
+ * - http resource
+ *
+ *
+ *
+ * If a profile is active, the profile resource is only loaded if the unprofiled resource is available in the same
+ * location. This is to keep a consistent loading order and matching with the unprofiled resource. Profiles are not
+ * taken into account if the location is a directory.
+ */
+@Experimental("Loads sources by location")
+public abstract class AbstractLocationConfigSourceLoader {
+ private static final Converter URI_CONVERTER = new URIConverter();
+
+ protected abstract String[] getFileExtensions();
+
+ protected abstract ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException;
+
+ protected ConfigSource loadConfigSource(final URL url) throws IOException {
+ return this.loadConfigSource(url, ConfigSource.DEFAULT_ORDINAL);
+ }
+
+ protected List loadConfigSources(final String location) {
+ return loadConfigSources(new String[] { location });
+ }
+
+ protected List loadConfigSources(final String location, final ClassLoader classLoader) {
+ return loadConfigSources(new String[] { location }, classLoader);
+ }
+
+ protected List loadConfigSources(final String[] locations) {
+ return loadConfigSources(locations, SecuritySupport.getContextClassLoader());
+ }
+
+ protected List loadConfigSources(final String[] locations, final ClassLoader classLoader) {
+ if (locations == null || locations.length == 0) {
+ return Collections.emptyList();
+ }
+
+ final List configSources = new ArrayList<>();
+ for (String location : locations) {
+ final URI uri = URI_CONVERTER.convert(location);
+ if (uri.getScheme() == null || uri.getScheme().equals("file")) {
+ configSources.addAll(tryFileSystem(uri));
+ configSources.addAll(tryClassPath(uri, classLoader));
+ } else if (uri.getScheme().equals("jar")) {
+ configSources.addAll(tryJar(uri));
+ } else if (uri.getScheme().startsWith("http")) {
+ configSources.addAll(tryHttpResource(uri));
+ } else {
+ throw ConfigMessages.msg.schemeNotSupported(uri.getScheme());
+ }
+ }
+ return configSources;
+ }
+
+ protected List tryFileSystem(final URI uri) {
+ final List configSources = new ArrayList<>();
+ final Path urlPath = Paths.get(uri.getPath());
+ if (Files.isRegularFile(urlPath)) {
+ consumeAsPath(toURL(urlPath.toUri()), new ConfigSourcePathConsumer(configSources));
+ } else if (Files.isDirectory(urlPath)) {
+ try (DirectoryStream paths = Files.newDirectoryStream(urlPath, this::validExtension)) {
+ for (Path path : paths) {
+ addConfigSource(path.toUri(), configSources);
+ }
+ } catch (IOException e) {
+ throw ConfigMessages.msg.failedToLoadResource(e);
+ }
+ }
+ return configSources;
+ }
+
+ protected List tryClassPath(final URI uri, final ClassLoader classLoader) {
+ final List configSources = new ArrayList<>();
+ try {
+ consumeAsPaths(uri.getPath(), new ConfigSourcePathConsumer(configSources));
+ } catch (IOException e) {
+ throw ConfigMessages.msg.failedToLoadResource(e);
+ } catch (IllegalArgumentException e) {
+ configSources.addAll(fallbackToUnknownProtocol(uri, classLoader));
+ }
+ return configSources;
+ }
+
+ protected List tryJar(final URI uri) {
+ final List configSources = new ArrayList<>();
+ try {
+ consumeAsPath(toURL(uri), new ConfigSourcePathConsumer(configSources));
+ } catch (Exception e) {
+ throw ConfigMessages.msg.failedToLoadResource(e);
+ }
+ return configSources;
+ }
+
+ protected List fallbackToUnknownProtocol(final URI uri, final ClassLoader classLoader) {
+ final List configSources = new ArrayList<>();
+ try {
+ Enumeration resources = classLoader.getResources(uri.toString());
+ while (resources.hasMoreElements()) {
+ final URL resourceUrl = resources.nextElement();
+ if (validExtension(resourceUrl.getFile())) {
+ final ConfigSource mainSource = addConfigSource(resourceUrl, configSources);
+ configSources.add(new ConfigurableConfigSource((ProfileConfigSourceFactory) profiles -> {
+ final List profileSources = new ArrayList<>();
+ for (int i = profiles.size() - 1; i >= 0; i--) {
+ final int ordinal = mainSource.getOrdinal() + profiles.size() - i + 1;
+ final URI profileUri = addProfileName(uri, profiles.get(i));
+ try {
+ final Enumeration profileResources = classLoader.getResources(profileUri.toString());
+ while (profileResources.hasMoreElements()) {
+ final URL profileUrl = profileResources.nextElement();
+ addProfileConfigSource(profileUrl, ordinal, profileSources);
+ }
+ } catch (IOException e) {
+ // It is ok to not find the resource here, because it is an optional profile resource.
+ }
+ }
+ return profileSources;
+ }));
+ }
+ }
+ } catch (IOException e) {
+ throw ConfigMessages.msg.failedToLoadResource(e);
+ }
+ return configSources;
+ }
+
+ protected List tryHttpResource(final URI uri) {
+ final List configSources = new ArrayList<>();
+ if (validExtension(uri.getPath())) {
+ final ConfigSource mainSource = addConfigSource(uri, configSources);
+ configSources.addAll(tryProfiles(uri, mainSource));
+ }
+ return configSources;
+ }
+
+ protected List tryProfiles(final URI uri, final ConfigSource mainSource) {
+ final List configSources = new ArrayList<>();
+ configSources.add(new ConfigurableConfigSource((ProfileConfigSourceFactory) profiles -> {
+ final List profileSources = new ArrayList<>();
+ for (int i = profiles.size() - 1; i >= 0; i--) {
+ final int ordinal = mainSource.getOrdinal() + profiles.size() - i + 1;
+ final URI profileUri = addProfileName(uri, profiles.get(i));
+ addProfileConfigSource(toURL(profileUri), ordinal, profileSources);
+ }
+ return profileSources;
+ }));
+ return configSources;
+ }
+
+ private static URL toURL(final URI uri) {
+ try {
+ return uri.toURL();
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private ConfigSource addConfigSource(final URI uri, final List configSources) {
+ return addConfigSource(toURL(uri), configSources);
+ }
+
+ private ConfigSource addConfigSource(final URL url, final List configSources) {
+ try {
+ final ConfigSource configSource = loadConfigSource(url);
+ if (!configSource.getPropertyNames().isEmpty()) {
+ configSources.add(configSource);
+ }
+ return configSource;
+ } catch (IOException e) {
+ throw ConfigMessages.msg.failedToLoadResource(e);
+ }
+ }
+
+ private void addProfileConfigSource(final URL profileToFileName, final int ordinal,
+ final List profileSources) {
+ try {
+ final ConfigSource configSource = loadConfigSource(profileToFileName, ordinal);
+ if (!configSource.getPropertyNames().isEmpty()) {
+ profileSources.add(configSource);
+ }
+ } catch (FileNotFoundException | NoSuchFileException e) {
+ // It is ok to not find the resource here, because it is an optional profile resource.
+ } catch (IOException e) {
+ throw ConfigMessages.msg.failedToLoadResource(e);
+ }
+ }
+
+ private boolean validExtension(final Path fileName) {
+ return validExtension(fileName.getFileName().toString());
+ }
+
+ private boolean validExtension(final String resourceName) {
+ for (String s : getFileExtensions()) {
+ if (resourceName.endsWith(s)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static URI addProfileName(final URI uri, final String profile) {
+ if ("jar".equals(uri.getScheme())) {
+ return URI.create("jar:" + addProfileName(URI.create(uri.getSchemeSpecificPart()), profile));
+ }
+
+ final String fileName = uri.getPath();
+ assert fileName != null;
+
+ final int dot = fileName.lastIndexOf(".");
+ final String fileNameProfile;
+ if (dot != -1) {
+ fileNameProfile = fileName.substring(0, dot) + "-" + profile + fileName.substring(dot);
+ } else {
+ fileNameProfile = fileName + "-" + profile;
+ }
+
+ try {
+ return new URI(uri.getScheme(),
+ uri.getAuthority(),
+ uri.getHost(),
+ uri.getPort(),
+ fileNameProfile,
+ uri.getQuery(),
+ uri.getFragment());
+ } catch (URISyntaxException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static class URIConverter implements Converter {
+ private static final long serialVersionUID = -4852082279190307320L;
+
+ @Override
+ public URI convert(final String value) {
+ try {
+ return new URI(value);
+ } catch (URISyntaxException e) {
+ throw ConfigMessages.msg.uriSyntaxInvalid(e, value);
+ }
+ }
+ }
+
+ private class ConfigSourcePathConsumer implements Consumer {
+ private final List configSources;
+
+ public ConfigSourcePathConsumer(final List configSources) {
+ this.configSources = configSources;
+ }
+
+ @Override
+ public void accept(final Path path) {
+ final AbstractLocationConfigSourceLoader loader = AbstractLocationConfigSourceLoader.this;
+ if (loader.validExtension(path.getFileName().toString())) {
+ final ConfigSource mainSource = loader.addConfigSource(path.toUri(), configSources);
+ configSources.addAll(loader.tryProfiles(path.toUri(), mainSource));
+ }
+ }
+ }
+
+ interface ProfileConfigSourceFactory extends ConfigSourceFactory {
+ @Override
+ default Iterable getConfigSources(final ConfigSourceContext context) {
+ final List profiles = context.getProfiles();
+ if (profiles.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return getProfileConfigSources(profiles);
+ }
+
+ Iterable getProfileConfigSources(final List profiles);
+ }
+}
diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMessages.java b/implementation/src/main/java/io/smallrye/config/ConfigMessages.java
index ac544c7ec..d8dc0a13e 100644
--- a/implementation/src/main/java/io/smallrye/config/ConfigMessages.java
+++ b/implementation/src/main/java/io/smallrye/config/ConfigMessages.java
@@ -112,4 +112,13 @@ interface ConfigMessages {
@Message(id = 32, value = "Expected a float value, got \"%s\"")
NumberFormatException floatExpected(String value);
+
+ @Message(id = 33, value = "Scheme %s not supported")
+ IllegalArgumentException schemeNotSupported(String scheme);
+
+ @Message(id = 34, value = "URI Syntax invalid %s")
+ IllegalArgumentException uriSyntaxInvalid(@Cause Throwable cause, String uri);
+
+ @Message(id = 35, value = "Failed to load resource")
+ IllegalStateException failedToLoadResource(@Cause Throwable cause);
}
diff --git a/implementation/src/main/java/io/smallrye/config/ConfigurableConfigSource.java b/implementation/src/main/java/io/smallrye/config/ConfigurableConfigSource.java
index 7ad3665d8..fccd10f07 100644
--- a/implementation/src/main/java/io/smallrye/config/ConfigurableConfigSource.java
+++ b/implementation/src/main/java/io/smallrye/config/ConfigurableConfigSource.java
@@ -1,12 +1,11 @@
package io.smallrye.config;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
import org.eclipse.microprofile.config.spi.ConfigSource;
@@ -42,7 +41,18 @@ public int getOrdinal() {
return factory.getPriority().orElse(DEFAULT_ORDINAL);
}
- List getConfigSources(ConfigSourceContext context) {
- return StreamSupport.stream(factory.getConfigSources(context).spliterator(), false).collect(Collectors.toList());
+ List getConfigSources(final ConfigSourceContext context) {
+ return unwrap(context, new ArrayList<>());
+ }
+
+ private List unwrap(final ConfigSourceContext context, final List configSources) {
+ for (final ConfigSource configSource : factory.getConfigSources(context)) {
+ if (configSource instanceof ConfigurableConfigSource) {
+ configSources.addAll(((ConfigurableConfigSource) configSource).getConfigSources(context));
+ } else {
+ configSources.add(configSource);
+ }
+ }
+ return configSources;
}
}
diff --git a/implementation/src/main/java/io/smallrye/config/PropertiesConfigSource.java b/implementation/src/main/java/io/smallrye/config/PropertiesConfigSource.java
index 0baa9b380..4b3ca09a6 100644
--- a/implementation/src/main/java/io/smallrye/config/PropertiesConfigSource.java
+++ b/implementation/src/main/java/io/smallrye/config/PropertiesConfigSource.java
@@ -42,6 +42,10 @@ public PropertiesConfigSource(URL url) throws IOException {
super(NAME_PREFIX + url.toString() + "]", ConfigSourceUtil.urlToMap(url));
}
+ public PropertiesConfigSource(URL url, int ordinal) throws IOException {
+ super(NAME_PREFIX + url.toString() + "]", ConfigSourceUtil.urlToMap(url), ordinal);
+ }
+
public PropertiesConfigSource(Properties properties, String source) {
super(NAME_PREFIX + source + "]", ConfigSourceUtil.propertiesToMap(properties));
}
diff --git a/implementation/src/main/java/io/smallrye/config/PropertiesLocationConfigSourceFactory.java b/implementation/src/main/java/io/smallrye/config/PropertiesLocationConfigSourceFactory.java
new file mode 100644
index 000000000..6361dd690
--- /dev/null
+++ b/implementation/src/main/java/io/smallrye/config/PropertiesLocationConfigSourceFactory.java
@@ -0,0 +1,18 @@
+package io.smallrye.config;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+
+public class PropertiesLocationConfigSourceFactory extends AbstractLocationConfigSourceFactory {
+ @Override
+ public String[] getFileExtensions() {
+ return new String[] { "properties" };
+ }
+
+ @Override
+ protected ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException {
+ return new PropertiesConfigSource(url, ordinal);
+ }
+}
diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java
index fc2aa121b..7c8f6838d 100644
--- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java
+++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java
@@ -325,7 +325,7 @@ private static class ConfigSources implements Serializable {
sortInterceptors.addAll(mapSources(sources));
// Add all interceptors
sortInterceptors.addAll(mapInterceptors(interceptors));
- sortInterceptors.sort(Comparator.comparingInt(ConfigSourceInterceptorWithPriority::getPriority));
+ sortInterceptors.sort(null);
// Create the initial chain and init each element with the current context
SmallRyeConfigSourceInterceptorContext current = new SmallRyeConfigSourceInterceptorContext(EMPTY, null);
@@ -335,7 +335,7 @@ private static class ConfigSources implements Serializable {
// Init all late sources. Late sources are converted to the interceptor API and sorted again
sortInterceptors.addAll(mapLateSources(current, sources, getProfiles(sortInterceptors)));
- sortInterceptors.sort(Comparator.comparingInt(ConfigSourceInterceptorWithPriority::getPriority));
+ sortInterceptors.sort(null);
// Rebuild the chain with the late sources and collect new instances of the interceptors
// The new instance will ensure that we get rid of references to factories and other stuff and keep only
@@ -366,7 +366,7 @@ private static class ConfigSources implements Serializable {
final List newInterceptors = Arrays
.asList(sources.getInterceptors().toArray(new ConfigSourceInterceptorWithPriority[oldSize + 1]));
newInterceptors.set(oldSize, new ConfigSourceInterceptorWithPriority(configSource));
- newInterceptors.sort(Comparator.comparingInt(ConfigSourceInterceptorWithPriority::getPriority));
+ newInterceptors.sort(null);
SmallRyeConfigSourceInterceptorContext current = new SmallRyeConfigSourceInterceptorContext(EMPTY, null);
for (ConfigSourceInterceptorWithPriority configSourceInterceptor : newInterceptors) {
@@ -379,6 +379,7 @@ private static class ConfigSources implements Serializable {
}
private static List mapSources(final List sources) {
+ ConfigSourceInterceptorWithPriority.raiseLoadPriority();
final List sourcesWithPriority = new ArrayList<>();
for (ConfigSource source : sources) {
if (!(source instanceof ConfigurableConfigSource)) {
@@ -419,6 +420,7 @@ private static List mapLateSources(
}
lateSources.sort(Comparator.comparingInt(ConfigurableConfigSource::getOrdinal));
+ ConfigSourceInterceptorWithPriority.raiseLoadPriority();
final List sourcesWithPriority = new ArrayList<>();
for (ConfigurableConfigSource configurableSource : lateSources) {
final List configSources = configurableSource.getConfigSources(new ConfigSourceContext() {
@@ -485,6 +487,7 @@ static class ConfigSourceInterceptorWithPriority implements Comparable init;
private final int priority;
+ private final int loadPriority = loadPrioritySequence--;
private final String name;
private ConfigSourceInterceptor interceptor;
@@ -529,14 +532,18 @@ ConfigSourceInterceptorWithPriority initialized(final ConfigSourceInterceptorCon
return new ConfigSourceInterceptorWithPriority(this.getInterceptor(context), this.priority, this.name);
}
- int getPriority() {
- return this.priority;
+ private static int loadPrioritySequence = 0;
+ private static int loadPrioritySequenceNumber = 1;
+
+ static void raiseLoadPriority() {
+ loadPrioritySequenceNumber++;
+ loadPrioritySequence = 1000 * loadPrioritySequenceNumber;
}
@Override
public int compareTo(final ConfigSourceInterceptorWithPriority other) {
int res = Integer.compare(this.priority, other.priority);
- return res != 0 ? res : this.name.compareTo(other.name);
+ return res != 0 ? res : Integer.compare(this.loadPriority, other.loadPriority);
}
}
diff --git a/implementation/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory b/implementation/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory
new file mode 100644
index 000000000..65f7fb789
--- /dev/null
+++ b/implementation/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory
@@ -0,0 +1 @@
+io.smallrye.config.PropertiesLocationConfigSourceFactory
diff --git a/implementation/src/test/java/io/smallrye/config/PropertiesLocationConfigSourceFactoryTest.java b/implementation/src/test/java/io/smallrye/config/PropertiesLocationConfigSourceFactoryTest.java
new file mode 100644
index 000000000..9c587c0d2
--- /dev/null
+++ b/implementation/src/test/java/io/smallrye/config/PropertiesLocationConfigSourceFactoryTest.java
@@ -0,0 +1,526 @@
+package io.smallrye.config;
+
+import static io.smallrye.config.AbstractLocationConfigSourceFactory.SMALLRYE_LOCATIONS;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.StreamSupport.stream;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.StringWriter;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Properties;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.exporter.ZipExporter;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import com.sun.net.httpserver.HttpServer;
+
+class PropertiesLocationConfigSourceFactoryTest {
+ @Test
+ void systemFile() {
+ SmallRyeConfig config = new SmallRyeConfigBuilder()
+ .addDiscoveredSources()
+ .addDefaultInterceptors()
+ .withProfile("dev")
+ .withDefaultValue(SMALLRYE_LOCATIONS, String.join(",", "./src/test/resources/additional.properties"))
+ .build();
+
+ assertEquals("1234", config.getRawValue("my.prop"));
+ assertNull(config.getRawValue("more.prop"));
+ assertEquals(1, countSources(config));
+ }
+
+ @Test
+ void systemFolder() {
+ SmallRyeConfig config = buildConfig("./src/test/resources");
+
+ assertEquals("1234", config.getRawValue("my.prop"));
+ assertEquals("5678", config.getRawValue("more.prop"));
+ assertEquals(3, countSources(config));
+ }
+
+ @Test
+ void http() {
+ SmallRyeConfig config = buildConfig(
+ "https://raw.githubusercontent.com/smallrye/smallrye-config/master/implementation/src/test/resources/config-values.properties");
+
+ assertEquals("abc", config.getRawValue("my.prop"));
+ assertEquals(1, countSources(config));
+ }
+
+ @Test
+ void classpath() {
+ SmallRyeConfig config = buildConfig("additional.properties");
+
+ assertEquals("1234", config.getRawValue("my.prop"));
+ assertEquals(1, countSources(config));
+ }
+
+ @Test
+ void all() {
+ SmallRyeConfig config = buildConfig("./src/test/resources",
+ "https://raw.githubusercontent.com/smallrye/smallrye-config/master/implementation/src/test/resources/config-values.properties");
+
+ assertEquals("1234", config.getRawValue("my.prop"));
+ assertEquals("5678", config.getRawValue("more.prop"));
+ assertEquals(4, countSources(config));
+ }
+
+ @Test
+ void notFound() {
+ SmallRyeConfig config = buildConfig("not.found");
+
+ assertNull(config.getRawValue("my.prop"));
+ assertEquals(0, countSources(config));
+ }
+
+ @Test
+ void noPropertiesFile() {
+ SmallRyeConfig config = buildConfig("./src/test/resources/random.yml");
+
+ assertEquals(0, countSources(config));
+ }
+
+ @Test
+ void multipleResourcesInClassPath(@TempDir Path tempDir) throws Exception {
+ JavaArchive jarOne = ShrinkWrap
+ .create(JavaArchive.class, "resources-one.jar")
+ .addAsResource(new StringAsset("my.prop.one=1234\n"), "resources.properties");
+
+ Path filePathOne = tempDir.resolve("resources-one.jar");
+ jarOne.as(ZipExporter.class).exportTo(filePathOne.toFile());
+
+ JavaArchive jarTwo = ShrinkWrap
+ .create(JavaArchive.class, "resources-two.jar")
+ .addAsResource(new StringAsset("my.prop.two=5678\n"), "resources.properties");
+
+ Path filePathTwo = tempDir.resolve("resources-two.jar");
+ jarTwo.as(ZipExporter.class).exportTo(filePathTwo.toFile());
+
+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {
+ new URL("jar:file:" + filePathOne.toString() + "!/"),
+ new URL("jar:file:" + filePathTwo.toString() + "!/")
+ }, contextClassLoader);
+ Thread.currentThread().setContextClassLoader(urlClassLoader);
+
+ SmallRyeConfig config = buildConfig("resources.properties");
+
+ assertEquals("1234", config.getRawValue("my.prop.one"));
+ assertEquals("5678", config.getRawValue("my.prop.two"));
+ assertEquals(2, countSources(config));
+
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ }
+
+ @Test
+ void jar(@TempDir Path tempDir) throws Exception {
+ JavaArchive jarOne = ShrinkWrap
+ .create(JavaArchive.class, "resources-one.jar")
+ .addAsResource(new StringAsset("my.prop.one=1234\n"), "resources.properties");
+
+ Path filePathOne = tempDir.resolve("resources-one.jar");
+ jarOne.as(ZipExporter.class).exportTo(filePathOne.toFile());
+
+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {
+ new URL("jar:file:" + filePathOne.toString() + "!/")
+ }, contextClassLoader);
+ Thread.currentThread().setContextClassLoader(urlClassLoader);
+
+ SmallRyeConfig config = buildConfig("jar:file:" + filePathOne.toString() + "!/resources.properties");
+
+ assertEquals("1234", config.getRawValue("my.prop.one"));
+ assertEquals(1, countSources(config));
+
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ }
+
+ @Test
+ void invalidHttp() {
+ assertThrows(IllegalStateException.class,
+ () -> buildConfig("https://raw.githubusercontent.com/smallrye/smallrye-config/notfound.properties"));
+ buildConfig("https://github.com/smallrye/smallrye-config/blob/3cc4809734d7fbd03852a20b5870ca743a2427bc/pom.xml");
+ }
+
+ @Test
+ void priorityLoadOrder(@TempDir Path tempDir) throws Exception {
+ JavaArchive jarOne = ShrinkWrap
+ .create(JavaArchive.class, "resources-one.jar")
+ .addAsResource(new StringAsset("my.prop.one=1234\n" +
+ "my.prop.common=1\n" +
+ "my.prop.jar.common=1\n"), "META-INF/microprofile-config.properties");
+
+ Path filePathOne = tempDir.resolve("resources-one.jar");
+ jarOne.as(ZipExporter.class).exportTo(filePathOne.toFile());
+
+ JavaArchive jarTwo = ShrinkWrap
+ .create(JavaArchive.class, "resources-two.jar")
+ .addAsResource(new StringAsset("my.prop.two=5678\n" +
+ "my.prop.common=2\n" +
+ "my.prop.jar.common=2\n"), "META-INF/microprofile-config.properties");
+
+ Path filePathTwo = tempDir.resolve("resources-two.jar");
+ jarTwo.as(ZipExporter.class).exportTo(filePathTwo.toFile());
+
+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {
+ new URL("jar:file:" + filePathOne.toString() + "!/"),
+ new URL("jar:file:" + filePathTwo.toString() + "!/")
+ }, contextClassLoader);
+ Thread.currentThread().setContextClassLoader(urlClassLoader);
+
+ Properties mainProperties = new Properties();
+ mainProperties.setProperty("config_ordinal", "100");
+ mainProperties.setProperty("my.prop.main", "main");
+ mainProperties.setProperty("my.prop.common", "main");
+ File mainFile = tempDir.resolve("microprofile-config.properties").toFile();
+ mainProperties.store(new FileOutputStream(mainFile), null);
+
+ Properties fallbackProperties = new Properties();
+ fallbackProperties.setProperty("config_ordinal", "100");
+ fallbackProperties.setProperty("my.prop.fallback", "fallback");
+ fallbackProperties.setProperty("my.prop.common", "fallback");
+ File fallbackFile = tempDir.resolve("fallback.properties").toFile();
+ fallbackProperties.store(new FileOutputStream(fallbackFile), null);
+
+ SmallRyeConfig config = new SmallRyeConfigBuilder()
+ .addDefaultSources()
+ .addDiscoveredSources()
+ .addDefaultInterceptors()
+ .withDefaultValue(SMALLRYE_LOCATIONS, mainFile.toString() + "," + fallbackFile)
+ .build();
+
+ // Check if all sources are up
+ assertEquals("1234", config.getRawValue("my.prop.one"));
+ assertEquals("5678", config.getRawValue("my.prop.two"));
+ assertEquals("main", config.getRawValue("my.prop.main"));
+ assertEquals("fallback", config.getRawValue("my.prop.fallback"));
+ // This should be loaded by the first defined source in the locations configuration
+ assertEquals("main", config.getRawValue("my.prop.common"));
+ // This should be loaded by the first discovered source in the classpath
+ assertEquals("1", config.getRawValue("my.prop.jar.common"));
+ assertEquals(4, countSources(config));
+ assertTrue(stream(config.getConfigSources().spliterator(), false)
+ .filter(PropertiesConfigSource.class::isInstance)
+ .allMatch(configSource -> configSource.getOrdinal() == 100));
+
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ }
+
+ @Test
+ void profiles(@TempDir Path tempDir) throws Exception {
+ Properties mainProperties = new Properties();
+ mainProperties.setProperty("config_ordinal", "150");
+ mainProperties.setProperty("my.prop.main", "main");
+ mainProperties.setProperty("my.prop.common", "main");
+ mainProperties.setProperty("my.prop.profile", "main");
+ mainProperties.store(new FileOutputStream(tempDir.resolve("config.properties").toFile()), null);
+
+ Properties commonProperties = new Properties();
+ commonProperties.setProperty("my.prop.common", "common");
+ commonProperties.setProperty("my.prop.profile", "common");
+ commonProperties.store(new FileOutputStream(tempDir.resolve("config-common.properties").toFile()), null);
+
+ Properties devProperties = new Properties();
+ devProperties.setProperty("my.prop.dev", "dev");
+ devProperties.setProperty("my.prop.profile", "dev");
+ devProperties.store(new FileOutputStream(tempDir.resolve("config-dev.properties").toFile()), null);
+
+ SmallRyeConfig config = new SmallRyeConfigBuilder()
+ .addDefaultSources()
+ .addDiscoveredSources()
+ .addDefaultInterceptors()
+ .withProfile("common,dev")
+ .withDefaultValue(SMALLRYE_LOCATIONS, tempDir.resolve("config.properties").toFile().toString())
+ .build();
+
+ assertEquals("main", config.getRawValue("my.prop.main"));
+ assertEquals("common", config.getRawValue("my.prop.common"));
+ assertEquals("dev", config.getRawValue("my.prop.profile"));
+ }
+
+ @Test
+ void onlyProfileFile(@TempDir Path tempDir) throws Exception {
+ Properties devProperties = new Properties();
+ devProperties.setProperty("my.prop.dev", "dev");
+ devProperties.setProperty("my.prop.profile", "dev");
+ devProperties.store(new FileOutputStream(tempDir.resolve("config-dev.properties").toFile()), null);
+
+ SmallRyeConfig config = new SmallRyeConfigBuilder()
+ .addDiscoveredSources()
+ .addDefaultInterceptors()
+ .withProfile("common,dev")
+ .withDefaultValue(SMALLRYE_LOCATIONS, tempDir.resolve("config.properties").toFile().toString())
+ .build();
+
+ assertNull(config.getRawValue("my.prop.profile"));
+ }
+
+ @Test
+ void profilesClasspath(@TempDir Path tempDir) throws Exception {
+ JavaArchive jarOne = ShrinkWrap
+ .create(JavaArchive.class, "resources-one.jar")
+ .addAsResource(new StringAsset(
+ "config_ordinal=150\n" +
+ "my.prop.main=main\n" +
+ "my.prop.common=main\n" +
+ "my.prop.profile=main\n"),
+ "META-INF/config.properties")
+ .addAsResource(new StringAsset(
+ "my.prop.common=common\n" +
+ "my.prop.profile=common\n"),
+ "META-INF/config-common.properties")
+ .addAsResource(new StringAsset(
+ "my.prop.dev=dev\n" +
+ "my.prop.profile=dev\n"),
+ "META-INF/config-dev.properties");
+
+ Path filePathOne = tempDir.resolve("resources-one.jar");
+ jarOne.as(ZipExporter.class).exportTo(filePathOne.toFile());
+
+ JavaArchive jarTwo = ShrinkWrap
+ .create(JavaArchive.class, "resources-two.jar")
+ .addAsResource(new StringAsset(
+ "config_ordinal=150\n" +
+ "my.prop.main=main\n" +
+ "my.prop.common=main\n" +
+ "my.prop.profile=main\n"),
+ "META-INF/config.properties");
+
+ Path filePathTwo = tempDir.resolve("resources-two.jar");
+ jarTwo.as(ZipExporter.class).exportTo(filePathTwo.toFile());
+
+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {
+ new URL("jar:file:" + filePathOne.toString() + "!/"),
+ new URL("jar:file:" + filePathTwo.toString() + "!/"),
+ }, contextClassLoader);
+ Thread.currentThread().setContextClassLoader(urlClassLoader);
+
+ SmallRyeConfig config = new SmallRyeConfigBuilder()
+ .addDefaultSources()
+ .addDiscoveredSources()
+ .addDefaultInterceptors()
+ .withProfile("common,dev")
+ .withDefaultValue(SMALLRYE_LOCATIONS, "META-INF/config.properties")
+ .build();
+
+ assertEquals("main", config.getRawValue("my.prop.main"));
+ assertEquals("common", config.getRawValue("my.prop.common"));
+ assertEquals("dev", config.getRawValue("my.prop.profile"));
+
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ }
+
+ @Test
+ void profilesJar(@TempDir Path tempDir) throws Exception {
+ JavaArchive jarOne = ShrinkWrap
+ .create(JavaArchive.class, "resources-one.jar")
+ .addAsResource(new StringAsset(
+ "config_ordinal=150\n" +
+ "my.prop.main=main\n" +
+ "my.prop.common=main\n" +
+ "my.prop.profile=main\n"),
+ "META-INF/config.properties")
+ .addAsResource(new StringAsset(
+ "my.prop.common=common\n" +
+ "my.prop.profile=common\n"),
+ "META-INF/config-common.properties")
+ .addAsResource(new StringAsset(
+ "my.prop.dev=dev\n" +
+ "my.prop.profile=dev\n"),
+ "META-INF/config-dev.properties");
+
+ Path filePathOne = tempDir.resolve("resources-one.jar");
+ jarOne.as(ZipExporter.class).exportTo(filePathOne.toFile());
+
+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {
+ new URL("jar:file:" + filePathOne.toString() + "!/")
+ }, contextClassLoader);
+ Thread.currentThread().setContextClassLoader(urlClassLoader);
+
+ SmallRyeConfig config = new SmallRyeConfigBuilder()
+ .addDefaultSources()
+ .addDiscoveredSources()
+ .addDefaultInterceptors()
+ .withProfile("common,dev")
+ .withDefaultValue(SMALLRYE_LOCATIONS, "jar:file:" + filePathOne.toString() + "!/META-INF/config.properties")
+ .build();
+
+ assertEquals("main", config.getRawValue("my.prop.main"));
+ assertEquals("common", config.getRawValue("my.prop.common"));
+ assertEquals("dev", config.getRawValue("my.prop.profile"));
+
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ }
+
+ @Test
+ void profilesHttp() throws Exception {
+ HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
+ server.createContext("/config.properties", exchange -> {
+ Properties mainProperties = new Properties();
+ mainProperties.setProperty("config_ordinal", "150");
+ mainProperties.setProperty("my.prop.main", "main");
+ mainProperties.setProperty("my.prop.common", "main");
+ mainProperties.setProperty("my.prop.profile", "main");
+ StringWriter writer = new StringWriter();
+ mainProperties.store(writer, null);
+ byte[] bytes = writer.toString().getBytes();
+ exchange.sendResponseHeaders(200, writer.toString().length());
+ exchange.getResponseBody().write(bytes);
+ exchange.getResponseBody().flush();
+ exchange.getResponseBody().close();
+ });
+
+ server.createContext("/config-common.properties", exchange -> {
+ Properties commonProperties = new Properties();
+ commonProperties.setProperty("my.prop.common", "common");
+ commonProperties.setProperty("my.prop.profile", "common");
+ StringWriter writer = new StringWriter();
+ commonProperties.store(writer, null);
+ byte[] bytes = writer.toString().getBytes();
+ exchange.sendResponseHeaders(200, writer.toString().length());
+ exchange.getResponseBody().write(bytes);
+ exchange.getResponseBody().flush();
+ exchange.getResponseBody().close();
+ });
+
+ server.createContext("/config-dev.properties", exchange -> {
+ Properties devProperties = new Properties();
+ devProperties.setProperty("my.prop.dev", "dev");
+ devProperties.setProperty("my.prop.profile", "dev");
+ StringWriter writer = new StringWriter();
+ devProperties.store(writer, null);
+ byte[] bytes = writer.toString().getBytes();
+ exchange.sendResponseHeaders(200, writer.toString().length());
+ exchange.getResponseBody().write(bytes);
+ exchange.getResponseBody().flush();
+ exchange.getResponseBody().close();
+ });
+
+ server.start();
+
+ SmallRyeConfig config = new SmallRyeConfigBuilder()
+ .addDefaultSources()
+ .addDiscoveredSources()
+ .addDefaultInterceptors()
+ .withProfile("common,dev,unknown")
+ .withDefaultValue(SMALLRYE_LOCATIONS, "http://localhost:8080/config.properties")
+ .build();
+
+ assertEquals("main", config.getRawValue("my.prop.main"));
+ assertEquals("common", config.getRawValue("my.prop.common"));
+ assertEquals("dev", config.getRawValue("my.prop.profile"));
+
+ server.stop(0);
+ }
+
+ @Test
+ void mixedProfiles(@TempDir Path tempDir) throws Exception {
+ Properties mainProperties = new Properties();
+ mainProperties.setProperty("config_ordinal", "150");
+ mainProperties.setProperty("my.prop.main", "main-file");
+ mainProperties.setProperty("my.prop.main.file", "main-file");
+ mainProperties.setProperty("my.prop.common", "main-file");
+ mainProperties.setProperty("my.prop.profile", "main-file");
+ mainProperties.setProperty("order", "5");
+ mainProperties.store(new FileOutputStream(tempDir.resolve("config.properties").toFile()), null);
+
+ Properties commonProperties = new Properties();
+ commonProperties.setProperty("my.prop.common", "common-file");
+ commonProperties.setProperty("my.prop.profile", "common-file");
+ commonProperties.setProperty("order", "3");
+ commonProperties.store(new FileOutputStream(tempDir.resolve("config-common.properties").toFile()), null);
+
+ Properties devProperties = new Properties();
+ devProperties.setProperty("my.prop.dev", "dev-file");
+ devProperties.setProperty("my.prop.profile", "dev-file");
+ devProperties.setProperty("order", "1");
+ devProperties.store(new FileOutputStream(tempDir.resolve("config-dev.properties").toFile()), null);
+
+ JavaArchive jarOne = ShrinkWrap
+ .create(JavaArchive.class, "resources-one.jar")
+ .addAsResource(new StringAsset(
+ "config_ordinal=150\n" +
+ "my.prop.main=main-cp\n" +
+ "my.prop.main.cp=main-cp\n" +
+ "my.prop.common=main-cp\n" +
+ "my.prop.profile=main-cp\n" +
+ "order=6\n"),
+ "config.properties")
+ .addAsResource(new StringAsset(
+ "my.prop.common=common-cp\n" +
+ "my.prop.profile=common-cp\n" +
+ "order=4\n"),
+ "config-common.properties")
+ .addAsResource(new StringAsset(
+ "my.prop.dev=dev-cp\n" +
+ "my.prop.profile=dev-cp\n" +
+ "order=2\n"),
+ "config-dev.properties");
+
+ Path filePathOne = tempDir.resolve("resources-one.jar");
+ jarOne.as(ZipExporter.class).exportTo(filePathOne.toFile());
+
+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {
+ new URL("jar:file:" + filePathOne.toString() + "!/"),
+ }, contextClassLoader);
+ Thread.currentThread().setContextClassLoader(urlClassLoader);
+
+ SmallRyeConfig config = new SmallRyeConfigBuilder()
+ .addDefaultSources()
+ .addDiscoveredSources()
+ .addDefaultInterceptors()
+ .withProfile("common,dev")
+ .withDefaultValue(SMALLRYE_LOCATIONS,
+ tempDir.resolve("config.properties").toFile().toString() + "," + "config.properties")
+ .build();
+
+ assertEquals("main-file", config.getRawValue("my.prop.main.file"));
+ assertEquals("main-cp", config.getRawValue("my.prop.main.cp"));
+ assertEquals("main-file", config.getRawValue("my.prop.main"));
+ assertEquals("common-file", config.getRawValue("my.prop.common"));
+ assertEquals("dev-file", config.getRawValue("my.prop.profile"));
+
+ final List sources = stream(config.getConfigSources().spliterator(), false)
+ .filter(PropertiesConfigSource.class::isInstance).collect(toList());
+ assertEquals(6, sources.size());
+ assertEquals("1", sources.get(0).getValue("order"));
+ assertEquals("2", sources.get(1).getValue("order"));
+ assertEquals("3", sources.get(2).getValue("order"));
+ assertEquals("4", sources.get(3).getValue("order"));
+ assertEquals("5", sources.get(4).getValue("order"));
+ assertEquals("6", sources.get(5).getValue("order"));
+
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ }
+
+ private static SmallRyeConfig buildConfig(String... locations) {
+ return new SmallRyeConfigBuilder()
+ .addDiscoveredSources()
+ .addDefaultInterceptors()
+ .withDefaultValue(SMALLRYE_LOCATIONS, String.join(",", locations))
+ .build();
+ }
+
+ private static int countSources(SmallRyeConfig config) {
+ return (int) stream(config.getConfigSources().spliterator(), false).filter(PropertiesConfigSource.class::isInstance)
+ .count();
+ }
+}
diff --git a/implementation/src/test/resources/additional.properties b/implementation/src/test/resources/additional.properties
new file mode 100644
index 000000000..47092f36b
--- /dev/null
+++ b/implementation/src/test/resources/additional.properties
@@ -0,0 +1,2 @@
+config_ordinal=500
+my.prop=1234
diff --git a/implementation/src/test/resources/more.properties b/implementation/src/test/resources/more.properties
new file mode 100644
index 000000000..32f773ffd
--- /dev/null
+++ b/implementation/src/test/resources/more.properties
@@ -0,0 +1 @@
+more.prop=5678
diff --git a/implementation/src/test/resources/random.yml b/implementation/src/test/resources/random.yml
new file mode 100644
index 000000000..7ff71e7d5
--- /dev/null
+++ b/implementation/src/test/resources/random.yml
@@ -0,0 +1,15 @@
+admin:
+ users:
+ -
+ email: "joe@gmail.com"
+ username: "joe"
+ password: "123456"
+ roles:
+ - "Moderator"
+ - "Admin"
+ -
+ email: "jack@gmail.com"
+ username: "jack"
+ password: "654321"
+ roles:
+ - "Moderator"
diff --git a/sources/yaml/pom.xml b/sources/yaml/pom.xml
index ed375b8a0..769664c84 100644
--- a/sources/yaml/pom.xml
+++ b/sources/yaml/pom.xml
@@ -28,6 +28,10 @@
io.smallrye.config
smallrye-config-common
+
+ io.smallrye.config
+ smallrye-config
+
io.smallrye.common
smallrye-common-constraint
@@ -39,13 +43,13 @@
junit-jupiter
- io.smallrye.config
- smallrye-config
+ jakarta.annotation
+ jakarta.annotation-api
test
- jakarta.annotation
- jakarta.annotation-api
+ org.jboss.shrinkwrap
+ shrinkwrap-impl-base
test
diff --git a/sources/yaml/src/main/java/io/smallrye/config/source/yaml/YamlConfigSource.java b/sources/yaml/src/main/java/io/smallrye/config/source/yaml/YamlConfigSource.java
index c5ab4a6e2..3a3f128a4 100644
--- a/sources/yaml/src/main/java/io/smallrye/config/source/yaml/YamlConfigSource.java
+++ b/sources/yaml/src/main/java/io/smallrye/config/source/yaml/YamlConfigSource.java
@@ -2,6 +2,7 @@
import java.io.IOException;
import java.io.InputStream;
+import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -25,11 +26,11 @@
* @author David M. Lloyd
*/
public class YamlConfigSource extends MapBackedConfigSource {
+ private static final long serialVersionUID = -418186029484956531L;
+ private static final String NAME_PREFIX = "YamlConfigSource[source=";
static final int ORDINAL = ConfigSource.DEFAULT_ORDINAL + 10;
- private static final long serialVersionUID = -418186029484956531L;
-
private final Set propertyNames;
public YamlConfigSource(String name, Map source, int ordinal) {
@@ -41,6 +42,14 @@ public YamlConfigSource(String name, InputStream stream) throws IOException {
this(name, stream, ORDINAL);
}
+ public YamlConfigSource(URL url) throws IOException {
+ this(NAME_PREFIX + url.toString() + "]", url.openStream());
+ }
+
+ public YamlConfigSource(URL url, int ordinal) throws IOException {
+ this(NAME_PREFIX + url.toString() + "]", url.openStream(), ordinal);
+ }
+
public YamlConfigSource(String name, InputStream stream, int defaultOrdinal) throws IOException {
this(name, streamToMap(stream), defaultOrdinal);
}
diff --git a/sources/yaml/src/main/java/io/smallrye/config/source/yaml/YamlLocationConfigSourceFactory.java b/sources/yaml/src/main/java/io/smallrye/config/source/yaml/YamlLocationConfigSourceFactory.java
new file mode 100644
index 000000000..77c7dd7ae
--- /dev/null
+++ b/sources/yaml/src/main/java/io/smallrye/config/source/yaml/YamlLocationConfigSourceFactory.java
@@ -0,0 +1,23 @@
+package io.smallrye.config.source.yaml;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+
+import io.smallrye.config.AbstractLocationConfigSourceFactory;
+
+public class YamlLocationConfigSourceFactory extends AbstractLocationConfigSourceFactory {
+ @Override
+ public String[] getFileExtensions() {
+ return new String[] {
+ "yaml",
+ "yml"
+ };
+ }
+
+ @Override
+ protected ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException {
+ return new YamlConfigSource(url, ordinal);
+ }
+}
diff --git a/sources/yaml/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory b/sources/yaml/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory
new file mode 100644
index 000000000..20fae323e
--- /dev/null
+++ b/sources/yaml/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory
@@ -0,0 +1 @@
+io.smallrye.config.source.yaml.YamlLocationConfigSourceFactory
diff --git a/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlLocationConfigSourceFactoryTest.java b/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlLocationConfigSourceFactoryTest.java
new file mode 100644
index 000000000..f1459a28f
--- /dev/null
+++ b/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlLocationConfigSourceFactoryTest.java
@@ -0,0 +1,164 @@
+package io.smallrye.config.source.yaml;
+
+import static io.smallrye.config.AbstractLocationConfigSourceFactory.SMALLRYE_LOCATIONS;
+import static java.util.stream.StreamSupport.stream;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.exporter.ZipExporter;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import io.smallrye.config.SmallRyeConfig;
+import io.smallrye.config.SmallRyeConfigBuilder;
+
+class YamlLocationConfigSourceFactoryTest {
+ @Test
+ void systemFile() {
+ SmallRyeConfig config = buildConfig("./src/test/resources/additional.yml");
+
+ assertEquals("1234", config.getRawValue("my.prop"));
+ assertNull(config.getRawValue("more.prop"));
+ assertEquals(1, countSources(config));
+ }
+
+ @Test
+ void systemFolder() {
+ SmallRyeConfig config = buildConfig("./src/test/resources");
+
+ assertEquals("1234", config.getRawValue("my.prop"));
+ assertEquals("5678", config.getRawValue("more.prop"));
+ assertEquals(7, countSources(config));
+ }
+
+ @Test
+ void webResource() {
+ SmallRyeConfig config = buildConfig(
+ "https://raw.githubusercontent.com/smallrye/smallrye-config/master/sources/yaml/src/test/resources/example-profiles.yml");
+
+ assertEquals("default", config.getRawValue("foo.bar"));
+ assertEquals(1, countSources(config));
+ }
+
+ @Test
+ void classpath() {
+ SmallRyeConfig config = buildConfig("additional.yml");
+
+ assertEquals("1234", config.getRawValue("my.prop"));
+ assertEquals(1, countSources(config));
+ }
+
+ @Test
+ void all() {
+ SmallRyeConfig config = buildConfig("./src/test/resources",
+ "https://raw.githubusercontent.com/smallrye/smallrye-config/master/sources/yaml/src/test/resources/example-profiles.yml");
+
+ assertEquals("1234", config.getRawValue("my.prop"));
+ assertEquals("5678", config.getRawValue("more.prop"));
+ assertEquals(8, countSources(config));
+ }
+
+ @Test
+ void notFound() {
+ SmallRyeConfig config = buildConfig("not.found");
+
+ assertNull(config.getRawValue("my.prop"));
+ assertEquals(0, countSources(config));
+ }
+
+ @Test
+ void noPropertiesFile() {
+ SmallRyeConfig config = buildConfig("./src/test/resources/random.properties");
+
+ assertEquals(0, countSources(config));
+ }
+
+ @Test
+ void multipleResourcesInClassPath(@TempDir Path tempDir) throws Exception {
+ JavaArchive jarOne = ShrinkWrap
+ .create(JavaArchive.class, "resources-one.jar")
+ .addAsResource(new StringAsset("my:\n" +
+ " prop:\n" +
+ " one: 1234\n"), "resources.yml");
+
+ Path filePathOne = tempDir.resolve("resources-one.jar");
+ jarOne.as(ZipExporter.class).exportTo(filePathOne.toFile());
+
+ JavaArchive jarTwo = ShrinkWrap
+ .create(JavaArchive.class, "resources-two.jar")
+ .addAsResource(new StringAsset("my:\n" +
+ " prop:\n" +
+ " two: 5678\n"), "resources.yml");
+
+ Path filePathTwo = tempDir.resolve("resources-two.jar");
+ jarTwo.as(ZipExporter.class).exportTo(filePathTwo.toFile());
+
+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {
+ new URL("jar:file:" + filePathOne.toString() + "!/"),
+ new URL("jar:file:" + filePathTwo.toString() + "!/")
+ }, contextClassLoader);
+ Thread.currentThread().setContextClassLoader(urlClassLoader);
+
+ SmallRyeConfig config = buildConfig("resources.yml");
+
+ assertEquals("1234", config.getRawValue("my.prop.one"));
+ assertEquals("5678", config.getRawValue("my.prop.two"));
+ assertEquals(2, countSources(config));
+
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ }
+
+ @Test
+ void jar(@TempDir Path tempDir) throws Exception {
+ JavaArchive jarOne = ShrinkWrap
+ .create(JavaArchive.class, "resources-one.jar")
+ .addAsResource(new StringAsset("my:\n" +
+ " prop:\n" +
+ " one: 1234\n"), "resources.yml");
+
+ Path filePathOne = tempDir.resolve("resources-one.jar");
+ jarOne.as(ZipExporter.class).exportTo(filePathOne.toFile());
+
+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {
+ new URL("jar:file:" + filePathOne.toString() + "!/")
+ }, contextClassLoader);
+ Thread.currentThread().setContextClassLoader(urlClassLoader);
+
+ SmallRyeConfig config = buildConfig("jar:file:" + filePathOne.toString() + "!/resources.yml");
+
+ assertEquals("1234", config.getRawValue("my.prop.one"));
+ assertEquals(1, countSources(config));
+
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ }
+
+ @Test
+ void invalidWebResource() {
+ assertThrows(IllegalStateException.class,
+ () -> buildConfig("https://raw.githubusercontent.com/smallrye/smallrye-config/notfound.yml"));
+ buildConfig("https://github.com/smallrye/smallrye-config/blob/3cc4809734d7fbd03852a20b5870ca743a2427bc/pom.xml");
+ }
+
+ private static SmallRyeConfig buildConfig(String... locations) {
+ return new SmallRyeConfigBuilder()
+ .addDiscoveredSources()
+ .addDefaultInterceptors()
+ .withDefaultValue(SMALLRYE_LOCATIONS, String.join(",", locations))
+ .build();
+ }
+
+ private static int countSources(SmallRyeConfig config) {
+ return (int) stream(config.getConfigSources().spliterator(), false).filter(
+ YamlConfigSource.class::isInstance).count();
+ }
+}
diff --git a/sources/yaml/src/test/resources/additional.yml b/sources/yaml/src/test/resources/additional.yml
new file mode 100644
index 000000000..725459e01
--- /dev/null
+++ b/sources/yaml/src/test/resources/additional.yml
@@ -0,0 +1,2 @@
+my:
+ prop: 1234
diff --git a/sources/yaml/src/test/resources/more.yml b/sources/yaml/src/test/resources/more.yml
new file mode 100644
index 000000000..63cb34480
--- /dev/null
+++ b/sources/yaml/src/test/resources/more.yml
@@ -0,0 +1,2 @@
+more:
+ prop: 5678
diff --git a/testsuite/extra/pom.xml b/testsuite/extra/pom.xml
index 28c99ff25..6acc54317 100644
--- a/testsuite/extra/pom.xml
+++ b/testsuite/extra/pom.xml
@@ -44,6 +44,11 @@
jakarta.annotation-api
provided
+
+ io.smallrye.config
+ smallrye-config-source-yaml
+ ${project.version}
+
org.jboss.weld
weld-api
diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/location/LocationConfigTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/location/LocationConfigTest.java
new file mode 100644
index 000000000..e511a1f45
--- /dev/null
+++ b/testsuite/extra/src/test/java/io/smallrye/config/test/location/LocationConfigTest.java
@@ -0,0 +1,36 @@
+package io.smallrye.config.test.location;
+
+import static org.testng.Assert.assertEquals;
+
+import javax.inject.Inject;
+
+import org.eclipse.microprofile.config.Config;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.testng.Arquillian;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.testng.annotations.Test;
+
+public class LocationConfigTest extends Arquillian {
+ @Deployment
+ public static WebArchive deploy() {
+ return ShrinkWrap.create(WebArchive.class, "LocationConfigTest.war")
+ .addAsResource(new StringAsset("smallrye.config.locations=config.properties,config.yml"),
+ "META-INF/microprofile-config.properties")
+ .addAsResource(new StringAsset("my.prop=1234"), "config.properties")
+ .addAsResource(new StringAsset("my:\n" +
+ " yml: 1234\n"), "config.yml")
+ .as(WebArchive.class);
+ }
+
+ @Inject
+ Config config;
+
+ @Test
+ public void testLocationConfig() {
+ assertEquals(config.getValue("smallrye.config.locations", String.class), "config.properties,config.yml");
+ assertEquals(config.getValue("my.prop", String.class), "1234");
+ assertEquals(config.getValue("my.yml", String.class), "1234");
+ }
+}