diff --git a/implementation/pom.xml b/implementation/pom.xml index cfefd635f..385608d02 100644 --- a/implementation/pom.xml +++ b/implementation/pom.xml @@ -115,6 +115,11 @@ io.smallrye.testing smallrye-testing-utilities + + org.jboss.shrinkwrap + shrinkwrap-impl-base + test + 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..fc311629d --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceFactory.java @@ -0,0 +1,32 @@ +package io.smallrye.config; + +import static io.smallrye.config.Converters.STRING_CONVERTER; +import static io.smallrye.config.Converters.newArrayConverter; + +import java.net.URI; +import java.util.Collections; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.common.annotation.Experimental; + +/** + * This {@code AbstractLocationConfigSourceFactory} allows to initialize additional config locations with the + * configuration {@link AbstractLocationConfigSourceFactory#SMALLRYE_LOCATIONS}. The configuration support multiple + * locations separated by a comma and each much represent a valid {@link URI}. + */ +@Experimental("Loads additional config locations") +public abstract class AbstractLocationConfigSourceFactory extends AbstractLocationConfigSourceLoader + implements ConfigSourceFactory { + public static final String SMALLRYE_LOCATIONS = "smallrye.config.locations"; + + @Override + public Iterable 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: + * + *

    + *
  1. file or directory
  2. + *
  3. classpath resource
  4. + *
  5. jar resource
  6. + *
  7. http resource
  8. + *
+ *

+ * + * 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"); + } +}