Skip to content

Commit

Permalink
Add support for validating @ConfigProperties interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
geoand committed Feb 15, 2021
1 parent 1035287 commit 60f4666
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 141 deletions.
4 changes: 4 additions & 0 deletions extensions/arc/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http-dev-console-spi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator-spi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,16 @@
import static io.quarkus.arc.deployment.configproperties.ConfigPropertiesUtil.createReadOptionalValueAndConvertIfNeeded;
import static io.quarkus.arc.deployment.configproperties.ConfigPropertiesUtil.determineSingleGenericType;
import static io.quarkus.arc.deployment.configproperties.ConfigPropertiesUtil.registerImplicitConverter;
import static io.quarkus.arc.deployment.configproperties.ValidationUtil.*;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import javax.enterprise.context.Dependent;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Default;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.DeploymentException;
import javax.inject.Inject;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.inject.ConfigProperty;
Expand All @@ -41,26 +36,18 @@
import io.quarkus.deployment.bean.JavaBeanUtil;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem;
import io.quarkus.gizmo.BranchResult;
import io.quarkus.gizmo.BytecodeCreator;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.FieldCreator;
import io.quarkus.gizmo.FieldDescriptor;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.runtime.util.HashUtil;

final class ClassConfigPropertiesUtil {

private static final Logger LOGGER = Logger.getLogger(ClassConfigPropertiesUtil.class);

private static final String VALIDATOR_CLASS = "javax.validation.Validator";
private static final String HIBERNATE_VALIDATOR_IMPL_CLASS = "org.hibernate.validator.HibernateValidator";
private static final String CONSTRAINT_VIOLATION_EXCEPTION_CLASS = "javax.validation.ConstraintViolationException";

private final IndexView applicationIndex;
private final YamlListObjectHandler yamlListObjectHandler;
private final ClassCreator producerClassCreator;
Expand All @@ -85,67 +72,6 @@ final class ClassConfigPropertiesUtil {
this.configProperties = configProperties;
}

/**
* Generates a class like the following:
*
* <pre>
* &#64;ApplicationScoped
* public class EnsureValidation {
*
* &#64;Inject
* MyConfig myConfig;
*
* &#64;Inject
* OtherProperties other;
*
* public void onStartup(@Observes StartupEvent ev) {
* myConfig.toString();
* other.toString();
* }
* }
* </pre>
*
* This class is useful in order to ensure that validation errors will prevent application startup
*/
static void generateStartupObserverThatInjectsConfigClass(ClassOutput classOutput, Set<DotName> configClasses) {
try (ClassCreator classCreator = ClassCreator.builder().classOutput(classOutput)
.className(ConfigPropertiesUtil.PACKAGE_TO_PLACE_GENERATED_CLASSES + ".ConfigPropertiesObserver")
.build()) {
classCreator.addAnnotation(Dependent.class);

Map<DotName, FieldDescriptor> configClassToFieldDescriptor = new HashMap<>(configClasses.size());

for (DotName configClass : configClasses) {
String configClassStr = configClass.toString();
FieldCreator fieldCreator = classCreator
.getFieldCreator(
configClass.isInner() ? configClass.local()
: configClass.withoutPackagePrefix() + "_" + HashUtil.sha1(configClassStr),
configClassStr)
.setModifiers(Modifier.PUBLIC); // done to prevent warning during the build
fieldCreator.addAnnotation(Inject.class);

configClassToFieldDescriptor.put(configClass, fieldCreator.getFieldDescriptor());
}

try (MethodCreator methodCreator = classCreator.getMethodCreator("onStartup", void.class, StartupEvent.class)) {
methodCreator.getParameterAnnotations(0).addAnnotation(Observes.class);
for (DotName configClass : configClasses) {
/*
* We call toString on the bean which ensure that bean is created thus ensuring
* validation is actually performed
*/
ResultHandle field = methodCreator.readInstanceField(configClassToFieldDescriptor.get(configClass),
methodCreator.getThis());
methodCreator.invokeVirtualMethod(
MethodDescriptor.ofMethod(configClass.toString(), "toString", String.class.getName()),
field);
}
methodCreator.returnValue(null); // the method doesn't need to do anything
}
}
}

/**
* @return true if the configuration class needs validation
*/
Expand Down Expand Up @@ -208,7 +134,7 @@ boolean addProducerMethodForClassConfigProperties(ClassLoader classLoader, Class
failOnMismatchingMember, methodCreator);

if (needsValidation) {
createValidationCodePath(methodCreator, configObject, prefixStr);
createValidationCodePath(methodCreator, configObject);
} else {
methodCreator.returnValue(configObject);
}
Expand All @@ -217,23 +143,6 @@ boolean addProducerMethodForClassConfigProperties(ClassLoader classLoader, Class
return needsValidation;
}

private static boolean needsValidation() {
/*
* Hibernate Validator has minimum overhead if the class is unconstrained,
* so we'll just pass all config classes to it if it's present
*/
return isHibernateValidatorInClasspath();
}

private static boolean isHibernateValidatorInClasspath() {
try {
Class.forName(HIBERNATE_VALIDATOR_IMPL_CLASS, false, Thread.currentThread().getContextClassLoader());
return true;
} catch (ClassNotFoundException e) {
return false;
}
}

private ResultHandle populateConfigObject(ClassLoader classLoader, ClassInfo configClassInfo, String prefixStr,
ConfigProperties.NamingStrategy namingStrategy, boolean failOnMismatchingMember, MethodCreator methodCreator) {
String configObjectClassStr = configClassInfo.name().toString();
Expand Down Expand Up @@ -505,26 +414,4 @@ private static boolean shouldCheckForDefaultValue(ClassInfo configPropertiesClas
return !Modifier.isFinal(field.flags()) && Modifier.isPublic(field.flags());
}

/**
* Create code that uses the validator in order to validate the entire object
* If errors are found an IllegalArgumentException is thrown and the message
* is constructed by calling the previously generated VIOLATION_SET_TO_STRING_METHOD
*/
private static void createValidationCodePath(MethodCreator bytecodeCreator, ResultHandle configObject,
String configPrefix) {
ResultHandle validationResult = bytecodeCreator.invokeInterfaceMethod(
MethodDescriptor.ofMethod(VALIDATOR_CLASS, "validate", Set.class, Object.class, Class[].class),
bytecodeCreator.getMethodParam(1), configObject,
bytecodeCreator.newArray(Class.class, 0));
ResultHandle constraintSetIsEmpty = bytecodeCreator.invokeInterfaceMethod(
MethodDescriptor.ofMethod(Set.class, "isEmpty", boolean.class), validationResult);
BranchResult constraintSetIsEmptyBranch = bytecodeCreator.ifNonZero(constraintSetIsEmpty);
constraintSetIsEmptyBranch.trueBranch().returnValue(configObject);

BytecodeCreator constraintSetIsEmptyFalse = constraintSetIsEmptyBranch.falseBranch();

ResultHandle exception = constraintSetIsEmptyFalse.newInstance(
MethodDescriptor.ofConstructor(CONSTRAINT_VIOLATION_EXCEPTION_CLASS, Set.class.getName()), validationResult);
constraintSetIsEmptyFalse.throwException(exception);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.hibernate.validator.spi.ClassToBeValidatedBuildItem;

public class ConfigPropertiesBuildStep {

Expand Down Expand Up @@ -110,6 +111,7 @@ void setup(CombinedIndexBuildItem combinedIndex,
BuildProducer<RunTimeConfigurationDefaultBuildItem> defaultConfigValues,
BuildProducer<ReflectiveMethodBuildItem> reflectiveMethods,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses,
BuildProducer<ClassToBeValidatedBuildItem> interfaceImplToBeValidatedProducer,
BuildProducer<ConfigPropertyBuildItem> configProperties) {
if (configPropertiesMetadataList.isEmpty()) {
return;
Expand All @@ -129,6 +131,7 @@ void setup(CombinedIndexBuildItem combinedIndex,
producerClassCreator.addAnnotation(Singleton.class);

Set<DotName> configClassesThatNeedValidation = new HashSet<>(configPropertiesMetadataList.size());
Set<DotName> configInterfacesThatNeedValidation = new HashSet<>(configPropertiesMetadataList.size());
IndexView index = combinedIndex.getIndex();
YamlListObjectHandler yamlListObjectHandler = new YamlListObjectHandler(nonBeansClassOutput, index, reflectiveClasses);
ClassConfigPropertiesUtil classConfigPropertiesUtil = new ClassConfigPropertiesUtil(index,
Expand All @@ -137,6 +140,7 @@ void setup(CombinedIndexBuildItem combinedIndex,
InterfaceConfigPropertiesUtil interfaceConfigPropertiesUtil = new InterfaceConfigPropertiesUtil(index,
yamlListObjectHandler, nonBeansClassOutput, producerClassCreator, capabilities, defaultConfigValues,
configProperties, reflectiveClasses);
Set<String> interfaceImplsToBeValidated = new HashSet<>();
for (ConfigPropertiesMetadataBuildItem configPropertiesMetadata : configPropertiesMetadataList) {
ClassInfo classInfo = configPropertiesMetadata.getClassInfo();

Expand All @@ -148,14 +152,19 @@ void setup(CombinedIndexBuildItem combinedIndex,
*/

Map<DotName, GeneratedClass> interfaceToGeneratedClass = new HashMap<>();
interfaceConfigPropertiesUtil.generateImplementationForInterfaceConfigProperties(
String generatedClassName = interfaceConfigPropertiesUtil.generateImplementationForInterfaceConfigProperties(
classInfo, configPropertiesMetadata.getPrefix(),
configPropertiesMetadata.getNamingStrategy(),
interfaceToGeneratedClass);
for (Map.Entry<DotName, GeneratedClass> entry : interfaceToGeneratedClass.entrySet()) {
interfaceConfigPropertiesUtil.addProducerMethodForInterfaceConfigProperties(entry.getKey(),
boolean needsValidation = interfaceConfigPropertiesUtil.addProducerMethodForInterfaceConfigProperties(
entry.getKey(),
configPropertiesMetadata.getPrefix(), configPropertiesMetadata.isNeedsQualifier(),
entry.getValue());
if (needsValidation) {
configInterfacesThatNeedValidation.add(classInfo.name());
interfaceImplsToBeValidated.add(generatedClassName);
}
}
} else {
/*
Expand All @@ -174,9 +183,14 @@ void setup(CombinedIndexBuildItem combinedIndex,

producerClassCreator.close();

if (!configClassesThatNeedValidation.isEmpty()) {
ClassConfigPropertiesUtil.generateStartupObserverThatInjectsConfigClass(beansClassOutput,
configClassesThatNeedValidation);
if (!configClassesThatNeedValidation.isEmpty() || !configInterfacesThatNeedValidation.isEmpty()) {
ValidationUtil.generateStartupObserverThatInjectsConfigClass(beansClassOutput,
configClassesThatNeedValidation, configInterfacesThatNeedValidation);
}
if (!interfaceImplsToBeValidated.isEmpty()) {
for (String impl : interfaceImplsToBeValidated) {
interfaceImplToBeValidatedProducer.produce(new ClassToBeValidatedBuildItem(DotName.createSimple(impl)));
}
}
}
}
Loading

0 comments on commit 60f4666

Please sign in to comment.