Skip to content

Commit

Permalink
Add generic custom constraint
Browse files Browse the repository at this point in the history
  • Loading branch information
Mihael Cacko committed Sep 29, 2023
1 parent 56b87e5 commit 4943850
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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<? extends Payload>[] 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();
}
}
1 change: 1 addition & 0 deletions nrich-validation/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -43,7 +44,7 @@ public class ValidationRuntimeHintsRegistrar implements RuntimeHintsRegistrar {

public static final List<TypeReference> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Generic, Object> {

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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

}
Original file line number Diff line number Diff line change
@@ -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<ConstraintViolation<GenericValidTestRequest>> constraintViolationList = validator.validate(request);

// then
assertThat(constraintViolationList).isEmpty();
}

@Test
void shouldNotReportErrorWhenValueIsValid() {
// given
GenericValidTestRequest request = new GenericValidTestRequest("4adf9bf9-2656-468b-880a-706ff704e6b4");

// when
Set<ConstraintViolation<GenericValidTestRequest>> constraintViolationList = validator.validate(request);

// then
assertThat(constraintViolationList).isEmpty();
}

@Test
void shouldReportErrorWhenValueIsNotValid() {
// given
GenericValidTestRequest request = new GenericValidTestRequest("4adf9bf9-2656-xxxx-xxxx-706ff704e6b4");

// when
Set<ConstraintViolation<GenericValidTestRequest>> constraintViolationList = validator.validate(request);

// then
assertThat(constraintViolationList).isNotEmpty();
}
}

0 comments on commit 4943850

Please sign in to comment.