Formalist extension added (Apache Bval lisbrary wrapper)

master
Edward M. Kagan 6 years ago
parent ed862ec2fe
commit 0d85d73c9b

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.pagan.quarkus</groupId>
<artifactId>janitor-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>formalist-deployment</artifactId>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-server-common-spi</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
<type>jar</type>
</dependency>
<!-- <dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>-->
<dependency>
<groupId>org.pagan.quarkus</groupId>
<artifactId>formalist</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${quarkus.platform.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,350 @@
package org.pagan.formalist;
import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
import java.lang.annotation.Repeatable;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import javax.validation.ClockProvider;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.MessageInterpolator;
import javax.validation.ParameterNameProvider;
import javax.validation.TraversableResolver;
import javax.validation.Valid;
import javax.validation.executable.ValidateOnExecution;
import javax.validation.valueextraction.ValueExtractor;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.CompositeIndex;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
import io.quarkus.arc.deployment.BeanContainerListenerBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveFieldBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem;
import io.quarkus.deployment.logging.LogCleanupFilterBuildItem;
import io.quarkus.deployment.recording.RecorderContext;
//import org.pagan.formalist.HibernateValidatorBuildTimeConfig;
import org.pagan.formalist.HibernateValidatorRecorder;
import org.pagan.formalist.ValidatorProvider;
import org.pagan.formalist.interceptor.MethodValidationInterceptor;
import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceMethodAnnotationsBuildItem;
//import io.quarkus.runtime.LocalesBuildTimeConfig;
class HibernateValidatorProcessor {
private static final String META_INF_VALIDATION_XML = "META-INF/validation.xml";
private static final DotName CONSTRAINT_VALIDATOR_FACTORY = DotName
.createSimple(ConstraintValidatorFactory.class.getName());
private static final DotName MESSAGE_INTERPOLATOR = DotName.createSimple(MessageInterpolator.class.getName());
// private static final DotName LOCALE_RESOLVER =
// DotName.createSimple(LocaleResolver.class.getName());
private static final DotName TRAVERSABLE_RESOLVER = DotName.createSimple(TraversableResolver.class.getName());
private static final DotName PARAMETER_NAME_PROVIDER = DotName.createSimple(ParameterNameProvider.class.getName());
private static final DotName CLOCK_PROVIDER = DotName.createSimple(ClockProvider.class.getName());
// private static final DotName SCRIPT_EVALUATOR_FACTORY =
// DotName.createSimple(ScriptEvaluatorFactory.class.getName());
// private static final DotName GETTER_PROPERTY_SELECTION_STRATEGY = DotName
// .createSimple(GetterPropertySelectionStrategy.class.getName());
private static final DotName CONSTRAINT_VALIDATOR = DotName.createSimple(ConstraintValidator.class.getName());
private static final DotName VALUE_EXTRACTOR = DotName.createSimple(ValueExtractor.class.getName());
private static final DotName VALIDATE_ON_EXECUTION = DotName.createSimple(ValidateOnExecution.class.getName());
private static final DotName VALID = DotName.createSimple(Valid.class.getName());
private static final DotName REPEATABLE = DotName.createSimple(Repeatable.class.getName());
private static final Pattern BUILT_IN_CONSTRAINT_REPEATABLE_CONTAINER_PATTERN = Pattern.compile("\\$List$");
// @BuildStep
// HotDeploymentWatchedFileBuildItem configFile() {
// return new HotDeploymentWatchedFileBuildItem(META_INF_VALIDATION_XML);
// }
// @BuildStep
// LogCleanupFilterBuildItem logCleanup() {
// return new
// LogCleanupFilterBuildItem("org.hibernate.validator.internal.util.Version",
// "HV000001:");
// }
@BuildStep
void registerAdditionalBeans(BuildProducer<AdditionalBeanBuildItem> additionalBeans,
BuildProducer<UnremovableBeanBuildItem> unremovableBean, Capabilities capabilities) {
// The bean encapsulating the Validator and ValidatorFactory
additionalBeans.produce(new AdditionalBeanBuildItem(ValidatorProvider.class));
// The CDI interceptor which will validate the methods annotated with
// @MethodValidated
additionalBeans.produce(new AdditionalBeanBuildItem(MethodValidationInterceptor.class));
if (capabilities.isPresent(Capability.RESTEASY)) {
// The CDI interceptor which will validate the methods annotated with
// @JaxrsEndPointValidated
additionalBeans.produce(
new AdditionalBeanBuildItem("org.pagan.formalist.jaxrs.JaxrsEndPointValidationInterceptor"));
// additionalBeans.produce(new AdditionalBeanBuildItem(
// "io.quarkus.hibernate.validator.runtime.jaxrs.ResteasyContextLocaleResolver"));
}
// Do not remove the Bean Validation beans
unremovableBean.produce(new UnremovableBeanBuildItem(new Predicate<BeanInfo>() {
@Override
public boolean test(BeanInfo beanInfo) {
return beanInfo.hasType(CONSTRAINT_VALIDATOR) || beanInfo.hasType(CONSTRAINT_VALIDATOR_FACTORY)
|| beanInfo.hasType(MESSAGE_INTERPOLATOR) || beanInfo.hasType(TRAVERSABLE_RESOLVER)
|| beanInfo.hasType(PARAMETER_NAME_PROVIDER) || beanInfo.hasType(CLOCK_PROVIDER)
|| beanInfo.hasType(VALUE_EXTRACTOR);
// || beanInfo.hasType(SCRIPT_EVALUATOR_FACTORY)
// || beanInfo.hasType(GETTER_PROPERTY_SELECTION_STRATEGY)
// || beanInfo.hasType(LOCALE_RESOLVER);
}
}));
}
@BuildStep
@Record(STATIC_INIT)
public void build(HibernateValidatorRecorder recorder, RecorderContext recorderContext,
BuildProducer<ReflectiveFieldBuildItem> reflectiveFields,
BuildProducer<ReflectiveMethodBuildItem> reflectiveMethods,
BuildProducer<AnnotationsTransformerBuildItem> annotationsTransformers,
BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, CombinedIndexBuildItem combinedIndexBuildItem,
BuildProducer<FeatureBuildItem> feature,
BuildProducer<BeanContainerListenerBuildItem> beanContainerListener,
ShutdownContextBuildItem shutdownContext,
List<AdditionalJaxRsResourceMethodAnnotationsBuildItem> additionalJaxRsResourceMethodAnnotations,
Capabilities capabilities
// ,LocalesBuildTimeConfig localesBuildTimeConfig
// ,HibernateValidatorBuildTimeConfig hibernateValidatorBuildTimeConfig
) throws Exception {
feature.produce(new FeatureBuildItem("formalist"));
// we use both indexes to support both generated beans and jars that contain no
// CDI beans but only Validation annotations
IndexView indexView = CompositeIndex.create(beanArchiveIndexBuildItem.getIndex(),
combinedIndexBuildItem.getIndex());
Set<DotName> consideredAnnotations = new HashSet<>();
// Set<String> builtinConstraints = ConstraintHelper.getBuiltinConstraints();
// Collect the constraint annotations provided by Hibernate Validator and Bean
// Validation
// contributeBuiltinConstraints(builtinConstraints, consideredAnnotations);
// Add the constraint annotations present in the application itself
for (AnnotationInstance constraint : indexView
.getAnnotations(DotName.createSimple(Constraint.class.getName()))) {
consideredAnnotations.add(constraint.target().asClass().name());
if (constraint.target().asClass().annotations().containsKey(REPEATABLE)) {
for (AnnotationInstance repeatableConstraint : constraint.target().asClass().annotations()
.get(REPEATABLE)) {
consideredAnnotations.add(repeatableConstraint.value().asClass().name());
}
}
}
// Also consider elements that are marked with @Valid
consideredAnnotations.add(VALID);
// Also consider elements that are marked with @ValidateOnExecution
consideredAnnotations.add(VALIDATE_ON_EXECUTION);
Set<DotName> classNamesToBeValidated = new HashSet<>();
Map<DotName, Set<String>> inheritedAnnotationsToBeValidated = new HashMap<>();
Set<String> detectedBuiltinConstraints = new HashSet<>();
for (DotName consideredAnnotation : consideredAnnotations) {
Collection<AnnotationInstance> annotationInstances = indexView.getAnnotations(consideredAnnotation);
if (annotationInstances.isEmpty()) {
continue;
}
// we trim the repeatable container suffix if needed
String builtinConstraintCandidate = BUILT_IN_CONSTRAINT_REPEATABLE_CONTAINER_PATTERN
.matcher(consideredAnnotation.toString()).replaceAll("");
// if (builtinConstraints.contains(builtinConstraintCandidate)) {
// detectedBuiltinConstraints.add(builtinConstraintCandidate);
// }
for (AnnotationInstance annotation : annotationInstances) {
if (annotation.target().kind() == AnnotationTarget.Kind.FIELD) {
contributeClass(classNamesToBeValidated, indexView,
annotation.target().asField().declaringClass().name());
reflectiveFields.produce(new ReflectiveFieldBuildItem(annotation.target().asField()));
contributeClassMarkedForCascadingValidation(classNamesToBeValidated, indexView,
consideredAnnotation, annotation.target().asField().type());
} else if (annotation.target().kind() == AnnotationTarget.Kind.METHOD) {
contributeClass(classNamesToBeValidated, indexView,
annotation.target().asMethod().declaringClass().name());
// we need to register the method for reflection as it could be a getter
reflectiveMethods.produce(new ReflectiveMethodBuildItem(annotation.target().asMethod()));
contributeClassMarkedForCascadingValidation(classNamesToBeValidated, indexView,
consideredAnnotation, annotation.target().asMethod().returnType());
contributeMethodsWithInheritedValidation(inheritedAnnotationsToBeValidated, indexView,
annotation.target().asMethod());
} else if (annotation.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
contributeClass(classNamesToBeValidated, indexView,
annotation.target().asMethodParameter().method().declaringClass().name());
// a getter does not have parameters so it's a pure method: no need for
// reflection in this case
contributeClassMarkedForCascadingValidation(classNamesToBeValidated, indexView,
consideredAnnotation,
// FIXME this won't work in the case of synthetic parameters
annotation.target().asMethodParameter().method().parameters()
.get(annotation.target().asMethodParameter().position()));
contributeMethodsWithInheritedValidation(inheritedAnnotationsToBeValidated, indexView,
annotation.target().asMethodParameter().method());
} else if (annotation.target().kind() == AnnotationTarget.Kind.CLASS) {
contributeClass(classNamesToBeValidated, indexView, annotation.target().asClass().name());
// no need for reflection in the case of a class level constraint
}
}
}
// Add the annotations transformer to add @MethodValidated annotations on the
// methods requiring validation
Set<DotName> additionalJaxRsMethodAnnotationsDotNames = new HashSet<>(
additionalJaxRsResourceMethodAnnotations.size());
for (AdditionalJaxRsResourceMethodAnnotationsBuildItem additionalJaxRsResourceMethodAnnotation : additionalJaxRsResourceMethodAnnotations) {
additionalJaxRsMethodAnnotationsDotNames
.addAll(additionalJaxRsResourceMethodAnnotation.getAnnotationClasses());
}
annotationsTransformers.produce(new AnnotationsTransformerBuildItem(new MethodValidatedAnnotationsTransformer(
consideredAnnotations, additionalJaxRsMethodAnnotationsDotNames, inheritedAnnotationsToBeValidated)));
Set<Class<?>> classesToBeValidated = new HashSet<>();
for (DotName className : classNamesToBeValidated) {
classesToBeValidated.add(recorderContext.classProxy(className.toString()));
}
beanContainerListener
.produce(new BeanContainerListenerBuildItem(recorder.initializeValidatorFactory(classesToBeValidated,
detectedBuiltinConstraints, hasXmlConfiguration(),
// capabilities.isPresent(Capability.HIBERNATE_ORM),
shutdownContext
// ,localesBuildTimeConfig
// ,hibernateValidatorBuildTimeConfig
)));
}
// @BuildStep
// NativeImageConfigBuildItem nativeImageConfig() {
// return NativeImageConfigBuildItem.builder()
// .addResourceBundle(AbstractMessageInterpolator.DEFAULT_VALIDATION_MESSAGES)
// .addResourceBundle(AbstractMessageInterpolator.USER_VALIDATION_MESSAGES)
// .addResourceBundle(AbstractMessageInterpolator.CONTRIBUTOR_VALIDATION_MESSAGES)
// .build();
// }
private static void contributeBuiltinConstraints(Set<String> builtinConstraints,
Set<DotName> consideredAnnotationsCollector) {
for (String builtinConstraint : builtinConstraints) {
consideredAnnotationsCollector.add(DotName.createSimple(builtinConstraint));
// for all built-in constraints, we follow a strict convention for repeatable
// annotations,
// they are all inner classes called List
// while not all our built-in constraints are repeatable, let's avoid loading
// the class to check
consideredAnnotationsCollector.add(DotName.createSimple(builtinConstraint + "$List"));
}
}
private static void contributeClass(Set<DotName> classNamesCollector, IndexView indexView, DotName className) {
classNamesCollector.add(className);
for (ClassInfo subclass : indexView.getAllKnownSubclasses(className)) {
if (Modifier.isAbstract(subclass.flags())) {
// we can avoid adding the abstract classes here: either they are parent classes
// and they will be dealt with by Hibernate Validator or they are child classes
// without any proper implementation and we can ignore them.
continue;
}
classNamesCollector.add(subclass.name());
}
for (ClassInfo implementor : indexView.getAllKnownImplementors(className)) {
if (Modifier.isAbstract(implementor.flags())) {
// we can avoid adding the abstract classes here: either they are parent classes
// and they will be dealt with by Hibernate Validator or they are child classes
// without any proper implementation and we can ignore them.
continue;
}
classNamesCollector.add(implementor.name());
}
}
private static void contributeClassMarkedForCascadingValidation(Set<DotName> classNamesCollector,
IndexView indexView, DotName consideredAnnotation, Type type) {
if (VALID != consideredAnnotation) {
return;
}
DotName className = getClassName(type);
if (className != null) {
contributeClass(classNamesCollector, indexView, className);
}
}
private static void contributeMethodsWithInheritedValidation(
Map<DotName, Set<String>> inheritedAnnotationsToBeValidated, IndexView indexView, MethodInfo method) {
ClassInfo clazz = method.declaringClass();
if (Modifier.isInterface(clazz.flags())) {
// Remember annotated interface methods that must be validated
inheritedAnnotationsToBeValidated.computeIfAbsent(clazz.name(), k -> new HashSet<String>())
.add(method.name().toString());
}
}
private static DotName getClassName(Type type) {
switch (type.kind()) {
case CLASS:
case PARAMETERIZED_TYPE:
return type.name();
case ARRAY:
return getClassName(type.asArrayType().component());
default:
return null;
}
}
private static boolean hasXmlConfiguration() {
return Thread.currentThread().getContextClassLoader().getResource(META_INF_VALIDATION_XML) != null;
}
}

@ -0,0 +1,111 @@
package org.pagan.formalist;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.logging.Logger;
import io.quarkus.arc.processor.AnnotationsTransformer;
import org.pagan.formalist.annotataions.MethodValidated;
import org.pagan.formalist.annotataions.JaxrsEndPointValidated;
/**
* Add {@link MethodValidated} annotations to the methods requiring validation.
*/
public class MethodValidatedAnnotationsTransformer implements AnnotationsTransformer {
private static final Logger LOGGER = Logger.getLogger(MethodValidatedAnnotationsTransformer.class.getPackage().getName());
private static final DotName[] JAXRS_METHOD_ANNOTATIONS = {
DotName.createSimple("javax.ws.rs.GET"),
DotName.createSimple("javax.ws.rs.HEAD"),
DotName.createSimple("javax.ws.rs.DELETE"),
DotName.createSimple("javax.ws.rs.OPTIONS"),
DotName.createSimple("javax.ws.rs.PATCH"),
DotName.createSimple("javax.ws.rs.POST"),
DotName.createSimple("javax.ws.rs.PUT"),
};
private final Set<DotName> consideredAnnotations;
private final Collection<DotName> effectiveJaxRsMethodDefiningAnnotations;
private final Map<DotName, Set<String>> inheritedAnnotationsToBeValidated;
MethodValidatedAnnotationsTransformer(Set<DotName> consideredAnnotations,
Collection<DotName> additionalJaxRsMethodAnnotationsDotNames,
Map<DotName, Set<String>> inheritedAnnotationsToBeValidated) {
this.consideredAnnotations = consideredAnnotations;
this.inheritedAnnotationsToBeValidated = inheritedAnnotationsToBeValidated;
this.effectiveJaxRsMethodDefiningAnnotations = new ArrayList<>(
JAXRS_METHOD_ANNOTATIONS.length + additionalJaxRsMethodAnnotationsDotNames.size());
effectiveJaxRsMethodDefiningAnnotations.addAll(Arrays.asList(JAXRS_METHOD_ANNOTATIONS));
effectiveJaxRsMethodDefiningAnnotations.addAll(additionalJaxRsMethodAnnotationsDotNames);
}
@Override
public boolean appliesTo(Kind kind) {
return Kind.METHOD == kind;
}
@Override
public void transform(TransformationContext transformationContext) {
MethodInfo method = transformationContext.getTarget().asMethod();
if (requiresValidation(method)) {
if (Modifier.isStatic(method.flags())) {
// We don't support validating methods on static methods yet as it used to not be supported by CDI/Weld
// Supporting it will require some work in Hibernate Validator so we are going back to the old behavior of ignoring them but we log a warning.
LOGGER.warnf(
"Hibernate Validator does not support constraints on static methods yet. Constraints on %s are ignored.",
method.declaringClass().name().toString() + "#" + method.toString());
return;
}
if (isJaxrsMethod(method)) {
transformationContext.transform().add(DotName.createSimple(JaxrsEndPointValidated.class.getName())).done();
} else {
transformationContext.transform().add(DotName.createSimple(MethodValidated.class.getName())).done();
}
}
}
private boolean requiresValidation(MethodInfo method) {
if (method.annotations().isEmpty()) {
// This method has no annotations of its own: look for inherited annotations
ClassInfo clazz = method.declaringClass();
String methodName = method.name();
for (Map.Entry<DotName, Set<String>> validatedMethod : inheritedAnnotationsToBeValidated.entrySet()) {
if (clazz.interfaceNames().contains(validatedMethod.getKey())
&& validatedMethod.getValue().contains(methodName)) {
return true;
}
}
return false;
}
for (DotName consideredAnnotation : consideredAnnotations) {
if (method.hasAnnotation(consideredAnnotation)) {
return true;
}
}
return false;
}
private boolean isJaxrsMethod(MethodInfo method) {
for (DotName jaxrsMethodAnnotation : effectiveJaxRsMethodDefiningAnnotations) {
if (method.hasAnnotation(jaxrsMethodAnnotation)) {
return true;
}
}
return false;
}
}

@ -0,0 +1 @@
pattern.message=Value is not in line with the pattern

@ -0,0 +1,2 @@
quarkus.locales=en,en-US,fr-FR
quarkus.default-locale=fr-FR

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.pagan.quarkus</groupId>
<artifactId>extensions</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>formalist-parent</artifactId>
<name>${project.artifactId}</name>
<packaging>pom</packaging>
<modules>
<module>deployment</module>
<module>runtime</module>
</modules>
</project>

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.pagan.quarkus</groupId>
<artifactId>formalist-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>formalist</artifactId>
<name>${project.artifactId}</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<!-- <dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core-deployment</artifactId>
</dependency>-->
<dependency>
<groupId>org.apache.bval</groupId>
<artifactId>bval-jsr</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
<type>jar</type>
</dependency>
<!-- EL implementation -->
<!-- <dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
</dependency>-->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-core</artifactId>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
<exclusion>
<groupId>net.jcip</groupId>
<artifactId>jcip-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.spec.javax.xml.bind</groupId>
<artifactId>jboss-jaxb-api_2.3_spec</artifactId>
</exclusion>
<exclusion>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- <dependency>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>svm</artifactId>
</dependency>-->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-bootstrap-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<executions>
<execution>
<goals>
<goal>extension-descriptor</goal>
</goals>
<configuration>
<deployment>${project.groupId}:${project.artifactId}-deployment:${project.version}</deployment>
</configuration>
</execution>
</executions>
<!-- <configuration>
<excludedArtifacts>
<excludedArtifact>javax.validation:validation-api</excludedArtifact>
</excludedArtifacts>
</configuration>-->
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${quarkus.platform.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,79 @@
package org.pagan.formalist;
import java.util.Set;
import javax.validation.ClockProvider;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.MessageInterpolator;
import javax.validation.ParameterNameProvider;
import javax.validation.TraversableResolver;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import javax.validation.valueextraction.ValueExtractor;
import io.quarkus.arc.Arc;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.arc.runtime.BeanContainer;
import io.quarkus.arc.runtime.BeanContainerListener;
import io.quarkus.runtime.ShutdownContext;
import io.quarkus.runtime.annotations.Recorder;
import org.apache.bval.jsr.ApacheValidationProvider;
import org.apache.bval.jsr.ApacheValidatorConfiguration;
@Recorder
public class HibernateValidatorRecorder {
public BeanContainerListener initializeValidatorFactory(Set<Class<?>> classesToBeValidated,
Set<String> detectedBuiltinConstraints, boolean hasXmlConfiguration, ShutdownContext shutdownContext) {
return (BeanContainer container) -> {
ApacheValidatorConfiguration configuration = Validation.byProvider(ApacheValidationProvider.class)
.configure();
if (!hasXmlConfiguration) {
configuration.ignoreXmlConfiguration();
}
InstanceHandle<ConstraintValidatorFactory> configuredConstraintValidatorFactory = Arc.container()
.instance(ConstraintValidatorFactory.class);
configuration.constraintValidatorFactory(configuredConstraintValidatorFactory.get());
InstanceHandle<MessageInterpolator> configuredMessageInterpolator = Arc.container()
.instance(MessageInterpolator.class);
if (configuredMessageInterpolator.isAvailable()) {
configuration.messageInterpolator(configuredMessageInterpolator.get());
}
InstanceHandle<TraversableResolver> configuredTraversableResolver = Arc.container()
.instance(TraversableResolver.class);
if (configuredTraversableResolver.isAvailable()) {
configuration.traversableResolver(configuredTraversableResolver.get());
} else {
configuration.traversableResolver(new TraverseAllTraversableResolver());
}
InstanceHandle<ParameterNameProvider> configuredParameterNameProvider = Arc.container()
.instance(ParameterNameProvider.class);
if (configuredParameterNameProvider.isAvailable()) {
configuration.parameterNameProvider(configuredParameterNameProvider.get());
}
InstanceHandle<ClockProvider> configuredClockProvider = Arc.container().instance(ClockProvider.class);
if (configuredClockProvider.isAvailable()) {
configuration.clockProvider(configuredClockProvider.get());
}
// Automatically add all the values extractors declared as beans
for (ValueExtractor<?> valueExtractor : Arc.container().beanManager().createInstance()
.select(ValueExtractor.class)) {
configuration.addValueExtractor(valueExtractor);
}
ValidatorFactory validatorFactory = configuration.buildValidatorFactory();
ValidatorHolder.initialize(validatorFactory);
// Close the ValidatorFactory on shutdown
shutdownContext.addShutdownTask(validatorFactory::close);
};
}
}

@ -0,0 +1,25 @@
package org.pagan.formalist;
import java.lang.annotation.ElementType;
import javax.validation.Path;
import javax.validation.Path.Node;
import javax.validation.TraversableResolver;
class TraverseAllTraversableResolver implements TraversableResolver {
TraverseAllTraversableResolver() {
}
@Override
public boolean isReachable(Object traversableObject, Node traversableProperty, Class<?> rootBeanType,
Path pathToTraversableObject, ElementType elementType) {
return true;
}
@Override
public boolean isCascadable(Object traversableObject, Node traversableProperty, Class<?> rootBeanType,
Path pathToTraversableObject, ElementType elementType) {
return true;
}
}

@ -0,0 +1,24 @@
package org.pagan.formalist;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class ValidatorHolder {
private static ValidatorFactory validatorFactory;
private static Validator validator;
static void initialize(ValidatorFactory validatorFactory) {
ValidatorHolder.validatorFactory = validatorFactory;
ValidatorHolder.validator = validatorFactory.getValidator();
}
static ValidatorFactory getValidatorFactory() {
return validatorFactory;
}
static Validator getValidator() {
return validator;
}
}

@ -0,0 +1,22 @@
package org.pagan.formalist;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Named;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
@ApplicationScoped
public class ValidatorProvider {
@Produces
@Named("quarkus-formalist-validator-factory")
public ValidatorFactory factory() {
return ValidatorHolder.getValidatorFactory();
}
@Produces
public Validator validator() {
return ValidatorHolder.getValidator();
}
}

@ -0,0 +1,20 @@
package org.pagan.formalist.annotataions;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.interceptor.InterceptorBinding;
/**
* Marker class to indicate a JAX-RS end point should be validated.
*/
@Inherited
@InterceptorBinding
@Retention(value = RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface JaxrsEndPointValidated {
}

@ -0,0 +1,20 @@
package org.pagan.formalist.annotataions;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.interceptor.InterceptorBinding;
/**
* Marker class to indicate a method should be validated.
*/
@Inherited
@InterceptorBinding
@Retention(value = RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface MethodValidated {
}

@ -0,0 +1,159 @@
package org.pagan.formalist.interceptor;
import java.io.Serializable;
import java.lang.reflect.Member;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Set;
import javax.inject.Inject;
import javax.interceptor.InvocationContext;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ElementKind;
import javax.validation.Path;
import javax.validation.Validator;
import javax.validation.executable.ExecutableValidator;
/**
* NOTE: this is a copy of the interceptor present in //. For now, I prefer not
* depending on this artifact but this might change in the future.
* <p>
* An interceptor which performs a validation of the Bean Validation constraints
* specified at the parameters and/or return values of intercepted methods using
* the method validation functionality provided by Hibernate Validator.
*
* @author Gunnar Morling
* @author Hardy Ferentschik
*/
public abstract class AbstractMethodValidationInterceptor implements Serializable {
/**
* The validator to be used for method validation.
* <p>
* Although the concrete validator is not necessarily serializable (and HV's
* implementation indeed isn't) it is still alright to have it as non-transient
* field here. Upon passivation not the validator itself will be serialized, but
* the proxy injected here, which in turn is serializable.
* </p>
*/
@Inject
Validator validator;
/**
* Validates the Bean Validation constraints specified at the parameters and/or
* return value of the intercepted method.
*
* @param ctx The context of the intercepted method invocation.
* @param customValidator Custom Validator
*
* @return The result of the method invocation.
*
* @throws Exception Any exception caused by the intercepted method invocation.
* A {@link ConstraintViolationException} in case at least one
* constraint violation occurred either during parameter or
* return value validation.
*/
protected Object validateMethodInvocation(InvocationContext ctx) throws Exception {
ExecutableValidator executableValidator = validator.forExecutables();
Set<ConstraintViolation<Object>> violations = executableValidator.validateParameters(ctx.getTarget(),
ctx.getMethod(), ctx.getParameters());
if (!violations.isEmpty()) {
throw new ConstraintViolationException(getMessage(ctx.getMethod(), ctx.getParameters(), violations),
violations);
}
Object result = ctx.proceed();
violations = executableValidator.validateReturnValue(ctx.getTarget(), ctx.getMethod(), result);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(getMessage(ctx.getMethod(), ctx.getParameters(), violations),
violations);
}
return result;
}
/**
* Validates the Bean Validation constraints specified at the parameters and/or
* return value of the intercepted constructor.
*
* @param ctx The context of the intercepted constructor invocation.
*
* @throws Exception Any exception caused by the intercepted constructor
* invocation. A {@link ConstraintViolationException} in case
* at least one constraint violation occurred either during
* parameter or return value validation.
*/
protected void validateConstructorInvocation(InvocationContext ctx) throws Exception {
ExecutableValidator executableValidator = validator.forExecutables();
Set<? extends ConstraintViolation<?>> violations = executableValidator
.validateConstructorParameters(ctx.getConstructor(), ctx.getParameters());
if (!violations.isEmpty()) {
throw new ConstraintViolationException(getMessage(ctx.getConstructor(), ctx.getParameters(), violations),
violations);
}
ctx.proceed();
Object createdObject = ctx.getTarget();
violations = validator.forExecutables().validateConstructorReturnValue(ctx.getConstructor(), createdObject);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(getMessage(ctx.getConstructor(), ctx.getParameters(), violations),
violations);
}
}
private String getMessage(Member member, Object[] args, Set<? extends ConstraintViolation<?>> violations) {
StringBuilder message = new StringBuilder();
message.append(violations.size());
message.append(" constraint violation(s) occurred during method validation.");
message.append("\nConstructor or Method: ");
message.append(member);
message.append("\nArgument values: ");
message.append(Arrays.toString(args));
message.append("\nConstraint violations: ");
int i = 1;
for (ConstraintViolation<?> constraintViolation : violations) {
Path.Node leafNode = getLeafNode(constraintViolation);
message.append("\n (");
message.append(i);
message.append(")");
message.append(" Kind: ");
message.append(leafNode.getKind());
if (leafNode.getKind() == ElementKind.PARAMETER) {
message.append("\n parameter index: ");
message.append(leafNode.as(Path.ParameterNode.class).getParameterIndex());
}
message.append("\n message: ");
message.append(constraintViolation.getMessage());
message.append("\n root bean: ");
message.append(constraintViolation.getRootBean());
message.append("\n property path: ");
message.append(constraintViolation.getPropertyPath());
message.append("\n constraint: ");
message.append(constraintViolation.getConstraintDescriptor().getAnnotation());
i++;
}
return message.toString();
}
private Path.Node getLeafNode(ConstraintViolation<?> constraintViolation) {
Iterator<Path.Node> nodes = constraintViolation.getPropertyPath().iterator();
Path.Node leafNode = null;
while (nodes.hasNext()) {
leafNode = nodes.next();
}
return leafNode;
}
}

@ -0,0 +1,26 @@
package org.pagan.formalist.interceptor;
import org.pagan.formalist.annotataions.MethodValidated;
import javax.annotation.Priority;
import javax.interceptor.AroundConstruct;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
@MethodValidated
@Interceptor
@Priority(Interceptor.Priority.PLATFORM_AFTER + 800)
public class MethodValidationInterceptor extends AbstractMethodValidationInterceptor {
@AroundInvoke
@Override
public Object validateMethodInvocation(InvocationContext ctx) throws Exception {
return super.validateMethodInvocation(ctx);
}
@AroundConstruct
@Override
public void validateConstructorInvocation(InvocationContext ctx) throws Exception {
super.validateConstructorInvocation(ctx);
}
}

@ -0,0 +1,50 @@
package org.pagan.formalist.jaxrs;
import java.util.Iterator;
import javax.validation.ConstraintViolation;
import javax.validation.ElementKind;
import javax.validation.Path.Node;
import org.jboss.resteasy.api.validation.ConstraintType;
import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;
import org.jboss.resteasy.spi.validation.ConstraintTypeUtil;
public class ConstraintTypeUtil20 implements ConstraintTypeUtil {
@Override
public ConstraintType.Type getConstraintType(Object o) {
if (!(o instanceof ConstraintViolation)) {
throw new RuntimeException(Messages.MESSAGES.unknownObjectPassedAsConstraintViolation(o));
}
ConstraintViolation<?> v = ConstraintViolation.class.cast(o);
Iterator<Node> nodes = v.getPropertyPath().iterator();
Node firstNode = nodes.next();
switch (firstNode.getKind()) {
case BEAN:
return ConstraintType.Type.CLASS;
case CONSTRUCTOR:
case METHOD:
Node secondNode = nodes.next();
if (secondNode.getKind() == ElementKind.PARAMETER
|| secondNode.getKind() == ElementKind.CROSS_PARAMETER) {
return ConstraintType.Type.PARAMETER;
} else if (secondNode.getKind() == ElementKind.RETURN_VALUE) {
return ConstraintType.Type.RETURN_VALUE;
} else {
throw new RuntimeException(Messages.MESSAGES.unexpectedPathNodeViolation(secondNode.getKind()));
}
case PROPERTY:
return ConstraintType.Type.PROPERTY;
case CROSS_PARAMETER:
case PARAMETER:
case RETURN_VALUE:
case CONTAINER_ELEMENT: // we shouldn't encounter these element types at the root
default:
throw new RuntimeException(Messages.MESSAGES.unexpectedPathNode(firstNode.getKind()));
}
}
}

@ -0,0 +1,51 @@
package org.pagan.formalist.jaxrs;
import org.pagan.formalist.annotataions.JaxrsEndPointValidated;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.Priority;
import javax.interceptor.AroundConstruct;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import javax.validation.ConstraintViolationException;
import javax.ws.rs.core.MediaType;
import org.jboss.resteasy.util.MediaTypeHelper;
import org.pagan.formalist.interceptor.AbstractMethodValidationInterceptor;
@JaxrsEndPointValidated
@Interceptor
@Priority(Interceptor.Priority.PLATFORM_AFTER + 800)
public class JaxrsEndPointValidationInterceptor extends AbstractMethodValidationInterceptor {
@AroundInvoke
@Override
public Object validateMethodInvocation(InvocationContext ctx) throws Exception {
try {
return super.validateMethodInvocation(ctx);
} catch (ConstraintViolationException e) {
throw new ResteasyViolationExceptionImpl(e.getConstraintViolations(), getAccept(ctx.getMethod()));
}
}
@AroundConstruct
@Override
public void validateConstructorInvocation(InvocationContext ctx) throws Exception {
super.validateConstructorInvocation(ctx);
}
private List<MediaType> getAccept(Method method) {
MediaType[] producedMediaTypes = MediaTypeHelper.getProduces(method.getDeclaringClass(), method);
if (producedMediaTypes == null) {
return Collections.emptyList();
}
return Arrays.asList(producedMediaTypes);
}
}

@ -0,0 +1,31 @@
package org.pagan.formalist.jaxrs;
import java.util.List;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.ws.rs.core.MediaType;
import org.jboss.resteasy.api.validation.ResteasyViolationException;
import org.jboss.resteasy.core.ResteasyContext;
import org.jboss.resteasy.spi.ResteasyConfiguration;
import org.jboss.resteasy.spi.validation.ConstraintTypeUtil;
public class ResteasyViolationExceptionImpl extends ResteasyViolationException {
private static final long serialVersionUID = 657697354453281559L;
public ResteasyViolationExceptionImpl(final Set<? extends ConstraintViolation<?>> constraintViolations,
final List<MediaType> accept) {
super(constraintViolations, accept);
}
@Override
public ConstraintTypeUtil getConstraintTypeUtil() {
return new ConstraintTypeUtil20();
}
@Override
protected ResteasyConfiguration getResteasyConfiguration() {
return ResteasyContext.getContextData(ResteasyConfiguration.class);
}
}

@ -0,0 +1,108 @@
package org.pagan.formalist.jaxrs;
import java.util.Iterator;
import java.util.List;
import javax.validation.ConstraintDeclarationException;
import javax.validation.ConstraintDefinitionException;
import javax.validation.GroupDefinitionException;
import javax.validation.ValidationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.jboss.resteasy.api.validation.ResteasyViolationException;
import org.jboss.resteasy.api.validation.Validation;
import org.jboss.resteasy.api.validation.ViolationReport;
@Provider
public class ResteasyViolationExceptionMapper implements ExceptionMapper<ValidationException> {
@Override
public Response toResponse(ValidationException exception) {
if (exception instanceof ConstraintDefinitionException) {
return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
}
if (exception instanceof ConstraintDeclarationException) {
return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
}
if (exception instanceof GroupDefinitionException) {
return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
}
if (exception instanceof ResteasyViolationException) {
ResteasyViolationException resteasyViolationException = ResteasyViolationException.class.cast(exception);
Exception e = resteasyViolationException.getException();
if (e != null) {
return buildResponse(unwrapException(e), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
} else if (resteasyViolationException.getReturnValueViolations().isEmpty()) {
return buildViolationReportResponse(resteasyViolationException, Status.BAD_REQUEST);
} else {
return buildViolationReportResponse(resteasyViolationException, Status.INTERNAL_SERVER_ERROR);
}
}
return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
}
protected Response buildResponse(Object entity, String mediaType, Status status) {
ResponseBuilder builder = Response.status(status).entity(entity);
builder.type(MediaType.TEXT_PLAIN);
builder.header(Validation.VALIDATION_HEADER, "true");
return builder.build();
}
protected Response buildViolationReportResponse(ResteasyViolationException exception, Status status) {
ResponseBuilder builder = Response.status(status);
builder.header(Validation.VALIDATION_HEADER, "true");
// Check standard media types.
MediaType mediaType = getAcceptMediaType(exception.getAccept());
if (mediaType != null) {
builder.type(mediaType);
builder.entity(new ViolationReport(exception));
return builder.build();
}
// Default media type.
builder.type(MediaType.TEXT_PLAIN);
builder.entity(exception.toString());
return builder.build();
}
protected String unwrapException(Throwable t) {
StringBuffer sb = new StringBuffer();
doUnwrapException(sb, t);
return sb.toString();
}
private void doUnwrapException(StringBuffer sb, Throwable t) {
if (t == null) {
return;
}
sb.append(t.toString());
if (t.getCause() != null && t != t.getCause()) {
sb.append('[');
doUnwrapException(sb, t.getCause());
sb.append(']');
}
}
private MediaType getAcceptMediaType(List<MediaType> accept) {
Iterator<MediaType> it = accept.iterator();
while (it.hasNext()) {
MediaType mt = it.next();
if (MediaType.APPLICATION_XML_TYPE.getType().equals(mt.getType())
&& MediaType.APPLICATION_XML_TYPE.getSubtype().equals(mt.getSubtype())) {
return MediaType.APPLICATION_XML_TYPE;
}
if (MediaType.APPLICATION_JSON_TYPE.getType().equals(mt.getType())
&& MediaType.APPLICATION_JSON_TYPE.getSubtype().equals(mt.getSubtype())) {
return MediaType.APPLICATION_JSON_TYPE;
}
}
return null;
}
}

@ -0,0 +1,12 @@
---
name: "Formalist (Apache Bval) Validator"
metadata:
short-name: "bean validation"
keywords:
- "formalist-validator"
- "bean-validation"
- "validation"
categories:
- "web"
- "data"
status: "stable"

@ -0,0 +1 @@
org.pagan.formalist.jaxrs.ResteasyViolationExceptionMapper

@ -32,7 +32,6 @@
<artifactId>quarkus-security</artifactId> <artifactId>quarkus-security</artifactId>
<version>${quarkus.platform.version}</version> <version>${quarkus.platform.version}</version>
</dependency> </dependency>
<!-- <dependency> <!-- <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId> <artifactId>quarkus-core</artifactId>

@ -26,6 +26,7 @@
<module>cayenne</module> <module>cayenne</module>
<module>jedis</module> <module>jedis</module>
<module>janitor</module> <module>janitor</module>
<module>formalist</module>
<!--<module>cautus</module>--> <!--<module>cautus</module>-->
<module>demo</module> <module>demo</module>
</modules> </modules>

Loading…
Cancel
Save