From bedbff5ef6662124c178ad4a2e14f060df2894cc Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Tue, 15 Oct 2024 17:24:33 +0100 Subject: [PATCH] Add @ConfigMapping beanStyleGetter to enable / disable bean style getter names matching with configuration names (#1239) --- .../io/smallrye/config/ConfigMapping.java | 16 ++++- .../smallrye/config/ConfigMappingContext.java | 47 +++++++++----- .../config/ConfigMappingGenerator.java | 36 +++++++---- .../config/ConfigMappingInterface.java | 17 ++--- .../ConfigMappingNamingStrategyTest.java | 64 +++++++++++++++++++ .../io/smallrye/config/ObjectCreatorTest.java | 8 ++- 6 files changed, 146 insertions(+), 42 deletions(-) diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMapping.java b/implementation/src/main/java/io/smallrye/config/ConfigMapping.java index 16577577e..0710f75b0 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMapping.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMapping.java @@ -52,7 +52,21 @@ */ NamingStrategy namingStrategy() default NamingStrategy.KEBAB_CASE; - enum NamingStrategy { + /** + * Match config mapping method names and configuration properties using bean-style getter names. This removes the + * prefixes get or is and decapitalizes the next character. By default, bean-style getter + * matching is disabled. + *

+ * Config mapping declarations should prefer to use simple names that match one-to-one with their configuration + * names. Bean-style getter matching allows multiple method names to match the same configuration name. For + * instance, getFoo and isFoo both match foo, which may not be intended. + * + * @return a boolean true to enable bean style getter names matching, or false to disable + * it. + */ + boolean beanStyleGetters() default false; + + enum NamingStrategy implements Function { /** * The method name is used as is to map the configuration property. */ diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java index 4a95875b4..405fd3da4 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java @@ -1,5 +1,6 @@ package io.smallrye.config; +import static io.smallrye.config.ConfigMappingLoader.configMappingNames; import static io.smallrye.config.ConfigValidationException.Problem; import static io.smallrye.config.ProfileConfigSourceInterceptor.activeName; import static io.smallrye.config.common.utils.StringUtil.unindexed; @@ -38,8 +39,9 @@ public final class ConfigMappingContext { private final Map, Map> roots = new IdentityHashMap<>(); private final Map, Converter> converterInstances = new IdentityHashMap<>(); - private NamingStrategy namingStrategy; - private String rootPath; + private NamingStrategy namingStrategy = NamingStrategy.KEBAB_CASE; + private boolean beanStyleGetters = false; + private String rootPath = null; private final StringBuilder nameBuilder = new StringBuilder(); private final Set usedProperties = new HashSet<>(); private final List problems = new ArrayList<>(); @@ -51,8 +53,7 @@ public Map>> get() { // All mapping names must be loaded first because of split mappings Map>> names = new HashMap<>(); for (Map.Entry, Set> mapping : roots.entrySet()) { - for (Map.Entry>> entry : ConfigMappingLoader - .configMappingNames(mapping.getKey()).entrySet()) { + for (Map.Entry>> entry : configMappingNames(mapping.getKey()).entrySet()) { names.putIfAbsent(entry.getKey(), new HashMap<>()); names.get(entry.getKey()).putAll(entry.getValue()); } @@ -73,9 +74,7 @@ public Map>> get() { for (Map.Entry, Set> mapping : roots.entrySet()) { Map mappingObjects = new HashMap<>(); for (String rootPath : mapping.getValue()) { - applyNamingStrategy(null); applyRootPath(rootPath); - applyNameBuilder(rootPath); mappingObjects.put(rootPath, (ConfigMappingObject) constructRoot(mapping.getKey())); } this.roots.put(mapping.getKey(), mappingObjects); @@ -88,8 +87,10 @@ T constructRoot(Class interfaceType) { public T constructGroup(Class interfaceType) { NamingStrategy namingStrategy = this.namingStrategy; + boolean beanStyleGetters = this.beanStyleGetters; T mappingObject = ConfigMappingLoader.configMappingObject(interfaceType, this); - this.namingStrategy = applyNamingStrategy(namingStrategy); + applyNamingStrategy(namingStrategy); + applyBeanStyleGetters(beanStyleGetters); return mappingObject; } @@ -121,23 +122,37 @@ public Converter getConverterInstance(Class BEAN_STYLE_GETTERS = new Function() { + @Override + public String apply(final String name) { + if (name.startsWith("get") && name.length() > 3) { + return Character.toLowerCase(name.charAt(3)) + name.substring(4); + } else if (name.startsWith("is") && name.length() > 2) { + return Character.toLowerCase(name.charAt(2)) + name.substring(3); + } + return name; + } + }; + + public Function propertyName() { + return beanStyleGetters ? BEAN_STYLE_GETTERS.andThen(namingStrategy) : namingStrategy; } public StringBuilder getNameBuilder() { diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java index 7992604d4..7dffea9f6 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.Function; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -110,7 +111,7 @@ public class ConfigMappingGenerator { private static final int V_MAPPING_CONTEXT = 1; private static final int V_STRING_BUILDER = 2; private static final int V_LENGTH = 3; - private static final int V_NAMING_STRATEGY = 4; + private static final int V_NAMING_FUNCTION = 4; /** * Generates the backing implementation of an interface annotated with the {@link ConfigMapping} annotation. @@ -164,19 +165,25 @@ static byte[] generate(final ConfigMappingInterface mapping) { ctor.visitLabel(ctorLenStart); ctor.visitVarInsn(ISTORE, V_LENGTH); - Label ctorNsStart = new Label(); - ctor.visitLabel(ctorNsStart); - ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT); + Label ctorNfStart = new Label(); + ctor.visitLabel(ctorNfStart); - if (mapping.hasNamingStrategy()) { + if (mapping.hasConfigMapping()) { + ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT); ctor.visitFieldInsn(GETSTATIC, I_NAMING_STRATEGY, mapping.getNamingStrategy().name(), "L" + I_NAMING_STRATEGY + ";"); - } else { - ctor.visitInsn(ACONST_NULL); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "applyNamingStrategy", "(L" + I_NAMING_STRATEGY + ";)V", + false); + + ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT); + ctor.visitInsn(mapping.isBeanStyleGetters() ? ICONST_1 : ICONST_0); + ctor.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "applyBeanStyleGetters", "(Ljava/lang/Boolean;)V", false); } - ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "applyNamingStrategy", - "(L" + I_NAMING_STRATEGY + ";)L" + I_NAMING_STRATEGY + ";", false); - ctor.visitVarInsn(ASTORE, V_NAMING_STRATEGY); + + ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "propertyName", "()Ljava/util/function/Function;", false); + ctor.visitVarInsn(ASTORE, V_NAMING_FUNCTION); addProperties(visitor, ctor, new HashSet<>(), mapping, mapping.getClassInternalName()); @@ -185,8 +192,7 @@ static byte[] generate(final ConfigMappingInterface mapping) { ctor.visitLocalVariable("mc", 'L' + I_MAPPING_CONTEXT + ';', null, ctorStart, ctorEnd, V_MAPPING_CONTEXT); ctor.visitLocalVariable("sb", 'L' + I_STRING_BUILDER + ';', null, ctorSbStart, ctorEnd, V_STRING_BUILDER); ctor.visitLocalVariable("len", "I", null, ctorLenStart, ctorEnd, V_LENGTH); - ctor.visitLocalVariable("ns", "Lio/smallrye/config/ConfigMapping$NamingStrategy;", null, ctorNsStart, ctorEnd, - V_NAMING_STRATEGY); + ctor.visitLocalVariable("nf", "Ljava/util/function/Function;", null, ctorNfStart, ctorEnd, V_NAMING_FUNCTION); ctor.visitEnd(); ctor.visitMaxs(0, 0); visitor.visitEnd(); @@ -711,9 +717,11 @@ private static void appendPropertyName(final MethodVisitor ctor, final Property if (property.hasPropertyName()) { ctor.visitLdcInsn(property.getPropertyName()); } else { - ctor.visitVarInsn(ALOAD, V_NAMING_STRATEGY); + ctor.visitVarInsn(ALOAD, V_NAMING_FUNCTION); ctor.visitLdcInsn(property.getPropertyName()); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_NAMING_STRATEGY, "apply", "(L" + I_STRING + ";)L" + I_STRING + ";", false); + ctor.visitMethodInsn(INVOKEINTERFACE, getInternalName(Function.class), "apply", + "(L" + I_OBJECT + ";)L" + I_OBJECT + ";", true); + ctor.visitTypeInsn(CHECKCAST, "java/lang/String"); } ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ';', diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java index 7edc1a368..0b61b88c7 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java @@ -60,12 +60,8 @@ protected ConfigMappingInterface computeValue(final Class type) { toStringMethod = (ToStringMethod) property; } } - if (toStringMethod == null) { - toStringMethod = ToStringMethod.NONE; - } - this.properties = filteredProperties.toArray(new Property[0]); - this.toStringMethod = toStringMethod; + this.toStringMethod = toStringMethod != null ? toStringMethod : ToStringMethod.NONE; } static String getImplementationClassName(Class type) { @@ -153,7 +149,11 @@ private static Map getSuperProperties(ConfigMappingInterface t return properties; } - public boolean hasNamingStrategy() { + ToStringMethod getToStringMethod() { + return toStringMethod; + } + + public boolean hasConfigMapping() { return interfaceType.getAnnotation(ConfigMapping.class) != null; } @@ -162,8 +162,9 @@ public NamingStrategy getNamingStrategy() { return configMapping != null ? configMapping.namingStrategy() : NamingStrategy.KEBAB_CASE; } - ToStringMethod getToStringMethod() { - return toStringMethod; + public boolean isBeanStyleGetters() { + ConfigMapping configMapping = interfaceType.getAnnotation(ConfigMapping.class); + return configMapping != null && configMapping.beanStyleGetters(); } String getClassInternalName() { diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingNamingStrategyTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingNamingStrategyTest.java index 1c45aadf1..969a58d42 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingNamingStrategyTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingNamingStrategyTest.java @@ -13,6 +13,7 @@ import java.util.Optional; import org.eclipse.microprofile.config.inject.ConfigProperties; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; public class ConfigMappingNamingStrategyTest { @@ -283,4 +284,67 @@ interface VerbatimDefaults { String verbatimDefault(); } } + + @Test + @Disabled + void beanStyleGetters() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "get.name", "value", + "get.boolean", "true", + "get.get", "value", + "get.is", "true", + "get.x", "value", + "get.nested.name", "value", + "get.nested.reset.get-name", "value", + "get.nested.reset.get-reset-again.name", "value")) + .withMapping(BeanStyleGetters.class) + .build(); + + BeanStyleGetters mapping = config.getConfigMapping(BeanStyleGetters.class); + assertEquals("value", mapping.getName()); + assertTrue(mapping.isBoolean()); + assertEquals("value", mapping.get()); + assertTrue(mapping.is()); + assertEquals("value", mapping.getX()); + assertEquals("value", mapping.isX()); + assertEquals("value", mapping.getNested().getName()); + assertEquals("value", mapping.getNested().getReset().getName()); + assertEquals("value", mapping.getNested().getReset().getResetAgain().getName()); + } + + @ConfigMapping(prefix = "get", beanStyleGetters = true) + interface BeanStyleGetters { + String getName(); + + boolean isBoolean(); + + String get(); + + boolean is(); + + String getX(); + + String isX(); + + Nested getNested(); + + interface Nested { + String getName(); + + Reset getReset(); + + @ConfigMapping + interface Reset { + String getName(); + + ResetAgain getResetAgain(); + + @ConfigMapping(beanStyleGetters = true) + interface ResetAgain { + String getName(); + } + } + } + } } diff --git a/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java index 78daece46..49d90ca23 100644 --- a/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java +++ b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java @@ -340,7 +340,6 @@ void unnamedKeys() { ConfigMappingContext context = new ConfigMappingContext(config, ConfigMappingLoader.getConfigMapping(UnnamedKeys.class).getNames(), new HashMap<>()); context.applyRootPath("unnamed"); - context.getNameBuilder().append("unnamed"); UnnamedKeys mapping = new UnnamedKeysImpl(context); assertEquals("unnamed", mapping.map().get(null).value()); @@ -400,7 +399,6 @@ void mapDefaults() { ConfigMappingContext context = new ConfigMappingContext(config, ConfigMappingLoader.getConfigMapping(MapDefaults.class).getNames(), new HashMap<>()); context.applyRootPath("map"); - context.getNameBuilder().append("map."); MapDefaults mapping = new MapDefaultsImpl(context); assertEquals("value", mapping.defaults().get("one")); @@ -451,6 +449,7 @@ public MapDefaultsImpl(ConfigMappingContext context) { int length = sb.length(); ConfigMapping.NamingStrategy ns = ConfigMapping.NamingStrategy.KEBAB_CASE; + sb.append("."); sb.append(ns.apply("defaults")); ConfigMappingContext.ObjectCreator> defaults = context.new ObjectCreator>( sb.toString()) @@ -458,6 +457,7 @@ public MapDefaultsImpl(ConfigMappingContext context) { this.defaults = defaults.get(); sb.setLength(length); + sb.append("."); sb.append(ns.apply("defaults-nested")); ConfigMappingContext.ObjectCreator> defaultsNested = context.new ObjectCreator>( sb.toString()) @@ -474,6 +474,7 @@ public Nested get() { this.defaultsNested = defaultsNested.get(); sb.setLength(length); + sb.append("."); sb.append(ns.apply("defaults-list")); ConfigMappingContext.ObjectCreator>> defaultsList = context.new ObjectCreator>>( sb.toString()) @@ -509,7 +510,7 @@ void namingStrategy() { ConfigMappingContext context = new ConfigMappingContext(config, ConfigMappingLoader.getConfigMapping(Naming.class).getNames(), new HashMap<>()); - context.getNameBuilder().append("naming."); + context.applyRootPath("naming"); Naming naming = new NamingImpl(context); assertEquals("value", naming.nestedValue().value()); @@ -534,6 +535,7 @@ public NamingImpl(ConfigMappingContext context) { StringBuilder sb = context.getNameBuilder(); int length = sb.length(); + sb.append("."); sb.append(ns.apply("nestedValue")); ConfigMappingContext.ObjectCreator nestedValue = context.new ObjectCreator(sb.toString()) .group(Nested.class);