May 17, 2014 5:48:18 PM lutece lutece avatar   987    

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.

La mise en oeuvre de ce MessageInterpolator spécifique se fait par 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 = "")
  @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
@Email 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).