La spécification JSR 303 permet de valider les objets POJO grâce à des annotations ou un fichier de configuration XML. La validation JSR 303 Bean est donc un outil très flexible grâce auquel on peut vérifier la conformité des données.
JSR 303 Bean Validation est placée dans le package javax.validation. Il contient 5 autres sous-packages :
- javax.validation.bootstrap
- javax.validation.constraints
- javax.validation.groups
- javax.validation.metadata
- javax.validation.spi
Dans cet article on se concentrera sur les contraintes (javax.validation.constraints). Les autres parties seront rapidement parcourues.
Boostrap
Ce sous-package configure le fournisseur de validation. Il contient deux interfaces qui permettent d'initialiser Bean Validation. L'exemple de l'implémenetation d'une de ces interfaces est org.hibernate.validator.HibernateValidator de l'ORM Hibernate.
On constate que ce validateur permet de configurer plusieurs aspects de son fonctionnement avant l'exécution de la procédure de validation :
- MessageInterpolator : interprétateur lancé quand la validation pour un attribut échoue. Il a pour but de gérer les messages d'erreur retournés.
- TraversableResolver : détermine si une propriété peut être accédée par le fournisseur Bean Validation. Cette fonctionnalité peut être utile lors de la validation des éléments associés avec Java Persistence ou aussi bien pour des éléments chargés avec lazy loading.
- ConstraintValidatorFactory : initialise ConstraintValidator.
Constraints
Cette partie est essentielle car elle regroupe les contraintes de validation disponibles dans la spécification. Voici la liste des contraintes possibles : - AssertFalse / AssertTrue : l'élément doit renvoyer false (AssertFalse) ou true (AsserTrue). Dans le cas contraire, la validation échoue. - DecimalMax / DecimalMin : la valeur maximale et minimale de la décimale. - Digits : l'élément doit être numérique (les types acceptés : BigDecimal BigInteger String byte, short, int, long). Si l'élément est null, il est considéré comme valide. - Future / Past : la valeur doit être soit dans le futur, soit dans le passé. - Max / Min : la longueur maximale et minimale d'un élément. - NotNull / Null : l'élément doit être instance d'une classe (NotNull) ou null (Null). - Pattern : l'élément doit correspondre à l'expression régulière comprise dans cette annotation. - Size : la valeur de la largueur d'élément doit être comprise entre attributs minimum - maximum exprimés avec min et max. Cette contrainte s'applique à des types suivants : String, Collection, Map, Array (la taille est alors évaluée). Les éléments null sont considérés comme valides.
On peut également aller plus loin et concevoir une contrainte personnalisé. Elle doit hériter de la classe ConstraintValidator. La validation s'effectue dans sa méthode isValid() qui renvoie false en cas d'échec et true en cas d'une validation correcte. Voici l'exemple d'une contrainte personnalisée :
// First, we define an interface for annotation import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Constraint(validatedBy=IsEqualValidator.class) @Documented public @interface IsEqual { String message() default "Check if is equal"; Class>[] groups() default {}; Class extends Payload>[] payload() default {}; String value(); }
// Now we can define the validator class import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class IsEqualValidator implements ConstraintValidator<IsEqual, String> { private String toCompare; public void initialize(IsEqual constraintAnnotation) { toCompare = constraintAnnotation.value(); } public boolean isValid(String text,ConstraintValidatorContext context) { return text.equals(toCompare); } } // Exemple of utilisation : @IsEqual(value = "test")
Et l'affichage du résultat sur l'écran :
Violation found ConstraintViolationImpl{interpolatedMessage='Check if is equal', propertyPath=name, rootBeanClass=class Pupil, messageTemplate='Check if is equal'}
Groups
Cette partie permet de grouper les contraintes de validation. Elle peut s'avérer très utile lors de validation des attributs dans le cas d'héritage d'une classe.
Pour voir à quoi cela peut correspondre, imaginons la situation où l'on valide une entité représentant un écolier à de différents stades de vie :
public class Pupil { protected String name; } class PupilPrimarySchool extends Pupil { @Min(1) private int note; } class PupilHighSchool extends Pupil { @Min(1, groups = CheckHighSchool.class) private int primaryNote; @Min(1) private int highSchoolNote; }
Pour déclencher la validation uniquement pour le groupe CheckHighSchool, il suffit alors d'appeler le validateur de cette manière :
validator.validate(pupilHigh, CheckHighSchool.class );
Metadata
Grâce à ce sous-package il est possible de lire les contraintes qui sont attachées à une entité. Pour le constater regardons le code ci-dessous :
import java.util.Set; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import javax.validation.metadata.ConstraintDescriptor; import javax.validation.metadata.PropertyDescriptor; import javax.validation.metadata.BeanDescriptor; import javax.validation.constraints.NotNull; import javax.validation.constraints.Min; import javax.validation.constraints.Max; public class ValidationConstraints { public static void main(String[] args) { Set<ConstraintDescriptor<?>> constraints = null; // get default validation factory ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); // get constraints properties descriptor BeanDescriptor desc = validator.getConstraintsForClass(Person.class); String[] props = new String[]{"name", "gender"}; PropertyDescriptor pd = null; for (String pdName : props) { pd = desc.getConstraintsForProperty(pdName); constraints = pd.getConstraintDescriptors(); System.out.println("Constraints size for property "+pdName+" = " + constraints.size()); for (ConstraintDescriptor> constraint : constraints) { System.out.println("Found new annotation's constraint : " + constraint.getAnnotation().toString()); } } } } class Person { @NotNull @Min(value = 2, message = "Min length is 2") private String name; @NotNull @Max(value = 3, message = "Max length is 3") private String gender; }
On pourra voir le résultat d'output sur l'écran :
Constraints size for property name = 2 Found new annotation's constraint : @javax.validation.constraints.NotNull(message={javax.validation.constraints.NotNull.message}, payload=[], groups=[]) Found new annotation's constraint : @javax.validation.constraints.Min(message=Min length is 2, payload=[], groups=[], value=2) Constraints size for property gender = 2 Found new annotation's constraint : @javax.validation.constraints.NotNull(message={javax.validation.constraints.NotNull.message}, payload=[], groups=[]) Found new annotation's constraint : @javax.validation.constraints.Max(message=Max length is 3, payload=[], groups=[], value=3)
Pour éxecuter cet exemple on aura besoin des fichiers joints (Hibernate Validator 4.0.3 et Validation API 1.0.0).
Spi
Les implémentation des interfaces de cette partie permettent de manipuler la configuration du validateur. A la base, le fichier de configuration devrait être placé dans le répertoire META-INF/validation.xml.