diff --git a/documentation/src/main/docs/config/mappings.md b/documentation/src/main/docs/config/mappings.md index 72fc127c6..f6729f900 100644 --- a/documentation/src/main/docs/config/mappings.md +++ b/documentation/src/main/docs/config/mappings.md @@ -469,6 +469,13 @@ keys. A `Map` mapping is backed by an `HashMap`. +When populating a `Map`, `SmallRyeConfig` requires the configuration names listed in +`SmallRyeConfig#getPropertyNames` to find the `Map` keys. If a `ConfigSource` does not support +`getPropertyNames` (empty), the names must be provided by another `ConfigSource` that can do so. After retrieving the +map keys, `SmallRyeConfig` performs the lookup of the values with the regular `ConfigSource` ordinal ordering. Even if +a `ConfigSource` does not provide `getPropertyNames` it can provide the value by having the name listed in another +capable `ConfigSource`. + For collection types, the key requires the indexed format. The configuration name `server.aliases.localhost[0].name` maps to the `Map> aliases()` member, where `localhost` is the `Map` key, `[0]` is the index of the `List` collection where the `Alias` element will be stored, containing the name `prod`. @@ -511,6 +518,44 @@ Map localhost = server.aliases.get("localhost"); If the unnamed key (in this case `localhost`) is explicitly set in a property name, the mapping will throw an error. +### `@WithKeys` + +The `io.smallrye.config.WithKeys` annotation allows to define which `Map` keys must be loaded by +the configuration: + +```java +@ConfigMapping(prefix = "server") +public interface Server { + @WithKeys(KeysProvider.class) + Map aliases(); + + interface Alias { + String name(); + } + + class KeysProvider implements Supplier> { + @Override + public Iterable get() { + return List.of("dev", "test", "prod"); + } + } +} +``` + +In this case, `SmallRyeConfig` will look for the map keys `dev`, `test` and `prod` instead of discovering the keys +with `SmallRyeConfig#getPropertyNames`: + +```properties +servers.alias.dev.name=dev +servers.alias.test.name=test +servers.alias.prod.name=prod +``` + +The provided list will effectively substitute the lookup in `SmallRyeConfig#getPropertyNames`, thus enabling a +`ConfigSource` that does not list its properties, to contribute configuration to the `Map`. Each key must exist in +the final configuration (relative to the `Map` path segment), or the mapping will fail with a +`ConfigValidationException`. + ### `@WithDefaults` The `io.smallrye.config.WithDefaults` is a marker annotation to use only in a `Map` to return the default value for diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java index 2f6818b39..aa94eb9be 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java @@ -1,21 +1,19 @@ package io.smallrye.config; -import static io.smallrye.config.ConfigMappingInterface.getNames; import static io.smallrye.config.ConfigValidationException.Problem; import static io.smallrye.config.ProfileConfigSourceInterceptor.activeName; import static io.smallrye.config.common.utils.StringUtil.unindexed; -import static java.util.Collections.EMPTY_MAP; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.UndeclaredThrowableException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -134,7 +132,7 @@ public NamingStrategy applyNamingStrategy(final NamingStrategy namingStrategy) { public String applyRootPath(final String rootPath) { this.rootPath = rootPath; - return rootPath; + return this.rootPath; } public StringBuilder applyNameBuilder(final String rootPath) { @@ -235,7 +233,7 @@ private void matchPropertiesWithEnv(final Map, Set> roots) { /** * Finds and returns all indexes from a dotted Environment property name, related to its matched mapped * property name that must be replaced with a dash. This allows to set single environment variables as - * FOO_BAR_BAZ and match them to mappeds properties like foo.*.baz, + * FOO_BAR_BAZ and match them to mapped properties like foo.*.baz, * foo-bar.baz or any other combinations find in mappings, without the need of additional metadata. * * @param mappedProperty the mapping property name. @@ -418,20 +416,22 @@ public void accept(Function get) { public ObjectCreator map( final Class keyRawType, final Class> keyConvertWith) { - return map(keyRawType, keyConvertWith, null); + return map(keyRawType, keyConvertWith, null, Collections.emptyList()); } public ObjectCreator map( final Class keyRawType, final Class> keyConvertWith, - final String unnamedKey) { - return map(keyRawType, keyConvertWith, unnamedKey, (Class) null); + final String unnamedKey, + final Iterable keys) { + return map(keyRawType, keyConvertWith, unnamedKey, keys, (Class) null); } public ObjectCreator map( final Class keyRawType, final Class> keyConvertWith, final String unnamedKey, + final Iterable keys, final Class defaultClass) { Supplier supplier = null; @@ -448,13 +448,14 @@ public V get() { }; } - return map(keyRawType, keyConvertWith, unnamedKey, supplier); + return map(keyRawType, keyConvertWith, unnamedKey, keys, supplier); } public ObjectCreator map( final Class keyRawType, final Class> keyConvertWith, final String unnamedKey, + final Iterable keys, final Supplier defaultValue) { Converter keyConverter = keyConvertWith == null ? config.requireConverter(keyRawType) : getConverterInstance(keyConvertWith); @@ -471,41 +472,56 @@ public Object apply(final String path) { public void accept(Function get) { V value = (V) get.apply(path); if (value != null) { - map.put(unnamedKey.equals("") ? null : keyConverter.convert(unnamedKey), value); + map.put(unnamedKey.isEmpty() ? null : keyConverter.convert(unnamedKey), value); } } }); } + // single map key with the path plus the map key + // the key is used in the resulting Map and the value in the nested creators to append nested elements paths Map mapKeys = new HashMap<>(); + // single map key with all property names that share the same key Map> mapProperties = new HashMap<>(); - for (String propertyName : config.getPropertyNames()) { - if (propertyName.length() > path.length() + 1 // only consider properties bigger than the map path - && (path.isEmpty() || propertyName.charAt(path.length()) == '.') // next char must be a dot (for the key) - && propertyName.startsWith(path)) { // the property must start with the map path - - // Start at the map root path - NameIterator mapProperty = !path.isEmpty() - ? new NameIterator(unindexed(propertyName), path.length()) - : new NameIterator(unindexed(propertyName)); - // Move to the next key - mapProperty.next(); - - String mapKey = unindexed(mapProperty.getPreviousSegment()); - mapKeys.computeIfAbsent(mapKey, new Function() { - @Override - public String apply(final String s) { - return unindexed(propertyName.substring(0, mapProperty.getPosition())); - } - }); - mapProperties.computeIfAbsent(mapKey, new Function>() { - @Override - public List apply(final String s) { - return new ArrayList<>(); - } - }); - mapProperties.get(mapKey).add(propertyName); + if (keys != null) { + for (String key : keys) { + if (key.isEmpty()) { + mapKeys.put(key, path); + } else { + mapKeys.put(key, path + "." + quoted(key)); + } + } + } + if (mapKeys.isEmpty()) { + for (String propertyName : config.getPropertyNames()) { + if (propertyName.length() > path.length() + 1 // only consider properties bigger than the map path + && (path.isEmpty() || propertyName.charAt(path.length()) == '.') // next char must be a dot (for the key) + && propertyName.startsWith(path)) { // the property must start with the map path + + // Start at the map root path + NameIterator mapProperty = !path.isEmpty() + ? new NameIterator(unindexed(propertyName), path.length()) + : new NameIterator(unindexed(propertyName)); + // Move to the next key + mapProperty.next(); + + String mapKey = unindexed(mapProperty.getPreviousSegment()); + mapKeys.computeIfAbsent(mapKey, new Function() { + @Override + public String apply(final String s) { + return unindexed(propertyName.substring(0, mapProperty.getPosition())); + } + }); + + mapProperties.computeIfAbsent(mapKey, new Function>() { + @Override + public List apply(final String s) { + return new ArrayList<>(); + } + }); + mapProperties.get(mapKey).add(propertyName); + } } } @@ -513,8 +529,10 @@ public List apply(final String s) { nestedCreators.add(new Consumer<>() { @Override public void accept(Function get) { - // The properties may have been used ih the unnamed key, which cause clashes, so we skip them - if (unnamedKey != null) { + // When we use the unnamed key empty and nested elements, we don't know if + // properties reference a nested element name or a named key. Since unnamed key + // creator runs first, we know which property names were used and skip those. + if (unnamedKey != null && !unnamedKey.isEmpty() && !mapProperties.isEmpty()) { boolean allUsed = true; for (String mapProperty : mapProperties.get(mapKey.getKey())) { if (!usedProperties.contains(mapProperty)) { @@ -733,38 +751,45 @@ public ObjectCreator values( final Class> keyConvertWith, final Class valueRawType, final Class> valueConvertWith, + final Iterable keys, final String defaultValue) { for (Consumer> creator : creators) { Function values = new Function<>() { @Override public Object apply(final String propertyName) { - usedProperties.add(propertyName); - usedProperties.addAll(config.getMapKeys(propertyName).values()); Converter keyConverter = getConverter(keyRawType, keyConvertWith); Converter valueConverter = getConverter(valueRawType, valueConvertWith); - if (defaultValue == null) { - // TODO - We should use getValues here, but this makes the Map to be required. This is a breaking change - try { - return config.getOptionalValues(propertyName, keyConverter, valueConverter, HashMap::new) - .orElse(EMPTY_MAP); - } catch (NoSuchElementException e) { // Can be thrown by MapConverter, but mappings shouldn't use inline map values - return EMPTY_MAP; + Map mapKeys = new HashMap<>(); + if (keys != null) { + for (String key : keys) { + mapKeys.put(key, propertyName + "." + quoted(key)); } - } else { - IntFunction> mapFactory = new IntFunction<>() { + } + if (mapKeys.isEmpty()) { + mapKeys = config.getMapKeys(propertyName); + } + + IntFunction> mapFactory; + if (defaultValue != null) { + mapFactory = new IntFunction<>() { @Override public Map apply(final int value) { return new MapWithDefault<>(valueConverter.convert(defaultValue)); } }; - try { - return config.getOptionalValues(propertyName, keyConverter, valueConverter, mapFactory) - .orElse(mapFactory.apply(0)); - } catch (NoSuchElementException e) { // Can be thrown by MapConverter, but mappings shouldn't use inline map values - return mapFactory.apply(0); - } + } else { + mapFactory = new IntFunction>() { + @Override + public Map apply(final int size) { + return new HashMap<>(size); + } + }; } + + usedProperties.add(propertyName); + usedProperties.addAll(mapKeys.values()); + return config.getMapValues(mapKeys, keyConverter, valueConverter, mapFactory); } }; creator.accept(values); @@ -778,24 +803,29 @@ public > ObjectCreator values( final Class valueRawType, final Class> valueConvertWith, final Class collectionRawType, + final Iterable keys, final String defaultValue) { for (Consumer> creator : creators) { Function values = new Function<>() { @Override public Object apply(final String propertyName) { - usedProperties.add(propertyName); - usedProperties.addAll(config.getMapKeys(propertyName).values()); Converter keyConverter = getConverter(keyRawType, keyConvertWith); Converter valueConverter = getConverter(valueRawType, valueConvertWith); - IntFunction collectionFactory = (IntFunction) createCollectionFactory(collectionRawType); + Map mapKeys = new HashMap<>(); + if (keys != null) { + for (String key : keys) { + mapKeys.put(key, propertyName + "." + quoted(key)); + } + } + if (mapKeys.isEmpty()) { + mapKeys = config.getMapIndexedKeys(propertyName); + } - if (defaultValue == null) { - // TODO - We should use getValues here, but this makes the Map to be required. This is a breaking change - return config.getOptionalValues(propertyName, keyConverter, valueConverter, HashMap::new, - collectionFactory).orElse(new HashMap<>()); - } else { - IntFunction> mapFactory = new IntFunction<>() { + IntFunction collectionFactory = (IntFunction) createCollectionFactory(collectionRawType); + IntFunction> mapFactory; + if (defaultValue != null) { + mapFactory = new IntFunction<>() { @Override public Map apply(final int value) { return new MapWithDefault<>( @@ -803,10 +833,20 @@ public Map apply(final int value) { .convert(defaultValue)); } }; - - return config.getOptionalValues(propertyName, keyConverter, valueConverter, mapFactory, - collectionFactory).orElse(mapFactory.apply(0)); + } else { + mapFactory = new IntFunction>() { + @Override + public Map apply(final int size) { + return new HashMap<>(size); + } + }; } + + usedProperties.add(propertyName); + usedProperties.addAll(mapKeys.values()); + // map keys can be indexed or unindexed, so we need to find which ones exist to mark them as used + usedProperties.addAll(config.getMapKeys(propertyName).values()); + return config.getMapIndexedValues(mapKeys, keyConverter, valueConverter, mapFactory, collectionFactory); } }; creator.accept(values); @@ -837,6 +877,12 @@ private IntFunction> createCollectionFactory(final Class type) throw new IllegalArgumentException(); } + + private String quoted(final String key) { + NameIterator keyIterator = new NameIterator(key); + keyIterator.next(); + return keyIterator.hasNext() ? "\"" + key + "\"" : key; + } } static class MapWithDefault extends HashMap { diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java index c834d7164..7992604d4 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java @@ -61,6 +61,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import java.util.regex.Pattern; import org.eclipse.microprofile.config.inject.ConfigProperties; @@ -94,15 +95,16 @@ public class ConfigMappingGenerator { } private static final String I_CLASS = getInternalName(Class.class); + private static final String I_FIELD = getInternalName(Field.class); private static final String I_CONFIGURATION_OBJECT = getInternalName(ConfigMappingObject.class); private static final String I_MAPPING_CONTEXT = getInternalName(ConfigMappingContext.class); + private static final String I_NAMING_STRATEGY = getInternalName(NamingStrategy.class); private static final String I_OBJECT_CREATOR = getInternalName(ConfigMappingContext.ObjectCreator.class); private static final String I_OBJECT = getInternalName(Object.class); private static final String I_RUNTIME_EXCEPTION = getInternalName(RuntimeException.class); private static final String I_STRING_BUILDER = getInternalName(StringBuilder.class); private static final String I_STRING = getInternalName(String.class); - private static final String I_NAMING_STRATEGY = getInternalName(NamingStrategy.class); - private static final String I_FIELD = getInternalName(Field.class); + private static final String I_ITERABLE = getInternalName(Iterable.class); private static final int V_THIS = 0; private static final int V_MAPPING_CONTEXT = 1; @@ -519,13 +521,18 @@ private static void generateProperty(final MethodVisitor ctor, final Property pr } else { ctor.visitInsn(ACONST_NULL); } + if (mapProperty.hasKeyProvider()) { + generateMapKeysProvider(ctor, mapProperty.getKeysProvider()); + } else { + ctor.visitInsn(ACONST_NULL); + } if (mapProperty.hasDefaultValue() && mapProperty.getDefaultValue() != null) { ctor.visitLdcInsn(mapProperty.getDefaultValue()); } else { ctor.visitInsn(ACONST_NULL); } ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "values", "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_CLASS - + ";L" + I_CLASS + ";L" + I_STRING + ";)L" + I_OBJECT_CREATOR + ";", false); + + ";L" + I_CLASS + ";L" + I_ITERABLE + ";L" + I_STRING + ";)L" + I_OBJECT_CREATOR + ";", false); } else if (valueProperty.isGroup()) { ctor.visitLdcInsn(getType(mapProperty.getKeyRawType())); if (mapProperty.hasKeyConvertWith()) { @@ -538,17 +545,28 @@ private static void generateProperty(final MethodVisitor ctor, final Property pr } else { ctor.visitInsn(ACONST_NULL); } + if (mapProperty.hasKeyProvider()) { + generateMapKeysProvider(ctor, mapProperty.getKeysProvider()); + } else { + ctor.visitInsn(ACONST_NULL); + } if (mapProperty.hasDefaultValue()) { ctor.visitLdcInsn(getType(valueProperty.asGroup().getGroupType().getInterfaceType())); } else { ctor.visitInsn(ACONST_NULL); } ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "map", - "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_STRING + ";L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", + "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_STRING + ";L" + I_ITERABLE + ";L" + I_CLASS + ";)L" + + I_OBJECT_CREATOR + ";", false); ctor.visitLdcInsn(getType(valueProperty.asGroup().getGroupType().getInterfaceType())); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "lazyGroup", - "(L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false); + if (mapProperty.hasKeyProvider()) { + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "group", + "(L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false); + } else { + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "lazyGroup", + "(L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false); + } } else if (valueProperty.isCollection() && valueProperty.asCollection().getElement().isLeaf()) { ctor.visitLdcInsn(getType(mapProperty.getKeyRawType())); if (mapProperty.hasKeyConvertWith()) { @@ -564,13 +582,21 @@ private static void generateProperty(final MethodVisitor ctor, final Property pr ctor.visitInsn(ACONST_NULL); } ctor.visitLdcInsn(getType(mapProperty.getValueProperty().asCollection().getCollectionRawType())); + if (mapProperty.hasKeyProvider()) { + generateMapKeysProvider(ctor, mapProperty.getKeysProvider()); + } else { + ctor.visitInsn(ACONST_NULL); + } if (mapProperty.hasDefaultValue()) { ctor.visitLdcInsn(mapProperty.getDefaultValue()); } else { ctor.visitInsn(ACONST_NULL); } - ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "values", "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_CLASS - + ";L" + I_CLASS + ";L" + I_CLASS + ";L" + I_STRING + ";)L" + I_OBJECT_CREATOR + ";", false); + ctor.visitMethodInsn( + INVOKEVIRTUAL, I_OBJECT_CREATOR, "values", "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_CLASS + ";L" + + I_CLASS + ";L" + I_CLASS + ";L" + I_ITERABLE + ";L" + I_STRING + ";)L" + I_OBJECT_CREATOR + + ";", + false); } else { unwrapProperty(ctor, property); } @@ -635,8 +661,14 @@ private static void unwrapProperty(final MethodVisitor ctor, final Property prop } else { ctor.visitInsn(ACONST_NULL); } + if (mapProperty.hasKeyProvider()) { + generateMapKeysProvider(ctor, mapProperty.getKeysProvider()); + } else { + ctor.visitInsn(ACONST_NULL); + } ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "map", - "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_STRING + ";)L" + I_OBJECT_CREATOR + ";", false); + "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_STRING + ";L" + I_ITERABLE + ";)L" + I_OBJECT_CREATOR + ";", + false); generateProperty(ctor, mapProperty.getValueProperty()); } else if (property.isCollection()) { CollectionProperty collectionProperty = property.asCollection(); @@ -649,6 +681,15 @@ private static void unwrapProperty(final MethodVisitor ctor, final Property prop } } + private static void generateMapKeysProvider(final MethodVisitor ctor, + final Class>> mapKeysProvider) { + String provider = getInternalName(mapKeysProvider); + ctor.visitTypeInsn(NEW, provider); + ctor.visitInsn(DUP); + ctor.visitMethodInsn(INVOKESPECIAL, provider, "", "()V", false); + ctor.visitMethodInsn(INVOKEVIRTUAL, provider, "get", "()L" + I_ITERABLE + ";", false); + } + private static void appendPropertyName(final MethodVisitor ctor, final Property property) { if (property.isParentPropertyName()) { return; diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java index b34d6e9c8..7edc1a368 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java @@ -19,6 +19,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Supplier; import org.eclipse.microprofile.config.spi.Converter; @@ -32,7 +33,7 @@ public final class ConfigMappingInterface implements ConfigMappingMetadata { static final ConfigMappingInterface[] NO_TYPES = new ConfigMappingInterface[0]; static final Property[] NO_PROPERTIES = new Property[0]; - static final ClassValue cv = new ClassValue() { + static final ClassValue cv = new ClassValue<>() { protected ConfigMappingInterface computeValue(final Class type) { return createConfigurationInterface(type); } @@ -594,6 +595,7 @@ public LeafProperty asLeaf() { public static final class MapProperty extends Property { private final Type keyType; private final String keyUnnamed; + private final Class>> keysProvider; private final Class> keyConvertWith; private final Property valueProperty; private final boolean hasDefault; @@ -604,6 +606,7 @@ public static final class MapProperty extends Property { final String propertyName, final Type keyType, final String keyUnnamed, + final Class>> keysProvider, final Class> keyConvertWith, final Property valueProperty, final boolean hasDefault, @@ -612,6 +615,7 @@ public static final class MapProperty extends Property { super(method, propertyName); this.keyType = keyType; this.keyUnnamed = keyUnnamed; + this.keysProvider = keysProvider; this.keyConvertWith = keyConvertWith; this.valueProperty = valueProperty; this.hasDefault = hasDefault; @@ -634,6 +638,14 @@ public boolean hasKeyUnnamed() { return keyUnnamed != null; } + public Class>> getKeysProvider() { + return Assert.checkNotNullParam("keyProvider", keysProvider); + } + + public boolean hasKeyProvider() { + return keysProvider != null; + } + public Class> getKeyConvertWith() { return Assert.checkNotNullParam("keyConvertWith", keyConvertWith); } @@ -846,9 +858,14 @@ private static Property getPropertyDef(Method method, AnnotatedType type) { AnnotatedType keyType = typeOfParameter(type, 0); AnnotatedType valueType = typeOfParameter(type, 1); String defaultValue = getDefaultValue(method); - return new MapProperty(method, propertyName, keyType.getType(), getUnnamedKey(keyType, method), - getConverter(keyType, method), getPropertyDef(method, valueType), - defaultValue != null || hasDefaults(method), defaultValue); + return new MapProperty(method, + propertyName, keyType.getType(), + getUnnamedKey(keyType, method), + getKeysProvider(keyType, method), + getConverter(keyType, method), + getPropertyDef(method, valueType), + defaultValue != null || hasDefaults(method), + defaultValue); } if (rawType == List.class || rawType == Set.class) { AnnotatedType elementType = typeOfParameter(type, 0); @@ -940,6 +957,14 @@ private static String getUnnamedKey(final AnnotatedType type, final Method metho return annotation != null ? annotation.value() : null; } + private static Class>> getKeysProvider(final AnnotatedType type, final Method method) { + WithKeys annotation = type.getAnnotation(WithKeys.class); + if (annotation == null) { + annotation = method.getAnnotation(WithKeys.class); + } + return annotation != null ? annotation.value() : null; + } + private static Class> getConverter(final AnnotatedType type, final Method method) { WithConverter annotation = type.getAnnotation(WithConverter.class); // fallback to method diff --git a/implementation/src/main/java/io/smallrye/config/NameIterator.java b/implementation/src/main/java/io/smallrye/config/NameIterator.java index 245905967..36695ef60 100644 --- a/implementation/src/main/java/io/smallrye/config/NameIterator.java +++ b/implementation/src/main/java/io/smallrye/config/NameIterator.java @@ -329,12 +329,6 @@ public void next() { pos = getNextEnd(); } - public void next(int segments) { - for (int i = 0; i < segments; i++) { - next(); - } - } - public void previous() { pos = getPreviousStart() - 1; } diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java index a55fbfa86..92559d2d9 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java @@ -185,10 +185,11 @@ public > C getValues(String name, Class itemClass, public > C getValues(String name, Converter converter, IntFunction collectionFactory) { List indexedProperties = getIndexedProperties(name); - if (!indexedProperties.isEmpty()) { - return getIndexedValues(indexedProperties, converter, collectionFactory); + if (indexedProperties.isEmpty()) { + // Try legacy / MP comma separated values + return getValue(name, newCollectionConverter(converter, collectionFactory)); } - return getValue(name, newCollectionConverter(converter, collectionFactory)); + return getIndexedValues(indexedProperties, converter, collectionFactory); } public > C getIndexedValues(String name, Converter converter, @@ -283,21 +284,14 @@ public Map getValues( Converter keyConverter, Converter valueConverter, IntFunction> mapFactory) { - try { - return getValue(name, newMapConverter(keyConverter, valueConverter, mapFactory)); - } catch (NoSuchElementException e) { - Map mapKeys = getMapKeys(name); - if (mapKeys.isEmpty()) { - throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name)); - } - Map map = mapFactory.apply(mapKeys.size()); - for (Map.Entry entry : mapKeys.entrySet()) { - map.put(convertValue(ConfigValue.builder().withName(entry.getKey()).withValue(entry.getKey()).build(), - keyConverter), getValue(entry.getValue(), valueConverter)); - } - return map; + Map keys = getMapKeys(name); + if (keys.isEmpty()) { + // Try legacy MapConverter + return getValue(name, newMapConverter(keyConverter, valueConverter, mapFactory)); } + + return getMapValues(keys, keyConverter, valueConverter, mapFactory); } public > Map getValues( @@ -314,49 +308,77 @@ public > Map getValues( Converter valueConverter, IntFunction> mapFactory, IntFunction collectionFactory) { - try { + + Map keys = getMapIndexedKeys(name); + if (keys.isEmpty()) { + // Try legacy MapConverter return getValue(name, newMapConverter(keyConverter, newCollectionConverter(valueConverter, collectionFactory), mapFactory)); - } catch (NoSuchElementException e) { - Map mapCollectionKeys = getMapIndexedKeys(name); - if (mapCollectionKeys.isEmpty()) { - throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name)); - } - - Map map = mapFactory.apply(mapCollectionKeys.size()); - for (Map.Entry entry : mapCollectionKeys.entrySet()) { - map.put(convertValue(ConfigValue.builder().withName(entry.getKey()).withValue(entry.getKey()).build(), - keyConverter), getValues(entry.getValue(), valueConverter, collectionFactory)); - } - return map; } + + return getMapIndexedValues(keys, keyConverter, valueConverter, mapFactory, collectionFactory); } public Map getMapKeys(final String name) { - Map mapKeys = new HashMap<>(); + Map keys = new HashMap<>(); for (String propertyName : getPropertyNames()) { if (propertyName.length() > name.length() + 1 && (name.isEmpty() || propertyName.charAt(name.length()) == '.') && propertyName.startsWith(name)) { String key = unquoted(propertyName, name.isEmpty() ? 0 : name.length() + 1); - mapKeys.put(key, propertyName); + keys.put(key, propertyName); } } - return mapKeys; + return keys; + } + + Map getMapValues( + final Map keys, + final Converter keyConverter, + final Converter valueConverter, + final IntFunction> mapFactory) { + + Map map = mapFactory.apply(keys.size()); + for (Map.Entry entry : keys.entrySet()) { + // Use ConfigValue when converting the key to have proper error messages with the key name + K key = convertValue(ConfigValue.builder().withName(entry.getKey()).withValue(entry.getKey()).build(), + keyConverter); + V value = getValue(entry.getValue(), valueConverter); + map.put(key, value); + } + return map; } public Map getMapIndexedKeys(final String name) { - Map mapKeys = new HashMap<>(); + Map keys = new HashMap<>(); for (String propertyName : getPropertyNames()) { if (propertyName.length() > name.length() + 1 && (name.isEmpty() || propertyName.charAt(name.length()) == '.') && propertyName.startsWith(name)) { String unindexedName = unindexed(propertyName); String key = unquoted(unindexedName, name.isEmpty() ? 0 : name.length() + 1); - mapKeys.put(key, unindexedName); + keys.put(key, unindexedName); } } - return mapKeys; + return keys; + } + + > Map getMapIndexedValues( + final Map keys, + final Converter keyConverter, + final Converter valueConverter, + final IntFunction> mapFactory, + final IntFunction collectionFactory) { + + Map map = mapFactory.apply(keys.size()); + for (Map.Entry entry : keys.entrySet()) { + // Use ConfigValue when converting the key to have proper error messages with the key name + K key = convertValue(ConfigValue.builder().withName(entry.getKey()).withValue(entry.getKey()).build(), + keyConverter); + C value = getValues(entry.getValue(), valueConverter, collectionFactory); + map.put(key, value); + } + return map; } @Override @@ -549,12 +571,13 @@ public Optional> getOptionalValues(String name, Converter ke public Optional> getOptionalValues(String name, Converter keyConverter, Converter valueConverter, IntFunction> mapFactory) { - Map mapKeys = getMapKeys(name); - if (!mapKeys.isEmpty()) { - return Optional.of(getValues(name, keyConverter, valueConverter, mapFactory)); + Map keys = getMapKeys(name); + if (keys.isEmpty()) { + // Try legacy MapConverter + return getOptionalValue(name, newMapConverter(keyConverter, valueConverter, mapFactory)); } - return getOptionalValue(name, newMapConverter(keyConverter, valueConverter, mapFactory)); + return Optional.of(getMapValues(keys, keyConverter, valueConverter, mapFactory)); } public > Optional> getOptionalValues( @@ -572,17 +595,15 @@ public > Optional> getOptionalValues( Converter valueConverter, IntFunction> mapFactory, IntFunction collectionFactory) { - Optional> optionalValue = getOptionalValue(name, - newMapConverter(keyConverter, newCollectionConverter(valueConverter, collectionFactory), mapFactory)); - if (optionalValue.isPresent()) { - return optionalValue; - } - Map mapKeys = getMapIndexedKeys(name); - if (mapKeys.isEmpty()) { - return Optional.empty(); + Map keys = getMapIndexedKeys(name); + if (keys.isEmpty()) { + // Try legacy MapConverter + return getOptionalValue(name, + newMapConverter(keyConverter, newCollectionConverter(valueConverter, collectionFactory), mapFactory)); } - return Optional.of(getValues(name, keyConverter, valueConverter, mapFactory, collectionFactory)); + + return Optional.of(getMapIndexedValues(keys, keyConverter, valueConverter, mapFactory, collectionFactory)); } Map, Map> getMappings() { diff --git a/implementation/src/main/java/io/smallrye/config/WithKeys.java b/implementation/src/main/java/io/smallrye/config/WithKeys.java new file mode 100644 index 000000000..7605e5caa --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/WithKeys.java @@ -0,0 +1,39 @@ +package io.smallrye.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Supplier; + +/** + * Provide a list of map keys when populating {@link java.util.Map} types. + *

+ * When populating a {@link java.util.Map}, {@link SmallRyeConfig} requires the configuration names listed in + * {@link SmallRyeConfig#getPropertyNames()} to be able to find the {@link java.util.Map} keys. The provided list will + * effectively substitute the lookup in {@link SmallRyeConfig#getPropertyNames()}, thus enabling a + * {@link org.eclipse.microprofile.config.spi.ConfigSource} that does not list its properties, to contribute + * configuration to the {@link java.util.Map}. + *

+ * Each key must exist in the final configuration (relative to the {@link java.util.Map} path segment), or the mapping + * will fail with a {@link ConfigValidationException}. + *

+ * In the case of {@link java.util.Map} value references a {@link java.util.Collection}, {@link SmallRyeConfig} would + * still require the lookup in {@link SmallRyeConfig#getPropertyNames()}. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE_USE }) +public @interface WithKeys { + /** + * A {@link Class} implementing a {@link Supplier} of {@link Iterable} with the {@link java.util.Map} keys to look + * in the configuration. Keys containing a dot are quoted. + *

+ * The {@link Supplier} is instantiated when mapping the {@link java.util.Map}. It may be instanciated multiple + * times if the {@link Class} is used across multiple {@link WithKeys}. + * + * @return A {@link Class} implementing a {@link Supplier} of {@link Iterable} of {@link String} keys + */ + Class>> value(); +} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java index d097f75d5..1e236277d 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java @@ -15,6 +15,7 @@ import java.util.stream.Stream; import org.eclipse.microprofile.config.spi.Converter; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import io.smallrye.config.ConfigMappingCollectionsTest.ServerCollectionsSet.Environment; @@ -1031,4 +1032,24 @@ interface Nested { List list(); } } + + @Test + @Disabled(value = "@WithUnnamedKey not implemented for leaf Maps") + void withUnnamedLeaf() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withDefaultValue("unnamed.leaf", "unnamed") + .withDefaultValue("unnamed.leaf.one", "one") + .withMapping(WithUnnamedLeaf.class) + .build(); + + WithUnnamedLeaf mapping = config.getConfigMapping(WithUnnamedLeaf.class); + assertEquals("unnamed", mapping.leaf().get(null)); + assertEquals("one", mapping.leaf().get("one")); + } + + @ConfigMapping(prefix = "unnamed") + interface WithUnnamedLeaf { + //@WithUnnamedKey + Map leaf(); + } } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java index c77692931..e4295a9de 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java @@ -597,13 +597,13 @@ interface Maps { Map groupParentName(); } - public interface ComplexSample { + interface ComplexSample { ServerSub server(); Optional client(); } - public interface Converters { + interface Converters { @WithConverter(FooBarConverter.class) String foo(); diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingWithKeysTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingWithKeysTest.java new file mode 100644 index 000000000..73d9df305 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingWithKeysTest.java @@ -0,0 +1,303 @@ +package io.smallrye.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import io.smallrye.config.common.MapBackedConfigSource; + +class ConfigMappingWithKeysTest { + @Test + void withKeys() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new MapBackedConfigSource("", Map.of( + "map.leaf.one", "one", + "map.leaf.two", "two", + "map.leaf.dashed-key", "dashed-value", + "map.leaf.\"dotted.key\"", "dotted.value")) { + @Override + public Set getPropertyNames() { + return Collections.emptySet(); + } + }) + .withSources(new MapBackedConfigSource("", Map.of( + "map.nested.one.value", "one", + "map.nested.two.value", "two", + "map.nested.dashed-key.value", "dashed-value", + "map.nested.\"dotted.key\".value", "dotted.value")) { + @Override + public Set getPropertyNames() { + return Collections.emptySet(); + } + }) + .withSources(new MapBackedConfigSource("", Map.of( + "map.leaf-list.one[0]", "one", + "map.leaf-list.two[0]", "two", + "map.leaf-list.dashed-key[0]", "dashed-value", + "map.leaf-list.\"dotted.key\"[0]", "dotted.value")) { + @Override + public Set getPropertyNames() { + // indexed properties still need to query property names, because indexes are also dynamic + return super.getPropertyNames(); + } + }) + .withSources(new MapBackedConfigSource("", Map.of( + "map.nested-list.one[0].value", "one", + "map.nested-list.two[0].value", "two", + "map.nested-list.dashed-key[0].value", "dashed-value", + "map.nested-list.\"dotted.key\"[0].value", "dotted.value")) { + @Override + public Set getPropertyNames() { + // indexed properties still need to query property names, because indexes are also dynamic + return super.getPropertyNames(); + } + }) + .withMapping(WithMapKeys.class) + .build(); + + WithMapKeys mapping = config.getConfigMapping(WithMapKeys.class); + + assertEquals("one", mapping.leaf().get("one")); + assertEquals("two", mapping.leaf().get("two")); + assertEquals("dashed-value", mapping.leaf().get("dashed-key")); + assertEquals("dotted.value", mapping.leaf().get("dotted.key")); + + assertEquals("one", mapping.nested().get("one").value()); + assertEquals("two", mapping.nested().get("two").value()); + assertEquals("dashed-value", mapping.nested().get("dashed-key").value()); + assertEquals("dotted.value", mapping.nested().get("dotted.key").value()); + + assertEquals("one", mapping.leafList().get("one").get(0)); + assertEquals("two", mapping.leafList().get("two").get(0)); + assertEquals("dashed-value", mapping.leafList().get("dashed-key").get(0)); + assertEquals("dotted.value", mapping.leafList().get("dotted.key").get(0)); + + assertEquals("one", mapping.nestedList().get("one").get(0).value()); + assertEquals("two", mapping.nestedList().get("two").get(0).value()); + assertEquals("dashed-value", mapping.nestedList().get("dashed-key").get(0).value()); + assertEquals("dotted.value", mapping.nestedList().get("dotted.key").get(0).value()); + + // TODO - Implement remaining pieces + // - docs + } + + @ConfigMapping(prefix = "map") + interface WithMapKeys { + @WithKeys(KeysProvider.class) + Map leaf(); + + @WithKeys(KeysProvider.class) + Map nested(); + + @WithKeys(KeysProvider.class) + Map> leafList(); + + @WithKeys(KeysProvider.class) + Map> nestedList(); + + interface Nested { + String value(); + } + + class KeysProvider implements Supplier> { + @Override + public Iterable get() { + return List.of("one", "two", "dashed-key", "dotted.key"); + } + } + } + + @Test + void requiredKeys() { + assertThrows(ConfigValidationException.class, () -> new SmallRyeConfigBuilder() + .withDefaultValue("required.nested.required.value", "required") + .withMapping(EmptyKey.class).build()); + + assertThrows(ConfigValidationException.class, () -> new SmallRyeConfigBuilder() + .withDefaultValue("required.leaf.required", "required") + .withMapping(EmptyKey.class).build()); + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withDefaultValue("required.nested.required.value", "required") + .withDefaultValue("required.leaf.required", "required") + .withMapping(RequiredKeys.class) + .build(); + + RequiredKeys mapping = config.getConfigMapping(RequiredKeys.class); + assertEquals("required", mapping.leaf().get("required")); + assertEquals("required", mapping.nested().get("required").value()); + } + + @ConfigMapping(prefix = "required") + interface RequiredKeys { + @WithKeys(RequiredKeysProvider.class) + Map leaf(); + + @WithKeys(RequiredKeysProvider.class) + Map nested(); + + interface Nested { + String value(); + } + + class RequiredKeysProvider implements Supplier> { + @Override + public Iterable get() { + return List.of("required"); + } + } + } + + @Test + void emptyKey() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withDefaultValue("empty.nested.value", "value") + .withMapping(EmptyKey.class) + .build(); + + EmptyKey mapping = config.getConfigMapping(EmptyKey.class); + assertEquals("value", mapping.nested().get(null).value()); + } + + @ConfigMapping(prefix = "empty") + interface EmptyKey { + @WithKeys(EmptyKeyProdiver.class) + Map nested(); + + interface Nested { + String value(); + } + + class EmptyKeyProdiver implements Supplier> { + @Override + public Iterable get() { + return List.of(""); + } + } + } + + @Test + void emptyKeyWithUnnamedEmpty() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withDefaultValue("empty.nested.value", "value") + .withMapping(EmptyKeyWithUnnamedEmpty.class) + .build(); + + EmptyKeyWithUnnamedEmpty mapping = config.getConfigMapping(EmptyKeyWithUnnamedEmpty.class); + assertEquals("value", mapping.nested().get(null).value()); + } + + @ConfigMapping(prefix = "empty") + interface EmptyKeyWithUnnamedEmpty { + @WithUnnamedKey + @WithKeys(EmptyKeyProdiver.class) + Map nested(); + + interface Nested { + String value(); + } + + class EmptyKeyProdiver implements Supplier> { + @Override + public Iterable get() { + return List.of(""); + } + } + } + + @Test + void emptyKeyWithUnnamedDefault() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withDefaultValue("empty.nested.value", "value") + .withMapping(EmptyKeyWithUnnamedDefault.class) + .build(); + + EmptyKeyWithUnnamedDefault mapping = config.getConfigMapping(EmptyKeyWithUnnamedDefault.class); + assertEquals("value", mapping.nested().get(null).value()); + assertEquals("value", mapping.nested().get("default").value()); + } + + @ConfigMapping(prefix = "empty") + interface EmptyKeyWithUnnamedDefault { + @WithUnnamedKey("default") + @WithKeys(EmptyKeyProdiver.class) + Map nested(); + + interface Nested { + String value(); + } + + class EmptyKeyProdiver implements Supplier> { + @Override + public Iterable get() { + return List.of(""); + } + } + } + + @Test + void keysWithParentName() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new MapBackedConfigSource("", Map.of("parent.one.value", "one")) { + @Override + public Set getPropertyNames() { + return Collections.emptySet(); + } + }) + .withMapping(KeysWithParentName.class) + .build(); + + KeysWithParentName mapping = config.getConfigMapping(KeysWithParentName.class); + assertEquals("one", mapping.nested().get("one").value()); + } + + @ConfigMapping(prefix = "parent") + interface KeysWithParentName { + @WithParentName + @WithKeys(KeysProvider.class) + Map nested(); + + interface Nested { + String value(); + } + + class KeysProvider implements Supplier> { + @Override + public Iterable get() { + return List.of("one"); + } + } + } + + @Test + void additionalKeys() { + SmallRyeConfigBuilder builder = new SmallRyeConfigBuilder() + .withSources(new MapBackedConfigSource("", Map.of( + "additional.leaf.one", "one", "additional.leaf.two", "two")) { + }) + .withMapping(AdditionalKeys.class); + + // additional.leaf.two in does not map to any root + assertThrows(ConfigValidationException.class, builder::build); + } + + @ConfigMapping(prefix = "additional") + interface AdditionalKeys { + @WithKeys(KeysProvider.class) + Map leaf(); + + class KeysProvider implements Supplier> { + @Override + public Iterable get() { + return List.of("one"); + } + } + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java index 04cd356f8..50fbaee87 100644 --- a/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java +++ b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java @@ -2,6 +2,7 @@ import static io.smallrye.config.KeyValuesConfigSource.config; import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN; +import static java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -129,14 +130,14 @@ public ObjectCreatorImpl(ConfigMappingContext context) { sb.append(ns.apply("unnamed")); ConfigMappingContext.ObjectCreator> unnamed = context.new ObjectCreator>( sb.toString()) - .map(String.class, null, "unnamed") + .map(String.class, null, "unnamed", null) .lazyGroup(Nested.class); this.unnamed = unnamed.get(); sb.setLength(length); sb.append(ns.apply("list-map")); ConfigMappingContext.ObjectCreator>> listMap = context.new ObjectCreator>>( - sb.toString()).collection(List.class).values(String.class, null, String.class, null, null); + sb.toString()).collection(List.class).values(String.class, null, String.class, null, emptyList(), null); this.listMap = listMap.get(); sb.setLength(length); @@ -368,7 +369,7 @@ public UnnamedKeysImpl(ConfigMappingContext context) { ConfigMappingContext.ObjectCreator> map = context.new ObjectCreator>( sb.toString()) - .map(String.class, null, "") + .map(String.class, null, "", null) .lazyGroup(Nested.class); this.map = map.get(); sb.setLength(length); @@ -449,14 +450,14 @@ public MapDefaultsImpl(ConfigMappingContext context) { sb.append(ns.apply("defaults")); ConfigMappingContext.ObjectCreator> defaults = context.new ObjectCreator>( sb.toString()) - .values(String.class, null, String.class, null, "default"); + .values(String.class, null, String.class, null, emptyList(), "default"); this.defaults = defaults.get(); sb.setLength(length); sb.append(ns.apply("defaults-nested")); ConfigMappingContext.ObjectCreator> defaultsNested = context.new ObjectCreator>( sb.toString()) - .map(String.class, null, null, new Supplier() { + .map(String.class, null, null, null, new Supplier() { @Override public Nested get() { sb.append(".*");