4 nov. 2021, 14:21:29 Thomas Dumont

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 :

AnnotationDescription de la contrainteType sur lequel la contrainte peut s’appliquer
@NullL’élément doit être nulObject
@NotNullL’élément doit être non nulObject
@AssertTrueL’élément doit être trueboolean, Boolean
@AssertFalseL’élément doit être falseboolean, Boolean
@MinL’élément doit être supérieur à la valeur spécifiée dans l’annotationBigDecimal, BigInteger, byte, short, int, long
@MaxL’élément doit être inférieur à la valeur spécifiée dans l’annotationBigDecimal, BigInteger, byte, short, int, long
@DecimalMinL’élément doit être supérieur à la valeur spécifiée dans l’annotationBigDecimal, BigInteger, String, byte, short, int, long
@DecimalMaxL’élément doit être inférieur à la valeur spécifiée dans l’annotationBigDecimal, BigInteger, String, byte, short, int, long
@SizeL’élément doit être entre deux tailles spécifiéesString, Collection, Map, Array
@DigitsL’élément doit être un nombre compris dans une certaine fenêtreBigDecimal, BigInteger, String, byte, short, int, long
@PastL’élément doit être une date dans le passéDate, Calendar
@FutureL’élément doit être une date dans le futurDate, Calendar
@PatternL’élément doit respecter une expression régulièreString

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 configuration META-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 :

AnnotationCléMessageOrigine de l’annotation
@Size(min, max)portal.validation.message.sizeLa valeur du champ {0} est invalide. Le champ doit contenir entre {1} et {2} caractères.Standard javax.validation
@Size(min)portal.validation.message.sizeMinLa valeur du champ {0} doit avoir une taille supérieure à {2} caractères.Standard javax.validation
@Size(max)portal.validation.message.sizeMaxLa valeur du champ {0} doit avoir une taille inférieure à {2} caractères.Standard javax.validation
@Pattern(regexp)portal.validation.message.patternLe format du champ {0} ne respecte pas la forme {1}.Standard javax.validation
@Min(value)portal.validation.message.minLa valeur du champ {0} doit être supérieure à {1}.Standard javax.validation
@Max(value)portal.validation.message.maxLa valeur du champ {0} doit être inférieure à {1}.Standard javax.validation
@DecimalMin(value)portal.validation.message.decimalMinLa valeur du champ {0} doit être supérieure à {1}.Standard javax.validation
@DecimalMax(value)portal.validation.message.decimalMaxLa valeur du champ {0} doit être inférieure à {1}.Standard javax.validation
@Digits(integer,fraction)portal.validation.message.digitsLa 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
@Pastportal.validation.message.pastLa date du champ {0} doit être antérieure à la date du jour.Standard javax.validation
@Futureportal.validation.message.futureLa date du champ {0} doit être postérieure à la date du jour.Standard javax.validation
@NotEmptyportal.validation.message.notEmptyLe champ {0} est obligatoire. Veuillez le remplir SVP.Hibernate validators
@Emailportal.validation.message.emailLa 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 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 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).