Les annotations sont une manière très commode et élégante pour spécifier des contraintes invariantes sur un modèle de données. Vous pouvez, par exemple, indiquer qu'une propriété ne devrait pas être nulle, que le solde d'un compte devrait être strictement positif, etc. Ces contraintes de modèle de données sont déclarées dans le bean lui-même en annotant ses propriétés. Un validateur peut alors les lire et vérifier les violations de contraintes. Le mécanisme de validation peut être exécuté dans différentes couches de votre application (présentation, accès aux données) sans devoir dupliquer ces règles. Hibernate Validator a été conçu dans ce but.
Hibernate Validator fonctionne sur deux niveaux. D'abord, il est capable de vérifier des violations de contraintes sur les instances d'une classe en mémoire. Ensuite, il peut appliquer les contraintes au méta-modèle d'Hibernate et les incorporer au schéma de base de données généré.
Chaque annotation de contrainte est associée à l'implémentation du validateur responsable de vérifier la contrainte sur l'instance de l'entité. Un validateur peut aussi (optionnellement) appliquer la contrainte au méta-modèle d'Hibernate, permettant à Hibernate de générer le DDL qui exprime la contrainte. Avec le listener d'événements approprié, vous pouvez exécuter l'opération de vérification lors des insertions et des mises à jour effectuées par Hibernate. Hibernate Validator n'est pas limité à Hibernate. Vous pouvez facilement l'utiliser n'importe où dans votre application.
Lors de la vérification des instances à l'exécution, Hibernate Validator retourne des informations à propos des violations de contraintes dans un tableau de InvalidValues. Parmi d'autres informations, InvalidValue contient un message de description d'erreur qui peut inclure les valeurs des paramètres associés à l'annotation (p. ex. la limite de taille), et des chaînes de caractères qui peuvent être externalisées avec un ResourceBundle.
Une contrainte est représentée par une annotation. Une contrainte a généralement des attributs utilisés pour paramétrer les limites des contraintes. La contrainte s'applique à l'élément annoté.
Hibernate Validator arrive avec des contraintes intégrées, lesquelles couvrent la plupart des vérifications de données de base. Comme nous le verrons plus tard, vous n'êtes pas limité à celles-ci, vous pouvez écrire vos propres contraintes en une minute.
Tableau 4.1. Contraintes intégrées
Annotation | S'applique à | Vérification à l'exécution | Impact sur les méta-données d'Hibernate |
---|---|---|---|
@Length(min=, max=) | propriété (String) | vérifie si la longueur de la chaîne de caractères est comprise dans l'intervalle | la longueur de la colonne sera positionnée à max |
@Max(value=) | propriété (nombre ou chaîne de caractères représentant un nombre) | vérifie si la valeur est inférieure ou égale à max | ajoute une contrainte de vérification sur la colonne |
@Min(value=) | propriété (nombre ou chaîne de caractères représentant un nombre) | vérifie si la valeur est supérieure ou égale à max | ajoute une contrainte de vérification sur la colonne |
@NotNull | propriété | vérifie si la valeur n'est pas nulle | les colonnes sont marquées "not null" |
@Past | propriété (Date ou Calendar) | vérifie si la date est dans le passé | ajoute une contrainte de vérification sur la colonne |
@Future | propriété (Date ou Calendar) | vérifie si la date est dans le futur | aucun |
@Pattern(regex="regexp", flag=) | propriété (String) | vérifie si la propriété correspond à l'expression rationnelle donnée (pour "flag", voir java.util.regex.Pattern) | aucun |
@Range(min=, max=) | propriété (nombre ou chaîne de caractères représentant un nombre) | vérifie si la valeur est comprise entre min et max (inclus) | ajoute une contrainte de vérification sur la colonne |
@Size(min=, max=) | propriété (tableau, collection, map) | vérifie si la taille de l'élément est comprise entre min et max (inclus) | aucun |
@AssertFalse | propriété | vérifie que la méthode est évaluée à faux (utile pour les contraintes exprimées dans le code plutôt que dans les annotations) | aucun |
@AssertTrue | propriété | vérifie que la méthode est évaluée à vrai (utile pour les contraintes exprimées dans le code plutôt que dans les annotations) | aucun |
@Valid | propriété (objet) | exécute la validation récursivement sur l'objet associé. Si l'objet est une Collection ou un tableau, les éléments sont validés récursivement. Si l'objet est une Map, les éléments valeur sont validés récursivement. | aucun |
propriété (String) | vérifie si la chaîne de caractères est conforme à la spécification d'une adresse e-mail | aucun |
Hibernate Validator arrive avec un ensemble de messages d'erreur par défaut traduits dans environ dix langues (si la vôtre n'en fait pas partie, veuillez nous envoyer un patch). Vous pouvez surcharger ces messages en créant un ValidatorMessages.properties (ou ValidatorMessages_loc.properties) et en surchargeant les clefs dont vous avez besoin. Vous pouvez même ajouter votre propre ensemble de messages supplémentaire lorsque vous écrivez vos annotations de validation. Si Hibernate Validator ne peut pas trouver une clef à partir de votre resourceBundle ou de votre ValidatorMessage, il se repliera sur les valeurs intégrées par défaut.
Alternativement vous pouvez fournir un ResourceBundle pendant la vérification par programmation des règles de validation sur un bean, ou si vous voulez un mécanisme d'interpolation complètement différent, vous pouvez fournir une implémentation de org.hibernate.validator.MessageInterpolator (lisez la JavaDoc pour plus d'informations).
Etendre l'ensemble de contraintes intégrées est extrêment facile. N'importe quelle contrainte est constituée deux morceaux : le descripteur de contrainte (l'annotation) et le validateur de contrainte (la classe d'implémentation). Voici un simple descripteur personnalisé :
@ValidatorClass(CapitalizedValidator.class) @Target(METHOD) @Retention(RUNTIME) @Documented public @interface Capitalized { CapitalizeType type() default Capitalize.FIRST; String message() default "has incorrect capitalization"; }
type est un paramètre décrivant comment la propriété devrait être mise en majuscule. Ceci est un paramètre utilisateur complètement dépendant du fonctionnement de l'annotation.
message est la chaîne de caractères par défaut utilisée pour décrire la violation de contrainte et est obligatoire. Vous pouvez mettre la chaîne de caractères dans le code ou bien l'externaliser en partie ou complètement avec le mécanisme ResourceBundle Java. Les valeurs des paramètres sont injectées à l'intérieur du message quand la chaîne de caractères {parameter} est trouvée (dans notre exemple Capitalization is not {type} générerait Capitalization is not FIRST), externaliser toute la chaîne dans ValidatorMessages.properties est considéré comme une bonne pratique. Voir Messages d'erreur.
@ValidatorClass(CapitalizedValidator.class) @Target(METHOD) @Retention(RUNTIME) @Documented public @interface Capitalized { CapitalizeType type() default Capitalize.FIRST; String message() default "{validator.capitalized}"; } ... #in ValidatorMessages.properties validator.capitalized=Capitalization is not {type}
Comme vous pouvez le voir la notation {} est récursive.
Pour lier un descripteur à l'implémentation de son validateur, nous utilisons la méta-annotation @ValidatorClass. Le paramètre de la classe du validateur doit nommer une classe qui implémente Validator<ConstraintAnnotation>.
Nous devons maintenant implémenter le validateur (ie l'implémentation vérifiant la règle). Une implémentation de validation peut vérifier la valeur d'une propriété (en implémentant PropertyConstraint) et/ou peut modifier les méta-données de mapping d'Hibernate pour exprimer la contrainte au niveau de la base de données (en implémentant PersistentClassConstraint).
public class CapitalizedValidator implements Validator<Capitalized>, PropertyConstraint { private CapitalizeType type; // partie du contrat de Validator<Annotation>, // permet d'obtenir et d'utiliser les valeurs de l'annotation public void initialize(Capitalized parameters) { type = parameters.type(); } // partie du contrat de la contrainte de la propriété public boolean isValid(Object value) { if (value==null) return true; if ( !(value instanceof String) ) return false; String string = (String) value; if (type == CapitalizeType.ALL) { return string.equals( string.toUpperCase() ); } else { String first = string.substring(0,1); return first.equals( first.toUpperCase(); } } }
La méthode isValid() devrait retourner false si la contrainte a été violée. Pour plus d'exemples, référez-vous aux implémentations intégrées du validateur.
Nous avons seulement vu la validation au niveau propriété, mais vous pouvez écrire une annotation de validation au niveau d'un bean. Plutôt que de recevoir l'instance de retour d'une propriété, le bean lui-même sera passé au validateur. Pour activer la vérification de validation, annotez juste le bean lui-même. Un petit exemple peut être trouvé dans la suite de tests unitaires.
Maintenant que vous vous êtes familiarisés avec les annotations, la syntaxe devrait être connue.
public class Address { private String line1; private String line2; private String zip; private String state; private String country; private long id; // une chaîne non nulle de 20 caractères maximum @Length(max=20) @NotNull public String getCountry() { return country; } // une chaîne de caractères non nulle @NotNull public String getLine1() { return line1; } // pas de contrainte public String getLine2() { return line2; } // une chaîne non nulle de 3 caractères maximum @Length(max=3) @NotNull public String getState() { return state; } // une chaîne non nulle de 5 caractères maximum représentant un nombre // si la chaîne de caractères est plus longue, le message sera recherché // dans le resource bundle avec la clef 'long' @Length(max=5, message="{long}") @Pattern(regex="[0-9]+") @NotNull public String getZip() { return zip; } // devrait toujours être vrai @AssertTrue public boolean isValid() { return true; } // un nombre entre 1 et 2000 @Id @Min(1) @Range(max=2000) public long getId() { return id; } }
Bien que l'exemple montre seulement la validation de propriétés publiques, vous pouvez aussi annoter des champs avec n'importe quelle visibilité.
@MyBeanConstraint(max=45) public class Dog { @AssertTrue private boolean isMale; @NotNull protected String getName() { ... }; ... }
Vous pouvez aussi annoter des inferfaces. Hibernate Validator vérifiera toutes les classes parentes et les interfaces héritées ou implémentées par un bean donné pour lire les annotations appropriées du validateur.
public interface Named { @NotNull String getName(); ... } public class Dog implements Named { @AssertTrue private boolean isMale; public String getName() { ... }; }
La propriété "name" sera vérifiée pour la nullité lorsque le bean Dog sera validé.
Hibernate Validator est destiné à être utilisé pour implémenter une validation de données à plusieurs couches, où nous exprimons des contraintes à un seul endroit (le modèle de données annoté) et les appliquons aux différents niveaux de l'application.
Par défaut, Hibernate Annotations traduira les contraintes que vous avez définies sur vos entités en méta-données de mapping. Par exemple, si une propriété de votre entité est annotée avec @NotNull, ses colonnes seront déclarées comme not null dans le schéma DDL généré par Hibernate.
Hibernate Validator a deux listeners d'événements Hibernate intégrés. Quand un PreInsertEvent ou un PreUpdateEvent survient, les listeners vérifieront toutes les contraintes de l'instance de l'entité et lèveront une exception si une contrainte est violée. Fondamentalement, les objets seront vérifiés avant les insertions et avant les mises à jour effectuées par Hibernate. C'est le plus commode et la manière la plus simple d'activer le processus de validation. Sur une violation de contrainte, l'événement lèvera une exception d'exécution InvalidStateException (NdT : c'est une RuntimeException) laquelle contient un tableau d'InvalidValues décrivant chaque échec.
<hibernate-configuration> ... <event type="pre-update"> <listener class="org.hibernate.validator.event.ValidatePreUpdateEventListener"/> </event> <event type="pre-insert"> <listener class="org.hibernate.validator.event.ValidatePreInsertEventListener"/> </event> </hibernate-configuration>
Lors de l'utilisation d'Hibernate Entity Manager, le framework Validation est activé par défaut. Si les beans ne sont pas annotés avec des annotations de validation, il n'y a pas de coût en terme de performance.
Hibernate Validator peut être utilisé n'importe où dans le code de votre application.
ClassValidator personValidator = new ClassValidator( Person.class ); ClassValidator addressValidator = new ClassValidator( Address.class, ResourceBundle.getBundle("messages", Locale.ENGLISH) ); InvalidValue[] validationMessages = addressValidator.getInvalidValues(address);
Les deux premières lignes préparent Hibernate Validator pour la vérification de classes. La première s'appuie sur les messages d'erreur intégrés à Hibernate Validator (voir Messages d'erreur), la seconde utilise un resource bundle pour ses messages. Il est considéré comme une bonne pratique d'exécuter ces lignes une fois et de cacher les instances de validateur.
La troisième ligne valide en fait l'instance Address et retourne un tableau d'InvalidValues. Votre logique applicative sera alors capable de réagir aux échecs.
Vous pouvez aussi vérifier une propriété particulière plutôt que tout le bean. Ceci pourrait être utile lors d'interactions avec l'utilisateur propriété par propriété.
ClassValidator addressValidator = new ClassValidator( Address.class, ResourceBundle.getBundle("messages", Locale.ENGLISH) ); // récupère seulement les valeurs invalides de la propriété "city" InvalidValue[] validationMessages = addressValidator.getInvalidValues(address, "city"); // récupère seulement les valeurs potentielles invalides de la propriété "city" InvalidValue[] validationMessages = addressValidator.getPotentialInvalidValues("city", "Paris")
Comme un transporteur d'informations de validation, Hibernate fournit un tableau d'InvalidValues. Chaque InvalidValue a un groupe de méthodes décrivant les problèmes individuels.
getBeanClass() récupère le type du bean ayant échoué.
getBean() récupère l'instance du bean ayant échoué (s'il y en a, c'est-à-dire pas lors de l'utilisation de getPotentianInvalidValues()).
getValue() récupère la valeur ayant échouée.
getMessage() récupère le message d'erreur internationalisé.
getRootBean() récupère l'instance du bean racine ayant généré le problème (utile en conjonction avec @Valid), est nulle si getPotentianInvalidValues() est utilisée.
getPropertyPath() récupère le chemin (séparé par des points) de la propriété ayant échouée à partir du bean racine.