Validation de bean dans JPA
La validation des données est une tâche commune qui a lieu dans toutes les couches d'une application, notamment la persistance. L'API JPA (Java™ Persistence API) fournit le support de l'API Bean Validation afin que la validation des données puisse être réalisée à l'exécution. Cette rubrique inclut un scénario dans lequel la validation de bean est utilisée dans l'environnement JPA d'un exemple d'application qui est une galerie d'images numériques.
L'API Bean Validation fournit un service de validation transparent entre les différentes technologies de Java Enterprise Edition (Java EE) et des environnements Java Platform, Standard Edition (JSE). Outre JPA, ces technologies incluent JavaServer Faces (JSF) et Java EE Connector Architecture (JCA). Pour en savoir plus sur la validation de bean, consultez la rubrique API Bean Validation.
La validation de bean met en jeu trois concepts essentiels : les contraintes, la gestion des violations de contraintes et le valideur. Si vous exécutez vos applications dans un environnement intégré tel que JPA, vous n'avez pas besoin d'interface directe avec le valideur.
Les contraintes de validation sont des annotations ou du code XML qui sont ajoutés à une classe, un champ ou une méthode d'un composant JavaBeans. L'utilisateur peut employer les contraintes standard (telles que définies par la spécification officielle) ou définir ses propres contraintes. Elles servent à créer des définitions de contraintes à base d'expressions régulières et sont également utilisées pour composer des contraintes. Les contraintes standard sont définies par la spécification Bean Validation et sont donc disponibles avec chaque fournisseur de validation. Pour la liste des contraintes standard, consultez la rubrique Contraintes standard pour la validation de bean. Vous pouvez définir vos propres contraintes si les contraintes standard ne suffisent pas.
Contraintes et JPA
Le scénario suivant illustre l'emploi d'une contrainte standard dans l'architecture JPA d'un exemple d'application qui est une galerie d'images numériques.
package org.apache.openjpa.example.gallery.model;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@Entity
public class Image {
private long id;
private ImageType type;
private String fileName;
private byte[] data;
@Id
@GeneratedValue
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@NotNull(message="Image type must be specified.")
@Enumerated(EnumType.STRING)
public ImageType getType() {
return type;
}
public void setType(ImageType type) {
this.type = type;
}
@Pattern(regexp = ".*\\.jpg|.*\\.jpeg|.*\\.gif",
message="Only images of type JPEG or GIF are supported.")
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
}
La classe Image utilise deux contraintes standard, @NotNull et @Pattern. La contrainte @NotNull veille à ce qu'un élément ImageType soit spécifié, tandis que la contrainte @Pattern utilise un masque d'expression régulière pour garantir que l'image est dans l'un des formats acceptés (d'après son extension de nom de fichier). A chaque contrainte correspond une logique de validation qui est démarrée à l'exécution, lorsque l'entité Image est validée. Si l'une des contraintes n'est pas satisfaite, le fournisseur JPA émet une ConstraintViolationException accompagnée du message défini. La spécification JSR-303 prévoit également l'utilisation d'une variable dans l'attribut message. La variable peut ainsi contenir la clé d'un message dans un regroupement de ressources (resource bundle). Le regroupement de ressources prend en charge les messages spécifiques à l'environnement ainsi que la globalisation, la conversion et le support multiculturel des messages.
package org.apache.openjpa.example.gallery.constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import javax.validation.Constraint;
import javax.validation.Payload;
import org.apache.openjpa.example.gallery.model.ImageType;
@Documented
@Constraint(validatedBy = ImageContentValidator.class)
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface ImageContent {
String message() default "Image data is not a supported format.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
ImageType[] value() default { ImageType.GIF, ImageType.JPEG };
}
Ensuite, vous devez créer la classe du valideur, ImageContentValidator. La logique définie dans ce valideur sera implémentée par le fournisseur de
validation lorsque la contrainte sera validée. Dans le code, on peut voir que le lien entre la classe du valideur
et l'annotation de contrainte est établi par l'attribut
validatedBy de l'annotation
@Constraint.package org.apache.openjpa.example.gallery.constraint;
import java.util.Arrays;
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.openjpa.example.gallery.model.ImageType;
/**
* Simple check that file format is of a supported type
*/
public class ImageContentValidator implements ConstraintValidator<ImageContent, byte[]> {
private List<ImageType> allowedTypes = null;
/**
* Configure the constraint validator based on the image
* types it should support.
* @param constraint the constraint definition
*/
public void initialize(ImageContent constraint) {
allowedTypes = Arrays.asList(constraint.value());
}
/**
*Validate a specified value.
*/
public boolean isValid(byte[] value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
// Verify the GIF header is either GIF87 or GIF89
if (allowedTypes.contains(ImageType.GIF)) {
String gifHeader = new String(value, 0, 6);
if (value.length >= 6 &&
(gifHeader.equalsIgnoreCase("GIF87a") ||
gifHeader.equalsIgnoreCase("GIF89a"))) {
return true;
}
}
// Verify the JPEG begins with SOI and ends with EOI
if (allowedTypes.contains(ImageType.JPEG)) {
if (value.length >= 4 &&
value[0] == 0xff && value[1] == 0xd8 &&
value[value.length - 2] == 0xff && value[value.length -1] == 0xd9) {
return true;
}
}
// Unknown file format
return false;
}
}
Appliquez cette nouvelle contrainte à la méthode getData() sur la classe Image, par exemple :@ImageContent
public byte[] getData() {
return data;
}
Lors de la validation de l'attribut data, la méthode
isValid() de la classe ImageContentValidator est exécutée. Cette méthode contient
une logique simple, qui contrôle la validité du format des données binaires
de l'image. L'une des caractéristiques intéressantes de notre classe ImageContentValidator
est qu'elle peut aussi valider un type d'image spécifique. Par définition,
elle accepte les formats
JPEG et GIF, mais elle peut aussi se limiter à un format spécifique. Par exemple, en changeant le code de l'annotation comme suit,
vous demandez au valideur de n'autoriser que les données d'image au format
JPEG :@ImageContent(ImageType.JPEG)
public byte[] getData() {
return data;
}
Les contraintes de niveau type sont également à envisager,
car vous pourriez avoir besoin de valider des combinaisons d'attributs sur une
entité. Dans les exemples précédents, les contraintes de validation étaient utilisées
sur des attributs seuls.
Les contraintes dont l'action s'exerce au niveau du type (classe ou interface)
rendent possible une validation collective. Par exemple, les contraintes
appliquées à l'entité Image vérifient qu'un type d'image est défini (non Null),
que l'extension de nom de fichier de l'image est d'un type accepté et que
le format des données est correct compte tenu du type indiqué. Mais, par exemple, cela ne permet pas de valider collectivement qu'un nom de fichier img0.gif est de type GIF et que le format des données est pour une image de fichier GIF valide. Pour plus d'informations
sur les contraintes de niveau type, consultez le libre blanc
OpenJPA Bean Validation Primer et lisez plus particulièrement la section "Type-level constraints".Groupes de validation
La validation de bean utilise des groupes de validation pour déterminer quel type de validation appliquer et quand l'appliquer.

Pour plus d'informations sur les groupes de validation avec OpenJPA, consultez le libre blanc OpenJPA Bean Validation Primer et lisez plus particulièrement la section "Validation groups".
Modèle de domaine JPA
Outre l'entité Image, le modèle de domaine de notre exemple de galerie d'images contient les types persistants Album, Creator et Location. Une entité Album contient une référence à une collection d'entités Image. L'entité Creator contient une référence aux entités Album auxquelles à contribué le créateur désigné, et une référence aux entités Image qu'il a créées. L'on dispose ainsi d'une totale navigabilité vers et à partir de chaque entité dans le domaine. Un composant incorporable, appelé Location, a été ajouté à la classe Image pour permettre le stockage d'informations d'emplacement avec l'image.
private Location location;
@Valid
@Embedded
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
L'annotation @Valid assure la validation en chaîne des objets
incorporables dans un environnement JPA.
Par conséquent, lors de la validation d'une
entité Image, toute contrainte appliquée à l'emplacement (Location) référencé par cette entité
est également validée. Si l'annotation @Valid n'était pas spécifiée, l'emplacement ne serait pas validé. Dans un environnement JPA, la validation en chaîne via l'annotation @Valid n'est disponible que pour les
objets incorporables. Les entités et les collections d'entités référencées sont
validées séparément pour éviter une validation circulaire. API Bean Validation et environnement JPA
La spécification JPA simplifie l'intégration à l'API Bean Validation. Dans un environnement JSE, la validation de bean est activée par défaut dès lors que l'API Bean Validation et un fournisseur de validation de bean sont présents sur le chemin de classes de votre environnement d'exécution. Dans un environnement Java EE, le serveur d'applications inclut un fournisseur de validation de bean. Vous n'avez donc pas besoin d'en fournir un avec votre application. Dans les deux environnements, vous devez utiliser un fichier persistence.xml version 2.0 ou ultérieure.
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0" >
...
</persistence>
- AUTO (Automatique)
Active la validation de bean si un fournisseur de validation est disponible dans le chemin de classes. Le mode AUTO est la valeur par défaut.
- Rappel
Lorsque le mode CALLBACK est spécifié, un fournisseur de validation de bean doit être disponible et utilisable par le fournisseur JPA. Sinon, le fournisseur JPA émet une exception lors de l'instanciation d'une nouvelle fabrique de gestionnaire d'entité JPA.
- Aucun
Désactive la validation de bean pour une unité de persistance particulière.

<persistence-unit name="auto-validation">
...
<!-- Validation modes: AUTO, CALLBACK, NONE -->
<validation-mode>AUTO</validation-mode>
...
</persistence-unit>
L'autre moyen est de configurer
le mode de validation programmatiquement en spécifiant la
propriété javax.persistence.validation.mode
avec la valeur auto, callback ou none lors de la création d'une nouvelle fabrique
de gestionnaire d'entité JPA, comme dans l'exemple suivant :Map<String, String> props = new HashMap<String, String>();
props.put("javax.persistence.validation.mode", "callback");
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("validation", props);
<persistence-unit name="non-default-validation-groups">
<class>my.Entity</class>
<validation-mode>CALLBACK</validation-mode>
<properties>
<property name="javax.persistence.validation.group.pre-persist"
value="org.apache.openjpa.example.gallery.constraint.SequencedImageGroup"/>
<property name="javax.persistence.validation.group.pre-update"
value="org.apache.openjpa.example.gallery.constraint.SequencedImageGroup"/>
<property name="javax.persistence.validation.group.pre-remove"
value="javax.validation.groups.Default"/>
</property>
</persistence-unit>
L'exemple suivant montre différentes étapes du
cycle de vie JPA, y compris les opérations de persistance, de mise à jour et de suppression :EntityManagerFactory emf =
Persistence.createEntityManagerFactory("BeanValidation");
EntityManager em = emf.createEntityManager();
Location loc = new Location();
loc.setCity("Rochester");
loc.setState("MN");
loc.setZipCode("55901");
loc.setCountry("USA");
// Create an Image with non-matching type and file extension
Image img = new Image();
img.setType(ImageType.JPEG);
img.setFileName("Winter_01.gif");
loadImage(img);
img.setLocation(loc);
// *** PERSIST ***
try {
em.getTransaction().begin();
// Persist the entity with non-matching extension and type
em.persist(img);
} catch (ConstraintViolationException cve) {
// Transaction was marked for rollback, roll it back and
// start a new one
em.getTransaction().rollback();
em.getTransaction().begin();
// Fix the file type and re-try the persist.
img.setType(ImageType.GIF);
em.persist(img);
em.getTransaction().commit();
}
// *** UPDATE ***
try {
em.getTransaction().begin();
// Modify the file name to a non-matching file name
// and commit to trigger an update
img.setFileName("Winter_01.jpg");
em.getTransaction().commit();
} catch (ConstraintViolationException cve) {
// Handle the exception. The commit failed so the transaction
// was already rolled back.
handleConstraintViolation(cve);
}
// The update failure caused img to be detached. It must be merged back
// into the persistence context.
img = em.merge(img);
// *** REMOVE ***
em.getTransaction().begin();
try {
// Remove the type and commit to trigger removal
img.setType(ImageType.GIF);
em.remove(img);
} catch (ConstraintViolationException cve) {
// Rollback the active transaction and handle the exception
em.getTransaction().rollback();
handleConstraintViolation(cve);
}
em.close();
emf.close();
Exceptions
Des erreurs de validation peuvent se produire à n'importe quel stade du cycle de vie JPA.
private void handleConstraintViolation(ConstraintViolationException cve) {
Set<ConstraintViolation<?>> cvs = cve.getConstraintViolations();
for (ConstraintViolation<?> cv : cvs) {
System.out.println("------------------------------------------------");
System.out.println("Violation: " + cv.getMessage());
System.out.println("Entity: " + cv.getRootBeanClass().getSimpleName());
// The violation occurred on a leaf bean (embeddable)
if (cv.getLeafBean() != null && cv.getRootBean() != cv.getLeafBean()) {
System.out.println("Embeddable: " +
cv.getLeafBean().getClass().getSimpleName());
}
System.out.println("Attribute: " + cv.getPropertyPath());
System.out.println("Invalid value: " + cv.getInvalidValue());
}
}
Le traitement des violations de contraintes est généralement simple lorsqu'il
s'agit de contraintes de niveau attribut. En revanche, si vous utilisez un valideur de
niveau type avec des contraintes de niveau type, il devient plus difficile de
déterminer quels attributs ont échoué à la validation. De plus, au lieu d'un attribut particulier, c'est l'objet en entier qui
est renvoyé comme valeur non valide. Dans le cas où des informations d'échec spécifiques
sont nécessaires, il est possible d'utiliser une contrainte de niveau attribut ou une violation de
contrainte personnalisée, comme le décrit la spécification Bean Validation. Exemple
Le modèle JPA et le scénario d'application de galerie d'images décrits dans cette rubrique peuvent être implémentés au moyen d'un exemple que vous trouverez dans le livre blanc OpenJPA Bean Validation primer.