diff --git a/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceFactory.java b/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceFactory.java
new file mode 100644
index 000000000..e1c963219
--- /dev/null
+++ b/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceFactory.java
@@ -0,0 +1,237 @@
+package io.smallrye.config;
+
+import static io.smallrye.common.classloader.ClassPathUtils.consumeAsPath;
+import static io.smallrye.common.classloader.ClassPathUtils.consumeAsPaths;
+import static io.smallrye.config.Converters.newCollectionConverter;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+import org.eclipse.microprofile.config.spi.Converter;
+
+import io.smallrye.common.annotation.Experimental;
+
+/**
+ * This {@code AbstractLocationConfigSourceFactory} allows to initialize additional config locations with the
+ * configuration {@link AbstractLocationConfigSourceFactory#SMALLRYE_LOCATIONS}.
+ *
+ *
+ * Locations set in {@link AbstractLocationConfigSourceFactory#SMALLRYE_LOCATIONS} are loaded in order and from the
+ * following resources:
+ *
+ *
+ * - file or directory
+ * - classpath resource
+ * - jar resource
+ * - http resource
+ *
+ *
+ *
+ * If a profile is active, the profile resource is loaded if available and the unprofiled resource is also
+ * available. This is to keep a consistent order with the unprofiled resource. Profiles are not taken into account if
+ * the location is a directory.
+ */
+@Experimental("Load additional config locations")
+public abstract class AbstractLocationConfigSourceFactory implements ConfigSourceFactory {
+ public static final String SMALLRYE_LOCATIONS = "smallrye.config.locations";
+
+ 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);
+ }
+
+ @Override
+ public Iterable getConfigSources(final ConfigSourceContext context) {
+ final ConfigValue value = context.getValue(SMALLRYE_LOCATIONS);
+
+ final List configSources = new ArrayList<>();
+ if (value.getValue() != null) {
+ List resources = newCollectionConverter(new URLConverter(), ArrayList::new).convert(value.getValue());
+ if (resources != null) {
+ for (URL url : resources) {
+ tryFileSystem(url, configSources, context);
+ tryClassPath(url, configSources, context);
+ tryHttpResource(url, configSources, context);
+ }
+ }
+ }
+
+ return configSources;
+ }
+
+ private void tryFileSystem(final URL url, final List configSources, final ConfigSourceContext context) {
+ if (url.getProtocol().startsWith("file")) {
+ // Load single file and profiles
+ final Path urlPath = Paths.get(url.getFile()).toAbsolutePath().normalize();
+ if (Files.isRegularFile(urlPath)) {
+ consumeAsPath(toURL(urlPath.toUri()), new ConfigSourcePathConsumer(this, configSources, context));
+ } else if (Files.isDirectory(urlPath)) { // Load directory contents
+ try (Stream paths = Files.walk(urlPath, 1)) {
+ paths
+ .filter(Files::isRegularFile)
+ .filter(path -> validExtension(path.getFileName().toString()))
+ .forEach(path -> addConfigSource(toURL(path.toUri()), configSources));
+ } catch (IOException e) {
+ throw ConfigMessages.msg.failedToLoadResource(e);
+ }
+ }
+ }
+ }
+
+ private void tryClassPath(final URL url, final List configSources, final ConfigSourceContext context) {
+ if (url.getProtocol().startsWith("file")) {
+ try {
+ consumeAsPaths(url.getFile(), new ConfigSourcePathConsumer(this, configSources, context));
+ } catch (IOException e) {
+ throw ConfigMessages.msg.failedToLoadResource(e);
+ } catch (IllegalArgumentException e) {
+ fallbackToUnknownProtocol(url, configSources);
+ }
+ }
+
+ if (url.getProtocol().startsWith("jar")) {
+ consumeAsPath(url, new ConfigSourcePathConsumer(this, configSources, context));
+ }
+ }
+
+ private void fallbackToUnknownProtocol(final URL url, final List configSources) {
+ final ClassLoader classLoader = SecuritySupport.getContextClassLoader();
+ try {
+ Enumeration resources = classLoader.getResources(url.getFile());
+ while (resources.hasMoreElements()) {
+ final URL resourceUrl = resources.nextElement();
+ if (validExtension(resourceUrl.getFile())) {
+ addConfigSource(resourceUrl, configSources);
+ }
+ }
+
+ } catch (IOException e) {
+ throw ConfigMessages.msg.failedToLoadResource(e);
+ }
+ }
+
+ private void tryHttpResource(final URL url, final List configSources, final ConfigSourceContext context) {
+ if (url.getProtocol().startsWith("http")) {
+ if (validExtension(url.toString())) {
+ addConfigSource(url, configSources);
+ tryProfiles(url, configSources, context);
+ }
+ }
+ }
+
+ private void tryProfiles(final URL url, final List configSources, final ConfigSourceContext context) {
+ final List profiles = context.getProfiles();
+ for (int i = profiles.size() - 1; i >= 0; i--) {
+ final int lastOrdinal = configSources.get(configSources.size() - 1).getOrdinal();
+ final URL profileToFileName = addProfileToFileName(url, profiles.get(i));
+ consumeAsPath(profileToFileName,
+ path -> addProfileConfigSource(toURL(path.toUri()), lastOrdinal + 1, configSources));
+ }
+ }
+
+ private static URL toURL(final URI uri) {
+ try {
+ return uri.toURL();
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private void addConfigSource(final URL url, final List configSources) {
+ try {
+ final ConfigSource configSource = loadConfigSource(url);
+ if (!configSource.getProperties().isEmpty()) {
+ configSources.add(configSource);
+ }
+ } catch (IOException e) {
+ throw ConfigMessages.msg.failedToLoadResource(e);
+ }
+ }
+
+ private void addProfileConfigSource(final URL url, final int ordinal, final List configSources) {
+ try {
+ final ConfigSource configSource = loadConfigSource(url, ordinal);
+ if (!configSource.getProperties().isEmpty()) {
+ configSources.add(configSource);
+ }
+ } catch (FileNotFoundException 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 String resourceName) {
+ for (String s : getFileExtensions()) {
+ if (resourceName.endsWith(s)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static URL addProfileToFileName(final URL fileName, final String profile) {
+ final int dot = fileName.toString().lastIndexOf(".");
+ try {
+ if (dot != -1) {
+ return new URL(fileName.toString().substring(0, dot) + "-" + profile + fileName.toString().substring(dot));
+ } else {
+ return new URL(fileName + "-" + profile);
+ }
+ } catch (MalformedURLException e) {
+ // The original URL was already validated, so this shouldn't happen.
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static class URLConverter implements Converter {
+ private static final long serialVersionUID = -4852082279190307320L;
+
+ @Override
+ public URL convert(final String value) {
+ try {
+ return new URL(value);
+ } catch (MalformedURLException e) {
+ return convert("file:" + value);
+ }
+ }
+ }
+
+ private static class ConfigSourcePathConsumer implements Consumer {
+ private final AbstractLocationConfigSourceFactory factory;
+ private final List configSources;
+ private final ConfigSourceContext context;
+
+ public ConfigSourcePathConsumer(
+ final AbstractLocationConfigSourceFactory factory,
+ final List configSources, final ConfigSourceContext context) {
+ this.factory = factory;
+ this.configSources = configSources;
+ this.context = context;
+ }
+
+ @Override
+ public void accept(final Path path) {
+ if (factory.validExtension(path.getFileName().toString())) {
+ final URL pathUrl = toURL(path.toUri());
+ factory.addConfigSource(pathUrl, configSources);
+ factory.tryProfiles(pathUrl, configSources, context);
+ }
+ }
+ }
+}
diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMessages.java b/implementation/src/main/java/io/smallrye/config/ConfigMessages.java
index 6bde9479c..7b8a5d099 100644
--- a/implementation/src/main/java/io/smallrye/config/ConfigMessages.java
+++ b/implementation/src/main/java/io/smallrye/config/ConfigMessages.java
@@ -112,4 +112,7 @@ interface ConfigMessages {
@Message(id = 32, value = "Expected a float value, got \"%s\"")
NumberFormatException floatExpected(String value);
+
+ @Message(id = 33, value = "Failed to load resource")
+ IllegalStateException failedToLoadResource(@Cause Throwable cause);
}
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 06b464158..8da1eee95 100644
--- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java
+++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java
@@ -324,7 +324,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);
@@ -334,7 +334,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
@@ -365,7 +365,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) {
@@ -378,6 +378,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)) {
@@ -418,6 +419,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() {
@@ -484,6 +486,7 @@ static class ConfigSourceInterceptorWithPriority implements Comparable init;
private final int priority;
+ private final int loadPriority = loadPrioritySequence--;
private final String name;
private ConfigSourceInterceptor interceptor;
@@ -528,14 +531,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..34c688f4f
--- /dev/null
+++ b/implementation/src/test/java/io/smallrye/config/PropertiesLocationConfigSourceFactoryTest.java
@@ -0,0 +1,466 @@
+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.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;
+
+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() {
+ // TODO - Add resources in GH
+ }
+
+ @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");
+ }
+}