Skip to content

Commit c8bd4bf

Browse files
authored
add common validators (#51)
* move classes * add common validators
1 parent e6835ac commit c8bd4bf

25 files changed

Lines changed: 881 additions & 18 deletions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package it.aboutbits.springboot.toolbox.validation.annotation;
2+
3+
import it.aboutbits.springboot.toolbox.validation.validator.RepeatedFieldValidator;
4+
import jakarta.validation.Constraint;
5+
import jakarta.validation.Payload;
6+
7+
import java.lang.annotation.Documented;
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Repeatable;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
import java.lang.annotation.Target;
13+
14+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
15+
16+
@Target(ElementType.TYPE)
17+
@Retention(RUNTIME)
18+
@Constraint(validatedBy = RepeatedFieldValidator.class)
19+
@Documented
20+
@Repeatable(RepeatedField.List.class)
21+
public @interface RepeatedField {
22+
String message() default "{shared.error.validation.repetitionMismatch}";
23+
24+
String originalField();
25+
26+
String repeatedField();
27+
28+
Class<?>[] groups() default {};
29+
30+
Class<? extends Payload>[] payload() default {};
31+
32+
@Target({ElementType.TYPE})
33+
@Retention(RetentionPolicy.RUNTIME)
34+
@Documented
35+
@interface List {
36+
RepeatedField[] value();
37+
}
38+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package it.aboutbits.springboot.toolbox.validation.annotation;
2+
3+
import it.aboutbits.springboot.toolbox.validation.validator.ValidDateRangeValidator;
4+
import jakarta.validation.Constraint;
5+
import jakarta.validation.Payload;
6+
7+
import java.lang.annotation.Documented;
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Repeatable;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
import java.lang.annotation.Target;
13+
14+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
15+
16+
/**
17+
* Validator bean to check if the {@code to} date field is after the {@code from} date field.<br>
18+
* If one of the two fields is {@code null} this validation bean will still return true, as in a form, for example,
19+
* the {@code from} field could be specified.
20+
* <p>
21+
* Supports {@link java.time.LocalDateTime}, {@link java.time.LocalDate}, {@link java.time.OffsetDateTime},
22+
* {@link java.time.ZonedDateTime}, and {@link java.time.Instant}.
23+
*/
24+
@Documented
25+
@Constraint(validatedBy = ValidDateRangeValidator.class)
26+
@Target(ElementType.TYPE)
27+
@Retention(RUNTIME)
28+
@Repeatable(ValidDateRange.List.class)
29+
public @interface ValidDateRange {
30+
String message() default "{shared.error.validation.invalidDateRange}";
31+
32+
boolean allowEmptyRange() default false;
33+
34+
String fromDateField();
35+
36+
String toDateField();
37+
38+
Class<?>[] groups() default {};
39+
40+
Class<? extends Payload>[] payload() default {};
41+
42+
@Target({ElementType.TYPE})
43+
@Retention(RetentionPolicy.RUNTIME)
44+
@Documented
45+
@interface List {
46+
ValidDateRange[] value();
47+
}
48+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package it.aboutbits.springboot.toolbox.validation.annotation;
2+
3+
import it.aboutbits.springboot.toolbox.validation.validator.ValidNumericRangeValidator;
4+
import jakarta.validation.Constraint;
5+
import jakarta.validation.Payload;
6+
7+
import java.lang.annotation.Documented;
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Repeatable;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
import java.lang.annotation.Target;
13+
14+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
15+
16+
/**
17+
* Validator bean to check if the lower bound numeric field is not higher than the upper bound numeric field.<br>
18+
* If one of the two fields is {@code null} this validation bean will still return true, as in a form, for example,
19+
* only one field could be specified.
20+
* <p>
21+
* Supports numeric types like {@link Integer}, {@link Long}, {@link Float}, {@link Double}, etc.
22+
*/
23+
@Documented
24+
@Constraint(validatedBy = ValidNumericRangeValidator.class)
25+
@Target(ElementType.TYPE)
26+
@Retention(RUNTIME)
27+
@Repeatable(ValidNumericRange.List.class)
28+
public @interface ValidNumericRange {
29+
String message() default "{shared.error.validation.invalidNumericRange}";
30+
31+
boolean allowEqualValues() default true;
32+
33+
String lowerBoundField();
34+
35+
String upperBoundField();
36+
37+
Class<?>[] groups() default {};
38+
39+
Class<? extends Payload>[] payload() default {};
40+
41+
@Target({ElementType.TYPE})
42+
@Retention(RetentionPolicy.RUNTIME)
43+
@Documented
44+
@interface List {
45+
ValidNumericRange[] value();
46+
}
47+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package it.aboutbits.springboot.toolbox.validation.annotation;
2+
3+
import it.aboutbits.springboot.toolbox.validation.validator.ValidPasswordValidator;
4+
import jakarta.validation.Constraint;
5+
import jakarta.validation.Payload;
6+
7+
import java.lang.annotation.Documented;
8+
import java.lang.annotation.Retention;
9+
import java.lang.annotation.Target;
10+
11+
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
12+
import static java.lang.annotation.ElementType.FIELD;
13+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
14+
15+
@Documented
16+
@Constraint(validatedBy = ValidPasswordValidator.class)
17+
@Target({FIELD, ANNOTATION_TYPE})
18+
@Retention(RUNTIME)
19+
public @interface ValidPassword {
20+
String message() default "{shared.error.validation.invalidPassword}";
21+
22+
String lengthMessage() default "{shared.error.validation.invalidPassword.length}";
23+
24+
int minLength() default 8;
25+
26+
int maxLength() default 50;
27+
28+
Class<?>[] groups() default {};
29+
30+
Class<? extends Payload>[] payload() default {};
31+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package it.aboutbits.springboot.toolbox.validation.validator;
2+
3+
import it.aboutbits.springboot.toolbox.validation.annotation.RepeatedField;
4+
import jakarta.validation.ConstraintValidator;
5+
import jakarta.validation.ConstraintValidatorContext;
6+
import org.jspecify.annotations.NullMarked;
7+
import org.jspecify.annotations.Nullable;
8+
import org.springframework.beans.BeanWrapperImpl;
9+
10+
@NullMarked
11+
public class RepeatedFieldValidator implements ConstraintValidator<RepeatedField, Object> {
12+
private @Nullable String originalField;
13+
private @Nullable String repeatedField;
14+
private @Nullable String message;
15+
16+
@Override
17+
public void initialize(RepeatedField constraintAnnotation) {
18+
this.originalField = constraintAnnotation.originalField();
19+
this.repeatedField = constraintAnnotation.repeatedField();
20+
this.message = constraintAnnotation.message();
21+
}
22+
23+
@Override
24+
public boolean isValid(Object value, ConstraintValidatorContext context) {
25+
if (originalField == null || repeatedField == null) {
26+
throw new IllegalStateException("Both originalField and repeatedField must be set.");
27+
}
28+
29+
var fieldValue = new BeanWrapperImpl(value).getPropertyValue(originalField);
30+
var fieldMatchValue = new BeanWrapperImpl(value).getPropertyValue(repeatedField);
31+
32+
var isValid = fieldValue != null && fieldValue.equals(fieldMatchValue);
33+
34+
if (!isValid) {
35+
context.disableDefaultConstraintViolation();
36+
context
37+
.buildConstraintViolationWithTemplate(message)
38+
.addPropertyNode(repeatedField)
39+
.addConstraintViolation();
40+
}
41+
42+
return isValid;
43+
}
44+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package it.aboutbits.springboot.toolbox.validation.validator;
2+
3+
import it.aboutbits.springboot.toolbox.validation.annotation.ValidDateRange;
4+
import jakarta.validation.ConstraintValidator;
5+
import jakarta.validation.ConstraintValidatorContext;
6+
import lombok.RequiredArgsConstructor;
7+
import org.jspecify.annotations.NullMarked;
8+
import org.jspecify.annotations.Nullable;
9+
import org.springframework.beans.PropertyAccessorFactory;
10+
11+
import java.time.Instant;
12+
import java.time.LocalDate;
13+
import java.time.LocalDateTime;
14+
import java.time.OffsetDateTime;
15+
import java.time.ZonedDateTime;
16+
17+
@RequiredArgsConstructor
18+
@NullMarked
19+
public class ValidDateRangeValidator implements ConstraintValidator<ValidDateRange, Object> {
20+
@Nullable
21+
private String fromDateField;
22+
@Nullable
23+
private String toDateField;
24+
@Nullable
25+
private String message;
26+
private boolean allowEmptyRange = false;
27+
28+
@Override
29+
public void initialize(ValidDateRange constraintAnnotation) {
30+
this.fromDateField = constraintAnnotation.fromDateField();
31+
this.toDateField = constraintAnnotation.toDateField();
32+
this.message = constraintAnnotation.message();
33+
this.allowEmptyRange = constraintAnnotation.allowEmptyRange();
34+
}
35+
36+
public boolean isValid(Object value, ConstraintValidatorContext context) {
37+
if (fromDateField == null || toDateField == null || message == null) {
38+
throw new IllegalStateException("All fields must be set (fromDateField, toDateField, message).");
39+
}
40+
41+
var propertyAccessor = PropertyAccessorFactory.forBeanPropertyAccess(value);
42+
var fromDate = propertyAccessor.getPropertyValue(fromDateField);
43+
var toDate = propertyAccessor.getPropertyValue(toDateField);
44+
45+
if (fromDate == null || toDate == null) {
46+
return true; // Consider null dates valid; use @NotNull for null checks if needed
47+
}
48+
49+
boolean isValid = switch (fromDate) {
50+
case LocalDate from when toDate instanceof LocalDate to ->
51+
allowEmptyRange ? (to.isEqual(from) || to.isAfter(from)) : to.isAfter(from);
52+
case LocalDateTime from when toDate instanceof LocalDateTime to ->
53+
allowEmptyRange ? (to.isEqual(from) || to.isAfter(from)) : to.isAfter(from);
54+
case OffsetDateTime from when toDate instanceof OffsetDateTime to ->
55+
allowEmptyRange ? (to.isEqual(from) || to.isAfter(from)) : to.isAfter(from);
56+
case ZonedDateTime from when toDate instanceof ZonedDateTime to ->
57+
allowEmptyRange ? (to.isEqual(from) || to.isAfter(from)) : to.isAfter(from);
58+
case Instant from when toDate instanceof Instant to ->
59+
allowEmptyRange ? (to.equals(from) || to.isAfter(from)) : to.isAfter(from);
60+
default -> throw new IllegalArgumentException("Unsupported date data types!");
61+
};
62+
63+
if (!isValid) {
64+
context.disableDefaultConstraintViolation();
65+
context
66+
.buildConstraintViolationWithTemplate(message)
67+
.addPropertyNode(toDateField)
68+
.addConstraintViolation();
69+
}
70+
71+
return isValid;
72+
}
73+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package it.aboutbits.springboot.toolbox.validation.validator;
2+
3+
import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal;
4+
import it.aboutbits.springboot.toolbox.validation.annotation.ValidNumericRange;
5+
import jakarta.validation.ConstraintValidator;
6+
import jakarta.validation.ConstraintValidatorContext;
7+
import lombok.RequiredArgsConstructor;
8+
import org.jspecify.annotations.NullMarked;
9+
import org.jspecify.annotations.Nullable;
10+
import org.springframework.beans.PropertyAccessorFactory;
11+
12+
import java.math.BigDecimal;
13+
import java.math.BigInteger;
14+
15+
@RequiredArgsConstructor
16+
@NullMarked
17+
public class ValidNumericRangeValidator implements ConstraintValidator<ValidNumericRange, Object> {
18+
@Nullable
19+
private String lowerBoundField;
20+
@Nullable
21+
private String upperBoundField;
22+
@Nullable
23+
private String message;
24+
private boolean allowEqualValues = true;
25+
26+
@Override
27+
public void initialize(ValidNumericRange constraintAnnotation) {
28+
this.lowerBoundField = constraintAnnotation.lowerBoundField();
29+
this.upperBoundField = constraintAnnotation.upperBoundField();
30+
this.message = constraintAnnotation.message();
31+
this.allowEqualValues = constraintAnnotation.allowEqualValues();
32+
}
33+
34+
@Override
35+
public boolean isValid(Object value, ConstraintValidatorContext context) {
36+
if (lowerBoundField == null || upperBoundField == null || message == null) {
37+
throw new IllegalStateException("All fields must be set (lowerBoundField, upperBoundField, message).");
38+
}
39+
40+
var propertyAccessor = PropertyAccessorFactory.forBeanPropertyAccess(value);
41+
var lowerBound = propertyAccessor.getPropertyValue(lowerBoundField);
42+
var upperBound = propertyAccessor.getPropertyValue(upperBoundField);
43+
44+
if (lowerBound == null || upperBound == null) {
45+
return true; // Consider null values valid; use @NotNull for null checks if needed
46+
}
47+
48+
var isValid = false;
49+
50+
if (lowerBound instanceof ScaledBigDecimal lower && upperBound instanceof ScaledBigDecimal upper) {
51+
isValid = compareValues(lower.compareTo(upper));
52+
} else if (lowerBound instanceof ScaledBigDecimal || upperBound instanceof ScaledBigDecimal) {
53+
throw new IllegalArgumentException("Both fields must be of the same type!");
54+
} else {
55+
if (!(lowerBound instanceof Number) || !(upperBound instanceof Number)) {
56+
throw new IllegalArgumentException("Both fields must be numeric types!");
57+
}
58+
59+
isValid = compareNumbers((Number) lowerBound, (Number) upperBound);
60+
}
61+
62+
if (!isValid) {
63+
context.disableDefaultConstraintViolation();
64+
context
65+
.buildConstraintViolationWithTemplate(message)
66+
.addPropertyNode(upperBoundField)
67+
.addConstraintViolation();
68+
}
69+
70+
return isValid;
71+
}
72+
73+
private boolean compareNumbers(Number lowerBound, Number upperBound) {
74+
// Convert both numbers to BigDecimal for accurate comparison
75+
var lower = convertToBigDecimal(lowerBound);
76+
var upper = convertToBigDecimal(upperBound);
77+
78+
return compareValues(lower.compareTo(upper));
79+
}
80+
81+
private boolean compareValues(int comparisonResult) {
82+
if (allowEqualValues) {
83+
return comparisonResult <= 0; // lower <= upper
84+
} else {
85+
return comparisonResult < 0; // lower < upper
86+
}
87+
}
88+
89+
private BigDecimal convertToBigDecimal(Number number) {
90+
if (number instanceof BigDecimal bigDecimal) {
91+
return bigDecimal;
92+
} else if (number instanceof BigInteger bigInteger) {
93+
return new BigDecimal(bigInteger);
94+
} else if (number instanceof Double || number instanceof Float) {
95+
return BigDecimal.valueOf(number.doubleValue());
96+
} else {
97+
return BigDecimal.valueOf(number.longValue());
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)