From 5060bce29dbe872c7ee230ca188f484c2a83cd85 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 27 Jan 2026 08:16:13 +0100 Subject: [PATCH 1/3] move classes --- .../MaxScaledBigDecimalValidator.java | 2 +- .../MinScaledBigDecimalValidator.java | 2 +- .../NegativeOrZeroScaledBigDecimalValidator.java | 2 +- .../NegativeScaledBigDecimalValidator.java | 2 +- .../PositiveOrZeroScaledBigDecimalValidator.java | 2 +- .../PositiveScaledBigDecimalValidator.java | 2 +- .../META-INF/validation/scaled-big-decimal.xml | 12 ++++++------ .../MaxScaledBigDecimalValidatorTest.java | 2 +- .../MinScaledBigDecimalValidatorTest.java | 2 +- .../NegativeOrZeroScaledBigDecimalValidatorTest.java | 2 +- .../NegativeScaledBigDecimalValidatorTest.java | 2 +- .../PositiveOrZeroScaledBigDecimalValidatorTest.java | 2 +- .../PositiveScaledBigDecimalValidatorTest.java | 2 +- 13 files changed, 18 insertions(+), 18 deletions(-) rename src/main/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/MaxScaledBigDecimalValidator.java (91%) rename src/main/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/MinScaledBigDecimalValidator.java (92%) rename src/main/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/NegativeOrZeroScaledBigDecimalValidator.java (89%) rename src/main/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/NegativeScaledBigDecimalValidator.java (89%) rename src/main/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/PositiveOrZeroScaledBigDecimalValidator.java (89%) rename src/main/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/PositiveScaledBigDecimalValidator.java (89%) rename src/test/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/MaxScaledBigDecimalValidatorTest.java (94%) rename src/test/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/MinScaledBigDecimalValidatorTest.java (94%) rename src/test/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/NegativeOrZeroScaledBigDecimalValidatorTest.java (94%) rename src/test/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/NegativeScaledBigDecimalValidatorTest.java (94%) rename src/test/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/PositiveOrZeroScaledBigDecimalValidatorTest.java (94%) rename src/test/java/it/aboutbits/springboot/toolbox/validation/validator/{ => scaled_big_decimal}/PositiveScaledBigDecimalValidatorTest.java (94%) diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/MaxScaledBigDecimalValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MaxScaledBigDecimalValidator.java similarity index 91% rename from src/main/java/it/aboutbits/springboot/toolbox/validation/validator/MaxScaledBigDecimalValidator.java rename to src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MaxScaledBigDecimalValidator.java index 1bdc7ad..558d97c 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/MaxScaledBigDecimalValidator.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MaxScaledBigDecimalValidator.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.ConstraintValidator; diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/MinScaledBigDecimalValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MinScaledBigDecimalValidator.java similarity index 92% rename from src/main/java/it/aboutbits/springboot/toolbox/validation/validator/MinScaledBigDecimalValidator.java rename to src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MinScaledBigDecimalValidator.java index 18d50fc..d6dce1d 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/MinScaledBigDecimalValidator.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MinScaledBigDecimalValidator.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.ConstraintValidator; diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeOrZeroScaledBigDecimalValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeOrZeroScaledBigDecimalValidator.java similarity index 89% rename from src/main/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeOrZeroScaledBigDecimalValidator.java rename to src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeOrZeroScaledBigDecimalValidator.java index 827f52d..1bc95b2 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeOrZeroScaledBigDecimalValidator.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeOrZeroScaledBigDecimalValidator.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.ConstraintValidator; diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeScaledBigDecimalValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeScaledBigDecimalValidator.java similarity index 89% rename from src/main/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeScaledBigDecimalValidator.java rename to src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeScaledBigDecimalValidator.java index b4617fd..0eccaeb 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeScaledBigDecimalValidator.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeScaledBigDecimalValidator.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.ConstraintValidator; diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveOrZeroScaledBigDecimalValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveOrZeroScaledBigDecimalValidator.java similarity index 89% rename from src/main/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveOrZeroScaledBigDecimalValidator.java rename to src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveOrZeroScaledBigDecimalValidator.java index a5c5b26..8918867 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveOrZeroScaledBigDecimalValidator.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveOrZeroScaledBigDecimalValidator.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.ConstraintValidator; diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveScaledBigDecimalValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveScaledBigDecimalValidator.java similarity index 89% rename from src/main/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveScaledBigDecimalValidator.java rename to src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveScaledBigDecimalValidator.java index c2a6e18..2371386 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveScaledBigDecimalValidator.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveScaledBigDecimalValidator.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.ConstraintValidator; diff --git a/src/main/resources/META-INF/validation/scaled-big-decimal.xml b/src/main/resources/META-INF/validation/scaled-big-decimal.xml index 9d80254..37a972b 100644 --- a/src/main/resources/META-INF/validation/scaled-big-decimal.xml +++ b/src/main/resources/META-INF/validation/scaled-big-decimal.xml @@ -4,42 +4,42 @@ - it.aboutbits.springboot.toolbox.validation.validator.MaxScaledBigDecimalValidator + it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal.MaxScaledBigDecimalValidator - it.aboutbits.springboot.toolbox.validation.validator.MinScaledBigDecimalValidator + it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal.MinScaledBigDecimalValidator - it.aboutbits.springboot.toolbox.validation.validator.NegativeScaledBigDecimalValidator + it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal.NegativeScaledBigDecimalValidator - it.aboutbits.springboot.toolbox.validation.validator.NegativeOrZeroScaledBigDecimalValidator + it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal.NegativeOrZeroScaledBigDecimalValidator - it.aboutbits.springboot.toolbox.validation.validator.PositiveScaledBigDecimalValidator + it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal.PositiveScaledBigDecimalValidator - it.aboutbits.springboot.toolbox.validation.validator.PositiveOrZeroScaledBigDecimalValidator + it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal.PositiveOrZeroScaledBigDecimalValidator diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/MaxScaledBigDecimalValidatorTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MaxScaledBigDecimalValidatorTest.java similarity index 94% rename from src/test/java/it/aboutbits/springboot/toolbox/validation/validator/MaxScaledBigDecimalValidatorTest.java rename to src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MaxScaledBigDecimalValidatorTest.java index ab9a607..2dda326 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/MaxScaledBigDecimalValidatorTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MaxScaledBigDecimalValidatorTest.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.Validation; diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/MinScaledBigDecimalValidatorTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MinScaledBigDecimalValidatorTest.java similarity index 94% rename from src/test/java/it/aboutbits/springboot/toolbox/validation/validator/MinScaledBigDecimalValidatorTest.java rename to src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MinScaledBigDecimalValidatorTest.java index a9aef51..b0db913 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/MinScaledBigDecimalValidatorTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/MinScaledBigDecimalValidatorTest.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.Validation; diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeOrZeroScaledBigDecimalValidatorTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeOrZeroScaledBigDecimalValidatorTest.java similarity index 94% rename from src/test/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeOrZeroScaledBigDecimalValidatorTest.java rename to src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeOrZeroScaledBigDecimalValidatorTest.java index 1c567eb..11a09f8 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeOrZeroScaledBigDecimalValidatorTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeOrZeroScaledBigDecimalValidatorTest.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.Validation; diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeScaledBigDecimalValidatorTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeScaledBigDecimalValidatorTest.java similarity index 94% rename from src/test/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeScaledBigDecimalValidatorTest.java rename to src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeScaledBigDecimalValidatorTest.java index 6909a7f..0753760 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/NegativeScaledBigDecimalValidatorTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/NegativeScaledBigDecimalValidatorTest.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.Validation; diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveOrZeroScaledBigDecimalValidatorTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveOrZeroScaledBigDecimalValidatorTest.java similarity index 94% rename from src/test/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveOrZeroScaledBigDecimalValidatorTest.java rename to src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveOrZeroScaledBigDecimalValidatorTest.java index 84f05e0..948f1d4 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveOrZeroScaledBigDecimalValidatorTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveOrZeroScaledBigDecimalValidatorTest.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.Validation; diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveScaledBigDecimalValidatorTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveScaledBigDecimalValidatorTest.java similarity index 94% rename from src/test/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveScaledBigDecimalValidatorTest.java rename to src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveScaledBigDecimalValidatorTest.java index 71fe925..71214e3 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/PositiveScaledBigDecimalValidatorTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/validator/scaled_big_decimal/PositiveScaledBigDecimalValidatorTest.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.validation.validator; +package it.aboutbits.springboot.toolbox.validation.validator.scaled_big_decimal; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import jakarta.validation.Validation; From d210abf4c0da1d0af6de409cc7a7cd3846b27d53 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 27 Jan 2026 08:16:28 +0100 Subject: [PATCH 2/3] add common validators --- .../validation/annotation/RepeatedField.java | 39 ++++++ .../validation/annotation/ValidDateRange.java | 48 +++++++ .../annotation/ValidNumericRange.java | 47 +++++++ .../validation/annotation/ValidPassword.java | 31 +++++ .../validator/RepeatedFieldValidator.java | 44 +++++++ .../validator/ValidDateRangeValidator.java | 73 +++++++++++ .../validator/ValidNumericRangeValidator.java | 100 ++++++++++++++ .../validator/ValidPasswordValidator.java | 56 ++++++++ .../annotation/RepeatedFieldTest.java | 101 ++++++++++++++ .../annotation/ValidDateRangeTest.java | 93 +++++++++++++ .../annotation/ValidNumericRangeTest.java | 109 ++++++++++++++++ .../annotation/ValidPasswordTest.java | 123 ++++++++++++++++++ 12 files changed, 864 insertions(+) create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedField.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidDateRange.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidNumericRange.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidPassword.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/validation/validator/RepeatedFieldValidator.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidDateRangeValidator.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidNumericRangeValidator.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidPasswordValidator.java create mode 100644 src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedFieldTest.java create mode 100644 src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidDateRangeTest.java create mode 100644 src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidNumericRangeTest.java create mode 100644 src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidPasswordTest.java diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedField.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedField.java new file mode 100644 index 0000000..0ad3626 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedField.java @@ -0,0 +1,39 @@ +package it.aboutbits.springboot.toolbox.validation.annotation; + +import it.aboutbits.springboot.toolbox.validation.validator.RepeatedFieldValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target(ElementType.TYPE) +@Retention(RUNTIME) +@Constraint(validatedBy = RepeatedFieldValidator.class) +@Documented +@Repeatable(RepeatedField.List.class) +public @interface RepeatedField { + + String message() default "{shared.error.validation.repetitionMismatch}"; + + String originalField(); + + String repeatedField(); + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface List { + RepeatedField[] value(); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidDateRange.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidDateRange.java new file mode 100644 index 0000000..dba23ce --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidDateRange.java @@ -0,0 +1,48 @@ +package it.aboutbits.springboot.toolbox.validation.annotation; + +import it.aboutbits.springboot.toolbox.validation.validator.ValidDateRangeValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Validator bean to check if the {@code to} date field is after the {@code from} date field.
+ * If one of the two fields is {@code null} this validation bean will still return true, as in a form, for example, + * the {@code from} field could be specified. + *

+ * Supports {@link java.time.LocalDateTime}, {@link java.time.LocalDate}, {@link java.time.OffsetDateTime}, + * {@link java.time.ZonedDateTime}, and {@link java.time.Instant}. + */ +@Documented +@Constraint(validatedBy = ValidDateRangeValidator.class) +@Target(ElementType.TYPE) +@Retention(RUNTIME) +@Repeatable(ValidDateRange.List.class) +public @interface ValidDateRange { + String message() default "{shared.error.validation.invalidDateRange}"; + + boolean allowEmptyRange() default false; + + String fromDateField(); + + String toDateField(); + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface List { + ValidDateRange[] value(); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidNumericRange.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidNumericRange.java new file mode 100644 index 0000000..11c3ce3 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidNumericRange.java @@ -0,0 +1,47 @@ +package it.aboutbits.springboot.toolbox.validation.annotation; + +import it.aboutbits.springboot.toolbox.validation.validator.ValidNumericRangeValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Validator bean to check if the lower bound numeric field is not higher than the upper bound numeric field.
+ * If one of the two fields is {@code null} this validation bean will still return true, as in a form, for example, + * only one field could be specified. + *

+ * Supports numeric types like {@link Integer}, {@link Long}, {@link Float}, {@link Double}, etc. + */ +@Documented +@Constraint(validatedBy = ValidNumericRangeValidator.class) +@Target(ElementType.TYPE) +@Retention(RUNTIME) +@Repeatable(ValidNumericRange.List.class) +public @interface ValidNumericRange { + String message() default "{shared.error.validation.invalidNumericRange}"; + + boolean allowEqualValues() default true; + + String lowerBoundField(); + + String upperBoundField(); + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface List { + ValidNumericRange[] value(); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidPassword.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidPassword.java new file mode 100644 index 0000000..2f84021 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidPassword.java @@ -0,0 +1,31 @@ +package it.aboutbits.springboot.toolbox.validation.annotation; + +import it.aboutbits.springboot.toolbox.validation.validator.ValidPasswordValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Constraint(validatedBy = ValidPasswordValidator.class) +@Target({FIELD, ANNOTATION_TYPE}) +@Retention(RUNTIME) +public @interface ValidPassword { + String message() default "{shared.error.validation.invalidPassword}"; + + String lengthMessage() default "{shared.error.validation.invalidPassword.length}"; + + int minLength() default 8; + + int maxLength() default 50; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/RepeatedFieldValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/RepeatedFieldValidator.java new file mode 100644 index 0000000..2617726 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/RepeatedFieldValidator.java @@ -0,0 +1,44 @@ +package it.aboutbits.springboot.toolbox.validation.validator; + +import it.aboutbits.springboot.toolbox.validation.annotation.RepeatedField; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.springframework.beans.BeanWrapperImpl; + +@NullMarked +public class RepeatedFieldValidator implements ConstraintValidator { + private @Nullable String originalField; + private @Nullable String repeatedField; + private @Nullable String message; + + @Override + public void initialize(RepeatedField constraintAnnotation) { + this.originalField = constraintAnnotation.originalField(); + this.repeatedField = constraintAnnotation.repeatedField(); + this.message = constraintAnnotation.message(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + if (originalField == null || repeatedField == null) { + throw new IllegalStateException("Both originalField and repeatedField must be set."); + } + + var fieldValue = new BeanWrapperImpl(value).getPropertyValue(originalField); + var fieldMatchValue = new BeanWrapperImpl(value).getPropertyValue(repeatedField); + + var isValid = fieldValue != null && fieldValue.equals(fieldMatchValue); + + if (!isValid) { + context.disableDefaultConstraintViolation(); + context + .buildConstraintViolationWithTemplate(message) + .addPropertyNode(repeatedField) + .addConstraintViolation(); + } + + return isValid; + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidDateRangeValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidDateRangeValidator.java new file mode 100644 index 0000000..5db440b --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidDateRangeValidator.java @@ -0,0 +1,73 @@ +package it.aboutbits.springboot.toolbox.validation.validator; + +import it.aboutbits.springboot.toolbox.validation.annotation.ValidDateRange; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.springframework.beans.PropertyAccessorFactory; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; + +@RequiredArgsConstructor +@NullMarked +public class ValidDateRangeValidator implements ConstraintValidator { + @Nullable + private String fromDateField; + @Nullable + private String toDateField; + @Nullable + private String message; + private boolean allowEmptyRange = false; + + @Override + public void initialize(ValidDateRange constraintAnnotation) { + this.fromDateField = constraintAnnotation.fromDateField(); + this.toDateField = constraintAnnotation.toDateField(); + this.message = constraintAnnotation.message(); + this.allowEmptyRange = constraintAnnotation.allowEmptyRange(); + } + + public boolean isValid(Object value, ConstraintValidatorContext context) { + if (fromDateField == null || toDateField == null || message == null) { + throw new IllegalStateException("All fields must be set (fromDateField, toDateField, message)."); + } + + var propertyAccessor = PropertyAccessorFactory.forBeanPropertyAccess(value); + var fromDate = propertyAccessor.getPropertyValue(fromDateField); + var toDate = propertyAccessor.getPropertyValue(toDateField); + + if (fromDate == null || toDate == null) { + return true; // Consider null dates valid; use @NotNull for null checks if needed + } + + boolean isValid = switch (fromDate) { + case LocalDate from when toDate instanceof LocalDate to -> + allowEmptyRange ? (to.isEqual(from) || to.isAfter(from)) : to.isAfter(from); + case LocalDateTime from when toDate instanceof LocalDateTime to -> + allowEmptyRange ? (to.isEqual(from) || to.isAfter(from)) : to.isAfter(from); + case OffsetDateTime from when toDate instanceof OffsetDateTime to -> + allowEmptyRange ? (to.isEqual(from) || to.isAfter(from)) : to.isAfter(from); + case ZonedDateTime from when toDate instanceof ZonedDateTime to -> + allowEmptyRange ? (to.isEqual(from) || to.isAfter(from)) : to.isAfter(from); + case Instant from when toDate instanceof Instant to -> + allowEmptyRange ? (to.equals(from) || to.isAfter(from)) : to.isAfter(from); + default -> throw new IllegalArgumentException("Unsupported date data types!"); + }; + + if (!isValid) { + context.disableDefaultConstraintViolation(); + context + .buildConstraintViolationWithTemplate(message) + .addPropertyNode(toDateField) + .addConstraintViolation(); + } + + return isValid; + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidNumericRangeValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidNumericRangeValidator.java new file mode 100644 index 0000000..27b5ab5 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidNumericRangeValidator.java @@ -0,0 +1,100 @@ +package it.aboutbits.springboot.toolbox.validation.validator; + +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import it.aboutbits.springboot.toolbox.validation.annotation.ValidNumericRange; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.springframework.beans.PropertyAccessorFactory; + +import java.math.BigDecimal; +import java.math.BigInteger; + +@RequiredArgsConstructor +@NullMarked +public class ValidNumericRangeValidator implements ConstraintValidator { + @Nullable + private String lowerBoundField; + @Nullable + private String upperBoundField; + @Nullable + private String message; + private boolean allowEqualValues = true; + + @Override + public void initialize(ValidNumericRange constraintAnnotation) { + this.lowerBoundField = constraintAnnotation.lowerBoundField(); + this.upperBoundField = constraintAnnotation.upperBoundField(); + this.message = constraintAnnotation.message(); + this.allowEqualValues = constraintAnnotation.allowEqualValues(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + if (lowerBoundField == null || upperBoundField == null || message == null) { + throw new IllegalStateException("All fields must be set (lowerBoundField, upperBoundField, message)."); + } + + var propertyAccessor = PropertyAccessorFactory.forBeanPropertyAccess(value); + var lowerBound = propertyAccessor.getPropertyValue(lowerBoundField); + var upperBound = propertyAccessor.getPropertyValue(upperBoundField); + + if (lowerBound == null || upperBound == null) { + return true; // Consider null values valid; use @NotNull for null checks if needed + } + + var isValid = false; + + if (lowerBound instanceof ScaledBigDecimal lower && upperBound instanceof ScaledBigDecimal upper) { + isValid = compareValues(lower.compareTo(upper)); + } else if (lowerBound instanceof ScaledBigDecimal || upperBound instanceof ScaledBigDecimal) { + throw new IllegalArgumentException("Both fields must be of the same type!"); + } else { + if (!(lowerBound instanceof Number) || !(upperBound instanceof Number)) { + throw new IllegalArgumentException("Both fields must be numeric types!"); + } + + isValid = compareNumbers((Number) lowerBound, (Number) upperBound); + } + + if (!isValid) { + context.disableDefaultConstraintViolation(); + context + .buildConstraintViolationWithTemplate(message) + .addPropertyNode(upperBoundField) + .addConstraintViolation(); + } + + return isValid; + } + + private boolean compareNumbers(Number lowerBound, Number upperBound) { + // Convert both numbers to BigDecimal for accurate comparison + var lower = convertToBigDecimal(lowerBound); + var upper = convertToBigDecimal(upperBound); + + return compareValues(lower.compareTo(upper)); + } + + private boolean compareValues(int comparisonResult) { + if (allowEqualValues) { + return comparisonResult <= 0; // lower <= upper + } else { + return comparisonResult < 0; // lower < upper + } + } + + private BigDecimal convertToBigDecimal(Number number) { + if (number instanceof BigDecimal bigDecimal) { + return bigDecimal; + } else if (number instanceof BigInteger bigInteger) { + return new BigDecimal(bigInteger); + } else if (number instanceof Double || number instanceof Float) { + return BigDecimal.valueOf(number.doubleValue()); + } else { + return BigDecimal.valueOf(number.longValue()); + } + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidPasswordValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidPasswordValidator.java new file mode 100644 index 0000000..1e02cb2 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/validator/ValidPasswordValidator.java @@ -0,0 +1,56 @@ +package it.aboutbits.springboot.toolbox.validation.validator; + +import it.aboutbits.springboot.toolbox.validation.annotation.ValidPassword; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.commons.lang3.StringUtils; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public class ValidPasswordValidator implements ConstraintValidator { + private @Nullable String message; + private @Nullable String lengthMessage; + private int minLength = 8; + private int maxLength = 50; + + @Override + public void initialize(ValidPassword constraintAnnotation) { + this.message = constraintAnnotation.message(); + this.lengthMessage = constraintAnnotation.lengthMessage(); + this.minLength = constraintAnnotation.minLength(); + this.maxLength = constraintAnnotation.maxLength(); + } + + @Override + public boolean isValid(@Nullable String password, ConstraintValidatorContext context) { + if (message == null || lengthMessage == null) { + throw new IllegalStateException("Both message and lengthMessage must be set."); + } + + if (minLength > maxLength) { + throw new IllegalStateException("minLength must be less than maxLength."); + } + + if (password == null) { + return true; // Consider null as valid; use @NotNull for null checks if needed + } + + var length = password.length(); + var lengthValid = length >= minLength && length <= maxLength; + + if (StringUtils.isNotBlank(password) && lengthValid) { + return true; + } + context.disableDefaultConstraintViolation(); + if (!lengthValid) { + context.buildConstraintViolationWithTemplate(lengthMessage) + .addConstraintViolation(); + } else { + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + } + + return false; + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedFieldTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedFieldTest.java new file mode 100644 index 0000000..1c21d9d --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedFieldTest.java @@ -0,0 +1,101 @@ +package it.aboutbits.springboot.toolbox.validation.annotation; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@NullMarked +class RepeatedFieldTest { + private static final ValidatorFactory VALIDATOR_FACTORY = Validation.buildDefaultValidatorFactory(); + private final Validator validator = VALIDATOR_FACTORY.getValidator(); + + @Test + void testIsValidWithMatchingFields() { + var testObject = new TestObject("password123", "password123"); + + var result = validator.validate(testObject); + + assertThat(result).isEmpty(); + } + + @Test + void testIsValidWithNonMatchingFields() { + var testObject = new TestObject("password123", "differentPassword"); + + var result = validator.validate(testObject); + + assertThat(result).isNotEmpty(); + assertThat(result).anySatisfy(violation -> { + assertThat(violation.getPropertyPath().toString()).isEqualTo("repeatPassword"); + assertThat(violation.getMessageTemplate()).isEqualTo("{shared.error.validation.repetitionMismatch}"); + }); + } + + @Test + void testIsValidWithNullValues() { + var testObject = new TestObject(null, null); + + var result = validator.validate(testObject); + + // Based on implementation: fieldValue != null && fieldValue.equals(fieldMatchValue) + // So null, null should be INVALID + assertThat(result).isNotEmpty(); + } + + @Test + void testIsValidWithOneNullValue() { + var testObject = new TestObject("password123", null); + + var result = validator.validate(testObject); + + assertThat(result).isNotEmpty(); + } + + @Test + void testIsValidWithMultipleAnnotations() { + var testObject = new MultipleRepeatedFieldsObject( + "email@example.com", + "email@example.com", + "password", + "wrong-password" + ); + + var result = validator.validate(testObject); + + assertThat(result).hasSize(1); + assertThat(result).anySatisfy(violation -> { + assertThat(violation.getPropertyPath().toString()).isEqualTo("repeatPassword"); + }); + + testObject = new MultipleRepeatedFieldsObject( + "email@example.com", + "wrong-email@example.com", + "password", + "wrong-password" + ); + result = validator.validate(testObject); + assertThat(result).hasSize(2); + } + + @RepeatedField(originalField = "password", repeatedField = "repeatPassword") + record TestObject( + @Nullable String password, + @Nullable String repeatPassword + ) { + } + + @RepeatedField(originalField = "email", repeatedField = "repeatEmail") + @RepeatedField(originalField = "password", repeatedField = "repeatPassword") + record MultipleRepeatedFieldsObject( + @Nullable String email, + @Nullable String repeatEmail, + @Nullable String password, + @Nullable String repeatPassword + ) { + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidDateRangeTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidDateRangeTest.java new file mode 100644 index 0000000..80a859b --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidDateRangeTest.java @@ -0,0 +1,93 @@ +package it.aboutbits.springboot.toolbox.validation.annotation; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; + +@NullMarked +class ValidDateRangeTest { + private static final ValidatorFactory VALIDATOR_FACTORY = Validation.buildDefaultValidatorFactory(); + private final Validator validator = VALIDATOR_FACTORY.getValidator(); + + @Test + void testIsValidWithValidRange() { + var testObject = new TestObject( + LocalDate.of(2023, 1, 1), + LocalDate.of(2023, 1, 2) + ); + + var result = validator.validate(testObject); + + assertThat(result).isEmpty(); + } + + @Test + void testIsValidWithEqualValues() { + var testObject = new TestObject( + LocalDate.of(2023, 1, 1), + LocalDate.of(2023, 1, 1) + ); + + var result = validator.validate(testObject); + + // Default allowEmptyRange is false + assertThat(result).isNotEmpty(); + } + + @Test + void testIsValidWithEqualValuesWhenAllowed() { + var testObject = new TestObjectEmptyAllowed( + LocalDate.of(2023, 1, 1), + LocalDate.of(2023, 1, 1) + ); + + var result = validator.validate(testObject); + + assertThat(result).isEmpty(); + } + + @Test + void testIsValidWithInvalidRange() { + var testObject = new TestObject( + LocalDate.of(2023, 1, 2), + LocalDate.of(2023, 1, 1) + ); + + var result = validator.validate(testObject); + + assertThat(result).isNotEmpty(); + assertThat(result).anySatisfy(violation -> { + assertThat(violation.getPropertyPath().toString()).isEqualTo("toDate"); + }); + } + + @Test + void testIsValidWithNullValues() { + var testObject = new TestObject(null, null); + + var result = validator.validate(testObject); + + assertThat(result).isEmpty(); + } + + @ValidDateRange(fromDateField = "fromDate", toDateField = "toDate") + record TestObject( + @Nullable LocalDate fromDate, + @Nullable LocalDate toDate + ) { + } + + @ValidDateRange(fromDateField = "fromDate", toDateField = "toDate", allowEmptyRange = true) + record TestObjectEmptyAllowed( + @Nullable LocalDate fromDate, + @Nullable LocalDate toDate + ) { + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidNumericRangeTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidNumericRangeTest.java new file mode 100644 index 0000000..1e38fad --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidNumericRangeTest.java @@ -0,0 +1,109 @@ +package it.aboutbits.springboot.toolbox.validation.annotation; + +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@NullMarked +class ValidNumericRangeTest { + private static final ValidatorFactory VALIDATOR_FACTORY = Validation.buildDefaultValidatorFactory(); + private final Validator validator = VALIDATOR_FACTORY.getValidator(); + + @Test + void testIsValidWithScaledBigDecimalValidRange() { + var testObject = new TestObject( + ScaledBigDecimal.valueOf(10.5), + ScaledBigDecimal.valueOf(20.5) + ); + + var result = validator.validate(testObject); + + assertThat(result).isEmpty(); + } + + @Test + void testIsValidWithScaledBigDecimalEqualValues() { + var testObject = new TestObject( + ScaledBigDecimal.valueOf(15.0), + ScaledBigDecimal.valueOf(15.0) + ); + + var result = validator.validate(testObject); + + assertThat(result).isEmpty(); + } + + @Test + void testIsValidWithScaledBigDecimalEqualValuesWhenNotAllowed() { + var testObject = new TestObjectNoEqual( + ScaledBigDecimal.valueOf(15.0), + ScaledBigDecimal.valueOf(15.0) + ); + + var result = validator.validate(testObject); + + assertThat(result).isNotEmpty(); + } + + @Test + void testIsValidWithScaledBigDecimalInvalidRange() { + var testObject = new TestObject( + ScaledBigDecimal.valueOf(30.5), + ScaledBigDecimal.valueOf(20.5) + ); + + var result = validator.validate(testObject); + + assertThat(result).isNotEmpty(); + assertThat(result).anySatisfy(violation -> { + assertThat(violation.getPropertyPath().toString()).isEqualTo("upperBound"); + }); + } + + @Test + void testIsValidWithNullValues() { + var testObject = new TestObject(null, null); + + var result = validator.validate(testObject); + + assertThat(result).isEmpty(); + } + + @Test + void testIsValidWithNullLowerBound() { + var testObject = new TestObject(null, ScaledBigDecimal.valueOf(20.5)); + + var result = validator.validate(testObject); + + assertThat(result).isEmpty(); + } + + @Test + void testIsValidWithNullUpperBound() { + var testObject = new TestObject(ScaledBigDecimal.valueOf(10.5), null); + + var result = validator.validate(testObject); + + assertThat(result).isEmpty(); + } + + @ValidNumericRange(lowerBoundField = "lowerBound", upperBoundField = "upperBound") + record TestObject( + @Nullable Object lowerBound, + @Nullable Object upperBound + ) { + } + + @ValidNumericRange(lowerBoundField = "lowerBound", upperBoundField = "upperBound", allowEqualValues = false) + record TestObjectNoEqual( + @Nullable Object lowerBound, + @Nullable Object upperBound + ) { + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidPasswordTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidPasswordTest.java new file mode 100644 index 0000000..8c2770f --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/annotation/ValidPasswordTest.java @@ -0,0 +1,123 @@ +package it.aboutbits.springboot.toolbox.validation.annotation; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@NullMarked +class ValidPasswordTest { + private static final ValidatorFactory VALIDATOR_FACTORY = Validation.buildDefaultValidatorFactory(); + private final Validator validator = VALIDATOR_FACTORY.getValidator(); + + @Test + void testIsValidWithValidPassword() { + var testObject = new TestObject("validPassword123"); + + var result = validator.validate(testObject); + + assertThat(result).isEmpty(); + } + + @Test + void testIsValidWithNullPassword() { + var testObject = new TestObject(null); + + var result = validator.validate(testObject); + + // Password null is considered valid by the validator + assertThat(result).isEmpty(); + } + + @Test + void testIsValidWithBlankPassword() { + var testObject = new TestObject(""); + + var result = validator.validate(testObject); + + assertThat(result).isNotEmpty(); + assertThat(result).anySatisfy(violation -> { + assertThat(violation.getMessageTemplate()).isEqualTo("{shared.error.validation.invalidPassword.length}"); + }); + + testObject = new TestObject(" "); + result = validator.validate(testObject); + assertThat(result).isNotEmpty(); + // For blank but within length (e.g. 8 spaces), it should trigger the general message + } + + @Test + void testIsValidWithTooShortPassword() { + var testObject = new TestObject("short"); + + var result = validator.validate(testObject); + + assertThat(result).isNotEmpty(); + assertThat(result).anySatisfy(violation -> { + assertThat(violation.getMessageTemplate()).isEqualTo("{shared.error.validation.invalidPassword.length}"); + }); + } + + @Test + void testIsValidWithTooLongPassword() { + var testObject = new TestObject("thisPasswordIsWayTooLongAndShouldFailValidationBecauseItExceedsFiftyCharacters"); + + var result = validator.validate(testObject); + + assertThat(result).isNotEmpty(); + assertThat(result).anySatisfy(violation -> { + assertThat(violation.getMessageTemplate()).isEqualTo("{shared.error.validation.invalidPassword.length}"); + }); + } + + @Test + void testIsValidWithCustomLengths() { + var testObject = new CustomLengthObject("pass"); // 4 chars, min is 5 + + var result = validator.validate(testObject); + + assertThat(result).isNotEmpty(); + assertThat(result).anySatisfy(violation -> { + assertThat(violation.getMessageTemplate()).isEqualTo("too short"); + }); + + testObject = new CustomLengthObject("password123"); // 11 chars, max is 10 + result = validator.validate(testObject); + assertThat(result).isNotEmpty(); + assertThat(result).anySatisfy(violation -> { + assertThat(violation.getMessageTemplate()).isEqualTo("too short"); // The message template in CustomLengthObject is "too short" for lengthMessage + }); + } + + @Test + void testIsValidWithSpacesOnly() { + // " " is 8 spaces, so lengthValid is true, but StringUtils.isNotBlank is false + var testObject = new TestObject(" "); + + var result = validator.validate(testObject); + + assertThat(result).isNotEmpty(); + assertThat(result).anySatisfy(violation -> { + assertThat(violation.getMessageTemplate()).isEqualTo("{shared.error.validation.invalidPassword}"); + }); + } + + record TestObject( + @ValidPassword @Nullable String password + ) { + } + + record CustomLengthObject( + @ValidPassword( + minLength = 5, + maxLength = 10, + lengthMessage = "too short", + message = "invalid" + ) @Nullable String password + ) { + } +} From c0083e4416ea1ef656eb3c87de063c84c95224b7 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 27 Jan 2026 09:24:00 +0100 Subject: [PATCH 3/3] remove blank line --- .../springboot/toolbox/validation/annotation/RepeatedField.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedField.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedField.java index 0ad3626..e9fbce9 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedField.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/annotation/RepeatedField.java @@ -19,7 +19,6 @@ @Documented @Repeatable(RepeatedField.List.class) public @interface RepeatedField { - String message() default "{shared.error.validation.repetitionMismatch}"; String originalField();