Skip to content

Commit

Permalink
Support Map keys without quotes for non group elements (smallrye#900)
Browse files Browse the repository at this point in the history
  • Loading branch information
radcortez authored Mar 6, 2023
1 parent 4310ea3 commit 7375af4
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 78 deletions.
15 changes: 10 additions & 5 deletions documentation/src/main/docs/config/mappings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, List<Alias>> aliases()` member, where `localhost` is the `Map` key, `[0]` is the index of the
`List<Alias>` where the `Alias` element will be stored, containing the name `prod`.
`List<Alias>` 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ final class ConfigMappingProvider implements Serializable {
private static final KeyMap<BiConsumer<ConfigMappingContext, NameIterator>> IGNORE_EVERYTHING;

static {
final KeyMap<BiConsumer<ConfigMappingContext, NameIterator>> map = new KeyMap<>();
KeyMap<BiConsumer<ConfigMappingContext, NameIterator>> map = new KeyMap<>();
map.putRootValue(DO_NOTHING);
//noinspection CollectionAddedToSelf
map.putAny(map);
Expand Down Expand Up @@ -103,10 +103,13 @@ static void ignoreRecursively(KeyMap<BiConsumer<ConfigMappingContext, NameIterat
if (root.getAny() == null) {
root.putAny(IGNORE_EVERYTHING);
} else {
ignoreRecursively(root.getAny());
var any = root.getAny();
if (root != any) {
ignoreRecursively(any);
}
}

for (final KeyMap<BiConsumer<ConfigMappingContext, NameIterator>> value : root.values()) {
for (var value : root.values()) {
ignoreRecursively(value);
}
}
Expand Down Expand Up @@ -148,6 +151,13 @@ private void processEagerGroup(
final ConfigMappingInterface group,
final BiFunction<ConfigMappingContext, NameIterator, ConfigMappingObject> 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<String> usedProperties = new HashSet<>();
for (int i = 0; i < group.getPropertyCount(); i++) {
Expand All @@ -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(
Expand Down Expand Up @@ -287,7 +292,6 @@ private void processLazyGroupInGroup(
final ConfigMappingInterface group,
final BiConsumer<ConfigMappingContext, NameIterator> matchAction,
final HashSet<String> usedProperties) {

int pc = group.getPropertyCount();
int pathLen = currentPath.size();
for (int i = 0; i < pc; i++) {
Expand All @@ -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,
Expand Down Expand Up @@ -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<? extends Converter<?>> 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) {
Expand All @@ -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)) {
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -690,23 +699,23 @@ static class GetOrCreateEnclosingGroupInMap implements BiFunction<ConfigMappingC
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public ConfigMappingObject apply(final ConfigMappingContext context, final NameIterator ni) {
NameIterator mapPath = toMapPath(ni);
Map<?, ?> 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()));
Expand All @@ -723,7 +732,7 @@ public ConfigMappingObject apply(final ConfigMappingContext context, final NameI
.createCollectionFactory(collectionRawType);
// Get all the available indexes
List<Integer> 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) {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,6 @@ interface MapOfListWithConverter {
class KeyConverter implements Converter<String> {
@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")) {
Expand All @@ -709,7 +708,6 @@ class ListConverter implements Converter<List<String>> {

@Override
public List<String> convert(final String value) throws IllegalArgumentException, NullPointerException {
System.out.println("ListConverter.convert");
return DELEGATE.convert(value);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> map();

Map<String, List<String>> list();
}
}
20 changes: 16 additions & 4 deletions implementation/src/test/java/io/smallrye/config/KeyMapTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,10 @@ void findOrAdd() {

@Test
void findOrAddDotted() {
// KeyMap<String> 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<String> 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<String> varArgs = new KeyMap<>();
varArgs.findOrAdd("foo", "bar.bar", "baz").putRootValue("value");
Expand All @@ -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<String> map = new KeyMap<>();
KeyMap<String> 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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> 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
Expand Down

0 comments on commit 7375af4

Please sign in to comment.