From 3e9f52172ac76b4f6e62f89325921191393d1b81 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Thu, 24 Aug 2023 13:15:38 -0300 Subject: [PATCH] Introduce SmallRyeConfig.subset - Fixes #981 --- implementation/pom.xml | 5 + .../io/smallrye/config/SmallRyeConfig.java | 97 ++++++++++++------- .../smallrye/config/SmallRyeSubsetConfig.java | 91 +++++++++++++++++ .../smallrye/config/SmallRyeConfigTest.java | 47 +++++---- 4 files changed, 188 insertions(+), 52 deletions(-) create mode 100644 implementation/src/main/java/io/smallrye/config/SmallRyeSubsetConfig.java diff --git a/implementation/pom.xml b/implementation/pom.xml index c8b2e6ec8..8c53ef206 100644 --- a/implementation/pom.xml +++ b/implementation/pom.xml @@ -82,6 +82,11 @@ io.smallrye.testing smallrye-testing-utilities + + org.assertj + assertj-core + test + diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java index c4c3c98ed..827aa35c6 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java @@ -15,9 +15,13 @@ */ package io.smallrye.config; -import static io.smallrye.config.ConfigSourceInterceptor.EMPTY; -import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores; -import static io.smallrye.config.common.utils.StringUtil.toLowerCaseAndDotted; +import io.smallrye.common.annotation.Experimental; +import io.smallrye.config.SmallRyeConfigBuilder.InterceptorWithPriority; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; +import org.eclipse.microprofile.config.spi.Converter; import java.io.ObjectStreamException; import java.io.Serializable; @@ -39,14 +43,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.IntFunction; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.eclipse.microprofile.config.spi.ConfigSourceProvider; -import org.eclipse.microprofile.config.spi.Converter; - -import io.smallrye.common.annotation.Experimental; -import io.smallrye.config.SmallRyeConfigBuilder.InterceptorWithPriority; +import static io.smallrye.config.ConfigSourceInterceptor.EMPTY; +import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores; +import static io.smallrye.config.common.utils.StringUtil.toLowerCaseAndDotted; /** * @author Jeff Mesnil (c) 2017 Red Hat inc. @@ -112,7 +111,7 @@ public > C getValues(String name, Converter conver } public > C getIndexedValues(String name, Converter converter, - IntFunction collectionFactory) { + IntFunction collectionFactory) { List indexedProperties = getIndexedProperties(name); if (indexedProperties.isEmpty()) { throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name)); @@ -142,7 +141,7 @@ public List getIndexedPropertiesIndexes(final String property) { if (propertyName.startsWith(property) && propertyName.length() > property.length()) { int index = property.length(); if (propertyName.charAt(index) == '[') { - for (;;) { + for (; ; ) { if (propertyName.charAt(index) == ']') { try { indexes.add(Integer.parseInt(propertyName.substring(property.length() + 1, index))); @@ -172,14 +171,14 @@ public T getValue(String name, Class aClass) { /** * Return the content of the direct sub properties as the requested type of Map. * - * @param name The configuration property name + * @param name The configuration property name * @param kClass the type into which the keys should be converted * @param vClass the type into which the values should be converted - * @param the key type - * @param the value type + * @param the key type + * @param the value type * @return the resolved property value as an instance of the requested Map (not {@code null}) * @throws IllegalArgumentException if a key or a value cannot be converted to the specified types - * @throws NoSuchElementException if no direct sub properties could be found. + * @throws NoSuchElementException if no direct sub properties could be found. */ public Map getValues(String name, Class kClass, Class vClass) { final Map result = getValuesAsMap(name, requireConverter(kClass), requireConverter(vClass)); @@ -192,11 +191,11 @@ public Map getValues(String name, Class kClass, Class vClass) /** * Return the content of the direct sub properties as the requested type of Map. * - * @param name The configuration property name - * @param keyConverter The converter to use for the keys. + * @param name The configuration property name + * @param keyConverter The converter to use for the keys. * @param valueConverter The converter to use for the values. - * @param The type of the keys. - * @param The type of the values. + * @param The type of the keys. + * @param The type of the values. * @return the resolved property value as an instance of the requested Map or {@code null} if it could not be found. * @throws IllegalArgumentException if a key or a value cannot be converted to the specified types */ @@ -223,7 +222,6 @@ public Map getValuesAsMap(String name, Converter keyConverter, C } /** - * * This method handles calls from both {@link Config#getValue} and {@link Config#getOptionalValue}.
*/ @SuppressWarnings("unchecked") @@ -246,17 +244,17 @@ public T getValue(String name, Converter converter) { /** * This method handles converting values for both CDI injections and programatical calls.
*
- * + *

* Calls for converting non-optional values ({@link Config#getValue} and "Injecting Native Values") * should throw an {@link Exception} for each of the following:
- * + *

* 1. {@link IllegalArgumentException} - if the property cannot be converted by the {@link Converter} to the specified type *
* 2. {@link NoSuchElementException} - if the property is not defined
* 3. {@link NoSuchElementException} - if the property is defined as an empty string
* 4. {@link NoSuchElementException} - if the {@link Converter} returns {@code null}
*
- * + *

* Calls for converting optional values ({@link Config#getOptionalValue} and "Injecting Optional Values") * should only throw an {@link Exception} for #1 ({@link IllegalArgumentException} when the property cannot be converted to * the specified type). @@ -312,7 +310,7 @@ public T convertValue(ConfigValue configValue, Converter converter) { * Determine whether the raw value of a configuration property is exactly equal to the expected given * value. * - * @param name the property name (must not be {@code null}) + * @param name the property name (must not be {@code null}) * @param expected the expected value (may be {@code null}) * @return {@code true} if the values are equal, {@code false} otherwise */ @@ -344,11 +342,11 @@ public Optional getOptionalValue(String name, Class aClass) { /** * Return the content of the direct sub properties as the requested type of Map. * - * @param name The configuration property name + * @param name The configuration property name * @param kClass the type into which the keys should be converted * @param vClass the type into which the values should be converted - * @param the key type - * @param the value type + * @param the key type + * @param the value type * @return the resolved property value as an instance of the requested Map (not {@code null}) * @throws IllegalArgumentException if a key or a value cannot be converted to the specified types */ @@ -365,12 +363,12 @@ public Optional> getOptionalValues(final String propertyName, final } public > Optional getOptionalValues(String name, Class itemClass, - IntFunction collectionFactory) { + IntFunction collectionFactory) { return getOptionalValues(name, requireConverter(itemClass), collectionFactory); } public > Optional getOptionalValues(String name, Converter converter, - IntFunction collectionFactory) { + IntFunction collectionFactory) { final Optional optionalValue = getOptionalValue(name, Converters.newCollectionConverter(converter, collectionFactory)); if (optionalValue.isPresent()) { @@ -381,7 +379,7 @@ public > Optional getOptionalValues(String name, C } public > Optional getIndexedOptionalValues(String name, Converter converter, - IntFunction collectionFactory) { + IntFunction collectionFactory) { List indexedProperties = getIndexedProperties(name); if (indexedProperties.isEmpty()) { return Optional.empty(); @@ -459,11 +457,42 @@ public Optional getConfigSource(final String name) { return Optional.empty(); } + /** + * Return a {@link Config} containing every key from the current {@link Config} that starts with the specified + * prefix. The prefix is removed from the keys in the subset. For example, if the configuration contains the following + * properties: + * + *

+     *    prefix.number = 1
+     *    prefix.string = Hello
+     *    prefixed.foo = bar
+     *    prefix = World
+     * 
+ *

+ * the Configuration returned by {@code subset("prefix")} will contain the properties: + * + *

+     *    number = 1
+     *    string = Hello
+     *    = World
+     * 
+ *

+ * (The key for the value "World" is an empty string) + *

+ * + * @param prefix The prefix used to select the properties. + * @return a subset configuration + */ + @Experimental("Return a subset of the configuration") + public Config subset(final String prefix) { + return new SmallRyeSubsetConfig(prefix, this); + } + public T convert(String value, Class asType) { return value != null ? requireConverter(asType).convert(value) : null; } - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({"unchecked", "rawtypes"}) private Converter> getOptionalConverter(Class asType) { return optionalConverters.computeIfAbsent(asType, clazz -> Converters.newOptionalConverter(requireConverter((Class) clazz))); @@ -733,7 +762,7 @@ private static List getConfigurableSources(final List< * If FOO_BAR is present a property foo.bar is required. */ private static Set generateDottedProperties(final List sources, - final SmallRyeConfigSourceInterceptorContext current) { + final SmallRyeConfigSourceInterceptorContext current) { // Collect all known properties Set properties = new HashSet<>(); Iterator iterateNames = current.iterateNames(); diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeSubsetConfig.java b/implementation/src/main/java/io/smallrye/config/SmallRyeSubsetConfig.java new file mode 100644 index 000000000..011c00171 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeSubsetConfig.java @@ -0,0 +1,91 @@ +package io.smallrye.config; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigValue; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.Converter; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * @author George Gastaldi + */ +public class SmallRyeSubsetConfig implements Config { + + private final String prefix; + + private final Config delegate; + + public SmallRyeSubsetConfig(String prefix, Config delegate) { + this.prefix = prefix; + this.delegate = delegate; + } + + @Override + public T getValue(String propertyName, Class propertyType) { + return delegate.getValue(toSubsetPropertyName(propertyName), propertyType); + } + + @Override + public ConfigValue getConfigValue(String propertyName) { + return delegate.getConfigValue(toSubsetPropertyName(propertyName)); + } + + @Override + public List getValues(String propertyName, Class propertyType) { + return delegate.getValues(toSubsetPropertyName(propertyName), propertyType); + } + + @Override + public Optional getOptionalValue(String propertyName, Class propertyType) { + return delegate.getOptionalValue(toSubsetPropertyName(propertyName), propertyType); + } + + @Override + public Optional> getOptionalValues(String propertyName, Class propertyType) { + return delegate.getOptionalValues(toSubsetPropertyName(propertyName), propertyType); + } + + @Override + public Iterable getPropertyNames() { + return StreamSupport.stream(delegate.getPropertyNames().spliterator(), false) + .map(this::chopSubsetPropertyName) + .collect(Collectors.toSet()); + } + + @Override + public Iterable getConfigSources() { + return delegate.getConfigSources(); + } + + @Override + public Optional> getConverter(Class forType) { + return delegate.getConverter(forType); + } + + @Override + public T unwrap(Class type) { + return delegate.unwrap(type); + } + + private String toSubsetPropertyName(String propertyName) { + if (propertyName.isBlank()) { + return prefix; + } else { + return prefix + "." + propertyName; + } + } + + private String chopSubsetPropertyName(String propertyName) { + if (propertyName.equalsIgnoreCase(prefix)) { + return ""; + } else if (propertyName.startsWith(prefix)) { + return propertyName.substring(prefix.length() + 1); + } else { + return propertyName; + } + } +} diff --git a/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java b/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java index 9d9f4476d..768daa57f 100644 --- a/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java +++ b/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java @@ -1,17 +1,11 @@ package io.smallrye.config; -import static io.smallrye.config.Converters.STRING_CONVERTER; -import static io.smallrye.config.KeyValuesConfigSource.config; -import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; -import static java.util.stream.Collectors.toSet; -import static java.util.stream.StreamSupport.stream; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -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 io.smallrye.config.common.AbstractConfigSource; +import io.smallrye.config.common.MapBackedConfigSource; +import org.assertj.core.api.Assertions; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Arrays; @@ -23,12 +17,14 @@ import java.util.Optional; import java.util.Set; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.junit.jupiter.api.Test; - -import io.smallrye.config.common.AbstractConfigSource; -import io.smallrye.config.common.MapBackedConfigSource; +import static io.smallrye.config.Converters.STRING_CONVERTER; +import static io.smallrye.config.KeyValuesConfigSource.config; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.stream.Collectors.toSet; +import static java.util.stream.StreamSupport.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; class SmallRyeConfigTest { @Test @@ -405,4 +401,19 @@ void emptyPropertyNames() { assertEquals("value", config.getRawValue("")); } + + @Test + void subset() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "app.foo", "bar", + "app.foo.user", "guest", + "app.foo.password", "apassword")) + .build(); + Config subset = config.subset("app.foo"); + assertEquals("bar", subset.getValue("", String.class)); + assertEquals("guest", subset.getValue("user", String.class)); + assertEquals("apassword", subset.getValue("password", String.class)); + assertThat(subset.getPropertyNames()).containsAnyOf("", "user", "password"); + } }