Bean Validation API defines several built-in constraint annotations which are very useful in many situations. However, there are still some cases where these standard constraints are not enough and you have to create your own custom constraint. With Bean Validation this task is pretty easy and straightforward.
In the article Validating HTML forms in Spring using Bean Validation we have built a simple Spring MVC application using built-in constraints only. This time we will introduce a new text form field for entering favourite day of a week (in place of the age field) and add a custom constraint to this field. The new field will be checked if it a part of workweek or weekend depending on the attributes set in the constraint. Additionally, the constraint will allow specifying whether the comparison is case-sensitive or not.
Defining custom annotation
The first step is creating a custom annotation @DayOfWeek which represents a custom constraints:
package com.example.beanvalidationcustomconstraint; import java.lang.annotation.Documented; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import java.lang.annotation.Retention; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Documented @Constraint(validatedBy = DayOfWeekValidator.class) @Target({ METHOD, FIELD, ANNOTATION_TYPE }) @Retention(RUNTIME) public @interface DayOfWeek { String message() default "{com.example.beanvalidationcustomconstraint.DayOfWeek.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; DayOfWeekType[] value() default { }; boolean ignoreCase() default false; }
The annotation contains three mandatory attributes: message, groups and payload. The first attribute specifies a message to show or a reference to it if the validation fails. In this case the message attribute references the actual message stored in ValidationMessages.properties file or one of it internationalized versions. Attribute groups allows the definition of validation groups but we won’t use any in this example. The last one payload specifies extra data to be used by the clients of this constraint – we also do not use any in this example.
The are also two other attributes which are more interesting from our point of view and are used to provide additional settings for the custom constraint. The value is a default attribute (used when no other attribute name is specified when using the annotation) and in our case it holds an array of allowed day types:
package com.example.beanvalidationcustomconstraint; public enum DayOfWeekType { WORKWEEK, WEEKEND }
The ignoreCase attribute specifies whether the constraint should use case-sensitive or case-insensitive string comparisons. If these attributes are not specified, they default to an empty array and false, respectively.
We also annotate the newly created annotation with @Documented to enable showing it in JavaDoc for elements annotated with it, @Constraint to indicate that this is a Bean Validation constraint annotation and to specify the custom validator associated with it, @Target to inform that the annotation can be attached to methods, fields and other annotations and @Retention to specify that we want the annotation to be available at runtime via reflection.
Defining the validator
The created annotation does not contain logic which performs actual validation but instead it refers to the class DayOfWeekValidator using @Constraint annotation. The actual validator looks like this:
package com.example.beanvalidationcustomconstraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class DayOfWeekValidator implements ConstraintValidator<DayOfWeek, String> { private DayOfWeekType[] allowedTypes; private boolean ignoreCase; @Override public void initialize(DayOfWeek constraint) { allowedTypes = constraint.value(); ignoreCase = constraint.ignoreCase(); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (value == null) return true; for (DayOfWeekType type : allowedTypes) { switch (type) { case WORKWEEK: if (isWorkWeek(value)) return true; break; case WEEKEND: if (isWeekEnd(value)) return true; } } return false; } private boolean isWorkWeek(String value) { return equalsDay(value, "Monday") || equalsDay(value, "Tuesday") || equalsDay(value, "Wednesday") || equalsDay(value, "Thursday") || equalsDay(value, "Friday"); } private boolean isWeekEnd(String value) { return equalsDay(value, "Saturday") || equalsDay(value, "Sunday"); } private boolean equalsDay(String value1, String value2) { return ignoreCase ? value1.equalsIgnoreCase(value2) : value1.equals(value2); } }
The custom validator implements generic ConstraintValidator interface with two type parameters: the type of the custom constraint annotation and the type of the element which can be validated using this validator. Then we implement initialize() method which fetches the attributes/settings of the custom constraint and isValid() method which performs the actual validation and returns true if the validation finished successfully or false otherwise.
Using the constraint
Once we have the annotation and the validator ready, we can use it in the same way as any other built-in constraint:
package com.example.beanvalidationcustomconstraint; import java.io.Serializable; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class Person implements Serializable { private static final long serialVersionUID = 3297423984732894L; @Size(min = 1, max = 20, message = "{firstNameInvalid}") private String firstName; @Size(min = 1, max = 40, message = "{lastNameInvalid}") private String lastName; @NotNull @DayOfWeek(value = DayOfWeekType.WEEKEND, ignoreCase = true) private String favouriteDayOfWeek; // constructor, setters and getters }
In this case we allow only Saturday and Sunday (ignoring the letter case) as the value of favouriteDayOfWeek field. Because we use Spring MVC, the validation will take place when the user tries to submit the form.
Defining custom constraints using composition
Sometimes we don’t even need to define a validator for the custom constraint. It is possible if we can represent our custom constraint as a conjunction of already existing constraints. In this example we specify a constraint which is met only if @NotNull, @Min and @Max constraints are met:
package com.example.beanvalidationcustomconstraint; import java.lang.annotation.Documented; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import java.lang.annotation.Retention; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; @NotNull @Min(0) @Max(10) @Documented @Constraint(validatedBy = {}) @Target({ METHOD, FIELD, ANNOTATION_TYPE }) @Retention(RUNTIME) public @interface Range { String message() default "Range is not valid"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
This is especially useful if the same combination of constraints are applied to many different fields or methods.
Conclusion
Bean Validation is very extensible and allows us to define virtually any custom constraint and use it in the same way as the built-in ones.
The sample code for this example was tested with JBoss and is available at GitHub.
Pingback: Validating HTML forms in Spring using Bean Validation | softwarecave
Pingback: Custom annotations in Java | softwarecave
You could certainly see your expertise in the
article you write. The sector hopes for even more passionate writers such
as you who aren’t afraid to mention how they believe. All
the time go after your heart.
Shouldn’t it be :
if (value == null)
return false; //instead of return true? If null, I am assuming it shouldn’t be valid?