Skip to content

Commit

Permalink
Support a fixed list of Map keys statically @WithKeys
Browse files Browse the repository at this point in the history
  • Loading branch information
radcortez committed Sep 10, 2024
1 parent e768190 commit 8e47fc1
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -418,19 +418,21 @@ public void accept(Function<String, Object> get) {
public <K> ObjectCreator<T> map(
final Class<K> keyRawType,
final Class<? extends Converter<K>> keyConvertWith) {
return map(keyRawType, keyConvertWith, null);
return map(keyRawType, keyConvertWith, new ArrayList<>(), null);
}

public <K> ObjectCreator<T> map(
final Class<K> keyRawType,
final Class<? extends Converter<K>> keyConvertWith,
final List<String> keys,
final String unnamedKey) {
return map(keyRawType, keyConvertWith, unnamedKey, (Class<?>) null);
return map(keyRawType, keyConvertWith, keys, unnamedKey, (Class<?>) null);
}

public <K, V> ObjectCreator<T> map(
final Class<K> keyRawType,
final Class<? extends Converter<K>> keyConvertWith,
final List<String> keys,
final String unnamedKey,
final Class<V> defaultClass) {

Expand All @@ -448,12 +450,13 @@ public V get() {
};
}

return map(keyRawType, keyConvertWith, unnamedKey, supplier);
return map(keyRawType, keyConvertWith, keys, unnamedKey, supplier);
}

public <K, V> ObjectCreator<T> map(
final Class<K> keyRawType,
final Class<? extends Converter<K>> keyConvertWith,
final List<String> keys,
final String unnamedKey,
final Supplier<V> defaultValue) {
Converter<K> keyConverter = keyConvertWith == null ? config.requireConverter(keyRawType)
Expand All @@ -471,49 +474,61 @@ public Object apply(final String path) {
public void accept(Function<String, Object> 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<String, String> mapKeys = new HashMap<>();
// single map key with all property names that share the same key
Map<String, List<String>> 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<String, String>() {
@Override
public String apply(final String s) {
return unindexed(propertyName.substring(0, mapProperty.getPosition()));
}
});

mapProperties.computeIfAbsent(mapKey, new Function<String, List<String>>() {
@Override
public List<String> apply(final String s) {
return new ArrayList<>();
}
});
mapProperties.get(mapKey).add(propertyName);
if (keys != null && !keys.isEmpty()) {
for (String key : keys) {
mapKeys.put(key, path + "." + key);
}
} else {
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<String, String>() {
@Override
public String apply(final String s) {
return unindexed(propertyName.substring(0, mapProperty.getPosition()));
}
});

mapProperties.computeIfAbsent(mapKey, new Function<String, List<String>>() {
@Override
public List<String> apply(final String s) {
return new ArrayList<>();
}
});
mapProperties.get(mapKey).add(propertyName);
}
}
}

for (Map.Entry<String, String> mapKey : mapKeys.entrySet()) {
nestedCreators.add(new Consumer<>() {
@Override
public void accept(Function<String, Object> get) {
// The properties may have been used ih the unnamed key, which cause clashes, so we skip them
// When we use the unnamed key 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) {
boolean allUsed = true;
for (String mapProperty : mapProperties.get(mapKey.getKey())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -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_LIST = getInternalName(List.class);

private static final int V_THIS = 0;
private static final int V_MAPPING_CONTEXT = 1;
Expand Down Expand Up @@ -533,6 +535,22 @@ private static void generateProperty(final MethodVisitor ctor, final Property pr
} else {
ctor.visitInsn(ACONST_NULL);
}
List<String> keys = mapProperty.getKeys();
if (!keys.isEmpty()) {
ctor.visitTypeInsn(NEW, "java/util/ArrayList");
ctor.visitInsn(DUP);
ctor.visitMethodInsn(INVOKESPECIAL, "java/util/ArrayList", "<init>", "()V", false);
ctor.visitVarInsn(ASTORE, 5);
for (String key : keys) {
ctor.visitVarInsn(ALOAD, 5);
ctor.visitLdcInsn(key);
ctor.visitMethodInsn(INVOKEINTERFACE, I_LIST, "add", "(L" + I_OBJECT + ";)Z", true);
ctor.visitInsn(POP);
}
ctor.visitVarInsn(ALOAD, 5);
} else {
ctor.visitInsn(ACONST_NULL);
}
if (mapProperty.hasKeyUnnamed()) {
ctor.visitLdcInsn(mapProperty.getKeyUnnamed());
} else {
Expand All @@ -544,11 +562,17 @@ private static void generateProperty(final MethodVisitor ctor, final Property pr
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_LIST + ";L" + I_STRING + ";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 (keys.isEmpty()) {
ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "lazyGroup",
"(L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false);
} else {
ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "group",
"(L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false);
}
} else if (valueProperty.isCollection() && valueProperty.asCollection().getElement().isLeaf()) {
ctor.visitLdcInsn(getType(mapProperty.getKeyRawType()));
if (mapProperty.hasKeyConvertWith()) {
Expand Down Expand Up @@ -630,13 +654,29 @@ private static void unwrapProperty(final MethodVisitor ctor, final Property prop
} else {
ctor.visitInsn(ACONST_NULL);
}
List<String> keys = mapProperty.getKeys();
if (!keys.isEmpty()) {
ctor.visitTypeInsn(NEW, "java/util/ArrayList");
ctor.visitInsn(DUP);
ctor.visitMethodInsn(INVOKESPECIAL, "java/util/ArrayList", "<init>", "()V", false);
ctor.visitVarInsn(ASTORE, 5);
for (String key : keys) {
ctor.visitVarInsn(ALOAD, 5);
ctor.visitLdcInsn(key);
ctor.visitMethodInsn(INVOKEINTERFACE, I_LIST, "add", "(L" + I_OBJECT + ";)Z", true);
ctor.visitInsn(POP);
}
ctor.visitVarInsn(ALOAD, 5);
} else {
ctor.visitInsn(ACONST_NULL);
}
if (mapProperty.hasKeyUnnamed()) {
ctor.visitLdcInsn(mapProperty.getKeyUnnamed());
} 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_LIST + ";L" + I_STRING + ";)L" + I_OBJECT_CREATOR + ";", false);
generateProperty(ctor, mapProperty.getValueProperty());
} else if (property.isCollection()) {
CollectionProperty collectionProperty = property.asCollection();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ public LeafProperty asLeaf() {

public static final class MapProperty extends Property {
private final Type keyType;
private final List<String> keys;
private final String keyUnnamed;
private final Class<? extends Converter<?>> keyConvertWith;
private final Property valueProperty;
Expand All @@ -603,6 +604,7 @@ public static final class MapProperty extends Property {
final Method method,
final String propertyName,
final Type keyType,
final List<String> keys,
final String keyUnnamed,
final Class<? extends Converter<?>> keyConvertWith,
final Property valueProperty,
Expand All @@ -611,6 +613,7 @@ public static final class MapProperty extends Property {

super(method, propertyName);
this.keyType = keyType;
this.keys = keys;
this.keyUnnamed = keyUnnamed;
this.keyConvertWith = keyConvertWith;
this.valueProperty = valueProperty;
Expand All @@ -630,6 +633,10 @@ public String getKeyUnnamed() {
return keyUnnamed;
}

public List<String> getKeys() {
return keys;
}

public boolean hasKeyUnnamed() {
return keyUnnamed != null;
}
Expand Down Expand Up @@ -846,7 +853,8 @@ 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),
return new MapProperty(method, propertyName, keyType.getType(),
getKeys(keyType, method), getUnnamedKey(keyType, method),
getConverter(keyType, method), getPropertyDef(method, valueType),
defaultValue != null || hasDefaults(method), defaultValue);
}
Expand Down Expand Up @@ -932,6 +940,14 @@ private static boolean hasDefaults(final Method method) {
return method.getAnnotation(WithDefaults.class) != null;
}

private static List<String> getKeys(final AnnotatedType type, final Method method) {
WithKeys annotation = type.getAnnotation(WithKeys.class);
if (annotation == null) {
annotation = method.getAnnotation(WithKeys.class);
}
return annotation != null ? List.of(annotation.value()) : Collections.emptyList();
}

private static String getUnnamedKey(final AnnotatedType type, final Method method) {
WithUnnamedKey annotation = type.getAnnotation(WithUnnamedKey.class);
if (annotation == null) {
Expand Down
31 changes: 31 additions & 0 deletions implementation/src/main/java/io/smallrye/config/WithKeys.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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.List;
import java.util.function.Supplier;

/**
* Provides a list of map keys when populating {@link java.util.Map} types. The provided list will effectively
* substitute the lookup in {@link SmallRyeConfig#getPropertyNames()} for map keys. Each key must exist in the final
* configuration (relative in the mapping path segment), or the mapping will fail with a
* {@link java.util.NoSuchElementException}.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE_USE })
public @interface WithKeys {
String[] value() default {};

Class<? extends Supplier<List<String>>> provider() default EmptySupplier.class;

class EmptySupplier implements Supplier<List<String>> {
@Override
public List<String> get() {
return List.of();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import io.smallrye.config.ConfigMappingCollectionsTest.ServerCollectionsSet.Environment;
import io.smallrye.config.ConfigMappingCollectionsTest.ServerCollectionsSet.Environment.App;
import io.smallrye.config.common.MapBackedConfigSource;

public class ConfigMappingCollectionsTest {
@ConfigMapping(prefix = "server")
Expand Down Expand Up @@ -1031,4 +1032,45 @@ interface Nested {
List<String> list();
}
}

@Test
void knownMapKeys() {
SmallRyeConfig config = new SmallRyeConfigBuilder()
.withSources(new MapBackedConfigSource("", Map.of(
"map.values.one", "one",
"map.nested.one.value", "one",
"map.nested.two.value", "two")) {
@Override
public Set<String> getPropertyNames() {
return super.getPropertyNames();
//return Collections.emptySet();
}
})
.withMapping(KnownMapKeys.class)
.build();

KnownMapKeys mapping = config.getConfigMapping(KnownMapKeys.class);
//assertEquals("one", mapping.values().get("one"));
assertEquals("one", mapping.nested().get("one").value());
assertEquals("two", mapping.nested().get("two").value());

// TODO - Implement remaining pieces
// - supplier
// - dashed keys
// - leaf maps, collection maps
// - combination @WithParentName and @WithUnnamedKey
}

@ConfigMapping(prefix = "map")
interface KnownMapKeys {
@WithKeys({ "one", "two" })
Map<String, String> values();

//@WithKeys({"one", "two"})
Map<String, Nested> nested();

interface Nested {
String value();
}
}
}
Loading

0 comments on commit 8e47fc1

Please sign in to comment.