diff --git a/nrich-validation-api/src/main/java/net/croz/nrich/validation/api/constraint/Generic.java b/nrich-validation-api/src/main/java/net/croz/nrich/validation/api/constraint/Generic.java new file mode 100644 index 000000000..1172d0b7e --- /dev/null +++ b/nrich-validation-api/src/main/java/net/croz/nrich/validation/api/constraint/Generic.java @@ -0,0 +1,71 @@ +/* + * 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.constraint; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + + +/** + * The annotated element is validated against a provided SpEL expression + */ +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(Generic.List.class) +@Documented +@Constraint(validatedBy = {}) +public @interface Generic { + + String message() default "{nrich.constraint.generic.invalid.message}"; + + Class[] groups() default {}; + + /** + * SpEL expression that is evaluated + * + * @return SpEL expression + */ + String value(); + + Class[] payload() default {}; + + /** + * Defines several {@link Generic} annotations on the same element. + * + * @see Generic + */ + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + Generic[] value(); + } +} diff --git a/nrich-validation/build.gradle b/nrich-validation/build.gradle index 71c7f4cce..fcdc3d0b1 100644 --- a/nrich-validation/build.gradle +++ b/nrich-validation/build.gradle @@ -7,6 +7,7 @@ dependencies { compileOnly "org.projectlombok:lombok" implementation "org.hibernate.validator:hibernate-validator" + implementation "org.springframework:spring-expression" runtimeOnly "org.apache.tomcat.embed:tomcat-embed-el" diff --git a/nrich-validation/src/main/java/net/croz/nrich/validation/aot/ValidationRuntimeHintsRegistrar.java b/nrich-validation/src/main/java/net/croz/nrich/validation/aot/ValidationRuntimeHintsRegistrar.java index 5767dd777..a1e5ab7af 100644 --- a/nrich-validation/src/main/java/net/croz/nrich/validation/aot/ValidationRuntimeHintsRegistrar.java +++ b/nrich-validation/src/main/java/net/croz/nrich/validation/aot/ValidationRuntimeHintsRegistrar.java @@ -18,6 +18,7 @@ package net.croz.nrich.validation.aot; import net.croz.nrich.validation.constraint.mapping.DefaultConstraintMappingContributor; +import net.croz.nrich.validation.constraint.validator.GenericValidator; import net.croz.nrich.validation.constraint.validator.InListValidator; import net.croz.nrich.validation.constraint.validator.MaxSizeInBytesValidator; import net.croz.nrich.validation.constraint.validator.NotNullWhenValidator; @@ -43,7 +44,7 @@ public class ValidationRuntimeHintsRegistrar implements RuntimeHintsRegistrar { public static final List TYPE_REFERENCE_LIST = Collections.unmodifiableList(TypeReference.listOf( DefaultConstraintMappingContributor.class, ValidOibValidator.class, ValidSearchPropertiesValidator.class, ValidRangeValidator.class, MaxSizeInBytesValidator.class, - NotNullWhenValidator.class, NullWhenValidator.class, ValidFileValidator.class, ValidFileResolvableValidator.class, InListValidator.class + NotNullWhenValidator.class, NullWhenValidator.class, ValidFileValidator.class, ValidFileResolvableValidator.class, InListValidator.class, GenericValidator.class )); @Override diff --git a/nrich-validation/src/main/java/net/croz/nrich/validation/constraint/mapping/DefaultConstraintMappingContributor.java b/nrich-validation/src/main/java/net/croz/nrich/validation/constraint/mapping/DefaultConstraintMappingContributor.java index 9e2f100b7..47f558979 100644 --- a/nrich-validation/src/main/java/net/croz/nrich/validation/constraint/mapping/DefaultConstraintMappingContributor.java +++ b/nrich-validation/src/main/java/net/croz/nrich/validation/constraint/mapping/DefaultConstraintMappingContributor.java @@ -17,6 +17,7 @@ package net.croz.nrich.validation.constraint.mapping; +import net.croz.nrich.validation.api.constraint.Generic; import net.croz.nrich.validation.api.constraint.InList; import net.croz.nrich.validation.api.constraint.MaxSizeInBytes; import net.croz.nrich.validation.api.constraint.NotNullWhen; @@ -26,6 +27,7 @@ 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.GenericValidator; import net.croz.nrich.validation.constraint.validator.InListValidator; import net.croz.nrich.validation.constraint.validator.MaxSizeInBytesValidator; import net.croz.nrich.validation.constraint.validator.NotNullWhenValidator; @@ -50,5 +52,6 @@ public void createConstraintMappings(ConstraintMappingBuilder builder) { builder.addConstraintMapping().constraintDefinition(ValidFile.class).validatedBy(ValidFileValidator.class); builder.addConstraintMapping().constraintDefinition(ValidFileResolvable.class).validatedBy(ValidFileResolvableValidator.class); builder.addConstraintMapping().constraintDefinition(InList.class).validatedBy(InListValidator.class); + builder.addConstraintMapping().constraintDefinition(Generic.class).validatedBy(GenericValidator.class); } } diff --git a/nrich-validation/src/main/java/net/croz/nrich/validation/constraint/validator/GenericValidator.java b/nrich-validation/src/main/java/net/croz/nrich/validation/constraint/validator/GenericValidator.java new file mode 100644 index 000000000..b5f3575b6 --- /dev/null +++ b/nrich-validation/src/main/java/net/croz/nrich/validation/constraint/validator/GenericValidator.java @@ -0,0 +1,51 @@ +/* + * 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.validator; + +import net.croz.nrich.validation.api.constraint.Generic; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class GenericValidator implements ConstraintValidator { + + private String spelExpression; + + private ExpressionParser expressionParser; + + @Override + public void initialize(Generic constraintAnnotation) { + spelExpression = constraintAnnotation.value(); + expressionParser = new SpelExpressionParser(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + // will be validated by other constraints + if (value == null) { + return true; + } + + Expression expression = expressionParser.parseExpression(spelExpression); + + return expression.getValue(value, Boolean.class); + } +} diff --git a/nrich-validation/src/test/java/net/croz/nrich/validation/constraint/stub/GenericValidTestRequest.java b/nrich-validation/src/test/java/net/croz/nrich/validation/constraint/stub/GenericValidTestRequest.java new file mode 100644 index 000000000..2fdf82ce6 --- /dev/null +++ b/nrich-validation/src/test/java/net/croz/nrich/validation/constraint/stub/GenericValidTestRequest.java @@ -0,0 +1,31 @@ +/* + * 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.stub; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.croz.nrich.validation.api.constraint.Generic; + +@RequiredArgsConstructor +@Getter +public class GenericValidTestRequest { + + @Generic(value = "#this matches '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'") + private final String uuid; + +} diff --git a/nrich-validation/src/test/java/net/croz/nrich/validation/constraint/validator/GenericValidatorTest.java b/nrich-validation/src/test/java/net/croz/nrich/validation/constraint/validator/GenericValidatorTest.java new file mode 100644 index 000000000..462c259e2 --- /dev/null +++ b/nrich-validation/src/test/java/net/croz/nrich/validation/constraint/validator/GenericValidatorTest.java @@ -0,0 +1,56 @@ +package net.croz.nrich.validation.constraint.validator; + +import net.croz.nrich.validation.ValidationTestConfiguration; +import net.croz.nrich.validation.constraint.stub.GenericValidTestRequest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringJUnitConfig(ValidationTestConfiguration.class) +public class GenericValidatorTest { + + @Autowired + private Validator validator; + + @Test + void shouldNotReportErrorForNullValue() { + // given + GenericValidTestRequest request = new GenericValidTestRequest(null); + + // when + Set> constraintViolationList = validator.validate(request); + + // then + assertThat(constraintViolationList).isEmpty(); + } + + @Test + void shouldNotReportErrorWhenValueIsValid() { + // given + GenericValidTestRequest request = new GenericValidTestRequest("4adf9bf9-2656-468b-880a-706ff704e6b4"); + + // when + Set> constraintViolationList = validator.validate(request); + + // then + assertThat(constraintViolationList).isEmpty(); + } + + @Test + void shouldReportErrorWhenValueIsNotValid() { + // given + GenericValidTestRequest request = new GenericValidTestRequest("4adf9bf9-2656-xxxx-xxxx-706ff704e6b4"); + + // when + Set> constraintViolationList = validator.validate(request); + + // then + assertThat(constraintViolationList).isNotEmpty(); + } +}