Validation des Beans (JSR 303)
Introduction
La JSR 303 définit un modèle de meta-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 future | 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 vaildation 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 personalisation à 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.
Voici comment doivent s'écrire les contraintes de validation sur les champs d'un objet métier :
// Person.java @NotEmpty(message = "") @Size(max = 20, message = "") 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 le 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 methodes 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 ou
- List<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 utlisant la convention suivante model.entity.<entity>.attribute.<attribute>.
Le prefix 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 prefix 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éfix (ce dernier faisant partie également des paramètre de configuration).