Skip to content

Commit

Permalink
Add support for automatic ConstraintValidator registration to avoid h…
Browse files Browse the repository at this point in the history
…aving to define validation.xmls
  • Loading branch information
agrancaric committed Oct 19, 2023
1 parent a7f51b3 commit b47c7f2
Show file tree
Hide file tree
Showing 17 changed files with 458 additions and 43 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ subprojects { Project subproject ->
implementation "org.apache.poi:poi:$apachePoiVersion"
implementation "org.apache.poi:poi-ooxml:$apachePoiVersion"
implementation "org.modelmapper:modelmapper:$modelMapperVersion"
implementation "org.reflections:reflections:$reflectionsVersion"
}
}

Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ gradleAggregateJavadocPluginVersion=8.3
gradleJgitverPluginVersion=0.10.0-rc03
gradlePublishPluginVersion=1.1.0
modelMapperVersion=3.1.1
reflectionsVersion=0.10.2
springBootVersion=3.1.3

# POM metadata
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2020-2023 CROZ d.o.o, the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package net.croz.nrich.validation.api.mapping;

import jakarta.validation.Configuration;

public interface ConstraintMappingRegistrar {

void registerConstraints(Configuration<?> validatorConfiguration);

}
33 changes: 27 additions & 6 deletions nrich-validation-spring-boot-starter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,45 @@ Note if using `nrich-bom` dependency versions should be omitted.

Configuration is done through a property file, available properties and descriptions are given bellow (all properties are prefixed with nrich.validation which is omitted for readability):

| property | description | default value |
|--------------------|------------------------------------------------------------------|---------------|
| register-messages | Whether default validation failure messages should be registered | true |
| property | description | default value |
|------------------------|------------------------------------------------------------------|------------------------------------------------|
| register-messages | Whether default validation failure messages should be registered | true |
| register-validators | Whether default validators should be registered | true |
| validator-package-list | List of packages from which to register validators | net.croz.nrich.validation.constraint.validator |

The default configuration values in yaml format for easier modification are given bellow:

```yaml

nrich.validation:
register-messages: true
register-messages: true
register-validators: true
validator-package-list: net.croz.nrich.validation.constraint.validator

```

### Using the module

Users should just add the dependency on classpath and then use the provided constraints. If custom messages are required they should be defined in `messages.properties` file
and default ones disabled then through `nrich.validation.register-messages` property set to false. A list of available constraints and descriptions is given bellow:
and default ones disabled then through `nrich.validation.register-messages` property set to false. There are two options for registering the modules validators
first one is by automatic registration which is enabled by default through property `nrich.validation.register-validators`, the other option is by defining a standard `validation.xml`
file in `META-INF` directory and registering `ConstraintMappingContributor` implementation `net.croz.nrich.validation.constraint.mapping.DefaultConstraintMappingContributor`.

i.e

```xml

<validation-config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration"
version="1.1">
<property name="hibernate.validator.constraint_mapping_contributors">net.croz.nrich.validation.constraint.mapping.DefaultConstraintMappingContributor</property>
</validation-config>


```

A list of available constraints and descriptions is given bellow:

| constraint | description |
|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
Expand All @@ -72,7 +94,6 @@ and default ones disabled then through `nrich.validation.register-messages` prop
| `@ValidRange` | Validates that the annotated element from property must be less than (or equal to if inclusive is true) to property |
| `@ValidSearchProperties ` | Validates that at least one group of annotated element must contain all properties that are not null (i.e. when searching users that either name is not null or first and last name are not null) |


#### File related constraints

Difference between `@ValidFile` and `@ValidFileResolvable` is that the former resolves allowed values from environment. Searched properties are given bellow but can be overridden on each constraint
Expand Down
4 changes: 2 additions & 2 deletions nrich-validation-spring-boot-starter/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ dependencies {
annotationProcessor "org.projectlombok:lombok"
compileOnly "org.projectlombok:lombok"

implementation "org.springframework.boot:spring-boot-autoconfigure"
implementation project(":nrich-validation")

runtimeOnly project(":nrich-validation")
implementation "org.springframework.boot:spring-boot-autoconfigure"

testRuntimeOnly "ch.qos.logback:logback-classic"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@
package net.croz.nrich.validation.starter.configuration;

import lombok.RequiredArgsConstructor;
import net.croz.nrich.validation.api.mapping.ConstraintMappingRegistrar;
import net.croz.nrich.validation.constraint.mapping.DefaultConstraintMappingRegistrar;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.validation.ValidationConfigurationCustomizer;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.AbstractResourceBasedMessageSource;

import java.util.List;

@Configuration(proxyBeanMethods = false)
public class NrichValidationAutoConfiguration {

Expand All @@ -43,9 +49,21 @@ public static class ValidationMessageSourceRegistrar implements InitializingBean

@Override
public void afterPropertiesSet() {
if (messageSource instanceof AbstractResourceBasedMessageSource) {
((AbstractResourceBasedMessageSource) messageSource).addBasenames(VALIDATION_MESSAGES_NAME);
if (messageSource instanceof AbstractResourceBasedMessageSource abstractResourceBasedMessageSource) {
abstractResourceBasedMessageSource.addBasenames(VALIDATION_MESSAGES_NAME);
}
}
}

@ConditionalOnProperty(name = "nrich.validation.register-validators", havingValue = "true", matchIfMissing = true)
@Bean
ConstraintMappingRegistrar constraintMappingRegistrar(@Value("${nrich.validation.validator-package-list:net.croz.nrich.validation.constraint.validator}") List<String> validatorPacakgeList) {
return new DefaultConstraintMappingRegistrar(validatorPacakgeList);
}

@ConditionalOnProperty(name = "nrich.validation.register-validators", havingValue = "true", matchIfMissing = true)
@Bean
ValidationConfigurationCustomizer validationConfigurationCustomizer(ConstraintMappingRegistrar constraintMappingRegistrar) {
return constraintMappingRegistrar::registerConstraints;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

package net.croz.nrich.validation.starter.configuration;

import net.croz.nrich.validation.api.mapping.ConstraintMappingRegistrar;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.validation.ValidationConfigurationCustomizer;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.MessageSource;
import org.springframework.context.support.AbstractResourceBasedMessageSource;
Expand All @@ -34,8 +36,11 @@ class NrichValidationAutoConfigurationTest {
@Test
void shouldConfigureDefaultConfiguration() {
// expect
contextRunner.run(context ->
assertThat(context).hasSingleBean(NrichValidationAutoConfiguration.ValidationMessageSourceRegistrar.class)
contextRunner.run(context -> {
assertThat(context).hasSingleBean(NrichValidationAutoConfiguration.ValidationMessageSourceRegistrar.class);
assertThat(context).hasSingleBean(ConstraintMappingRegistrar.class);
assertThat(context).hasSingleBean(ValidationConfigurationCustomizer.class);
}
);
}

Expand All @@ -47,6 +52,15 @@ void shouldNotRegisterValidationMessagesWhenDisabledViaProperty() {
);
}

@Test
void shouldNotRegisterValidatorsWhenDisabledViaProperty() {
// expect
contextRunner.withPropertyValues("nrich.validation.register-validators=false").run(context -> {
assertThat(context).doesNotHaveBean(ConstraintMappingRegistrar.class);
assertThat(context).doesNotHaveBean(ValidationConfigurationCustomizer.class);
});
}

@Test
void shouldRegisterMessagesWhenPossible() {
// given
Expand Down
24 changes: 23 additions & 1 deletion nrich-validation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ It also contains a list of messages for standard and additional constraints in e

## Setting up Spring beans

If automatic registration of messages is required then following configuration is required
If automatic registration of messages and validators (supported only for hibernate-validator) is required then following configuration is required
(this can also be registered through application.properties by manually including `validationMessages` when using Spring Boot):

```java
Expand All @@ -37,10 +37,32 @@ public class ApplicationConfiguration {
}
}
}

@Bean
Validator validator(ConstraintMappingRegistrar constraintMappingRegistrar) {
LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();

registerConstraintMappingContributors(localValidatorFactoryBean, constraintMappingRegistrar);

return localValidatorFactoryBean;
}

@Bean
ConstraintMappingRegistrar constraintMappingRegistrar() {
return new DefaultConstraintMappingRegistrar(List.of("net.croz.nrich.validation.constraint.validator"));
}

private void registerConstraintMappingContributors(LocalValidatorFactoryBean validator, ConstraintMappingRegistrar constraintMappingRegistrar) {
validator.setConfigurationInitializer(constraintMappingRegistrar::registerConstraints);
}
}

```

If validators don't need to be registered automatically then standard `validation.xml` file in `META-INF` directory should be provided. And inside `ConstraintMappingContributor`
implementation `net.croz.nrich.validation.constraint.mapping.DefaultConstraintMappingContributor` should be registered. In that case `ConstraintMappingRegistrar` bean
and `registerConstraintMappingContributors` method are not needed.

## Usage

The module provides following constraints:
Expand Down
2 changes: 2 additions & 0 deletions nrich-validation/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ dependencies {
compileOnly "org.projectlombok:lombok"

implementation "org.hibernate.validator:hibernate-validator"
implementation "org.slf4j:slf4j-api"
implementation "org.reflections:reflections"

runtimeOnly "org.apache.tomcat.embed:tomcat-embed-el"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@

package net.croz.nrich.validation.constraint.mapping;

import net.croz.nrich.validation.api.constraint.SpelExpression;
import net.croz.nrich.validation.api.constraint.InList;
import net.croz.nrich.validation.api.constraint.MaxSizeInBytes;
import net.croz.nrich.validation.api.constraint.NotNullWhen;
import net.croz.nrich.validation.api.constraint.NullWhen;
import net.croz.nrich.validation.api.constraint.SpelExpression;
import net.croz.nrich.validation.api.constraint.ValidFile;
import net.croz.nrich.validation.api.constraint.ValidFileResolvable;
import net.croz.nrich.validation.api.constraint.ValidOib;
import net.croz.nrich.validation.api.constraint.ValidRange;
import net.croz.nrich.validation.api.constraint.ValidSearchProperties;
import net.croz.nrich.validation.constraint.validator.SpelExpressionValidator;
import net.croz.nrich.validation.constraint.validator.InListValidator;
import net.croz.nrich.validation.constraint.validator.MaxSizeInBytesValidator;
import net.croz.nrich.validation.constraint.validator.NotNullWhenValidator;
import net.croz.nrich.validation.constraint.validator.NullWhenValidator;
import net.croz.nrich.validation.constraint.validator.SpelExpressionValidator;
import net.croz.nrich.validation.constraint.validator.ValidFileResolvableValidator;
import net.croz.nrich.validation.constraint.validator.ValidFileValidator;
import net.croz.nrich.validation.constraint.validator.ValidOibValidator;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2020-2023 CROZ d.o.o, the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package net.croz.nrich.validation.constraint.mapping;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.croz.nrich.validation.api.mapping.ConstraintMappingRegistrar;
import org.hibernate.validator.HibernateValidatorConfiguration;
import org.hibernate.validator.cfg.ConstraintMapping;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import org.reflections.util.ConfigurationBuilder;

import jakarta.validation.Configuration;
import jakarta.validation.ConstraintValidator;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

@Slf4j
@RequiredArgsConstructor
public class DefaultConstraintMappingRegistrar implements ConstraintMappingRegistrar {

private static final int INDEX_OF_CONSTRAINT_TYPE = 0;

private final List<String> constraintPacakgeList;

@Override
public void registerConstraints(Configuration<?> configuration) {
if (configuration instanceof HibernateValidatorConfiguration hibernateValidatorConfiguration) {
registerConstraintsInternal(hibernateValidatorConfiguration);
}
else {
log.warn("Unable to register validation configuration, automatic registration is only supported for hibernate validator");
}
}

protected <A extends Annotation> void registerConstraintsInternal(HibernateValidatorConfiguration configuration) {
org.reflections.Configuration reflectionsConfiguration = new ConfigurationBuilder()
.forPackages(constraintPacakgeList.toArray(new String[0]))
.setScanners(Scanners.SubTypes);

@SuppressWarnings("rawtypes")
Set<Class<? extends ConstraintValidator>> constraintValidators = new Reflections(reflectionsConfiguration).getSubTypesOf(ConstraintValidator.class);

constraintValidators.forEach(validatorClass -> {
@SuppressWarnings("unchecked")
Class<? extends ConstraintValidator<A, ?>> castedValidatorClass = (Class<? extends ConstraintValidator<A, ?>>) validatorClass;
Class<A> annotationClass = annotationClass(validatorClass);

registerConstraint(configuration, castedValidatorClass, annotationClass);
});
}

@SuppressWarnings("unchecked")
private <A extends Annotation> Class<A> annotationClass(Class<?> type) {
ParameterizedType parameterizedType = (ParameterizedType) Arrays.stream(type.getGenericInterfaces())
.filter(genericInterface -> ((ParameterizedType) genericInterface).getRawType().getTypeName().equals(ConstraintValidator.class.getName()))
.findFirst()
.orElseThrow();

return (Class<A>) parameterizedType.getActualTypeArguments()[INDEX_OF_CONSTRAINT_TYPE];
}

private <A extends Annotation> void registerConstraint(HibernateValidatorConfiguration hibernateValidatorConfiguration, Class<? extends ConstraintValidator<A, ?>> validator, Class<A> annotationClass) {
ConstraintMapping constraintMapping = hibernateValidatorConfiguration.createConstraintMapping();

constraintMapping.constraintDefinition(annotationClass).validatedBy(validator);

hibernateValidatorConfiguration.addMapping(constraintMapping);
}
}
24 changes: 0 additions & 24 deletions nrich-validation/src/main/resources/META-INF/validation.xml

This file was deleted.

Loading

0 comments on commit b47c7f2

Please sign in to comment.