Skip to content
4 changes: 2 additions & 2 deletions src/main/java/org/ays/auth/model/AysUserFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down
105 changes: 105 additions & 0 deletions src/main/java/org/ays/common/util/validation/OnlyInteger.java
Original file line number Diff line number Diff line change
@@ -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).
* <p>
* 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.
* </p>
*
* <p><strong>Example usage:</strong></p>
* <pre>
* {@code
* @OnlyInteger(sign = OnlyInteger.Sign.POSITIVE)
* private String age;
* }
* </pre>
*/
@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.
* <ul>
* <li>{@link Sign#POSITIVE} - value must be greater than 0</li>
* <li>{@link Sign#NEGATIVE} - value must be less than 0</li>
* <li>{@link Sign#ANY} - value can be positive, negative, or zero (default)</li>
* </ul>
*
* @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<? extends Payload>[] payload() default {};

}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* </p>
*
* <p><strong>Validation behavior:</strong></p>
* <ul>
* <li>If the input is {@code null} or blank, it is considered valid.</li>
* <li>The input must be an integer value (no decimal points or non-numeric characters).</li>
* <li>The sign of the number must match the configured {@link OnlyInteger.Sign} rule.</li>
* <li>Custom messages such as {@code "must be positive integer"} or {@code "must be negative integer"} are added on failure.</li>
* </ul>
*/
class OnlyIntegerValidator implements ConstraintValidator<OnlyInteger, String> {

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;
}

}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +20,7 @@
@Builder
public class EmergencyEvacuationApplicationFilter implements AysFilter {

@OnlyInteger(sign = OnlyInteger.Sign.POSITIVE)
@Size(min = 1, max = 10)
private String referenceNumber;

Expand Down Expand Up @@ -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 <C> The type of the class.
* @return A specification built based on the current filter configuration.
*/
@Override
Expand Down
Loading