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..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: + * + *

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