Skip to content

Commit

Permalink
Support toString in mapping (smallrye#785)
Browse files Browse the repository at this point in the history
  • Loading branch information
radcortez authored Jul 15, 2022
1 parent c4af450 commit 1483a2e
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 15 deletions.
11 changes: 10 additions & 1 deletion documentation/src/main/docs/config/mappings.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,15 @@ public interface Defaults {
No configuration properties are required. The `Defaults#foo()` will return the value `foo` and `Defaults#bar()` will
return the value `bar`.

## ToString

If the config mapping contains a `toString` method declaration, the config mapping instance will include a proper
implementation of the `toString` method.

!!! caution

Do not include a `toString` declaration in a config mapping with sensitive information

## Validation

A config mapping may combine annotations from [Bean Validation](https://beanvalidation.org/) to validate configuration
Expand All @@ -420,7 +429,7 @@ interface Server {
The application startup fails with a `io.smallrye.config.ConfigValidationException` if the configuration properties
values do not follow the contraints defined in `Server`.

!!! caution
!!! info

For validation to work, the `smallrye-config-validator` dependency is required in the classpath.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static org.objectweb.asm.Opcodes.ASTORE;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.GOTO;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
Expand All @@ -22,6 +23,7 @@
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.POP;
import static org.objectweb.asm.Opcodes.PUTFIELD;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Opcodes.V1_8;
import static org.objectweb.asm.Type.getDescriptor;
import static org.objectweb.asm.Type.getInternalName;
Expand Down Expand Up @@ -116,7 +118,7 @@ static byte[] generate(final ConfigMappingInterface mapping) {
MethodVisitor noArgsCtor = visitor.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
noArgsCtor.visitVarInsn(Opcodes.ALOAD, V_THIS);
noArgsCtor.visitMethodInsn(Opcodes.INVOKESPECIAL, I_OBJECT, "<init>", "()V", false);
noArgsCtor.visitInsn(Opcodes.RETURN);
noArgsCtor.visitInsn(RETURN);
noArgsCtor.visitEnd();
noArgsCtor.visitMaxs(0, 0);

Expand Down Expand Up @@ -163,13 +165,17 @@ static byte[] generate(final ConfigMappingInterface mapping) {
// stack: -
addProperties(visitor, ctor, fio, new HashSet<>(), mapping, mapping.getClassInternalName());
// stack: -
if (mapping.getToStringMethod().generate()) {
addToString(visitor, mapping);
}
// stack: -
fio.visitInsn(Opcodes.RETURN);
fio.visitLabel(fioEnd);
fio.visitLocalVariable("mc", 'L' + I_MAPPING_CONTEXT + ';', null, fioStart, fioEnd, V_MAPPING_CONTEXT);
fio.visitEnd();
fio.visitMaxs(0, 0);
// stack: -
ctor.visitInsn(Opcodes.RETURN);
ctor.visitInsn(RETURN);
ctor.visitLabel(ctorEnd);
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);
Expand Down Expand Up @@ -662,7 +668,7 @@ private static void addProperties(
ctor.visitLdcInsn(memberName);
ctor.visitVarInsn(Opcodes.ALOAD, V_THIS);
ctor.visitVarInsn(Opcodes.ALOAD, V_THIS);
ctor.visitFieldInsn(Opcodes.GETFIELD, className, memberName, fieldDesc);
ctor.visitFieldInsn(GETFIELD, className, memberName, fieldDesc);
ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "registerEnclosedField",
"(L" + I_CLASS + ";L" + I_STRING + ";L" + I_OBJECT + ";L" + I_OBJECT + ";)V",
false);
Expand Down Expand Up @@ -793,7 +799,7 @@ private static void addProperties(
// stack: -
mv.visitVarInsn(Opcodes.ALOAD, V_THIS);
// stack: this
mv.visitFieldInsn(Opcodes.GETFIELD, className, memberName, fieldDesc);
mv.visitFieldInsn(GETFIELD, className, memberName, fieldDesc);
// stack: obj
mv.visitInsn(getReturnInstruction(property));

Expand Down Expand Up @@ -902,6 +908,53 @@ private static String getSignature(final Field field) {
return null;
}

private static void addToString(final ClassVisitor visitor, final ConfigMappingInterface mapping) {
MethodVisitor ts = visitor.visitMethod(ACC_PUBLIC, "toString", "()L" + I_STRING + ";", null, null);
ts.visitCode();
ts.visitTypeInsn(NEW, I_STRING_BUILDER);
ts.visitInsn(DUP);
ts.visitMethodInsn(INVOKESPECIAL, I_STRING_BUILDER, "<init>", "()V", false);
ts.visitLdcInsn(mapping.getInterfaceType().getSimpleName() + "{");
ts.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ";", false);

Property[] properties = mapping.getProperties();
for (int i = 0, propertiesLength = properties.length; i < propertiesLength; i++) {
Property property = properties[i];
if (property.isDefaultMethod()) {
property = property.asDefaultMethod().getDefaultProperty();
}

String member = property.getMethod().getName();
ts.visitLdcInsn(member + "=");
ts.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ";",
false);
ts.visitVarInsn(ALOAD, V_THIS);
ts.visitFieldInsn(GETFIELD, mapping.getClassInternalName(), member,
getDescriptor(property.getMethod().getReturnType()));
if (property.isPrimitive()) {
ts.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append",
"(" + getDescriptor(property.asPrimitive().getPrimitiveType()) + ")L" + I_STRING_BUILDER + ";", false);
} else {
ts.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(L" + I_OBJECT + ";)L" + I_STRING_BUILDER + ";",
false);
}

if (i + 1 < propertiesLength) {
ts.visitLdcInsn(", ");
ts.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ";",
false);
}
}

ts.visitLdcInsn("}");
ts.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ";", false);
ts.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "toString", "()L" + I_STRING + ";", false);

ts.visitInsn(ARETURN);
ts.visitEnd();
ts.visitMaxs(0, 0);
}

private static boolean hasDefaultValue(final Class<?> klass, final Object value) {
if (value == null) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,33 @@ protected ConfigMappingInterface computeValue(final Class<?> type) {
private final Property[] properties;
private final Map<String, Property> propertiesByName;
private final NamingStrategy namingStrategy;
private final ToStringMethod toStringMethod;

ConfigMappingInterface(final Class<?> interfaceType, final ConfigMappingInterface[] superTypes,
final Property[] properties) {
this.interfaceType = interfaceType;
this.className = interfaceType.getName() + interfaceType.getName().hashCode() + "Impl";
this.superTypes = superTypes;
this.properties = properties;
this.propertiesByName = toPropertiesMap(properties);

List<Property> filteredProperties = new ArrayList<>();
Map<String, Property> propertiesByName = new HashMap<>();
ToStringMethod toStringMethod = null;
for (Property property : properties) {
if (!property.isToStringMethod()) {
filteredProperties.add(property);
propertiesByName.put(property.getMethod().getName(), property);
} else {
toStringMethod = (ToStringMethod) property;
}
}
if (toStringMethod == null) {
toStringMethod = ToStringMethod.NONE;
}

this.properties = filteredProperties.toArray(new Property[0]);
this.propertiesByName = propertiesByName;
this.namingStrategy = getNamingStrategy(interfaceType);
this.toStringMethod = toStringMethod;
}

/**
Expand Down Expand Up @@ -135,6 +153,10 @@ public NamingStrategy getNamingStrategy() {
return namingStrategy;
}

ToStringMethod getToStringMethod() {
return toStringMethod;
}

public String getClassName() {
return className;
}
Expand Down Expand Up @@ -213,6 +235,10 @@ public boolean isDefaultMethod() {
return false;
}

public boolean isToStringMethod() {
return false;
}

public PrimitiveProperty asPrimitive() {
throw new ClassCastException();
}
Expand Down Expand Up @@ -590,6 +616,31 @@ public DefaultMethodProperty asDefaultMethod() {
}
}

public static final class ToStringMethod extends Property {
private final boolean generate;

ToStringMethod() {
super(null, null);
this.generate = false;
}

ToStringMethod(final Method method) {
super(method, null);
this.generate = true;
}

@Override
public boolean isToStringMethod() {
return true;
}

public boolean generate() {
return generate;
}

static final ToStringMethod NONE = new ToStringMethod();
}

private static ConfigMappingInterface createConfigurationInterface(Class<?> interfaceType) {
if (!interfaceType.isInterface() || interfaceType.getTypeParameters().length != 0) {
return null;
Expand Down Expand Up @@ -652,6 +703,10 @@ static Property[] getProperties(Method[] methods, int si, int ti) {
}

private static Property getPropertyDef(Method method, Type type) {
if (isToStringMethod(method)) {
return new ToStringMethod(method);
}

Method defaultMethod = hasDefaultMethodImplementation(method);
if (defaultMethod != null) {
return new DefaultMethodProperty(method, defaultMethod, getPropertyDef(defaultMethod, type));
Expand Down Expand Up @@ -726,6 +781,13 @@ private static Property getPropertyDef(Method method, Type type) {
return new LeafProperty(method, propertyName, type, convertWith, annotation == null ? null : annotation.value());
}

private static boolean isToStringMethod(Method method) {
return method.getName().equals("toString") &&
method.getParameterCount() == 0 &&
method.getReturnType().equals(String.class) &&
!method.isDefault();
}

@SuppressWarnings("squid:S1872")
private static Method hasDefaultMethodImplementation(Method method) {
Class<?> methodClass = method.getDeclaringClass();
Expand Down Expand Up @@ -781,14 +843,6 @@ private static String getPropertyName(final AnnotatedElement element) {
}
}

private static Map<String, Property> toPropertiesMap(final Property[] properties) {
Map<String, Property> map = new HashMap<>();
for (Property p : properties) {
map.put(p.getMethod().getName(), p);
}
return map;
}

private static void getNested(final Property[] properties, final List<ConfigMappingInterface> nested) {
for (Property property : properties) {
if (property instanceof GroupProperty) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,40 @@ void validateAnnotations() {
.startsWith("SRCFG00044: The @ConfigProperties annotation can only be placed in classes"));
}

@ConfigMapping(prefix = "server")
interface ToStringMapping {
String host();

int port();

List<Alias> aliases();

String toString();

interface Alias {
String name();

String toString();
}
}

@Test
void generateToString() {
SmallRyeConfig config = new SmallRyeConfigBuilder()
.withMapping(ToStringMapping.class)
.withSources(config(
"server.host", "localhost",
"server.port", "8080",
"server.aliases[0].name", "prod-1",
"server.aliases[1].name", "prod-2"))
.withConverter(Version.class, 100, new VersionConverter())
.build();

ToStringMapping mapping = config.getConfigMapping(ToStringMapping.class);
assertEquals("ToStringMapping{host=localhost, port=8080, aliases=[Alias{name=prod-1}, Alias{name=prod-2}]}",
mapping.toString());
}

@ConfigMapping(prefix = "server")
interface Server {
String host();
Expand Down

0 comments on commit 1483a2e

Please sign in to comment.