Validation des Beans (JSR 303)
Table des matières
Introduction
La JSR 303 définit un modèle de méta-données et une API pour valider les Beans Java. Cette validation s’effectue en utilisant les annotations mais il est possible d’utiliser des fichiers XML. Cette JSR a été finalisée en novembre 2009 et fait partie de la spécification de JEE6 sortie un mois plus tard. Le support natif partiel a été introduit dans Lutece dans la version 3.0.8 avec la classe fr.paris.lutece.util.beanvalidation.BeanValidationUtil
. Un support plus complet est disponible dans la version 4.1 que nous allons décrire ci-dessous.
Rappel de le JSR 303
Voici les annotations des contraintes standards définies par la JSR :
Annotation | Description de la contrainte | Type sur lequel la contrainte peut s’appliquer |
---|---|---|
@Null | L’élément doit être nul | Object |
@NotNull | L’élément doit être non nul | Object |
@AssertTrue | L’élément doit être true | boolean, Boolean |
@AssertFalse | L’élément doit être false | boolean, Boolean |
@Min | L’élément doit être supérieur à la valeur spécifiée dans l’annotation | BigDecimal, BigInteger, byte, short, int, long |
@Max | L’élément doit être inférieur à la valeur spécifiée dans l’annotation | BigDecimal, BigInteger, byte, short, int, long |
@DecimalMin | L’élément doit être supérieur à la valeur spécifiée dans l’annotation | BigDecimal, BigInteger, String, byte, short, int, long |
@DecimalMax | L’élément doit être inférieur à la valeur spécifiée dans l’annotation | BigDecimal, BigInteger, String, byte, short, int, long |
@Size | L’élément doit être entre deux tailles spécifiées | String, Collection, Map, Array |
@Digits | L’élément doit être un nombre compris dans une certaine fenêtre | BigDecimal, BigInteger, String, byte, short, int, long |
@Past | L’élément doit être une date dans le passé | Date, Calendar |
@Future | L’élément doit être une date dans le futur | Date, Calendar |
@Pattern | L’élément doit respecter une expression régulière | String |
D’autres frameworks tels qu’Hibernate fournissent des contraintes intéressantes : @CreditCardNumber
, @Email
, @NotBlank
, @NotEmpty
, @Range
, @ScriptAssert
, @URL
. Ces contraintes peuvent s’appliquer à un attribut, une méthode ou à la classe. Dans la majorité des cas elles seront placées au niveau de l’attribut de la manière suivante :
// MyBean.java
@NotEmpty()
@Pattern(regexp = "[a-z-A-Z]")
@Size(max = 5)
private String _strName;
@Size(min = 10)
private String _strDescription;
@Min(value = 5)
private int _nAge;
@Email()
private String _strEmail;
@Past()
private Date _dateBirth;
@Future()
private Date _dateEndOfWorld;
@DecimalMin(value = "1500.0")
private BigDecimal _salary;
@DecimalMax(value = "100.0")
private BigDecimal _percent;
@Digits(integer = 15, fraction = 2)
private String _strCurrency;
La validation du bean à l’aide d’une instance de la classe Validator
produira un ensemble de violations de contraintes.
// Check constraints
Set<ConstraintViolation<Person>> errors = validator.validate(myBean);
if (errors.size() > 0) {
// Handle errors
...
}
L’implémentation proposée par Lutece à partir de la version 4.1
Apport de l’implémentation par rapport à l’implémentation de base
Le principal apport de l’implémentation proposée par Lutece réside dans la gestion des messages d’erreurs. En effet celle-ci propose :
- la gestion de la locale et le support du modèle de plugin (les messages d’erreurs peuvent être définis dans les fichiers de ressource internationalisés des plugins)
- la gestion de l’affichage de la liste des erreurs pour le back office
- un modèle normalisé prévoyant l’utilisation de messages standards ou spécifiques par champs.
Messages dans les fichiers ressources des plugins
L’implémentation de base de la JSR 303 propose des messages par défaut ou une personnalisation à l’aide d’un unique fichier bundle validation.properties
. Ceci ne peut convenir dans le contexte de plugins de Lutece. Cependant la norme prévoit de pouvoir modifier la gestion des messages en utilisant une implémentation spécifique de l’interface MessageInterpolator
. C’est le choix retenu pour Lutece. Une classe LuteceMessageInterpolateur
a été créée afin que les messages puissent utiliser les fonctionnalités de I18nService
et notamment l’écriture pour rechercher la valeur d’une clé. Les clés utilisées pour les messages peuvent ainsi être gérées à l’identique de celles des templates HTML, avec comme convention : `nom_du_plugin.validation.nom_objet_metier.nom_attribut.nom_contrainte.
La mise en oeuvre de ce
MessageInterpolator
spécifique se fait par le biais du fichier de configurationMETA-INF/validation.xml
.
Voici comment doivent s’écrire les contraintes de validation sur les champs d’un objet métier :
// Person.java
@NotEmpty(message = "{validation.person.personName.notEmpty}")
@Size(max = 20, message = "{validation.person.personName.size}")
private String _strPersonName;
Les clés sont déclarées dans les fichiers de ressources comme suit :
# myplugin_messages_fr.properties
validation.person.personName.notEmpty=Le champ 'Nom' est obligatoire. Veuillez le remplir SVP.
validation.person.personName.size=Le champ 'Nom' ne doit pas contenir plus de 20 caractères.
Messages standards proposés par l’implémentation
L’implémentation propose des messages par défaut pour un certain nombre d’annotations standards ou spécifiques (Hibernate).
Ces messages sont stockés dans les fichiers ressources du core : validation_messages*.properties
.
En voici la liste :
Annotation | Clé | Message | Origine de l’annotation |
---|---|---|---|
@Size(min, max) | portal.validation.message.size | La valeur du champ {0} est invalide. Le champ doit contenir entre {1} et {2} caractères. | Standard javax.validation |
@Size(min) | portal.validation.message.sizeMin | La valeur du champ {0} doit avoir une taille supérieure à {2} caractères. | Standard javax.validation |
@Size(max) | portal.validation.message.sizeMax | La valeur du champ {0} doit avoir une taille inférieure à {2} caractères. | Standard javax.validation |
@Pattern(regexp) | portal.validation.message.pattern | Le format du champ {0} ne respecte pas la forme {1}. | Standard javax.validation |
@Min(value) | portal.validation.message.min | La valeur du champ {0} doit être supérieure à {1}. | Standard javax.validation |
@Max(value) | portal.validation.message.max | La valeur du champ {0} doit être inférieure à {1}. | Standard javax.validation |
@DecimalMin(value) | portal.validation.message.decimalMin | La valeur du champ {0} doit être supérieure à {1}. | Standard javax.validation |
@DecimalMax(value) | portal.validation.message.decimalMax | La valeur du champ {0} doit être inférieure à {1}. | Standard javax.validation |
@Digits(integer,fraction) | portal.validation.message.digits | La valeur du champ {0} doit avoir une partie entière inférieure à {1} chiffres et une partie décimale inférieure à {2} chiffres. | Standard javax.validation |
@Past | portal.validation.message.past | La date du champ {0} doit être antérieure à la date du jour. | Standard javax.validation |
@Future | portal.validation.message.future | La date du champ {0} doit être postérieure à la date du jour. | Standard javax.validation |
@NotEmpty | portal.validation.message.notEmpty | Le champ {0} est obligatoire. Veuillez le remplir SVP. | Hibernate validators |
portal.validation.message.email | La valeur du champ {0} ne correspond pas à un email valide. | Hibernate validators |
L’écriture de ces contraintes dans le bean est donc de la forme suivante :
// MyBean.java
@NotEmpty(message = "Le champ {0} est obligatoire. Veuillez le remplir SVP.")
@Pattern(regexp = "[a-z-A-Z]", message = "Le format du champ {0} ne respecte pas la forme {1}.")
@Size(max = 5, message = "La valeur du champ {0} doit avoir une taille inférieure à {2} caractères.")
private String _strName;
@Size(min = 10, max = 50, message = "La valeur du champ {0} est invalide. Le champ doit contenir entre {1} et {2} caractères.")
private String _strDescription;
@Min(value = 5, message = "La valeur du champ {0} doit être supérieure à {1}.")
private int _nAge;
@Email(message = "La valeur du champ {0} ne correspond pas à un email valide.")
private String _strEmail;
@Past(message = "La date du champ {0} doit être antérieure à la date du jour.")
private Date _dateBirth;
@Future(message = "La date du champ {0} doit être postérieure à la date du jour.")
private Date _dateEndOfWorld;
@DecimalMin(value = "1500.0", message = "La valeur du champ {0} doit être supérieure à {1}.")
private BigDecimal _salary;
@DecimalMax(value = "100.0", message = "La valeur du champ {0} doit être inférieure à {1}.")
private BigDecimal _percent;
@Digits(integer = 15, fraction = 2, message = "La valeur du champ {0} doit avoir une partie entière inférieure à {1} chiffres et une partie décimale inférieure à {2} chiffres.")
private String _strCurrency;
@URL(message = "La valeur du champ {0} ne correspond pas à une URL valide.")
private String _strUrl;
Affichage des messages
Une nouvelle classe ValidationError
a été créée pour mettre en forme les messages de violation de contraintes en récupérant notamment le nom du champ et les paramètres de la contrainte.
La liste des méthodes de création de message dans le BackOffice de la classe AdminMessageService
a été étendue pour recevoir en paramètres des listes d’erreurs sous la forme :
Set<ConstraintViolation>
pour une récupération brute des messages de contraintes ouList<ValidationError>
pour bénéficier des messages standards avec les noms de champs (à privilégier)
Les noms des champs sont récupérés au travers des fichiers ressources. Les clés seront définies dans les fichiers ressources en utilisant la convention suivante : model.entity.<entity>.attribute.<attribute>
.
Le préfixe des clés pour un bean donné est donc : model.entity.mybean.attribute.
Par exemple, concernant l’attribut _strPersonName
de la classe Person
, voici la clé du fichier ressource :
# myplugin_messages_fr.properties
model.entity.person.attribute.personName=Personne
Dans un JspBean
du BackOffice, voici la déclaration du préfixe et la forme que prend la validation d’un bean :
// PersonJspBean.java
private static final String VALIDATION_ATTRIBUTES_PREFIX = "model.entity.person.attribute.";
...
public String doCreatePerson( ... ) {
...
// Check constraints
List<ValidationError> errors = validate(person, VALIDATION_ATTRIBUTES_PREFIX);
if (errors.size() > 0) {
return AdminMessageService.getMessageUrl(request, Messages.MESSAGE_INVALID_ENTRY, errors);
}
...
}
Options avancées de configuration des ValidationError
Les options par défaut utilisées pour convertir et mettre en forme les ConstraintViolation
en ValidationError
sont définies dans une classe DefaultValidatorErrorConfig
implémentant la classe ValidatorErrorConfig
.
Il est possible d’étendre cette classe pour traiter de nouveaux paramètres de contraintes ou modifier le rendu du nom champ (entouré de la balise <strong>
par défaut). Il faudra alors passer cette implémentation à la méthode validate
à la place du préfixe (ce dernier faisant partie également des paramètres de configuration).