diff --git a/documentation/src/main/docs/config/mappings.md b/documentation/src/main/docs/config/mappings.md index c3b29c183..a34067211 100644 --- a/documentation/src/main/docs/config/mappings.md +++ b/documentation/src/main/docs/config/mappings.md @@ -368,20 +368,25 @@ public interface Server { ```properties server.host=localhost server.port=8080 -server.form.login-page=login.html -server.form.error-page=error.html -server.form.landing-page=index.html +server.form.index=index.html +server.form.login.page=login.html +server.form.error.page=error.html server.aliases.localhost[0].name=prod server.aliases.localhost[1].name=127.0.0.1 +server.aliases.\"io.smallrye\"[0].name=smallrye ``` The configuration property name needs to specify an additional segment to act as the map key. The `server.form` matches -the `Server#form` `Map` and the segments `login-page`, `error-page` and `landing-page` represent the `Map` +the `Server#form` `Map` and the segments `index`, `login.page` and `error.page` represent the `Map` keys. 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` where the `Alias` element will be stored, containing the name `prod`. +`List` collection where the `Alias` element will be stored, containing the name `prod`. + +!!! info + + They `Map` key part in the configuration property name may require quotes to delimit the key. ## Defaults diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java index 666f6a703..08432e98f 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java @@ -45,7 +45,7 @@ final class ConfigMappingProvider implements Serializable { private static final KeyMap> IGNORE_EVERYTHING; static { - final KeyMap> map = new KeyMap<>(); + KeyMap> map = new KeyMap<>(); map.putRootValue(DO_NOTHING); //noinspection CollectionAddedToSelf map.putAny(map); @@ -103,10 +103,13 @@ static void ignoreRecursively(KeyMap> value : root.values()) { + for (var value : root.values()) { ignoreRecursively(value); } } @@ -148,6 +151,13 @@ private void processEagerGroup( final ConfigMappingInterface group, final BiFunction getEnclosingFunction) { + // Register super types first. The main mapping will override methods from the super types + int sc = group.getSuperTypeCount(); + for (int i = 0; i < sc; i++) { + processEagerGroup(currentPath, matchActions, defaultValues, namingStrategy, group.getSuperType(i), + getEnclosingFunction); + } + Class type = group.getInterfaceType(); HashSet usedProperties = new HashSet<>(); for (int i = 0; i < group.getPropertyCount(); i++) { @@ -168,11 +178,6 @@ private void processEagerGroup( memberName, property); } } - int sc = group.getSuperTypeCount(); - for (int i = 0; i < sc; i++) { - processEagerGroup(currentPath, matchActions, defaultValues, namingStrategy, group.getSuperType(i), - getEnclosingFunction); - } } private void processProperty( @@ -287,7 +292,6 @@ private void processLazyGroupInGroup( final ConfigMappingInterface group, final BiConsumer matchAction, final HashSet usedProperties) { - int pc = group.getPropertyCount(); int pathLen = currentPath.size(); for (int i = 0; i < pc; i++) { @@ -300,12 +304,6 @@ private void processLazyGroupInGroup( ni.next(); } } - if (matchActions.hasRootValue(currentPath)) { - while (currentPath.size() > pathLen) { - currentPath.removeLast(); - } - continue; - } if (usedProperties.add(String.join(".", String.join(".", currentPath), property.getMethod().getName()))) { boolean optional = property.isOptional(); processLazyPropertyInGroup(currentPath, matchActions, defaultValues, matchAction, usedProperties, @@ -445,45 +443,32 @@ private void processLazyMapValue( final ConfigMappingInterface enclosingGroup) { if (property.isLeaf()) { - if (matchActions.hasRootValue(currentPath)) { - currentPath.removeLast(); - return; - } - LeafProperty leafProperty = property.asLeaf(); Class> valConvertWith = leafProperty.getConvertWith(); Class valueRawType = leafProperty.getValueRawType(); + String mapPath = String.join(".", currentPath); addAction(currentPath, mapProperty, (mc, ni) -> { - StringBuilder sb = mc.getStringBuilder(); - // We may need to reset the StringBuilder because the delegate may not be a Map - boolean restore = false; - if (ni.getPosition() != -1) { - restore = true; - ni.previous(); - sb.setLength(0); - sb.append(ni.getAllPreviousSegments()); - } - Map map = getEnclosingMap.apply(mc, ni); - if (restore) { - ni.next(); - sb.setLength(0); - sb.append(ni.getAllPreviousSegments()); - } + // Place the cursor at the map path + NameIterator niAtMapPath = atMapPath(mapPath, ni); + Map map = getEnclosingMap.apply(mc, niAtMapPath); String rawMapKey; String configKey; boolean indexed = isIndexed(ni.getPreviousSegment()); if (indexed && ni.hasPrevious()) { - rawMapKey = normalizeIfIndexed(ni.getPreviousSegment()); - ni.previous(); - configKey = ni.getAllPreviousSegmentsWith(rawMapKey); - ni.next(); + rawMapKey = normalizeIfIndexed(niAtMapPath.getName().substring(niAtMapPath.getPosition() + 1)); + configKey = niAtMapPath.getAllPreviousSegmentsWith(rawMapKey); } else { - rawMapKey = ni.getPreviousSegment(); + rawMapKey = niAtMapPath.getName().substring(niAtMapPath.getPosition() + 1); configKey = ni.getAllPreviousSegments(); } + // Remove quotes if exists + if (rawMapKey.charAt(0) == '"' && rawMapKey.charAt(rawMapKey.length() - 1) == '"') { + rawMapKey = rawMapKey.substring(1, rawMapKey.length() - 1); + } + Converter keyConv; SmallRyeConfig config = mc.getConfig(); if (keyConvertWith != null) { @@ -507,6 +492,11 @@ private void processLazyMapValue( ((Map) map).put(keyConv.convert(rawMapKey), config.getValue(configKey, valueConv)); } }); + // action to match all segments of a key after the map path + KeyMap mapAction = matchActions.find(currentPath); + if (mapAction != null) { + mapAction.putAny(matchActions.find(currentPath)); + } // collections may also be represented without [] so we need to register both paths if (isCollection(currentPath)) { @@ -595,6 +585,25 @@ private static String indexName(final String name, final String groupPath, final return name; } + private static NameIterator atMapPath(final String mapPath, final NameIterator propertyName) { + int segments = 0; + NameIterator countSegments = new NameIterator(mapPath); + while (countSegments.hasNext()) { + segments++; + countSegments.next(); + } + + // We don't want the key; + segments = segments - 1; + + NameIterator propertyMap = new NameIterator(propertyName.getName()); + for (int i = 0; i < segments; i++) { + propertyMap.next(); + } + + return propertyMap; + } + private static String propertyName(final Property property, final ConfigMappingInterface group, final NamingStrategy namingStrategy) { return namingStrategy(namingStrategy, group.getNamingStrategy()).apply(property.getPropertyName()); @@ -690,23 +699,23 @@ static class GetOrCreateEnclosingGroupInMap implements BiFunction ourEnclosing = getEnclosingMap.apply(context, mapPath); + NameIterator atMapPath = atMapPath(mapPath, ni); + Map ourEnclosing = getEnclosingMap.apply(context, atMapPath); Converter keyConverter = context.getKeyConverter(enclosingGroup.getInterfaceType(), enclosingMap.getMethod().getName(), enclosingMap.getLevels() - 1); MapKey mapKey; if (enclosingMap.getValueProperty().isCollection()) { - mapKey = MapKey.collectionKey(mapPath.getNextSegment(), keyConverter); + mapKey = MapKey.collectionKey(atMapPath.getNextSegment(), keyConverter); } else { - mapKey = MapKey.key(mapPath.getNextSegment(), ni.getAllPreviousSegments(), keyConverter); + mapKey = MapKey.key(atMapPath.getNextSegment(), ni.getAllPreviousSegments(), keyConverter); } ConfigMappingObject val = (ConfigMappingObject) context.getEnclosedField(enclosingGroup.getInterfaceType(), mapKey.getKey(), ourEnclosing); if (val == null) { StringBuilder sb = context.getStringBuilder(); - sb.replace(0, sb.length(), mapPath.getAllPreviousSegmentsWith(mapKey.getKey())); + sb.replace(0, sb.length(), atMapPath.getAllPreviousSegmentsWith(mapKey.getKey())); context.applyNamingStrategy( namingStrategy(enclosedGroup.getGroupType().getNamingStrategy(), enclosingGroup.getNamingStrategy())); @@ -723,7 +732,7 @@ public ConfigMappingObject apply(final ConfigMappingContext context, final NameI .createCollectionFactory(collectionRawType); // Get all the available indexes List indexes = context.getConfig().getIndexedPropertiesIndexes( - mapPath.getAllPreviousSegmentsWith(normalizeIfIndexed(mapKey.getKey()))); + atMapPath.getAllPreviousSegmentsWith(normalizeIfIndexed(mapKey.getKey()))); collection = collectionFactory.apply(indexes.size()); // Initialize all expected elements in the list if (collection instanceof List) { @@ -752,25 +761,6 @@ public void accept(final ConfigMappingContext context, final NameIterator ni) { apply(context, ni); } - private NameIterator toMapPath(final NameIterator ni) { - int segments = 0; - NameIterator countSegments = new NameIterator(this.mapPath); - while (countSegments.hasNext()) { - segments++; - countSegments.next(); - } - - // We don't want the key; - segments = segments - 1; - - NameIterator mapPath = new NameIterator(ni.getName()); - for (int i = 0; i < segments; i++) { - mapPath.next(); - } - - return mapPath; - } - static class MapKey { private final String key; private final Object convertedKey; diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java index 6c707f695..a94981237 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java @@ -693,7 +693,6 @@ interface MapOfListWithConverter { class KeyConverter implements Converter { @Override public String convert(final String value) throws IllegalArgumentException, NullPointerException { - System.out.println("KeyConverter.convert"); if (value.equals("one")) { return "1"; } else if (value.equals("two")) { @@ -709,7 +708,6 @@ class ListConverter implements Converter> { @Override public List convert(final String value) throws IllegalArgumentException, NullPointerException { - System.out.println("ListConverter.convert"); return DELEGATE.convert(value); } } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java index 0cb0fe4d2..871929118 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java @@ -1858,4 +1858,42 @@ interface ExpressionDefaults { @WithDefault("${expression}") String expression(); } + + @Test + void mapKeys() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(MapKeys.class) + .withSources(config( + "keys.map.one", "1", + "keys.map.one.two", "2", + "keys.map.one.two.three", "3", + "keys.map.\"one.two.three.four\"", "4")) + .withSources(config( + "keys.list.one[0]", "1", + "keys.list.one.two[0]", "2", + "keys.list.one.two.three[0]", "3", + "keys.list.\"one.two.three.four\"[0]", "4")) + .build(); + + MapKeys mapping = config.getConfigMapping(MapKeys.class); + + assertEquals(4, mapping.map().size()); + assertEquals("1", mapping.map().get("one")); + assertEquals("2", mapping.map().get("one.two")); + assertEquals("3", mapping.map().get("one.two.three")); + assertEquals("4", mapping.map().get("one.two.three.four")); + + assertEquals("1", mapping.list().get("one").get(0)); + assertEquals("2", mapping.list().get("one.two").get(0)); + assertEquals("3", mapping.list().get("one.two.three").get(0)); + assertEquals("4", mapping.list().get("one.two.three.four").get(0)); + } + + @ConfigMapping(prefix = "keys") + interface MapKeys { + Map map(); + + Map> list(); + } } diff --git a/implementation/src/test/java/io/smallrye/config/KeyMapTest.java b/implementation/src/test/java/io/smallrye/config/KeyMapTest.java index 2b95c7d5c..379a5d06b 100644 --- a/implementation/src/test/java/io/smallrye/config/KeyMapTest.java +++ b/implementation/src/test/java/io/smallrye/config/KeyMapTest.java @@ -238,10 +238,10 @@ void findOrAdd() { @Test void findOrAddDotted() { - // KeyMap map = new KeyMap<>(); - // map.findOrAdd("map.\"quoted.key\".value").putRootValue("value"); - // assertEquals("value", map.findRootValue("map.\"quoted.key\".value")); - // assertNull(map.findRootValue("map.quoted.key.value")); + KeyMap map = new KeyMap<>(); + map.findOrAdd("map.\"quoted.key\".value").putRootValue("value"); + assertEquals("value", map.findRootValue("map.\"quoted.key\".value")); + assertNull(map.findRootValue("map.quoted.key.value")); KeyMap varArgs = new KeyMap<>(); varArgs.findOrAdd("foo", "bar.bar", "baz").putRootValue("value"); @@ -268,4 +268,16 @@ void findOrAddDotted() { assertEquals("value", iterator.findRootValue("foo.\"bar.bar\".baz")); assertEquals("value", iterable.findRootValue("foo.\"bar.bar\".baz")); } + + @Test + void star() { + KeyMap map = new KeyMap<>(); + KeyMap orAdd = map.findOrAdd("map.key.*"); + orAdd.putRootValue("value"); + orAdd.putAny(map.findOrAdd("map.key.*")); + + assertEquals("value", map.findRootValue("map.key.one")); + assertEquals("value", map.findRootValue("map.key.one.two")); + assertEquals("value", map.findRootValue("map.key.one.two.three")); + } } diff --git a/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java b/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java index 72be561c0..acd820030 100644 --- a/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java +++ b/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java @@ -356,12 +356,16 @@ void builderWithFlagSetters() { void getValuesAsMap() { SmallRyeConfig config = new SmallRyeConfigBuilder() .addDefaultInterceptors() - .withSources(config("my.prop.key", "value", "my.prop.key.nested", "value")) + .withSources(config( + "my.prop.key", "value", + "my.prop.key.nested", "value", + "my.prop.\"key.quoted\"", "value")) .build(); Map map = config.getValuesAsMap("my.prop", STRING_CONVERTER, STRING_CONVERTER); - assertEquals(1, map.size()); + assertEquals(2, map.size()); assertEquals("value", map.get("key")); + assertEquals("value", map.get("key.quoted")); } @Test