From a7bfd1a4cc3b418bfc3c63238e2adc897f420986 Mon Sep 17 00:00:00 2001 From: agitrubard Date: Thu, 17 Jul 2025 07:24:29 +0300 Subject: [PATCH 1/6] AYS-618 | `OnlyPositiveNumberValidator` Logic Has Been Fixed for Long Numbers --- .../util/validation/OnlyPositiveNumberValidator.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/ays/common/util/validation/OnlyPositiveNumberValidator.java b/src/main/java/org/ays/common/util/validation/OnlyPositiveNumberValidator.java index 2d18d35a5..3c3ef71e2 100644 --- a/src/main/java/org/ays/common/util/validation/OnlyPositiveNumberValidator.java +++ b/src/main/java/org/ays/common/util/validation/OnlyPositiveNumberValidator.java @@ -28,7 +28,15 @@ public boolean isValid(String number, ConstraintValidatorContext context) { return true; } - return NumberUtils.isDigits(number) && Long.parseLong(number) > 0; + if (!NumberUtils.isDigits(number)) { + return false; + } + + if (number.length() <= 19) { + return Long.parseLong(number) > 0; + } + + return !number.startsWith("-"); } } From eb8cdd95d9afeebbc3b9fe3130d1febd5ccf3161 Mon Sep 17 00:00:00 2001 From: agitrubard Date: Thu, 17 Jul 2025 08:57:21 +0300 Subject: [PATCH 2/6] AYS-618 | `@OnlyInteger` Annotation Has Been Created for String Validation --- .../common/util/validation/OnlyInteger.java | 105 ++++++++++++++ .../util/validation/OnlyIntegerValidator.java | 128 ++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 src/main/java/org/ays/common/util/validation/OnlyInteger.java create mode 100644 src/main/java/org/ays/common/util/validation/OnlyIntegerValidator.java diff --git a/src/main/java/org/ays/common/util/validation/OnlyInteger.java b/src/main/java/org/ays/common/util/validation/OnlyInteger.java new file mode 100644 index 000000000..9d550f02c --- /dev/null +++ b/src/main/java/org/ays/common/util/validation/OnlyInteger.java @@ -0,0 +1,105 @@ +package org.ays.common.util.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to validate that a numeric field is an integer and matches a specific sign constraint + * (e.g., positive, negative). + *

+ * This constraint is validated using the {@link OnlyIntegerValidator} class. + * It can be applied to fields of numeric-compatible types such as {@link String}, {@link Integer}, or {@link Long}. + * The value must be a valid integer string and conform to the defined {@link Sign} rule. + *

+ * + *

Example usage:

+ *
+ * {@code
+ *   @OnlyInteger(sign = OnlyInteger.Sign.POSITIVE)
+ *   private String age;
+ * }
+ * 
+ */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = OnlyIntegerValidator.class) +public @interface OnlyInteger { + + /** + * Defines the custom error message to be returned when validation fails. + * + * @return the validation error message + */ + String message() default "must be integer"; + + /** + * Specifies the expected sign of the validated number. + * + * + * @return the expected sign constraint + */ + Sign sign() default Sign.ANY; + + /** + * Enumeration representing supported sign constraints for integer validation. + */ + enum Sign { + + /** + * Indicates that the number must be a positive integer (> 0). + */ + POSITIVE, + + /** + * Indicates that the number must be a negative integer (< 0). + */ + NEGATIVE, + + /** + * Indicates that the number can be of any sign (positive, negative, or zero). + */ + ANY; + + /** + * Checks if this sign type requires the value to be positive. + * + * @return true if the sign is {@code POSITIVE}, false otherwise + */ + public boolean isPositive() { + return this == POSITIVE; + } + + /** + * Checks if this sign type requires the value to be negative. + * + * @return true if the sign is {@code NEGATIVE}, false otherwise + */ + public boolean isNegative() { + return this == NEGATIVE; + } + } + + /** + * Defines the validation groups that this constraint belongs to. + * + * @return the validation groups + */ + Class[] groups() default {}; + + /** + * Allows clients of the Bean Validation API to assign custom payload objects to this constraint. + * + * @return the custom payload objects + */ + Class[] payload() default {}; + +} diff --git a/src/main/java/org/ays/common/util/validation/OnlyIntegerValidator.java b/src/main/java/org/ays/common/util/validation/OnlyIntegerValidator.java new file mode 100644 index 000000000..2dcf63adb --- /dev/null +++ b/src/main/java/org/ays/common/util/validation/OnlyIntegerValidator.java @@ -0,0 +1,128 @@ +package org.ays.common.util.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; + +/** + * A custom validator implementation for the {@link OnlyInteger} annotation. + *

+ * Validates whether a given {@link String} value conforms to the expected sign constraint + * ({@code POSITIVE}, {@code NEGATIVE}, or {@code ANY}) and represents a valid integer value. + *

+ * + *

Validation behavior:

+ * + */ +class OnlyIntegerValidator implements ConstraintValidator { + + private OnlyInteger.Sign sign; + + /** + * Initializes the validator with the configured sign constraint from the {@link OnlyInteger} annotation. + * + * @param constraintAnnotation the annotation instance containing the configuration + */ + @Override + public void initialize(OnlyInteger constraintAnnotation) { + this.sign = constraintAnnotation.sign(); + } + + /** + * Validates whether the given string is a valid integer and conforms to the specified sign constraint. + * + * @param number the value to validate + * @param context the constraint validation context + * @return {@code true} if the number is valid or blank; {@code false} otherwise + */ + @Override + public boolean isValid(String number, ConstraintValidatorContext context) { + + if (StringUtils.isEmpty(number)) { + return true; + } + + if (this.isValidInteger(number)) { + return false; + } + + return this.isPositiveOrNegativeInteger(number, context); + } + + /** + * Checks whether the given string can be parsed as an integer. + * + * @param number the input string + * @return {@code true} if not an integer (i.e., invalid); {@code false} if it is parsable + */ + private boolean isValidInteger(String number) { + + if (!NumberUtils.isParsable(number)) { + return true; + } + + return number.contains("."); + } + + /** + * Validates the integer's sign according to the specified {@link org.ays.common.util.validation.OnlyInteger.Sign}. + * + * @param number the string representing an integer + * @param context the validation context + * @return {@code true} if the sign is valid; {@code false} otherwise + */ + private boolean isPositiveOrNegativeInteger(String number, ConstraintValidatorContext context) { + + if (sign.isPositive()) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("must be positive integer") + .addConstraintViolation(); + return this.isPositive(number); + } + + if (sign.isNegative()) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("must be negative integer") + .addConstraintViolation(); + return this.isNegative(number); + } + + return true; + } + + /** + * Checks whether the given string represents a positive integer. + * + * @param number the input string + * @return {@code true} if the number is greater than zero + */ + private boolean isPositive(String number) { + + if (!NumberUtils.isDigits(number)) { + return false; + } + + if (number.length() > Long.toString(Long.MAX_VALUE).length()) { + return true; + } + + return Long.parseLong(number) > 0; + } + + /** + * Checks whether the given string represents a negative integer. + * + * @param number the input string + * @return {@code true} if the number is lower than zero + */ + private boolean isNegative(String number) { + return !number.startsWith("-") && Long.parseLong(number) < 0; + } + +} From 703f69679f9f33323abda84cf2d650caaa5c41e3 Mon Sep 17 00:00:00 2001 From: agitrubard Date: Thu, 17 Jul 2025 08:58:31 +0300 Subject: [PATCH 3/6] AYS-618 | `@OnlyNumber` Annotation Has Been Used for `AysUserFilter.lineNumber` Field and Tests Have Been Fixed --- .../org/ays/auth/model/AysUserFilter.java | 4 +-- .../controller/AysUserControllerTest.java | 29 +++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/ays/auth/model/AysUserFilter.java b/src/main/java/org/ays/auth/model/AysUserFilter.java index dbaa9bb93..fea910fd3 100644 --- a/src/main/java/org/ays/auth/model/AysUserFilter.java +++ b/src/main/java/org/ays/auth/model/AysUserFilter.java @@ -10,7 +10,7 @@ import org.ays.auth.model.enums.AysUserStatus; import org.ays.common.model.AysFilter; import org.ays.common.util.validation.Name; -import org.ays.common.util.validation.OnlyPositiveNumber; +import org.ays.common.util.validation.OnlyInteger; import org.springframework.data.jpa.domain.Specification; import org.springframework.util.StringUtils; @@ -66,7 +66,7 @@ public class AysUserFilter implements AysFilter { @Setter public static class PhoneNumber { - @OnlyPositiveNumber + @OnlyInteger(sign = OnlyInteger.Sign.POSITIVE) @Size(min = 1, max = 10) private String lineNumber; diff --git a/src/test/java/org/ays/auth/controller/AysUserControllerTest.java b/src/test/java/org/ays/auth/controller/AysUserControllerTest.java index 8f37c6598..c4741bcc9 100644 --- a/src/test/java/org/ays/auth/controller/AysUserControllerTest.java +++ b/src/test/java/org/ays/auth/controller/AysUserControllerTest.java @@ -182,20 +182,23 @@ void givenUserListRequest_whenLastNameDoesNotValid_thenReturnValidationError(Str .findAll(Mockito.any(AysUserListRequest.class)); } - @ValueSource(strings = { - "", - "12345678912", - "12345678912367", - "-321", - "321.", - "12.34", - "1 234", - "-45", - "12a34", - " " + @CsvSource({ + "must be integer, 321.", + "must be integer, 12.34", + "must be integer, '12,34'", + "must be integer, 1 234", + "must be integer, 12a34", + "must be integer, #", + "must be integer, ' '", + "must be positive integer, -321", + "must be positive integer, 0", + "size must be between 1 and 10, ''", + "size must be between 1 and 10, 12345678912367", + "size must be between 1 and 10, 12345678912367564656464858", }) @ParameterizedTest - void givenUserListRequest_whenLineNumberIsNotValid_thenReturnValidationError(String mockLineNumber) throws Exception { + void givenUserListRequest_whenLineNumberIsNotValid_thenReturnValidationError(String mockSubErrorMessage, + String mockLineNumber) throws Exception { // Given AysUserFilter.PhoneNumber mockPhoneNumber = new AysUserFilter.PhoneNumber(); @@ -220,6 +223,8 @@ void givenUserListRequest_whenLineNumberIsNotValid_thenReturnValidationError(Str .isArray()) .andExpect(AysMockResultMatchersBuilders.subErrorsSize() .value(1)) + .andExpect(AysMockResultMatchersBuilders.subErrors("[*].message") + .value(mockSubErrorMessage)) .andExpect(AysMockResultMatchersBuilders.subErrors("[*].field") .value("lineNumber")); From 8eee725bf7232f894898b22ead1a391f1a80e299 Mon Sep 17 00:00:00 2001 From: agitrubard Date: Thu, 17 Jul 2025 09:00:58 +0300 Subject: [PATCH 4/6] AYS-618 | `@OnlyNumber` Annotation Has Been Used for `EmergencyEvacuationApplicationFilter.referenceNumber` Field --- .../model/filter/EmergencyEvacuationApplicationFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/ays/emergency_application/model/filter/EmergencyEvacuationApplicationFilter.java b/src/main/java/org/ays/emergency_application/model/filter/EmergencyEvacuationApplicationFilter.java index c2b4495c0..aa8326601 100644 --- a/src/main/java/org/ays/emergency_application/model/filter/EmergencyEvacuationApplicationFilter.java +++ b/src/main/java/org/ays/emergency_application/model/filter/EmergencyEvacuationApplicationFilter.java @@ -7,6 +7,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.ays.common.model.AysFilter; import org.ays.common.util.validation.NoSpecialCharacters; +import org.ays.common.util.validation.OnlyInteger; import org.ays.emergency_application.model.entity.EmergencyEvacuationApplicationEntity; import org.ays.emergency_application.model.enums.EmergencyEvacuationApplicationStatus; import org.hibernate.validator.constraints.Range; @@ -19,6 +20,7 @@ @Builder public class EmergencyEvacuationApplicationFilter implements AysFilter { + @OnlyInteger(sign = OnlyInteger.Sign.POSITIVE) @Size(min = 1, max = 10) private String referenceNumber; @@ -51,8 +53,6 @@ public class EmergencyEvacuationApplicationFilter implements AysFilter { /** * Converts this request's filter configuration into a {@link Specification} for querying. * - * @param clazz The class type to which the specification will be applied. - * @param The type of the class. * @return A specification built based on the current filter configuration. */ @Override From c940faf1c8d84898d5fb04e3401d9ba35cc2da65 Mon Sep 17 00:00:00 2001 From: agitrubard Date: Thu, 17 Jul 2025 09:01:46 +0300 Subject: [PATCH 5/6] AYS-618 | `EmergencyEvacuationApplicationFilter.java.referenceNumber` Field Validation Tests Have Been Improved --- ...cyEvacuationApplicationControllerTest.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/test/java/org/ays/emergency_application/controller/EmergencyEvacuationApplicationControllerTest.java b/src/test/java/org/ays/emergency_application/controller/EmergencyEvacuationApplicationControllerTest.java index 270c2ef0b..9314e6f9b 100644 --- a/src/test/java/org/ays/emergency_application/controller/EmergencyEvacuationApplicationControllerTest.java +++ b/src/test/java/org/ays/emergency_application/controller/EmergencyEvacuationApplicationControllerTest.java @@ -95,17 +95,28 @@ void givenValidEmergencyEvacuationApplicationListRequest_whenEmergencyEvacuation .findAll(Mockito.any(EmergencyEvacuationApplicationListRequest.class)); } - @ParameterizedTest - @ValueSource(strings = { - "", - "151201485621548562154851458614125461254125412" + @CsvSource({ + "must be integer, 321.", + "must be integer, 12.34", + "must be integer, '12,34'", + "must be integer, 1 234", + "must be integer, 12a34", + "must be integer, #", + "must be integer, ' '", + "must be positive integer, -321", + "must be positive integer, 0", + "size must be between 1 and 10, ''", + "size must be between 1 and 10, 12345678912367", + "size must be between 1 and 10, 12345678912367564656464858", }) - void givenInvalidEmergencyEvacuationApplicationListRequest_whenReferenceNumberNotValid_thenReturnValidationError(String referenceNumber) throws Exception { + @ParameterizedTest + void givenInvalidEmergencyEvacuationApplicationListRequest_whenReferenceNumberNotValid_thenReturnValidationError(String mockSubErrorMessage, + String mockReferenceNumber) throws Exception { // Given EmergencyEvacuationApplicationListRequest mockListRequest = new EmergencyEvacuationApplicationListRequestBuilder() .withValidValues() - .withReferenceNumber(referenceNumber) + .withReferenceNumber(mockReferenceNumber) .build(); // Then @@ -119,7 +130,13 @@ void givenInvalidEmergencyEvacuationApplicationListRequest_whenReferenceNumberNo .andExpect(AysMockResultMatchersBuilders.status() .isBadRequest()) .andExpect(AysMockResultMatchersBuilders.subErrors() - .isNotEmpty()); + .isArray()) + .andExpect(AysMockResultMatchersBuilders.subErrorsSize() + .value(1)) + .andExpect(AysMockResultMatchersBuilders.subErrors("[*].message") + .value(mockSubErrorMessage)) + .andExpect(AysMockResultMatchersBuilders.subErrors("[*].field") + .value("referenceNumber")); // Verify Mockito.verify(emergencyEvacuationApplicationService, Mockito.never()) From 6553b693d8e4807ffd577332b6ece01053bc3394 Mon Sep 17 00:00:00 2001 From: agitrubard Date: Thu, 17 Jul 2025 09:05:29 +0300 Subject: [PATCH 6/6] AYS-618 | Annotation Has Been Deleted Because It's Unused --- .../util/validation/OnlyPositiveNumber.java | 45 ------------------- .../OnlyPositiveNumberValidator.java | 42 ----------------- 2 files changed, 87 deletions(-) delete mode 100644 src/main/java/org/ays/common/util/validation/OnlyPositiveNumber.java delete mode 100644 src/main/java/org/ays/common/util/validation/OnlyPositiveNumberValidator.java diff --git a/src/main/java/org/ays/common/util/validation/OnlyPositiveNumber.java b/src/main/java/org/ays/common/util/validation/OnlyPositiveNumber.java deleted file mode 100644 index 2cd2dd28a..000000000 --- a/src/main/java/org/ays/common/util/validation/OnlyPositiveNumber.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.ays.common.util.validation; - -import jakarta.validation.Constraint; -import jakarta.validation.Payload; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation to validate that a numeric field contains only positive values. - *

- * This constraint is validated using the {@link OnlyPositiveNumberValidator} class. - * It can be applied to fields of numeric types (e.g., {@link Integer}, {@link Long}) to enforce positivity. - *

- */ -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = OnlyPositiveNumberValidator.class) -public @interface OnlyPositiveNumber { - - - /** - * Returns the error message when the number is not positive. - * - * @return the validation error message - */ - String message() default "must be positive number"; - - /** - * Returns the validation groups this constraint belongs to. - * - * @return the validation groups - */ - Class[] groups() default {}; - - /** - * Returns the custom payload objects associated with this constraint. - * - * @return the payload - */ - Class[] payload() default {}; - -} diff --git a/src/main/java/org/ays/common/util/validation/OnlyPositiveNumberValidator.java b/src/main/java/org/ays/common/util/validation/OnlyPositiveNumberValidator.java deleted file mode 100644 index 3c3ef71e2..000000000 --- a/src/main/java/org/ays/common/util/validation/OnlyPositiveNumberValidator.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.ays.common.util.validation; - -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; - -/** - * A custom validator implementation for the {@link OnlyPositiveNumber} annotation. - *

- * Validates whether the given {@link String} represents a positive numeric value. - * The input is considered valid if it is non-blank, contains only digits, and represents a number greater than zero. - *

- */ -class OnlyPositiveNumberValidator implements ConstraintValidator { - - /** - * Validates whether the provided {@code String} value is a positive number. - * - * @param number the string value to validate - * @param context the context in which the constraint is evaluated - * @return {@code true} if the value is blank or a positive number; {@code false} otherwise - */ - @Override - public boolean isValid(String number, ConstraintValidatorContext context) { - - if (StringUtils.isEmpty(number)) { - return true; - } - - if (!NumberUtils.isDigits(number)) { - return false; - } - - if (number.length() <= 19) { - return Long.parseLong(number) > 0; - } - - return !number.startsWith("-"); - } - -}