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.

Dans le premier exemple de code, une contrainte standard est ajoutée à une entité simple du modèle JPA appelé image. Une image a un ID, un type, un nom de fichier et des données. Le type d'image doit être spécifié et le nom de fichier de l'image doit inclure une extension valide, JPEG ou GIF. Le code montre l'entité Image annotée avec certaines contraintes de validation de bean standard appliquées.
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.

Vous pouvez créer vos propres valideurs et contraintes. Dans l'exemple précédent, l'entité Image utilisait la contrainte @Pattern pour valider le nom de fichier de l'image. Cependant, elle ne vérifiait pas la validité des données d'image proprement dites. Vous pouvez utiliser une contrainte basée sur un modèle. Vous ne bénéficiez pas de la souplesse qu'offre la création d'une contrainte dédiée à la vérification des contraintes sur les données. Dans le cas présent, vous pourriez créer une annotation de contrainte personnalisée, exerçant son action au niveau méthode. Le code suivant montre une contrainte personnalisée, appelée ImageContent.
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.

Il n'y a pas d'interface particulière à implémenter ni d'annotation spéciale à appliquer pour créer un groupe de validation. Un groupe de validation est simplement dénoté par une définition de classe.
Pratiques recommandées Pratiques recommandées: Lors de l'utilisation de groupes, utilisez des interfaces simples. Cela rendra les groupes de validation plus facilement utilisables dans divers environnements. A l'inverse, si vous utilisez une définition de classe ou d'entité comme groupe de validation, elle est susceptible de polluer le modèle d'objet d'une autre application en emmenant avec elle des classes de domaine et une logique qui n'ont pas de sens pour cette application. Par défaut, si aucun groupe de validation n'est spécifié sur une contrainte particulière, elle est validée en utilisant le groupe javax.validation.groups.Default. Créer un groupe personnaliser est aussi simple que de créer une nouvelle définition d'interface. bprac

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.

Les entités Album et Creator comportent des contraintes standard. L'emplacement incorporable est plus spécifique et mérite qu'on s'y attarde, car il fait la démonstration de l'utilisation de l'annotation @Valid pour valider des objets intégrés. Pour intégrer les informations d'emplacement dans une image, un nouveau champ et les propriétés persistantes correspondantes ont été ajoutés à la classe Image ; par exemple :
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.

Un fichier persistence.xml version 1.0 ne permet pas de configurer la validation de bean. L'utilisation d'un fichier persistence.xml version 2.0 ou ultérieure empêche une application JPA 1.0 pure de subir le démarrage de validation et l'impact de l'exécution. Il s'agit d'un point important, car une application basée sur JPA 1.0 n'a pas de moyen standard de désactiver la validation. Dans un environnement Java EE, activez la validation dans une application 1.0 existante en modifiant l'élément racine du fichier persistence.xml. L'exemple suivant correspond à un fichier persistence.xml :
<?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>
La validation de bean fournit trois modes de fonctionnement dans l'environnement JPA :
  • 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.

Le mode AUTO simplifie le déploiement, mais il peut entraîner des problèmes si la validation n'a pas lieu en raison d'un problème de configuration.
Pratiques recommandées Pratiques recommandées: Pour un comportement cohérent, utilisez explicitement le mode NONE ou CALLBACK.bprac
De plus, si NONE est spécifié, JPA est optimisé au démarrage et ne tente pas d'effectuer une validation imprévue. Le fait de désactiver explicitement la validation est particulièrement important dans un environnement Java EE, où le conteneur est mandaté pour fournir un fournisseur de validation. Par conséquent, sauf indication contraire, une application JPA version 2.0 ou ultérieure démarrée dans un conteneur est configurée avec la validation activée. Il en résulte un traitement supplémentaire durant les événements de cycle de vie.
Il existe deux façons de configurer les modes de validation dans JPA. La méthode la plus simple consiste à ajouter un élément de mode de validation au fichier persistence.xml avec le mode de validation voulu, comme indiqué dans l'exemple suivant :
 <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);
La validation de bean a lieu pendant le traitement des événements de cycle de vie JPA. Si elle est activée, la validation intervient à l'étape finale des événements de cycle de vie PrePersist, PreUpdate et PreRemove. Elle n'entre en action qu'une fois terminés tous les événements de cycle de vie définis par l'utilisateur, car certains de ces événements peuvent modifier l'entité qui fait l'objet de la validation. Par défaut, JPA active la validation pour le groupe de validation par défaut et pour les événements de cycle de vie PrePersist et PreUpdate. Si vous devez valider d'autres groupes de validation ou activer la validation pour l'événement PreRemove, vous définissez les groupes de validation pour valider chaque événement de cycle de vie dans le fichier persistence.xml, comme indiqué dans l' exemple suivant :
<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.

Si une ou plusieurs contraintes échouent à la validation durant un événement du cycle de vie, une ConstraintViolationException est émise par le fournisseur JPA. Elle inclut un ensemble de ConstraintViolations qui correspondent aux violations de contraintes qui ont eu lieu. Chaque objet ConstraintViolation contient des informations sur la contrainte, notamment : un message, le bean racine ou l'entité JPA, le bean feuille (information utile pour la validation d'objets incorporables JPA), l'attribut qui a échoué à la validation et la valeur qui a provoqué cet échec. Voici un exemple de routine de traitement d'une exception :
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.


Icône indiquant le type de rubrique Rubrique de concept



Icône d'horodatage Dernière mise à jour: last_date
http://www14.software.ibm.com/webapp/wsbroker/redirect?version=cord&product=was-nd-mp&topic=cdat_beanvaljpa
Nom du fichier : cdat_beanvaljpa.html