Skip to content

Commit

Permalink
Add @ConfigMapping beanStyleGetter to enable / disable bean style get…
Browse files Browse the repository at this point in the history
…ter names matching with configuration names (#1239)
  • Loading branch information
radcortez authored Oct 15, 2024
1 parent ecadfeb commit bedbff5
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>get</code> or <code>is</code> and decapitalizes the next character. By default, bean-style getter
* matching is <code>disabled</code>.
* <p>
* 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, <code>getFoo</code> and <code>isFoo</code> both match <code>foo</code>, which may not be intended.
*
* @return a boolean <code>true</code> to enable bean style getter names matching, or <code>false</code> to disable
* it.
*/
boolean beanStyleGetters() default false;

enum NamingStrategy implements Function<String, String> {
/**
* The method name is used as is to map the configuration property.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -38,8 +39,9 @@ public final class ConfigMappingContext {
private final Map<Class<?>, Map<String, ConfigMappingObject>> roots = new IdentityHashMap<>();
private final Map<Class<?>, 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<String> usedProperties = new HashSet<>();
private final List<Problem> problems = new ArrayList<>();
Expand All @@ -51,8 +53,7 @@ public Map<String, Map<String, Set<String>>> get() {
// All mapping names must be loaded first because of split mappings
Map<String, Map<String, Set<String>>> names = new HashMap<>();
for (Map.Entry<Class<?>, Set<String>> mapping : roots.entrySet()) {
for (Map.Entry<String, Map<String, Set<String>>> entry : ConfigMappingLoader
.configMappingNames(mapping.getKey()).entrySet()) {
for (Map.Entry<String, Map<String, Set<String>>> entry : configMappingNames(mapping.getKey()).entrySet()) {
names.putIfAbsent(entry.getKey(), new HashMap<>());
names.get(entry.getKey()).putAll(entry.getValue());
}
Expand All @@ -73,9 +74,7 @@ public Map<String, Map<String, Set<String>>> get() {
for (Map.Entry<Class<?>, Set<String>> mapping : roots.entrySet()) {
Map<String, ConfigMappingObject> 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);
Expand All @@ -88,8 +87,10 @@ <T> T constructRoot(Class<T> interfaceType) {

public <T> T constructGroup(Class<T> 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;
}

Expand Down Expand Up @@ -121,23 +122,37 @@ public <T> Converter<T> getConverterInstance(Class<? extends Converter<? extends
});
}

public NamingStrategy applyNamingStrategy(final NamingStrategy namingStrategy) {
public void applyNamingStrategy(final NamingStrategy namingStrategy) {
if (namingStrategy != null) {
this.namingStrategy = namingStrategy;
} else if (this.namingStrategy == null) {
this.namingStrategy = NamingStrategy.KEBAB_CASE;
}
return this.namingStrategy;
}

public String applyRootPath(final String rootPath) {
this.rootPath = rootPath;
return this.rootPath;
public void applyBeanStyleGetters(final Boolean beanStyleGetters) {
if (beanStyleGetters != null) {
this.beanStyleGetters = beanStyleGetters;
}
}

public StringBuilder applyNameBuilder(final String rootPath) {
public void applyRootPath(final String rootPath) {
this.rootPath = rootPath;
this.nameBuilder.replace(0, nameBuilder.length(), rootPath);
return this.nameBuilder;
}

private static final Function<String, String> BEAN_STYLE_GETTERS = new Function<String, String>() {
@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<String, String> propertyName() {
return beanStyleGetters ? BEAN_STYLE_GETTERS.andThen(namingStrategy) : namingStrategy;
}

public StringBuilder getNameBuilder() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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());

Expand All @@ -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();
Expand Down Expand Up @@ -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 + ';',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -153,7 +149,11 @@ private static Map<String, Property> getSuperProperties(ConfigMappingInterface t
return properties;
}

public boolean hasNamingStrategy() {
ToStringMethod getToStringMethod() {
return toStringMethod;
}

public boolean hasConfigMapping() {
return interfaceType.getAnnotation(ConfigMapping.class) != null;
}

Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -451,13 +449,15 @@ public MapDefaultsImpl(ConfigMappingContext context) {
int length = sb.length();
ConfigMapping.NamingStrategy ns = ConfigMapping.NamingStrategy.KEBAB_CASE;

sb.append(".");
sb.append(ns.apply("defaults"));
ConfigMappingContext.ObjectCreator<Map<String, String>> defaults = context.new ObjectCreator<Map<String, String>>(
sb.toString())
.values(String.class, null, String.class, null, emptyList(), "default");
this.defaults = defaults.get();
sb.setLength(length);

sb.append(".");
sb.append(ns.apply("defaults-nested"));
ConfigMappingContext.ObjectCreator<Map<String, Nested>> defaultsNested = context.new ObjectCreator<Map<String, Nested>>(
sb.toString())
Expand All @@ -474,6 +474,7 @@ public Nested get() {
this.defaultsNested = defaultsNested.get();
sb.setLength(length);

sb.append(".");
sb.append(ns.apply("defaults-list"));
ConfigMappingContext.ObjectCreator<Map<String, List<Nested>>> defaultsList = context.new ObjectCreator<Map<String, List<Nested>>>(
sb.toString())
Expand Down Expand Up @@ -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());
Expand All @@ -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<Nested> nestedValue = context.new ObjectCreator<Nested>(sb.toString())
.group(Nested.class);
Expand Down

0 comments on commit bedbff5

Please sign in to comment.