Skip to content

Commit

Permalink
Allow @ConfigProperties annotated classes to have a super class
Browse files Browse the repository at this point in the history
  • Loading branch information
geoand committed Apr 28, 2020
1 parent 13e538d commit c57df67
Show file tree
Hide file tree
Showing 4 changed files with 491 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -123,25 +123,12 @@ static boolean addProducerMethodForClassConfigProperties(ClassLoader classLoader
IndexView applicationIndex,
BuildProducer<ConfigPropertyBuildItem> configProperties) {

if (!DotNames.OBJECT.equals(configPropertiesClassInfo.superName())) {
throw new IllegalArgumentException(
"Classes annotated with @" + DotNames.CONFIG_PROPERTIES
+ " cannot extend other classes. Offending class is "
+ configPropertiesClassInfo);
}

if (!configPropertiesClassInfo.hasNoArgsConstructor()) {
throw new IllegalArgumentException(
"Class " + configPropertiesClassInfo + " which is annotated with " + DotNames.CONFIG_PROPERTIES
+ " must contain a no-arg constructor");
}

if (!Modifier.isPublic(configPropertiesClassInfo.flags())) {
throw new IllegalArgumentException(
"Class " + configPropertiesClassInfo + " which is annotated with " + DotNames.CONFIG_PROPERTIES
+ " must be public");
}

String configObjectClassStr = configPropertiesClassInfo.name().toString();

boolean needsValidation = needsValidation();
Expand Down Expand Up @@ -211,131 +198,157 @@ private static ResultHandle populateConfigObject(ClassLoader classLoader, ClassI
// Fields with a default value will be removed from this list at the end of the method.
List<ConfigPropertyBuildItemCandidate> configPropertyBuildItemCandidates = new ArrayList<>();

// For each field of the class try to pull it out of MP Config and call the corresponding setter
List<FieldInfo> fields = configClassInfo.fields();
for (FieldInfo field : fields) {
if (field.hasAnnotation(DotNames.CONFIG_PROPERTY)) {
LOGGER.warn(
"'@ConfigProperty' is ignored when added to a field of a class annotated with '@ConfigProperties'. Offending field is '"
+ field.name() + "' of class '" + field.declaringClass().toString() + "'");
}
boolean useFieldAccess = false;

String setterName = JavaBeanUtil.getSetterName(field.name());
Type fieldType = field.type();
MethodInfo setter = configClassInfo.method(setterName, fieldType);
if (setter == null) {
if (!Modifier.isPublic(field.flags()) || Modifier.isFinal(field.flags())) {
throw new IllegalArgumentException(
"Configuration properties class " + configClassInfo + " does not have a setter for field "
+ field + " nor is the field a public non-final field");
}
useFieldAccess = true;
}
if (!useFieldAccess && !Modifier.isPublic(setter.flags())) {
// go up the class hierarchy until we reach Object
ClassInfo currentClassInHierarchy = configClassInfo;
while (true) {
if (!Modifier.isPublic(currentClassInHierarchy.flags())) {
throw new IllegalArgumentException(
"Setter " + setterName + " of class " + configClassInfo + " must be public");
"Class '" + configObjectClassStr + "' which is annotated with '" + DotNames.CONFIG_PROPERTIES
+ "' must be public, as must be the case for all of its super classes");
}

/*
* If the object is part of the application we are dealing with a nested object
* What we do is simply recursively build it up based by adding the field name to the config name prefix
*/
DotName fieldTypeDotName = fieldType.name();
String fieldTypeStr = fieldTypeDotName.toString();
ClassInfo fieldTypeClassInfo = applicationIndex.getClassByName(fieldType.name());
if (fieldTypeClassInfo != null) {
if (!fieldTypeClassInfo.hasNoArgsConstructor()) {
throw new IllegalArgumentException(
"Nested configuration class " + fieldTypeClassInfo + " must contain a no-args constructor ");
// For each field of the class try to pull it out of MP Config and call the corresponding setter
List<FieldInfo> fields = currentClassInHierarchy.fields();
for (FieldInfo field : fields) {
if (field.hasAnnotation(DotNames.CONFIG_PROPERTY)) {
LOGGER.warn(
"'@ConfigProperty' is ignored when added to a field of a class annotated with '@ConfigProperties'. Offending field is '"
+ field.name() + "' of class '" + field.declaringClass().toString() + "'");
}

if (!Modifier.isPublic(fieldTypeClassInfo.flags())) {
boolean useFieldAccess = false;

String setterName = JavaBeanUtil.getSetterName(field.name());
Type fieldType = field.type();
MethodInfo setter = currentClassInHierarchy.method(setterName, fieldType);
if (setter == null) {
if (!Modifier.isPublic(field.flags()) || Modifier.isFinal(field.flags())) {
throw new IllegalArgumentException(
"Configuration properties class '" + configClassInfo + "' does not have a setter for field "
+ field + " nor is the field a public non-final field");
}
useFieldAccess = true;
}
if (!useFieldAccess && !Modifier.isPublic(setter.flags())) {
throw new IllegalArgumentException(
"Nested configuration class " + fieldTypeClassInfo + " must be public ");
"Setter '" + setterName + "' of class '" + configClassInfo + "' must be public");
}

ResultHandle nestedConfigObject = populateConfigObject(classLoader, fieldTypeClassInfo,
prefixStr + "." + namingStrategy.getName(field.name()), namingStrategy, methodCreator,
applicationIndex, configProperties);
createWriteValue(methodCreator, configObject, field, setter, useFieldAccess, nestedConfigObject);

} else {
String fullConfigName = prefixStr + "." + namingStrategy.getName(field.name());
ResultHandle config = methodCreator.getMethodParam(0);
if (DotNames.OPTIONAL.equals(fieldTypeDotName)) {
Type genericType = determineSingleGenericType(field.type(),
field.declaringClass().name());

// config.getOptionalValue
if (genericType.kind() != Type.Kind.PARAMETERIZED_TYPE) {
ResultHandle setterValue = methodCreator.invokeInterfaceMethod(
MethodDescriptor.ofMethod(Config.class, "getOptionalValue", Optional.class, String.class,
Class.class),
config, methodCreator.load(fullConfigName),
methodCreator.loadClass(genericType.name().toString()));
createWriteValue(methodCreator, configObject, field, setter, useFieldAccess, setterValue);
} else {
// convert the String value and populate an Optional with it
ReadOptionalResponse readOptionalResponse = createReadOptionalValueAndConvertIfNeeded(fullConfigName,
genericType, field.declaringClass().name(), methodCreator, config);
createWriteValue(readOptionalResponse.getIsPresentTrue(), configObject, field, setter, useFieldAccess,
readOptionalResponse.getIsPresentTrue().invokeStaticMethod(
MethodDescriptor.ofMethod(Optional.class, "of", Optional.class, Object.class),
readOptionalResponse.getValue()));

// set Optional.empty if the value isn't set
createWriteValue(readOptionalResponse.getIsPresentFalse(), configObject, field, setter, useFieldAccess,
readOptionalResponse.getIsPresentFalse().invokeStaticMethod(
MethodDescriptor.ofMethod(Optional.class, "empty", Optional.class)));
/*
* If the object is part of the application we are dealing with a nested object
* What we do is simply recursively build it up based by adding the field name to the config name prefix
*/
DotName fieldTypeDotName = fieldType.name();
String fieldTypeStr = fieldTypeDotName.toString();
ClassInfo fieldTypeClassInfo = applicationIndex.getClassByName(fieldType.name());
if (fieldTypeClassInfo != null) {
if (!fieldTypeClassInfo.hasNoArgsConstructor()) {
throw new IllegalArgumentException(
"Nested configuration class '" + fieldTypeClassInfo + "' must contain a no-args constructor ");
}
} else {
/*
* We want to support cases where the Config class defines a default value for fields
* by simply specifying the default value in its constructor
* For such cases the strategy we follow is that when a requested property does not exist
* we check the value from the corresponding getter (or read the field value if possible)
* and if the value is not null we don't fail
*/
if (shouldCheckForDefaultValue(configClassInfo, field)) {
String getterName = JavaBeanUtil.getGetterName(field.name(), fieldTypeDotName.toString());

ReadOptionalResponse readOptionalResponse = createReadOptionalValueAndConvertIfNeeded(fullConfigName,
fieldType, field.declaringClass().name(), methodCreator, config);
if (!Modifier.isPublic(fieldTypeClassInfo.flags())) {
throw new IllegalArgumentException(
"Nested configuration class '" + fieldTypeClassInfo + "' must be public ");
}

// call the setter if the optional contained data
createWriteValue(readOptionalResponse.getIsPresentTrue(), configObject, field, setter, useFieldAccess,
readOptionalResponse.getValue());
ResultHandle nestedConfigObject = populateConfigObject(classLoader, fieldTypeClassInfo,
prefixStr + "." + namingStrategy.getName(field.name()), namingStrategy, methodCreator,
applicationIndex, configProperties);
createWriteValue(methodCreator, configObject, field, setter, useFieldAccess, nestedConfigObject);

// if optional did not contain data, check the getter and see if there is a value
BytecodeCreator isPresentFalse = readOptionalResponse.getIsPresentFalse();
ResultHandle defaultValue;
if (useFieldAccess) {
defaultValue = isPresentFalse.readInstanceField(FieldDescriptor.of(field), configObject);
} else {
String fullConfigName = prefixStr + "." + namingStrategy.getName(field.name());
ResultHandle config = methodCreator.getMethodParam(0);
if (DotNames.OPTIONAL.equals(fieldTypeDotName)) {
Type genericType = determineSingleGenericType(field.type(),
field.declaringClass().name());

// config.getOptionalValue
if (genericType.kind() != Type.Kind.PARAMETERIZED_TYPE) {
ResultHandle setterValue = methodCreator.invokeInterfaceMethod(
MethodDescriptor.ofMethod(Config.class, "getOptionalValue", Optional.class, String.class,
Class.class),
config, methodCreator.load(fullConfigName),
methodCreator.loadClass(genericType.name().toString()));
createWriteValue(methodCreator, configObject, field, setter, useFieldAccess, setterValue);
} else {
defaultValue = isPresentFalse.invokeVirtualMethod(
MethodDescriptor.ofMethod(configObjectClassStr, getterName, fieldTypeStr),
configObject);
// convert the String value and populate an Optional with it
ReadOptionalResponse readOptionalResponse = createReadOptionalValueAndConvertIfNeeded(
fullConfigName,
genericType, field.declaringClass().name(), methodCreator, config);
createWriteValue(readOptionalResponse.getIsPresentTrue(), configObject, field, setter,
useFieldAccess,
readOptionalResponse.getIsPresentTrue().invokeStaticMethod(
MethodDescriptor.ofMethod(Optional.class, "of", Optional.class, Object.class),
readOptionalResponse.getValue()));

// set Optional.empty if the value isn't set
createWriteValue(readOptionalResponse.getIsPresentFalse(), configObject, field, setter,
useFieldAccess,
readOptionalResponse.getIsPresentFalse().invokeStaticMethod(
MethodDescriptor.ofMethod(Optional.class, "empty", Optional.class)));
}
} else {
/*
* In this case we want a missing property to cause an exception that we don't handle
* So we call config.getValue making sure to handle collection values
* We want to support cases where the Config class defines a default value for fields
* by simply specifying the default value in its constructor
* For such cases the strategy we follow is that when a requested property does not exist
* we check the value from the corresponding getter (or read the field value if possible)
* and if the value is not null we don't fail
*/
ResultHandle setterValue = createReadMandatoryValueAndConvertIfNeeded(
fullConfigName, fieldType,
field.declaringClass().name(), methodCreator, config);
createWriteValue(methodCreator, configObject, field, setter, useFieldAccess, setterValue);
if (shouldCheckForDefaultValue(currentClassInHierarchy, field)) {
String getterName = JavaBeanUtil.getGetterName(field.name(), fieldTypeDotName.toString());

ReadOptionalResponse readOptionalResponse = createReadOptionalValueAndConvertIfNeeded(
fullConfigName,
fieldType, field.declaringClass().name(), methodCreator, config);

// call the setter if the optional contained data
createWriteValue(readOptionalResponse.getIsPresentTrue(), configObject, field, setter,
useFieldAccess,
readOptionalResponse.getValue());

// if optional did not contain data, check the getter and see if there is a value
BytecodeCreator isPresentFalse = readOptionalResponse.getIsPresentFalse();
ResultHandle defaultValue;
if (useFieldAccess) {
defaultValue = isPresentFalse.readInstanceField(FieldDescriptor.of(field), configObject);
} else {
defaultValue = isPresentFalse.invokeVirtualMethod(
MethodDescriptor.ofMethod(configObjectClassStr, getterName, fieldTypeStr),
configObject);
}
} else {
/*
* In this case we want a missing property to cause an exception that we don't handle
* So we call config.getValue making sure to handle collection values
*/
ResultHandle setterValue = createReadMandatoryValueAndConvertIfNeeded(
fullConfigName, fieldType,
field.declaringClass().name(), methodCreator, config);
createWriteValue(methodCreator, configObject, field, setter, useFieldAccess, setterValue);

}
configPropertyBuildItemCandidates
.add(new ConfigPropertyBuildItemCandidate(field.name(), fullConfigName, fieldType));
}
configPropertyBuildItemCandidates
.add(new ConfigPropertyBuildItemCandidate(field.name(), fullConfigName, fieldType));
}
}

ConfigPropertyBuildItemCandidateUtil.removePropertiesWithDefaultValue(classLoader,
currentClassInHierarchy.name().toString(),
configPropertyBuildItemCandidates);

DotName superClassDotName = currentClassInHierarchy.superName();
if (superClassDotName.equals(DotNames.OBJECT)) {
break;
}
currentClassInHierarchy = applicationIndex.getClassByName(superClassDotName);
if (currentClassInHierarchy == null) {
break;
}
}

ConfigPropertyBuildItemCandidateUtil.removePropertiesWithDefaultValue(classLoader, configObjectClassStr,
configPropertyBuildItemCandidates);
for (ConfigPropertyBuildItemCandidate candidate : configPropertyBuildItemCandidates) {
configProperties
.produce(new ConfigPropertyBuildItem(candidate.getConfigPropertyName(), candidate.getConfigPropertyType()));
Expand Down
Loading

0 comments on commit c57df67

Please sign in to comment.