From e043b6a0e42aed62fd8cb66fdb36762113c0568f Mon Sep 17 00:00:00 2001 From: essobedo Date: Fri, 21 May 2021 13:45:13 +0200 Subject: [PATCH] Allow to fetch multiple config parameters as a Map --- .../config/inject/ConfigExtension.java | 15 +- .../config/inject/ConfigProducer.java | 7 + .../config/inject/ConfigProducerUtil.java | 163 +++++++++++++++--- .../config/inject/ConfigInjectionTest.java | 143 ++++++++++++++- doc/modules/ROOT/pages/config/config.adoc | 3 + .../ROOT/pages/config/indexed-properties.adoc | 2 +- .../ROOT/pages/config/map-support.adoc | 52 ++++++ .../smallrye/config/ConfigMappingClass.java | 16 +- .../config/ConfigMappingGenerator.java | 15 +- .../java/io/smallrye/config/Converters.java | 75 ++++++++ .../io/smallrye/config/SmallRyeConfig.java | 72 +++++++- .../smallrye/config/ConfigMappingsTest.java | 91 +++++++++- 12 files changed, 603 insertions(+), 51 deletions(-) create mode 100644 doc/modules/ROOT/pages/config/map-support.adoc diff --git a/cdi/src/main/java/io/smallrye/config/inject/ConfigExtension.java b/cdi/src/main/java/io/smallrye/config/inject/ConfigExtension.java index 1751febbd..8d53e7a78 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/ConfigExtension.java +++ b/cdi/src/main/java/io/smallrye/config/inject/ConfigExtension.java @@ -29,6 +29,7 @@ import java.lang.reflect.Type; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.OptionalDouble; import java.util.OptionalInt; @@ -173,9 +174,10 @@ protected void validate(@Observes AfterDeploymentValidation adv) { // Check if the name is part of the properties first. // Since properties can be a subset, then search for the actual property for a value. + // Check if it is a map // Finally also check if the property is indexed (might be a Collection with indexed properties). if ((!configNames.contains(name) && ConfigProducerUtil.getRawValue(name, config) == null) - && !isIndexed(type, name, config)) { + && !isMap(type) && !isIndexed(type, name, config)) { if (configProperty.defaultValue().equals(ConfigProperty.UNCONFIGURED_VALUE)) { adv.addDeploymentProblem( InjectionMessages.msg.noConfigValue(name, formatInjectionPoint(injectionPoint))); @@ -232,4 +234,15 @@ private static boolean isIndexed(Type type, String name, Config config) { && !((SmallRyeConfig) config).getIndexedPropertiesIndexes(name).isEmpty(); } + + /** + * Indicates whether the given type is a type of Map. + * + * @param type the type to check + * @return {@code true} if the given type is a type of Map, {@code false} otherwise. + */ + private static boolean isMap(final Type type) { + return type instanceof ParameterizedType && + Map.class.isAssignableFrom((Class) ((ParameterizedType) type).getRawType()); + } } diff --git a/cdi/src/main/java/io/smallrye/config/inject/ConfigProducer.java b/cdi/src/main/java/io/smallrye/config/inject/ConfigProducer.java index 451c58adc..0fcd84338 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/ConfigProducer.java +++ b/cdi/src/main/java/io/smallrye/config/inject/ConfigProducer.java @@ -135,6 +135,13 @@ protected List producesListConfigProperty(InjectionPoint ip) { return ConfigProducerUtil.getValue(ip, getConfig(ip)); } + @Dependent + @Produces + @ConfigProperty + protected Map producesMapConfigProperty(InjectionPoint ip) { + return ConfigProducerUtil.getValue(ip, getConfig(ip)); + } + @Dependent @Produces @ConfigProperty diff --git a/cdi/src/main/java/io/smallrye/config/inject/ConfigProducerUtil.java b/cdi/src/main/java/io/smallrye/config/inject/ConfigProducerUtil.java index 2bcb40f4b..53da59352 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/ConfigProducerUtil.java +++ b/cdi/src/main/java/io/smallrye/config/inject/ConfigProducerUtil.java @@ -1,6 +1,7 @@ package io.smallrye.config.inject; import static io.smallrye.config.Converters.newCollectionConverter; +import static io.smallrye.config.Converters.newMapConverter; import static io.smallrye.config.Converters.newOptionalConverter; import java.lang.annotation.Annotation; @@ -12,6 +13,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; @@ -27,8 +29,10 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.spi.Converter; +import io.smallrye.config.Converters; import io.smallrye.config.SecretKeys; import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.common.AbstractConverter; import io.smallrye.config.common.AbstractDelegatingConverter; /** @@ -51,19 +55,7 @@ private ConfigProducerUtil() { * @return the converted configuration value. */ public static T getValue(InjectionPoint injectionPoint, Config config) { - String name = getName(injectionPoint); - if (name == null) { - return null; - } - - String defaultValue = getDefaultValue(injectionPoint); - String rawValue = getRawValue(name, config); - if (hasCollection(injectionPoint.getType())) { - return convertValues(name, injectionPoint.getType(), rawValue, defaultValue, config); - } - - return ((SmallRyeConfig) config).convertValue(name, resolveDefault(rawValue, defaultValue), - resolveConverter(injectionPoint, config)); + return getValue(getName(injectionPoint), injectionPoint.getType(), getDefaultValue(injectionPoint), config); } /** @@ -71,7 +63,7 @@ public static T getValue(InjectionPoint injectionPoint, Config config) { * * @param name the name of the configuration property. * @param type the {@link Type} of the configuration value to convert. - * @param defaultValue the defaut value to use if no configuration value is found. + * @param defaultValue the default value to use if no configuration value is found. * @param config the current {@link Config} instance. * * @return the converted configuration value. @@ -80,8 +72,30 @@ public static T getValue(String name, Type type, String defaultValue, Config if (name == null) { return null; } - String resolvedValue = resolveValue(name, defaultValue, config); - return ((SmallRyeConfig) config).convertValue(name, resolvedValue, resolveConverter(type, config)); + if (hasCollection(type)) { + return convertValues(name, type, getRawValue(name, config), defaultValue, config); + } else if (hasMap(type)) { + return convertValues(name, type, defaultValue, config); + } + + return ((SmallRyeConfig) config).convertValue(name, resolveDefault(getRawValue(name, config), defaultValue), + resolveConverter(type, config)); + } + + /** + * Converts the direct sub properties of the given parent property as a Map. + * + * @param name the name of the parent property for which we want the direct sub properties as a Map. + * @param type the {@link Type} of the configuration value to convert. + * @param defaultValue the default value to convert in case no sub properties could be found. + * @param config the configuration from which the values are retrieved. + * @param the expected type of the configuration value to convert. + * + * @return the converted configuration value. + */ + private static T convertValues(String name, Type type, String defaultValue, Config config) { + return ((SmallRyeConfig) config).convertValue(name, null, + resolveConverter(type, config, (kC, vC) -> new StaticMapConverter<>(name, defaultValue, config, kC, vC))); } private static T convertValues(String name, Type type, String rawValue, String defaultValue, Config config) { @@ -98,7 +112,7 @@ private static T convertValues(String name, Type type, String rawValue, Stri for (String indexedProperty : indexedProperties) { // Never null by the rules of converValue collection.add( - ((SmallRyeConfig) config).convertValue(indexedProperty, resolveValue(indexedProperty, null, config), + ((SmallRyeConfig) config).convertValue(indexedProperty, getRawValue(indexedProperty, config), itemConverter)); } return collection; @@ -127,21 +141,17 @@ static String getRawValue(String name, Config config) { return SecretKeys.doUnlocked(() -> config.getConfigValue(name).getValue()); } - private static String resolveValue(String name, String defaultValue, Config config) { - String rawValue = getRawValue(name, config); - return resolveDefault(rawValue, defaultValue); - } - private static String resolveDefault(String rawValue, String defaultValue) { return rawValue != null ? rawValue : defaultValue; } - private static Converter resolveConverter(final InjectionPoint injectionPoint, final Config config) { - return resolveConverter(injectionPoint.getType(), config); + private static Converter resolveConverter(final Type type, final Config config) { + return resolveConverter(type, config, Converters::newMapConverter); } @SuppressWarnings("unchecked") - private static Converter resolveConverter(final Type type, final Config config) { + private static Converter resolveConverter(final Type type, final Config config, + final BiFunction, Converter, Converter>> mapConverterFactory) { Class rawType = rawTypeOf(type); if (type instanceof ParameterizedType) { ParameterizedType paramType = (ParameterizedType) type; @@ -150,11 +160,18 @@ private static Converter resolveConverter(final Type type, final Config c return (Converter) newCollectionConverter(resolveConverter(typeArgs[0], config), ArrayList::new); } else if (rawType == Set.class) { return (Converter) newCollectionConverter(resolveConverter(typeArgs[0], config), HashSet::new); + } else if (rawType == Map.class) { + return (Converter) mapConverterFactory.apply(resolveConverter(typeArgs[0], config), + resolveConverter(typeArgs[1], config)); } else if (rawType == Optional.class) { - return (Converter) newOptionalConverter(resolveConverter(typeArgs[0], config)); + return (Converter) newOptionalConverter(resolveConverter(typeArgs[0], config, mapConverterFactory)); } else if (rawType == Supplier.class) { - return resolveConverter(typeArgs[0], config); + return resolveConverter(typeArgs[0], config, mapConverterFactory); } + } else if (rawType == Map.class) { + // No parameterized types have been provided so it assumes that a Map of String is expected + return (Converter) mapConverterFactory.apply(resolveConverter(String.class, config), + resolveConverter(String.class, config)); } // just try the raw type return config.getConverter(rawType).orElseThrow(() -> InjectionMessages.msg.noRegisteredConverter(rawType)); @@ -208,6 +225,23 @@ private static Class rawTypeOf(final Type type) { } } + /** + * Indicates whether the given type is a type of Map or is a Supplier or Optional of Map. + * + * @param type the type to check + * @return {@code true} if the given type is a type of Map or is a Supplier or Optional of Map, + * {@code false} otherwise. + */ + private static boolean hasMap(final Type type) { + Class rawType = rawTypeOf(type); + if (rawType == Map.class) { + return true; + } else if (type instanceof ParameterizedType) { + return hasMap(((ParameterizedType) type).getActualTypeArguments()[0]); + } + return false; + } + private static boolean hasCollection(final Type type) { Class rawType = rawTypeOf(type); if (type instanceof ParameterizedType) { @@ -278,7 +312,7 @@ static String getConfigKey(InjectionPoint ip, ConfigProperty configProperty) { throw InjectionMessages.msg.noConfigPropertyDefaultName(ip); } - final static class IndexedCollectionConverter> extends AbstractDelegatingConverter { + static final class IndexedCollectionConverter> extends AbstractDelegatingConverter { private static final long serialVersionUID = 5186940408317652618L; private final IntFunction> collectionFactory; @@ -300,4 +334,77 @@ public C convert(final String value) throws IllegalArgumentException, NullPointe return (C) indexedConverter.apply((Converter) getDelegate(), collectionFactory); } } + + /** + * A {@code Converter} of a Map that gives the same Map content whatever the value to convert. It actually relies on + * its parameters to convert the sub properties of a fixed parent property as a Map. + * + * @param The type of the keys. + * @param The type of the values. + */ + static final class StaticMapConverter extends AbstractConverter> { + private static final long serialVersionUID = 402894491607011464L; + + /** + * The name of the parent property for which we want the direct sub properties as a Map. + */ + private final String name; + /** + * The default value to convert in case no sub properties could be found. + */ + private final String defaultValue; + /** + * The configuration from which the values are retrieved. + */ + private final Config config; + /** + * The converter to use for the keys. + */ + private final Converter keyConverter; + /** + * The converter to use the for values. + */ + private final Converter valueConverter; + + /** + * Construct a {@code StaticMapConverter} with the given parameters. + * + * @param name the name of the parent property for which we want the direct sub properties as a Map + * @param defaultValue the default value to convert in case no sub properties could be found + * @param config the configuration from which the values are retrieved + * @param keyConverter the converter to use for the keys + * @param valueConverter the converter to use the for values + */ + StaticMapConverter(String name, String defaultValue, Config config, Converter keyConverter, + Converter valueConverter) { + this.name = name; + this.defaultValue = defaultValue; + this.config = config; + this.keyConverter = keyConverter; + this.valueConverter = valueConverter; + } + + /** + * {@inheritDoc} + * + * Gives the sub properties as a Map if they exist, otherwise gives the default value converted with a + * {@code MapConverter}. + */ + @Override + public Map convert(String value) throws IllegalArgumentException, NullPointerException { + Map result = getValues(name, config, keyConverter, valueConverter); + if (result == null && defaultValue != null) { + result = newMapConverter(keyConverter, valueConverter).convert(defaultValue); + } + return result; + } + + /** + * @return the content of the direct sub properties as the requested type of Map. + */ + private static Map getValues(String name, Config config, Converter keyConverter, + Converter valueConverter) { + return SecretKeys.doUnlocked(() -> ((SmallRyeConfig) config).getValuesAsMap(name, keyConverter, valueConverter)); + } + } } diff --git a/cdi/src/test/java/io/smallrye/config/inject/ConfigInjectionTest.java b/cdi/src/test/java/io/smallrye/config/inject/ConfigInjectionTest.java index a05490e0b..82d215639 100644 --- a/cdi/src/test/java/io/smallrye/config/inject/ConfigInjectionTest.java +++ b/cdi/src/test/java/io/smallrye/config/inject/ConfigInjectionTest.java @@ -6,9 +6,15 @@ 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 java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; @@ -44,6 +50,38 @@ class ConfigInjectionTest { @Test void inject() { + assertEquals(2, configBean.getReasons().size()); + assertEquals("OK", configBean.getReasons().get(200)); + assertEquals("Created", configBean.getReasons().get(201)); + assertEquals(2, configBean.getReasonsSupplier().get().size()); + assertEquals("OK", configBean.getReasonsSupplier().get().get(200)); + assertEquals("Created", configBean.getReasonsSupplier().get().get(201)); + assertFalse(configBean.getReasonsOptional().isPresent()); + assertEquals(2, configBean.getVersions().size()); + assertEquals(new Version(1, "The version 1.2.3"), configBean.getVersions().get("v1")); + assertEquals(new Version(2, "The version 2.0.0"), configBean.getVersions().get("v2")); + assertEquals(2, configBean.getVersionsDefault().size()); + assertEquals(new Version(1, "The version 1;2;3"), configBean.getVersionsDefault().get("v1=1;2;3")); + assertEquals(new Version(2, "The version 2;1;0"), configBean.getVersionsDefault().get("v2=2;1;0")); + assertEquals(2, configBean.getNumbersList().size()); + assertEquals(4, configBean.getNumbersList().get("even").size()); + assertTrue(configBean.getNumbersList().get("even").containsAll(Arrays.asList(2, 4, 6, 8))); + assertEquals(5, configBean.getNumbersList().get("odd").size()); + assertTrue(configBean.getNumbersList().get("odd").containsAll(Arrays.asList(1, 3, 5, 7, 9))); + assertEquals(2, configBean.getNumbersSet().size()); + assertEquals(4, configBean.getNumbersSet().get("even").size()); + assertTrue(configBean.getNumbersSet().get("even").containsAll(Arrays.asList(2, 4, 6, 8))); + assertEquals(5, configBean.getNumbersSet().get("odd").size()); + assertTrue(configBean.getNumbersSet().get("odd").containsAll(Arrays.asList(1, 3, 5, 7, 9))); + assertEquals(2, configBean.getNumbersArray().size()); + assertEquals(4, configBean.getNumbersArray().get("even").length); + assertTrue(Arrays.asList(configBean.getNumbersArray().get("even")).containsAll(Arrays.asList(2, 4, 6, 8))); + assertEquals(5, configBean.getNumbersArray().get("odd").length); + assertTrue(Arrays.asList(configBean.getNumbersArray().get("odd")).containsAll(Arrays.asList(1, 3, 5, 7, 9))); + assertEquals(3, configBean.getNumbers().size()); + assertEquals(1, configBean.getNumbers().get("one")); + assertEquals(2, configBean.getNumbers().get("two")); + assertEquals(3, configBean.getNumbers().get("three")); assertEquals("1234", configBean.getMyProp()); assertEquals("1234", configBean.getExpansion()); assertEquals("12345678", configBean.getSecret()); @@ -80,6 +118,33 @@ void converters() { @ApplicationScoped static class ConfigBean { + @Inject + @ConfigProperty(name = "optional.reasons") + Optional> reasonsOptional; + @Inject + @ConfigProperty(name = "reasons") + Supplier> reasonsSupplier; + @Inject + @ConfigProperty(name = "reasons") + Map reasons; + @Inject + @ConfigProperty(name = "versions") + Map versions; + @Inject + @ConfigProperty(name = "default.versions", defaultValue = "v0.1=0.The version 0;v1\\=1;2;3=1.The version 1\\;2\\;3;v2\\=2;1;0=2.The version 2\\;1\\;0") + Map versionsDefault; + @Inject + @ConfigProperty(name = "nums") + Map numbers; + @Inject + @ConfigProperty(name = "lnums") + Map> numbersList; + @Inject + @ConfigProperty(name = "snums") + Map> numbersSet; + @Inject + @ConfigProperty(name = "anums") + Map numbersArray; @Inject @ConfigProperty(name = "my.prop") String myProp; @@ -107,6 +172,42 @@ static class ConfigBean { @ConfigProperty(name = "converted") Optional convertedValueOptional; + Optional> getReasonsOptional() { + return reasonsOptional; + } + + Supplier> getReasonsSupplier() { + return reasonsSupplier; + } + + Map getReasons() { + return reasons; + } + + Map getVersions() { + return versions; + } + + Map getVersionsDefault() { + return versionsDefault; + } + + Map> getNumbersList() { + return numbersList; + } + + Map> getNumbersSet() { + return numbersSet; + } + + Map getNumbersArray() { + return numbersArray; + } + + Map getNumbers() { + return numbers; + } + String getMyProp() { return myProp; } @@ -149,9 +250,16 @@ static void beforeAll() { SmallRyeConfig config = new SmallRyeConfigBuilder() .withSources(config("my.prop", "1234", "expansion", "${my.prop}", "secret", "12345678", "mp.config.profile", "prof", "my.prop.profile", "1234", "%prof.my.prop.profile", "5678", - "bad.property.expression.prop", "${missing.prop}")) + "bad.property.expression.prop", "${missing.prop}", "reasons.200", "OK", "reasons.201", "Created", + "versions.v1", "1.The version 1.2.3", "versions.v1.2", "1.The version 1.2.0", "versions.v2", + "2.The version 2.0.0", + "lnums.even", "2,4,6,8", "lnums.odd", "1,3,5,7,9", + "snums.even", "2,4,6,8", "snums.odd", "1,3,5,7,9", + "anums.even", "2,4,6,8", "anums.odd", "1,3,5,7,9", + "nums.one", "1", "nums.two", "2", "nums.three", "3")) .withSecretKeys("secret") .withConverter(ConvertedValue.class, 100, new ConvertedValueConverter()) + .withConverter(Version.class, 100, new VersionConverter()) .addDefaultInterceptors() .build(); ConfigProviderResolver.instance().registerConfig(config, Thread.currentThread().getContextClassLoader()); @@ -162,6 +270,39 @@ static void afterAll() { ConfigProviderResolver.instance().releaseConfig(ConfigProvider.getConfig()); } + static class Version { + int id; + String name; + + Version(int id, String name) { + this.id = id; + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Version version = (Version) o; + return id == version.id && Objects.equals(name, version.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + } + + static class VersionConverter implements Converter { + + @Override + public Version convert(String value) { + return new Version(Integer.parseInt(value.substring(0, 1)), value.substring(2)); + } + } + static class ConvertedValue { private final String value; diff --git a/doc/modules/ROOT/pages/config/config.adoc b/doc/modules/ROOT/pages/config/config.adoc index 547580a99..7f9a362d8 100644 --- a/doc/modules/ROOT/pages/config/config.adoc +++ b/doc/modules/ROOT/pages/config/config.adoc @@ -6,12 +6,15 @@ include::../attributes.adoc[] = Config * <> +* <> * <> * <> * <> include::indexed-properties.adoc[] +include::map-support.adoc[] + include::profiles.adoc[] include::locations.adoc[] diff --git a/doc/modules/ROOT/pages/config/indexed-properties.adoc b/doc/modules/ROOT/pages/config/indexed-properties.adoc index f470a1750..c86408ef4 100644 --- a/doc/modules/ROOT/pages/config/indexed-properties.adoc +++ b/doc/modules/ROOT/pages/config/indexed-properties.adoc @@ -6,7 +6,7 @@ for simple cases, but it becomes cumbersome and limited for more advanced cases. Indexed Properties provide a way to use indexes in config property names to map specific elements in a `Collection` type. Since the indexed element is part of the property name and not contained in the value, this can also be used to -map complex object types as `CollectionÂȘ elements. Consider: +map complex object types as `Collection` elements. Consider: [source,properties] ---- diff --git a/doc/modules/ROOT/pages/config/map-support.adoc b/doc/modules/ROOT/pages/config/map-support.adoc new file mode 100644 index 000000000..8c336c85c --- /dev/null +++ b/doc/modules/ROOT/pages/config/map-support.adoc @@ -0,0 +1,52 @@ +[[map-support]] +== Map Support + +SmallRye Config allows injecting multiple configuration parameters as a `Map` using the standard annotations (`@ConfigProperty` and `@ConfigProperties`) which can +be very helpful especially with a dynamic configuration. + +For example, let's say that I want to keep in my configuration the custom reason phrases to return to the end user according to the http status code. +In that particular use case, the configuration could then be something like the following properties file: + +[source,properties] +---- +server.reasons.200=My custom reason phrase for OK +server.reasons.201=My custom reason phrase for Created +... +---- + +The previous configuration could be injected directly into your bean using the standard annotations as next: + +With `@ConfigProperty` + +[source,java] +---- +@ApplicationScoped +public class ConfigBean { + + @Inject + @ConfigProperty(name = "server.reasons") <1> + Map reasons; <2> + +} +---- +<1> Provide the name of the parent configuration property from the annotation `@ConfigProperty`. +<2> Provide the expected type of `Map`, here the keys will automatically be converted into Integers, and the values into Strings. + +With `@ConfigProperties` + +[source,java] +---- +@ConfigProperties(prefix = "server") <1> +public class Config { + + Map reasons; <2> +} +---- +<1> Provide the prefix of the name of the parent configuration property from the annotation `@ConfigProperties`, here the prefix is `server`. +<2> Provide the suffix of the name of the parent configuration, and the expected type of `Map`, here the keys will automatically be converted into Integers, and the values into Strings. + +NOTE: Only the direct sub properties will be converted into a `Map` and injected into the target bean, the rest will be ignored. In other words, in the previous example, a property whose name is `reasons.200.a` would be ignored as not considered as a direct sub property. + +NOTE: The property will be considered as missing if no direct sub properties could be found. + +It is also possible to do the exact same thing programmatically by calling the non-standard method `SmallRyeConfig#getValues("server.reasons", Integer.class, String.class)` if the property is mandatory otherwise by calling the method `SmallRyeConfig#getOptionalValues("server.reasons", Integer.class, String.class)`. diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingClass.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingClass.java index 5b2a00f73..746a3c34f 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingClass.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingClass.java @@ -27,12 +27,26 @@ private static ConfigMappingClass createConfigurationClass(final Class classT return new ConfigMappingClass(classType); } + private static String generateInterfaceName(final Class classType) { + if (classType.isInterface() && classType.getTypeParameters().length == 0 || + Modifier.isAbstract(classType.getModifiers()) || + classType.isEnum()) { + throw new IllegalArgumentException(); + } + + return classType.getPackage().getName() + + "." + + classType.getSimpleName() + + classType.getName().hashCode() + + "I"; + } + private final Class classType; private final String interfaceName; public ConfigMappingClass(final Class classType) { this.classType = classType; - this.interfaceName = ConfigMappingGenerator.generateInterfaceName(classType); + this.interfaceName = generateInterfaceName(classType); } @Override diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java index 603e60377..f09ee3a0d 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java @@ -352,20 +352,6 @@ static byte[] generate(final Class classType, final String interfaceName) { return writer.toByteArray(); } - static String generateInterfaceName(final Class classType) { - if (classType.isInterface() && classType.getTypeParameters().length == 0 || - Modifier.isAbstract(classType.getModifiers()) || - classType.isEnum()) { - throw new IllegalArgumentException(); - } - - return classType.getPackage().getName() + - "." + - classType.getSimpleName() + - classType.getName().hashCode() + - "I"; - } - private static void addProperties( final ClassVisitor cv, final MethodVisitor ctor, @@ -858,6 +844,7 @@ private static String getSignature(final Field field) { if (typeName.indexOf('<') != -1 && typeName.indexOf('>') != -1) { String signature = "()L" + typeName.replace(".", "/"); signature = signature.replace("<", "", ";>;"); return signature; } diff --git a/implementation/src/main/java/io/smallrye/config/Converters.java b/implementation/src/main/java/io/smallrye/config/Converters.java index 42056c132..f96dc365c 100644 --- a/implementation/src/main/java/io/smallrye/config/Converters.java +++ b/implementation/src/main/java/io/smallrye/config/Converters.java @@ -36,11 +36,13 @@ import java.util.OptionalLong; import java.util.UUID; import java.util.function.IntFunction; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.eclipse.microprofile.config.spi.Converter; +import io.smallrye.config.common.AbstractConverter; import io.smallrye.config.common.AbstractDelegatingConverter; import io.smallrye.config.common.AbstractSimpleDelegatingConverter; import io.smallrye.config.common.utils.StringUtil; @@ -301,6 +303,21 @@ public static Converter newArrayConverter(Converter itemC return new ArrayConverter<>(itemConverter, arrayType); } + /** + * Get a converter that converts content of type {@code =;=...} into + * a {@code Map} using the given key and value converters. + * + * @param keyConverter the converter used to convert the keys + * @param valueConverter the converter used to convert the values + * @param the type of the keys + * @param the type of the values + * @return the new converter (not {@code null}) + */ + public static Converter> newMapConverter(Converter keyConverter, + Converter valueConverter) { + return new MapConverter<>(keyConverter, valueConverter); + } + /** * Get a converter which wraps another converter's result into an {@code Optional}. If the delegate converter * returns {@code null}, this converter returns {@link Optional#empty()}. @@ -980,4 +997,62 @@ public T convert(final String value) { return getDelegate().convert(value.trim()); } } + + /** + * A converter for a Map knowing that the expected format is {@code =;=...}. + *

+ * The special characters {@code =} and {@code ;} can be used respectively in the key and in the value + * if they are escaped with a backslash. + *

+ * It will ignore properties whose key contains sub namespaces, in other words if the name of a property + * contains the special character {@code .} it will be ignored. + * + * @param The type of the key + * @param The type of the value + */ + static class MapConverter extends AbstractConverter> { + private static final long serialVersionUID = 4343545736186221103L; + + /** + * The regular expression used to extract the values of a map. + */ + private static final Pattern MAP_VALUES_PATTERN = Pattern.compile(";?(([^=]|(?<=\\\\)=)+)=(([^;]|(?<=\\\\);)+)"); + /** + * The converter to use the for keys. + */ + private final Converter keyConverter; + /** + * The converter to use the for values. + */ + private final Converter valueConverter; + + /** + * Construct a {@code MapConverter} with the given converters. + * + * @param keyConverter the converter to use the for keys + * @param valueConverter the converter to use the for values + */ + MapConverter(Converter keyConverter, Converter valueConverter) { + this.keyConverter = keyConverter; + this.valueConverter = valueConverter; + } + + @Override + public Map convert(String value) throws IllegalArgumentException, NullPointerException { + if (value == null) { + return null; + } + final Matcher matcher = MAP_VALUES_PATTERN.matcher(value); + final Map map = new HashMap<>(); + while (matcher.find()) { + final String key = matcher.group(1).replace("\\=", "="); + if (key.indexOf('.') >= 0) { + // Ignore sub namespaces + continue; + } + map.put(keyConverter.convert(key), valueConverter.convert(matcher.group(3).replace("\\;", ";"))); + } + return map.isEmpty() ? null : map; + } + } } diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java index 43438d88c..865974cf8 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java @@ -117,6 +117,7 @@ private Map> buildConverters(final SmallRyeConfigBuilder buil return converters; } + @Override public List getValues(final String propertyName, final Class propertyType) { return getValues(propertyName, propertyType, ArrayList::new); } @@ -191,13 +192,62 @@ public T getValue(String name, Class aClass) { return getValue(name, requireConverter(aClass)); } + /** + * Return the content of the direct sub properties as the requested type of Map. + * + * @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 + * @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. + */ + @Experimental("Extension to retrieve mandatory sub properties as a Map") + public Map getValues(String name, Class kClass, Class vClass) { + final Map result = getValuesAsMap(name, requireConverter(kClass), requireConverter(vClass)); + if (result == null) { + throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name)); + } + return result; + } + + /** + * 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 valueConverter The converter to use for 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 + */ + public Map getValuesAsMap(String name, Converter keyConverter, Converter valueConverter) { + final String prefix = name.endsWith(".") ? name : name + "."; + final Map result = new HashMap<>(); + for (String propertyName : getPropertyNames()) { + if (propertyName.startsWith(prefix)) { + final String key = propertyName.substring(prefix.length()); + if (key.indexOf('.') >= 0) { + // Ignore sub namespaces + continue; + } + result.put(convertValue(propertyName + "#key", key, keyConverter), + convertValue(propertyName + "#value", getRawValue(propertyName), valueConverter)); + } + } + return result.isEmpty() ? null : result; + } + /** * * This method handles calls from both {@link Config#getValue} and {@link Config#getOptionalValue}.
*/ @SuppressWarnings("unchecked") public T getValue(String name, Converter converter) { - ConfigValue configValue = getConfigValue(name); + final ConfigValue configValue = getConfigValue(name); if (ConfigValueConverter.CONFIG_VALUE_CONVERTER.equals(converter)) { return (T) configValue; } @@ -209,7 +259,7 @@ public T getValue(String name, Converter converter) { } } - String value = configValue.getValue(); // Can return the empty String (which is not considered as null) + final String value = configValue.getValue(); // Can return the empty String (which is not considered as null) return convertValue(name, value, converter); } @@ -299,6 +349,22 @@ public Optional getOptionalValue(String name, Class aClass) { return getValue(name, getOptionalConverter(aClass)); } + /** + * Return the content of the direct sub properties as the requested type of Map. + * + * @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 + * @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 + */ + @Experimental("Extension to retrieve non mandatory sub properties as a Map") + public Optional> getOptionalValues(String name, Class kClass, Class vClass) { + return Optional.ofNullable(getValuesAsMap(name, requireConverter(kClass), requireConverter(vClass))); + } + public Optional getOptionalValue(String name, Converter converter) { return getValue(name, Converters.newOptionalConverter(converter)); } @@ -441,7 +507,7 @@ public T unwrap(final Class type) { throw ConfigMessages.msg.getTypeNotSupportedForUnwrapping(type); } - @Experimental("To retrive active profiles") + @Experimental("To retrieve active profiles") public List getProfiles() { return configSources.getProfiles(); } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java index 6db39c15b..b51385239 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java @@ -6,16 +6,27 @@ import static io.smallrye.config.KeyValuesConfigSource.config; import static java.util.Collections.singleton; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + import org.eclipse.microprofile.config.inject.ConfigProperties; +import org.eclipse.microprofile.config.spi.Converter; import org.junit.jupiter.api.Test; public class ConfigMappingsTest { @Test void registerMapping() throws Exception { SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(config("server.host", "localhost", "server.port", "8080")) + .withSources(config("server.host", "localhost", "server.port", "8080", + "server.reasons.200", "OK", "server.reasons.201", "Created", + "server.versions.v1", "1.The version 1.2.3", + "server.versions.v2", "2.The version 2.0.0", + "server.numbers.one", "1", "server.numbers.two", "2", "server.numbers.three", "3")) + .withConverter(Version.class, 100, new VersionConverter()) .build(); registerConfigMappings(config, singleton(configClassWithPrefix(Server.class, "server"))); @@ -23,12 +34,27 @@ void registerMapping() throws Exception { assertEquals("localhost", server.host()); assertEquals(8080, server.port()); + assertEquals(2, server.reasons().size()); + assertEquals("OK", server.reasons().get(200)); + assertEquals("Created", server.reasons().get(201)); + assertEquals(2, server.versions().size()); + assertEquals(new Version(1, "The version 1.2.3"), server.versions().get("v1")); + assertEquals(new Version(2, "The version 2.0.0"), server.versions().get("v2")); + assertEquals(3, server.numbers().size()); + assertEquals(1, server.numbers().get("one")); + assertEquals(2, server.numbers().get("two")); + assertEquals(3, server.numbers().get("three")); } @Test void registerProperties() throws Exception { SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(config("server.host", "localhost", "server.port", "8080")) + .withSources(config("server.host", "localhost", "server.port", "8080", + "server.reasons.200", "OK", "server.reasons.201", "Created", + "server.versions.v1", "1.The version 1.2.3", + "server.versions.v2", "2.The version 2.0.0", + "server.numbers.one", "1", "server.numbers.two", "2", "server.numbers.three", "3")) + .withConverter(Version.class, 100, new VersionConverter()) .build(); registerConfigProperties(config, singleton(configClassWithPrefix(ServerClass.class, "server"))); @@ -36,6 +62,25 @@ void registerProperties() throws Exception { assertEquals("localhost", server.host); assertEquals(8080, server.port); + assertEquals(2, server.reasons.size()); + assertEquals("OK", server.reasons.get(200)); + assertEquals("Created", server.reasons.get(201)); + assertEquals(2, server.versions.size()); + assertEquals(new Version(1, "The version 1.2.3"), server.versions.get("v1")); + assertEquals(new Version(2, "The version 2.0.0"), server.versions.get("v2")); + assertEquals(3, server.numbers.size()); + assertEquals(1, server.numbers.get("one")); + assertEquals(2, server.numbers.get("two")); + assertEquals(3, server.numbers.get("three")); + + Map versions = config.getValues("server.versions", String.class, Version.class); + assertEquals(2, versions.size()); + assertEquals(new Version(1, "The version 1.2.3"), versions.get("v1")); + assertEquals(new Version(2, "The version 2.0.0"), versions.get("v2")); + + Optional> versionsOptional = config.getOptionalValues("optional.versions", String.class, + Version.class); + assertFalse(versionsOptional.isPresent()); } @Test @@ -60,11 +105,53 @@ interface Server { String host(); int port(); + + Map reasons(); + + Map versions(); + + Map numbers(); } @ConfigProperties(prefix = "server") static class ServerClass { String host; int port; + Map reasons; + Map versions; + Map numbers; + } + + static class Version { + int id; + String name; + + Version(int id, String name) { + this.id = id; + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Version version = (Version) o; + return id == version.id && Objects.equals(name, version.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + } + + static class VersionConverter implements Converter { + + @Override + public Version convert(String value) { + return new Version(Integer.parseInt(value.substring(0, 1)), value.substring(2)); + } } }