From 8e47fc1f0ced3a5b4d194ee23ff15d75a37aeaf9 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Tue, 10 Sep 2024 18:33:43 +0100 Subject: [PATCH] Support a fixed list of Map keys statically @WithKeys --- .../smallrye/config/ConfigMappingContext.java | 77 +++++++++++-------- .../config/ConfigMappingGenerator.java | 52 +++++++++++-- .../config/ConfigMappingInterface.java | 18 ++++- .../java/io/smallrye/config/WithKeys.java | 31 ++++++++ .../config/ConfigMappingCollectionsTest.java | 42 ++++++++++ .../io/smallrye/config/ObjectCreatorTest.java | 6 +- 6 files changed, 185 insertions(+), 41 deletions(-) create mode 100644 implementation/src/main/java/io/smallrye/config/WithKeys.java diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java index 2f6818b39..d9987b9f2 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java @@ -418,19 +418,21 @@ public void accept(Function get) { public ObjectCreator map( final Class keyRawType, final Class> keyConvertWith) { - return map(keyRawType, keyConvertWith, null); + return map(keyRawType, keyConvertWith, new ArrayList<>(), null); } public ObjectCreator map( final Class keyRawType, final Class> keyConvertWith, + final List keys, final String unnamedKey) { - return map(keyRawType, keyConvertWith, unnamedKey, (Class) null); + return map(keyRawType, keyConvertWith, keys, unnamedKey, (Class) null); } public ObjectCreator map( final Class keyRawType, final Class> keyConvertWith, + final List keys, final String unnamedKey, final Class defaultClass) { @@ -448,12 +450,13 @@ public V get() { }; } - return map(keyRawType, keyConvertWith, unnamedKey, supplier); + return map(keyRawType, keyConvertWith, keys, unnamedKey, supplier); } public ObjectCreator map( final Class keyRawType, final Class> keyConvertWith, + final List keys, final String unnamedKey, final Supplier defaultValue) { Converter keyConverter = keyConvertWith == null ? config.requireConverter(keyRawType) @@ -471,41 +474,51 @@ 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 && !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() { + @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,7 +526,9 @@ 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 + // 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())) { diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java index c834d7164..217860fad 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java @@ -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; @@ -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; @@ -533,6 +535,22 @@ private static void generateProperty(final MethodVisitor ctor, final Property pr } else { ctor.visitInsn(ACONST_NULL); } + List keys = mapProperty.getKeys(); + if (!keys.isEmpty()) { + ctor.visitTypeInsn(NEW, "java/util/ArrayList"); + ctor.visitInsn(DUP); + ctor.visitMethodInsn(INVOKESPECIAL, "java/util/ArrayList", "", "()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 { @@ -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()) { @@ -630,13 +654,29 @@ private static void unwrapProperty(final MethodVisitor ctor, final Property prop } else { ctor.visitInsn(ACONST_NULL); } + List keys = mapProperty.getKeys(); + if (!keys.isEmpty()) { + ctor.visitTypeInsn(NEW, "java/util/ArrayList"); + ctor.visitInsn(DUP); + ctor.visitMethodInsn(INVOKESPECIAL, "java/util/ArrayList", "", "()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(); diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java index b34d6e9c8..78982c237 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java @@ -593,6 +593,7 @@ public LeafProperty asLeaf() { public static final class MapProperty extends Property { private final Type keyType; + private final List keys; private final String keyUnnamed; private final Class> keyConvertWith; private final Property valueProperty; @@ -603,6 +604,7 @@ public static final class MapProperty extends Property { final Method method, final String propertyName, final Type keyType, + final List keys, final String keyUnnamed, final Class> keyConvertWith, final Property valueProperty, @@ -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; @@ -630,6 +633,10 @@ public String getKeyUnnamed() { return keyUnnamed; } + public List getKeys() { + return keys; + } + public boolean hasKeyUnnamed() { return keyUnnamed != null; } @@ -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); } @@ -932,6 +940,14 @@ private static boolean hasDefaults(final Method method) { return method.getAnnotation(WithDefaults.class) != null; } + private static List 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) { 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..2e8f89ced --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/WithKeys.java @@ -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>> provider() default EmptySupplier.class; + + class EmptySupplier implements Supplier> { + @Override + public List get() { + return List.of(); + } + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java index d097f75d5..e27e6f37f 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java @@ -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") @@ -1031,4 +1032,45 @@ interface Nested { List 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 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 values(); + + //@WithKeys({"one", "two"}) + Map nested(); + + interface Nested { + String value(); + } + } } diff --git a/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java index 04cd356f8..41ed9daa8 100644 --- a/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java +++ b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java @@ -129,7 +129,7 @@ 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, null, "unnamed") .lazyGroup(Nested.class); this.unnamed = unnamed.get(); sb.setLength(length); @@ -368,7 +368,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); @@ -456,7 +456,7 @@ public MapDefaultsImpl(ConfigMappingContext context) { 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(".*");