diff --git a/javaxval/README.md b/javaxval/README.md index 2b4164671d..64903d7463 100644 --- a/javaxval/README.md +++ b/javaxval/README.md @@ -9,3 +9,4 @@ This module contains articles about Bean Validation. - [Difference Between @NotNull, @NotEmpty, and @NotBlank Constraints in Bean Validation](https://www.baeldung.com/java-bean-validation-not-null-empty-blank) - [Javax BigDecimal Validation](https://www.baeldung.com/javax-bigdecimal-validation) - [Grouping Javax Validation Constraints](https://www.baeldung.com/javax-validation-groups) +- [Javax Validations for Enums](https://www.baeldung.com/javax-validations-for-enums/) diff --git a/javaxval/src/main/java/org/baeldung/javaxval/enums/CustomerTypeSubSetValidator.java b/javaxval/src/main/java/org/baeldung/javaxval/enums/CustomerTypeSubSetValidator.java new file mode 100644 index 0000000000..c730cb937f --- /dev/null +++ b/javaxval/src/main/java/org/baeldung/javaxval/enums/CustomerTypeSubSetValidator.java @@ -0,0 +1,24 @@ +package org.baeldung.javaxval.enums; + +import java.util.Arrays; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.baeldung.javaxval.enums.constraints.CustomerTypeSubset; +import org.baeldung.javaxval.enums.demo.CustomerType; + +public class CustomerTypeSubSetValidator implements ConstraintValidator { + private CustomerType[] subset; + + @Override + public void initialize(CustomerTypeSubset constraint) { + this.subset = constraint.anyOf(); + } + + @Override + public boolean isValid(CustomerType value, ConstraintValidatorContext context) { + return value == null || Arrays.asList(subset) + .contains(value); + } +} diff --git a/javaxval/src/main/java/org/baeldung/javaxval/enums/EnumNamePatternValidator.java b/javaxval/src/main/java/org/baeldung/javaxval/enums/EnumNamePatternValidator.java new file mode 100644 index 0000000000..a279813461 --- /dev/null +++ b/javaxval/src/main/java/org/baeldung/javaxval/enums/EnumNamePatternValidator.java @@ -0,0 +1,33 @@ +package org.baeldung.javaxval.enums; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.baeldung.javaxval.enums.constraints.EnumNamePattern; + +public class EnumNamePatternValidator implements ConstraintValidator> { + private Pattern pattern; + + @Override + public void initialize(EnumNamePattern constraintAnnotation) { + try { + pattern = Pattern.compile(constraintAnnotation.regexp()); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("Given regex is invalid", e); + } + } + + @Override + public boolean isValid(Enum value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + + Matcher m = pattern.matcher(value.name()); + return m.matches(); + } +} diff --git a/javaxval/src/main/java/org/baeldung/javaxval/enums/EnumSubSetValidator.java b/javaxval/src/main/java/org/baeldung/javaxval/enums/EnumSubSetValidator.java new file mode 100644 index 0000000000..339b4fb03b --- /dev/null +++ b/javaxval/src/main/java/org/baeldung/javaxval/enums/EnumSubSetValidator.java @@ -0,0 +1,25 @@ +package org.baeldung.javaxval.enums; + +import java.lang.annotation.Annotation; +import java.util.Arrays; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public abstract class EnumSubSetValidator implements ConstraintValidator { + private U[] subset; + + protected void initialize(U[] subset) { + this.subset = subset; + } + + @Override + public boolean isValid(U value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + + return Arrays.asList(subset) + .contains(value); + } +} diff --git a/javaxval/src/main/java/org/baeldung/javaxval/enums/InheritedCustomerTypeSubSetValidator.java b/javaxval/src/main/java/org/baeldung/javaxval/enums/InheritedCustomerTypeSubSetValidator.java new file mode 100644 index 0000000000..1cd31c4187 --- /dev/null +++ b/javaxval/src/main/java/org/baeldung/javaxval/enums/InheritedCustomerTypeSubSetValidator.java @@ -0,0 +1,11 @@ +package org.baeldung.javaxval.enums; + +import org.baeldung.javaxval.enums.constraints.CustomerTypeSubset; +import org.baeldung.javaxval.enums.demo.CustomerType; + +public class InheritedCustomerTypeSubSetValidator extends EnumSubSetValidator { + @Override + public void initialize(CustomerTypeSubset constraint) { + super.initialize(constraint.anyOf()); + } +} diff --git a/javaxval/src/main/java/org/baeldung/javaxval/enums/ValueOfEnumValidator.java b/javaxval/src/main/java/org/baeldung/javaxval/enums/ValueOfEnumValidator.java new file mode 100644 index 0000000000..7184dae49b --- /dev/null +++ b/javaxval/src/main/java/org/baeldung/javaxval/enums/ValueOfEnumValidator.java @@ -0,0 +1,31 @@ +package org.baeldung.javaxval.enums; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.baeldung.javaxval.enums.constraints.ValueOfEnum; + +public class ValueOfEnumValidator implements ConstraintValidator { + private List acceptedValues; + + @Override + public void initialize(ValueOfEnum annotation) { + acceptedValues = Stream.of(annotation.enumClass() + .getEnumConstants()) + .map(Enum::name) + .collect(Collectors.toList()); + } + + @Override + public boolean isValid(CharSequence value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + + return acceptedValues.contains(value.toString()); + } +} diff --git a/javaxval/src/main/java/org/baeldung/javaxval/enums/constraints/CustomerTypeSubset.java b/javaxval/src/main/java/org/baeldung/javaxval/enums/constraints/CustomerTypeSubset.java new file mode 100644 index 0000000000..44009e6723 --- /dev/null +++ b/javaxval/src/main/java/org/baeldung/javaxval/enums/constraints/CustomerTypeSubset.java @@ -0,0 +1,45 @@ +package org.baeldung.javaxval.enums.constraints; + +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; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +import org.baeldung.javaxval.enums.CustomerTypeSubSetValidator; +import org.baeldung.javaxval.enums.demo.CustomerType; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = CustomerTypeSubSetValidator.class) +public @interface CustomerTypeSubset { + /** + * @return subset of CustomerType enum + */ + CustomerType[] anyOf(); + + /** + * @return the error message template + */ + String message() default "must be any of {anyOf}"; + + /** + * @return the groups the constraint belongs to + */ + Class[] groups() default {}; + + /** + * @return the payload associated to the constraint + */ + Class[] payload() default {}; +} diff --git a/javaxval/src/main/java/org/baeldung/javaxval/enums/constraints/EnumNamePattern.java b/javaxval/src/main/java/org/baeldung/javaxval/enums/constraints/EnumNamePattern.java new file mode 100644 index 0000000000..403cdcd0b4 --- /dev/null +++ b/javaxval/src/main/java/org/baeldung/javaxval/enums/constraints/EnumNamePattern.java @@ -0,0 +1,45 @@ +package org.baeldung.javaxval.enums.constraints; + +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; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +import org.baeldung.javaxval.enums.EnumNamePatternValidator; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = EnumNamePatternValidator.class) +public @interface EnumNamePattern { + + /** + * @return the regular expression to match + */ + String regexp(); + + /** + * @return the error message template + */ + String message() default "must match \"{regexp}\""; + + /** + * @return the groups the constraint belongs to + */ + Class[] groups() default {}; + + /** + * @return the payload associated to the constraint + */ + Class[] payload() default {}; +} \ No newline at end of file diff --git a/javaxval/src/main/java/org/baeldung/javaxval/enums/constraints/ValueOfEnum.java b/javaxval/src/main/java/org/baeldung/javaxval/enums/constraints/ValueOfEnum.java new file mode 100644 index 0000000000..0f9677d982 --- /dev/null +++ b/javaxval/src/main/java/org/baeldung/javaxval/enums/constraints/ValueOfEnum.java @@ -0,0 +1,44 @@ +package org.baeldung.javaxval.enums.constraints; + +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; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +import org.baeldung.javaxval.enums.ValueOfEnumValidator; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = ValueOfEnumValidator.class) +public @interface ValueOfEnum { + /** + * @return class containing enum values to which this String should match + */ + Class> enumClass(); + + /** + * @return the error message template + */ + String message() default "must be any of enum {enumClass}"; + + /** + * @return the groups the constraint belongs to + */ + Class[] groups() default {}; + + /** + * @return the payload associated to the constraint + */ + Class[] payload() default {}; +} \ No newline at end of file diff --git a/javaxval/src/main/java/org/baeldung/javaxval/enums/demo/Customer.java b/javaxval/src/main/java/org/baeldung/javaxval/enums/demo/Customer.java new file mode 100644 index 0000000000..db3486ab5f --- /dev/null +++ b/javaxval/src/main/java/org/baeldung/javaxval/enums/demo/Customer.java @@ -0,0 +1,77 @@ +package org.baeldung.javaxval.enums.demo; + +import javax.validation.constraints.NotNull; + +import org.baeldung.javaxval.enums.constraints.CustomerTypeSubset; +import org.baeldung.javaxval.enums.constraints.EnumNamePattern; +import org.baeldung.javaxval.enums.constraints.ValueOfEnum; + +public class Customer { + @ValueOfEnum(enumClass = CustomerType.class) + private String customerTypeString; + + @NotNull + @CustomerTypeSubset(anyOf = { CustomerType.NEW, CustomerType.OLD }) + private CustomerType customerTypeOfSubset; + + @EnumNamePattern(regexp = "NEW|DEFAULT") + private CustomerType customerTypeMatchesPattern; + + public Customer() { + } + + public Customer(String customerTypeString, CustomerType customerTypeOfSubset, CustomerType customerTypeMatchesPattern) { + this.customerTypeString = customerTypeString; + this.customerTypeOfSubset = customerTypeOfSubset; + this.customerTypeMatchesPattern = customerTypeMatchesPattern; + } + + public String getCustomerTypeString() { + return customerTypeString; + } + + public void setCustomerTypeString(String customerTypeString) { + this.customerTypeString = customerTypeString; + } + + public CustomerType getCustomerTypeOfSubset() { + return customerTypeOfSubset; + } + + public void setCustomerTypeOfSubset(CustomerType customerTypeOfSubset) { + this.customerTypeOfSubset = customerTypeOfSubset; + } + + public CustomerType getCustomerTypeMatchesPattern() { + return customerTypeMatchesPattern; + } + + public void setCustomerTypeMatchesPattern(CustomerType customerTypeMatchesPattern) { + this.customerTypeMatchesPattern = customerTypeMatchesPattern; + } + + public static class Builder { + private String customerTypeString; + private CustomerType customerTypeOfSubset = CustomerType.NEW; + private CustomerType customerTypeMatchesPattern; + + public Builder withCustomerTypeString(String customerTypeString) { + this.customerTypeString = customerTypeString; + return this; + } + + public Builder withCustomerTypeOfSubset(CustomerType customerTypeOfSubset) { + this.customerTypeOfSubset = customerTypeOfSubset; + return this; + } + + public Builder withCustomerTypeMatchesPattern(CustomerType customerTypeMatchesPattern) { + this.customerTypeMatchesPattern = customerTypeMatchesPattern; + return this; + } + + public Customer build() { + return new Customer(customerTypeString, customerTypeOfSubset, customerTypeMatchesPattern); + } + } +} diff --git a/javaxval/src/main/java/org/baeldung/javaxval/enums/demo/CustomerType.java b/javaxval/src/main/java/org/baeldung/javaxval/enums/demo/CustomerType.java new file mode 100644 index 0000000000..937f3a3817 --- /dev/null +++ b/javaxval/src/main/java/org/baeldung/javaxval/enums/demo/CustomerType.java @@ -0,0 +1,5 @@ +package org.baeldung.javaxval.enums.demo; + +public enum CustomerType { + NEW, OLD, DEFAULT +} diff --git a/javaxval/src/test/java/org/baeldung/javaxval/enums/CustomerTypeSubSetValidatorUnitTest.java b/javaxval/src/test/java/org/baeldung/javaxval/enums/CustomerTypeSubSetValidatorUnitTest.java new file mode 100644 index 0000000000..5aae504b23 --- /dev/null +++ b/javaxval/src/test/java/org/baeldung/javaxval/enums/CustomerTypeSubSetValidatorUnitTest.java @@ -0,0 +1,45 @@ +package org.baeldung.javaxval.enums; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; + +import org.baeldung.javaxval.enums.demo.Customer; +import org.baeldung.javaxval.enums.demo.CustomerType; +import org.baeldung.javaxval.enums.demo.CustomerUnitTest; +import org.junit.BeforeClass; +import org.junit.Test; + +public class CustomerTypeSubSetValidatorUnitTest { + + private static Validator validator; + + @BeforeClass + public static void setupValidatorInstance() { + validator = Validation.buildDefaultValidatorFactory() + .getValidator(); + } + + @Test + public void whenEnumAnyOfSubset_thenShouldNotReportConstraintViolations() { + Customer customer = new Customer.Builder().withCustomerTypeOfSubset(CustomerType.NEW) + .build(); + Set> violations = validator.validate(customer); + assertThat(violations.isEmpty()).isTrue(); + } + + @Test + public void whenEnumNotAnyOfSubset_thenShouldGiveOccurrenceOfConstraintViolations() { + Customer customer = new Customer.Builder().withCustomerTypeOfSubset(CustomerType.DEFAULT) + .build(); + Set> violations = validator.validate(customer); + assertThat(violations.size()).isEqualTo(1); + + assertThat(violations).anyMatch(CustomerUnitTest.havingPropertyPath("customerTypeOfSubset") + .and(CustomerUnitTest.havingMessage("must be any of [NEW, OLD]"))); + } +} \ No newline at end of file diff --git a/javaxval/src/test/java/org/baeldung/javaxval/enums/EnumNamePatternValidatorUnitTest.java b/javaxval/src/test/java/org/baeldung/javaxval/enums/EnumNamePatternValidatorUnitTest.java new file mode 100644 index 0000000000..48f7de2c34 --- /dev/null +++ b/javaxval/src/test/java/org/baeldung/javaxval/enums/EnumNamePatternValidatorUnitTest.java @@ -0,0 +1,54 @@ +package org.baeldung.javaxval.enums; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.baeldung.javaxval.enums.demo.CustomerType.DEFAULT; +import static org.baeldung.javaxval.enums.demo.CustomerType.OLD; + +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; + +import org.baeldung.javaxval.enums.demo.Customer; +import org.baeldung.javaxval.enums.demo.CustomerUnitTest; +import org.junit.BeforeClass; +import org.junit.Test; + +public class EnumNamePatternValidatorUnitTest { + + private static Validator validator; + + @BeforeClass + public static void setupValidatorInstance() { + validator = Validation.buildDefaultValidatorFactory() + .getValidator(); + } + + @Test + public void whenEnumMatchesRegex_thenShouldNotReportConstraintViolations() { + Customer customer = new Customer.Builder().withCustomerTypeMatchesPattern(DEFAULT) + .build(); + Set> violations = validator.validate(customer); + assertThat(violations.isEmpty()).isTrue(); + } + + @Test + public void whenEnumNull_thenShouldNotReportConstraintViolations() { + Customer customer = new Customer.Builder().withCustomerTypeMatchesPattern(null) + .build(); + Set> violations = validator.validate(customer); + assertThat(violations.isEmpty()).isTrue(); + } + + @Test + public void whenEnumDoesNotMatchRegex_thenShouldGiveOccurrenceOfConstraintViolations() { + Customer customer = new Customer.Builder().withCustomerTypeMatchesPattern(OLD) + .build(); + Set> violations = validator.validate(customer); + assertThat(violations.size()).isEqualTo(1); + + assertThat(violations).anyMatch(CustomerUnitTest.havingPropertyPath("customerTypeMatchesPattern") + .and(CustomerUnitTest.havingMessage("must match \"NEW|DEFAULT\""))); + } +} \ No newline at end of file diff --git a/javaxval/src/test/java/org/baeldung/javaxval/enums/ValueOfEnumValidatorUnitTest.java b/javaxval/src/test/java/org/baeldung/javaxval/enums/ValueOfEnumValidatorUnitTest.java new file mode 100644 index 0000000000..0784b58b77 --- /dev/null +++ b/javaxval/src/test/java/org/baeldung/javaxval/enums/ValueOfEnumValidatorUnitTest.java @@ -0,0 +1,52 @@ +package org.baeldung.javaxval.enums; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; + +import org.baeldung.javaxval.enums.demo.Customer; +import org.baeldung.javaxval.enums.demo.CustomerUnitTest; +import org.junit.BeforeClass; +import org.junit.Test; + +public class ValueOfEnumValidatorUnitTest { + + private static Validator validator; + + @BeforeClass + public static void setupValidatorInstance() { + validator = Validation.buildDefaultValidatorFactory() + .getValidator(); + } + + @Test + public void whenStringAnyOfEnum_thenShouldNotReportConstraintViolations() { + Customer customer = new Customer.Builder().withCustomerTypeString("DEFAULT") + .build(); + Set> violations = validator.validate(customer); + assertThat(violations.isEmpty()).isTrue(); + } + + @Test + public void whenStringNull_thenShouldNotReportConstraintViolations() { + Customer customer = new Customer.Builder().withCustomerTypeString(null) + .build(); + Set> violations = validator.validate(customer); + assertThat(violations.isEmpty()).isTrue(); + } + + @Test + public void whenStringNotAnyOfEnum_thenShouldGiveOccurrenceOfConstraintViolations() { + Customer customer = new Customer.Builder().withCustomerTypeString("test") + .build(); + Set> violations = validator.validate(customer); + assertThat(violations.size()).isEqualTo(1); + + assertThat(violations).anyMatch(CustomerUnitTest.havingPropertyPath("customerTypeString") + .and(CustomerUnitTest.havingMessage("must be any of enum class org.baeldung.javaxval.enums.demo.CustomerType"))); + } +} \ No newline at end of file diff --git a/javaxval/src/test/java/org/baeldung/javaxval/enums/demo/CustomerUnitTest.java b/javaxval/src/test/java/org/baeldung/javaxval/enums/demo/CustomerUnitTest.java new file mode 100644 index 0000000000..6a064b640f --- /dev/null +++ b/javaxval/src/test/java/org/baeldung/javaxval/enums/demo/CustomerUnitTest.java @@ -0,0 +1,66 @@ +package org.baeldung.javaxval.enums.demo; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; +import java.util.function.Predicate; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; + +import org.baeldung.javaxval.LocaleAwareUnitTest; +import org.junit.BeforeClass; +import org.junit.Test; + +public class CustomerUnitTest extends LocaleAwareUnitTest { + + private static Validator validator; + + @BeforeClass + public static void setupValidatorInstance() { + validator = Validation.buildDefaultValidatorFactory() + .getValidator(); + } + + @Test + public void whenAllAcceptable_thenShouldNotGiveConstraintViolations() { + Customer customer = new Customer(); + customer.setCustomerTypeOfSubset(CustomerType.NEW); + Set> violations = validator.validate(customer); + assertThat(violations).isEmpty(); + } + + @Test + public void whenAllNull_thenOnlyNotNullShouldGiveConstraintViolations() { + Customer customer = new Customer(); + Set> violations = validator.validate(customer); + assertThat(violations.size()).isEqualTo(1); + + assertThat(violations).anyMatch(havingPropertyPath("customerTypeOfSubset").and(havingMessage("must not be null"))); + } + + @Test + public void whenAllInvalid_thenViolationsShouldBeReported() { + Customer customer = new Customer(); + customer.setCustomerTypeString("invalid"); + customer.setCustomerTypeOfSubset(CustomerType.DEFAULT); + customer.setCustomerTypeMatchesPattern(CustomerType.OLD); + + Set> violations = validator.validate(customer); + assertThat(violations.size()).isEqualTo(3); + + assertThat(violations).anyMatch(havingPropertyPath("customerTypeString").and(havingMessage("must be any of enum class org.baeldung.javaxval.enums.demo.CustomerType"))); + assertThat(violations).anyMatch(havingPropertyPath("customerTypeOfSubset").and(havingMessage("must be any of [NEW, OLD]"))); + assertThat(violations).anyMatch(havingPropertyPath("customerTypeMatchesPattern").and(havingMessage("must match \"NEW|DEFAULT\""))); + } + + public static Predicate> havingMessage(String message) { + return l -> message.equals(l.getMessage()); + } + + public static Predicate> havingPropertyPath(String propertyPath) { + return l -> propertyPath.equals(l.getPropertyPath() + .toString()); + } +} \ No newline at end of file