Hands-On Guide: Crafting custom Regex Validator with Java annotations

Annotations, in plain English, refer to the process of appending notes or metadata to pre-existing text. In the realm of programming, it entails adding metadata to existing code. For instance, Java features a Deprecated annotation that signals that a specific method should no longer be utilized. If you're unfamiliar with the concept of annotations, I strongly suggest exploring the following resource: Annotations in Java.

Let's start by creating a validator annotation:

public @interface Validator {
    String regex();
    String message() default "Validation failed";
}

Syntax:

public @interface <AnnotationName> {
    Type field() default <defaultValue>;
}

The @interface keyword is used to declare the annotation type.

In the syntax, Type field() default <defaultValue>; indicates the declaration of a field within the annotation. Here you specify the data type followed by the variable name. If a default value <defaultValue> is not specified for a particular field, it becomes mandatory to add that while calling the annotation meaning the above can not only be used as @Validator(regex="", message="new message") but also as @Validator(regex="")

In addition to that, we have to specify where and when to use the annotation. It can be specified as:

@Target(ElementType.FIELD) 
@Retention(RetentionPolicy.RUNTIME) 
public @interface Validator { 
    String regex(); 
    String message() default "Validation failed"; 
}

@Target(ElementType) is used to specify the 'where' part of it, meaning it will indicate where this particular annotation can be used. Since we have specified FIELD, it can only be used above fields in classes.

@Retention(RetentionPolicy) is used to specify the 'when' part of it, as it describes when the annotation will come into action. Here, we have specified it as RUNTIME because this is a validation annotation and will operate at runtime.

We can now put it to use with a User class as it requires multiple validations.

public class User { 
    @RegexValidator(regex = "[A-Za-z ]", message = "Invalid name") 
    private String name;

    @RegexValidator(regex = "^\\d{10}$", message = "Invalid Phone Number") 
    private String phoneNumber; 

    public User(String name, String phoneNumber) { 
        this.name = name;
        this.phoneNumber = phoneNumber; 
    } 
}

However, the above code does nothing because there is no processor for this annotation. Below is the processor code:

public class RegexValidatorProcessor { 
    public static void validate(Object object) throws IllegalAccessException { 
        for (Field field : object.getClass().getDeclaredFields()) { 
            if (field.isAnnotationPresent(Validator.class)) { 
                field.setAccessible(true); 
                Object value = field.get(object);

                Validator annotation = field.getAnnotation(Validator.class); 
                String regex = annotation.regex(); 
                String message = annotation.message();
                if (value != null && !Pattern.matches(regex, value.toString())) { 
                    throw new IllegalArgumentException(message); 
                } 
            } 
        } 
    } 
}

Let me break it down for you:

for (Field field : object.getClass().getDeclaredFields()) { 
    if (field.isAnnotationPresent(Validator.class)) {} 
}

The above two lines of code will loop through the object and get all the fields to check for the presence of the Validator annotation.

field.setAccessible(true); 
Object value = field.get(object);

In the above code, first line will make sure that even private and protected fields are accessible, and the next line will get the value of the field from the object.

Validator annotation = field.getAnnotation(Validator.class);     
String regex = annotation.regex();                  
String message = annotation.message();                 
if (value != null && !Pattern.matches(regex, value.toString())) {                      
    throw new IllegalArgumentException(message);                  
}

These above lines will get the values of the annotation class fields(regex, message) and will check for the pattern match with the extracted field and the regex in the annotation. In case there is no match, an exception is thrown with the specified message.
Let's put the processor and user to use in our main method:

public class Main { 
    public static void main(String[] args) { 
        // Create a user with an invalid phone number 
        User user = new User("abcd", "1234"); 
        try { 
            // Validate the user object using the ValidatorProcessor 
            ValidatorProcessor.validate(user); 
        }
        catch (Exception e) { 
            e.printStackTrace();  
        } 
    } 
}

In the above code, we are creating a User class with an invalid phone number and using the processor to validate the object.

You can try and create your own custom annotations for other purposes as well like authentication etc as a post article activity

Cheers