Skip to content

Commit

Permalink
Add support for sub-interfaces in @ConfigProperties
Browse files Browse the repository at this point in the history
Fixes: #10511
  • Loading branch information
geoand committed Jul 7, 2020
1 parent e711e0d commit 1782a74
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.arc.deployment.configproperties;

import static io.quarkus.arc.deployment.configproperties.InterfaceConfigPropertiesUtil.*;

import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -21,6 +23,7 @@
import io.quarkus.arc.deployment.ConfigPropertyBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.configproperties.InterfaceConfigPropertiesUtil.GeneratedClass;
import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
Expand Down Expand Up @@ -134,13 +137,17 @@ void setup(CombinedIndexBuildItem combinedIndex,
* The generated producer bean simply needs to return an instance of the generated class
*/

String generatedClassName = InterfaceConfigPropertiesUtil.generateImplementationForInterfaceConfigProperties(
Map<DotName, GeneratedClass> interfaceToGeneratedClass = new HashMap<>();
generateImplementationForInterfaceConfigProperties(
classInfo, nonBeansClassOutput, combinedIndex.getIndex(), configPropertiesMetadata.getPrefix(),
configPropertiesMetadata.getNamingStrategy(), defaultConfigValues, configProperties);
InterfaceConfigPropertiesUtil.addProducerMethodForInterfaceConfigProperties(producerClassCreator,
classInfo.name(), configPropertiesMetadata.getPrefix(), configPropertiesMetadata.isNeedsQualifier(),
generatedClassName);

configPropertiesMetadata.getNamingStrategy(), defaultConfigValues, configProperties,
interfaceToGeneratedClass);
for (Map.Entry<DotName, GeneratedClass> entry : interfaceToGeneratedClass.entrySet()) {
addProducerMethodForInterfaceConfigProperties(entry.getKey(),
configPropertiesMetadata.getPrefix(), configPropertiesMetadata.isNeedsQualifier(),
producerClassCreator,
entry.getValue());
}
} else {
/*
* In this case the producer method contains all the logic to instantiate the config class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
import static io.quarkus.arc.deployment.configproperties.ConfigPropertiesUtil.createReadMandatoryValueAndConvertIfNeeded;
import static io.quarkus.arc.deployment.configproperties.ConfigPropertiesUtil.createReadOptionalValueAndConvertIfNeeded;
import static io.quarkus.arc.deployment.configproperties.ConfigPropertiesUtil.determineSingleGenericType;
import static io.quarkus.gizmo.MethodDescriptor.ofMethod;

import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

Expand All @@ -23,6 +26,10 @@
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.arc.Unremovable;
import io.quarkus.arc.config.ConfigProperties;
import io.quarkus.arc.deployment.ConfigPropertyBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
Expand Down Expand Up @@ -51,8 +58,9 @@ private InterfaceConfigPropertiesUtil() {
* }
* </pre>
*/
static void addProducerMethodForInterfaceConfigProperties(ClassCreator classCreator, DotName interfaceName,
String prefix, boolean needsQualifier, String generatedClassName) {
static void addProducerMethodForInterfaceConfigProperties(DotName interfaceName, String prefix, boolean needsQualifier,
ClassCreator classCreator,
GeneratedClass generatedClass) {
String methodName = "produce" + interfaceName.withoutPackagePrefix();
if (needsQualifier) {
// we need to differentiate the different producers of the same class
Expand All @@ -68,22 +76,42 @@ static void addProducerMethodForInterfaceConfigProperties(ClassCreator classCrea
} else {
method.addAnnotation(Default.class);
}
method.returnValue(method.newInstance(MethodDescriptor.ofConstructor(generatedClassName, Config.class),
if (generatedClass.isUnremovable()) {
method.addAnnotation(Unremovable.class);
}
method.returnValue(method.newInstance(MethodDescriptor.ofConstructor(generatedClass.getName(), Config.class),
method.getMethodParam(0)));
}
}

static String generateImplementationForInterfaceConfigProperties(ClassInfo originalInterface, ClassOutput classOutput,
static void generateImplementationForInterfaceConfigProperties(ClassInfo originalInterface, ClassOutput classOutput,
IndexView index, String prefixStr, ConfigProperties.NamingStrategy namingStrategy,
BuildProducer<RunTimeConfigurationDefaultBuildItem> defaultConfigValues,
BuildProducer<ConfigPropertyBuildItem> configProperties) {
BuildProducer<ConfigPropertyBuildItem> configProperties,
Map<DotName, GeneratedClass> interfaceToGeneratedClass) {

generateImplementationForInterfaceConfigPropertiesRec(originalInterface, originalInterface, classOutput, index,
prefixStr, namingStrategy, defaultConfigValues, configProperties, interfaceToGeneratedClass);
}

private static void generateImplementationForInterfaceConfigPropertiesRec(ClassInfo originalInterface,
ClassInfo currentInterface, ClassOutput classOutput,
IndexView index, String prefixStr, ConfigProperties.NamingStrategy namingStrategy,
BuildProducer<RunTimeConfigurationDefaultBuildItem> defaultConfigValues,
BuildProducer<ConfigPropertyBuildItem> configProperties,
Map<DotName, GeneratedClass> interfaceToGeneratedClass) {

Set<DotName> allInterfaces = new HashSet<>();
allInterfaces.add(originalInterface.name());
collectInterfacesRec(originalInterface, index, allInterfaces);
allInterfaces.add(currentInterface.name());
collectInterfacesRec(currentInterface, index, allInterfaces);

String generatedClassName = createName(currentInterface.name(), prefixStr);
// the generated class needs to be unremovable if it's not top-level interface since it will only be obtains via Arc.container().instance() in generated code
interfaceToGeneratedClass.put(currentInterface.name(),
new GeneratedClass(generatedClassName, !originalInterface.name().equals(currentInterface.name())));

String generatedClassName = createName(originalInterface.name(), prefixStr);
try (ClassCreator interfaceImplClassCreator = ClassCreator.builder().classOutput(classOutput)
.interfaces(originalInterface.name().toString()).className(generatedClassName)
.interfaces(currentInterface.name().toString()).className(generatedClassName)
.build()) {

FieldDescriptor configField = interfaceImplClassCreator.getFieldCreator("config", Config.class)
Expand Down Expand Up @@ -117,17 +145,37 @@ static String generateImplementationForInterfaceConfigProperties(ClassInfo origi
+ " is not a getter method since it returns void");
}

/*
* TODO examine if returning objects can be supported - currently it isn't supported because we don't try to
* create the return object
* and instead just rely on what MP Config gives us back
*/

NameAndDefaultValue nameAndDefaultValue = determinePropertyNameAndDefaultValue(method, namingStrategy);
String fullConfigName = prefixStr + "." + nameAndDefaultValue.getName();
try (MethodCreator methodCreator = interfaceImplClassCreator.getMethodCreator(method.name(),
method.returnType().name().toString())) {

if ((returnType.kind() == Type.Kind.CLASS)) {
ClassInfo returnTypeClassInfo = index.getClassByName(returnType.name());
if ((returnTypeClassInfo != null) && Modifier.isInterface(returnTypeClassInfo.flags())) {
// when the return type is an interface,
// 1) we generate an implementation
// 2) retrieve the implementation from Arc

generateImplementationForInterfaceConfigPropertiesRec(originalInterface, returnTypeClassInfo,
classOutput,
index, fullConfigName, namingStrategy, defaultConfigValues, configProperties,
interfaceToGeneratedClass);

ResultHandle arcContainer = methodCreator
.invokeStaticMethod(
MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class));
ResultHandle instanceHandle = methodCreator.invokeInterfaceMethod(
ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class,
Annotation[].class),
arcContainer, methodCreator.loadClass(returnTypeClassInfo.name().toString()),
methodCreator.newArray(Annotation.class, 0));
methodCreator.returnValue(methodCreator.invokeInterfaceMethod(
MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class), instanceHandle));
continue;
}
}

ResultHandle config = methodCreator.readInstanceField(configField, methodCreator.getThis());
String defaultValueStr = nameAndDefaultValue.getDefaultValue();
if (DotNames.OPTIONAL.equals(returnType.name())) {
Expand Down Expand Up @@ -196,8 +244,6 @@ static String generateImplementationForInterfaceConfigProperties(ClassInfo origi
}
}
}

return generatedClassName;
}

private static void collectInterfacesRec(ClassInfo current, IndexView index, Set<DotName> result) {
Expand Down Expand Up @@ -276,4 +322,22 @@ public String getDefaultValue() {
return defaultValue;
}
}

static class GeneratedClass {
private final String name;
private final boolean unremovable;

public GeneratedClass(String name, boolean unremovable) {
this.name = name;
this.unremovable = unremovable;
}

public String getName() {
return name;
}

public boolean isUnremovable() {
return unremovable;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.quarkus.arc.test.configproperties;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.config.ConfigProperties;
import io.quarkus.test.QuarkusUnitTest;

public class InterfaceWithOtherConfigPrefixTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(DummyBean.class, DummyProperties.class, Other.class)
.addAsResource(new StringAsset("dummy.other.name=test"), "application.properties"));

@Inject
DummyBean dummyBean;

@Test
public void testConfiguredValues() {
Other other = dummyBean.dummyProperties.getOther();
assertNotNull(other);
assertEquals("test", other.getName());
}

@Singleton
public static class DummyBean {

@Inject
public DummyProperties dummyProperties;
}

@ConfigProperties
public interface DummyProperties {

Other getOther();
}

public interface Other {

String getName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.quarkus.arc.test.configproperties;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Arrays;
import java.util.Collection;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.config.ConfigProperties;
import io.quarkus.test.QuarkusUnitTest;

public class InterfaceWithSubinterfaceConfigPrefixTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(DummyBean.class, DummyProperties.class, DummyProperties.SubDummy.class)
.addAsResource(new StringAsset(
"dummy.name=quarkus\ndummy.boolWD=true\ndummy.sub-dummy.foo=bar\ndummy.sub-dummy.other=some"),
"application.properties"));

@Inject
DummyBean dummyBean;

@Test
public void testConfiguredValues() {
assertEquals("quarkus", dummyBean.dummyProperties.getName());
assertTrue(dummyBean.dummyProperties.isBoolWithDef());
DummyProperties.SubDummy subDummy = dummyBean.dummyProperties.getSubDummy();
assertNotNull(subDummy);
assertEquals("some", subDummy.getOther());
assertEquals("bar", subDummy.getFooBar());
assertEquals(Arrays.asList(1, 2, 3), subDummy.getNumbers());
}

@Singleton
public static class DummyBean {

@Inject
public DummyProperties dummyProperties;
}

@ConfigProperties(prefix = "dummy")
public interface DummyProperties {

String getName();

@ConfigProperty(name = "boolWD", defaultValue = "false")
boolean isBoolWithDef();

SubDummy getSubDummy();

interface SubDummy {

@ConfigProperty(defaultValue = "1,2,3")
Collection<Integer> getNumbers();

@ConfigProperty(name = "foo")
String getFooBar();

String getOther();
}
}
}

0 comments on commit 1782a74

Please sign in to comment.