Valider ses POJOs en REST avec Bean validation, Spring MVC et JQuery


Cela fait maintenant un bon moment que je n’ai rien écrit sur ce blog, je vais donc profiter de mes vacances pour traiter d’un sujet plutôt récurrent sur les applications Java d’entreprise : la validation de vos beans.

Dans cet article, j’aborderai notamment :

  • Les principes de bean validation, sa mise en place et possibilités dans un contexte Spring MVC
  • La mise en place d’un controller orienté REST via Spring MVC (ce sera très rapide, là n’est pas l’objectif de cet article)
  • La validation d’un simple POJO via un test (un peu plus qu’unitaire) utilisant rest assured, illustrant la testabilité de votre application REST
  • Comment aller plus loin : faire des validations conditionnelles, gérer de manière centralisée le comportement de vos controllers spring sur une erreur de validation
  • Implémenter une validation unifiée client/serveur, basée sur la validation serveur via un appel AJAX

Le fil rouge du post sera simple : l’objectif est de fournir une page d’inscription dans lequel l’utilisateur doit pouvoir saisir un certain nombre d’informations plus ou moins obligatoires le concernant.

Un projet github a été créé pour l’occasion, avec l’ensemble des sources de cet article. N’hésitez pas à le consulter si vous avez des interrogations.

Bean Validation : principes et mise en place

Bean Validation est une JSR (la JSR-303) dont l’objectif est de fournir une API permettant de valider l’état d’une instance Java à un instant t et de générer autant de messages d’erreurs que de contraintes non vérifiées.
L’implémentation de référence de cette JSR est Hibernate Validator qui permet à hibernate, entre autres, de gérer de manière unifiée la cohérence entre les contraintes des POJOs et leur représentation en base de données (ex : un attribut non null sur le POJO sera retranscrit par un champ non null dans la table correspondante au POJO).
Une autre implémentation possible est le projet BVal chez Apache.

Sur un projet Java, pour pouvoir utiliser bean validation, vous aurez donc besoin de référencer :

  • L’API bean validation
  • L’implémentation que vous souhaitez utiliser

Un exemple de cette déclaration sur un projet maven pourrait être la suivante :

<!--?xml version="1.0" encoding="UTF-8"?-->
...

        ...

          javax.validation
          validation-api
          1.0.0.GA
          compile

            org.hibernate
            hibernate-validator
            4.2.0.Final
            <!--              Note : On aurait pu utiliser le scope runtime ici afin de ne pas référencer une classe de l'implémentation par inadvertance (objectif d'une JSR qui définit une API).             Dans notre cas, nous aurons besoin (plus tard) de référencer l'annotation @Email qui est une annotation "propriétaire" d'hibernate validator.             Jugeant que je ne vais a priori pas changer d'implémentation (à fortiori vu le "locking" dû à @Email) de bean validation, je "m'autorise" un scope runtime ici.             Pour info, les annotations propriétaires d'hibernate validator sont listées ici : http://docs.jboss.org/hibernate/validator/4.3/reference/en-US/html_single/#validator-defineconstraints-hv-constraints             -->
            compile

        ...

    ...

Les POJO manipulés : une illustration des possibilités de Bean Validation

Les POJO sont très certainement la pièce centrale de ce blog post. Ils illustrent bien les possibilités offertes par les annotations de bean validation.

Les voici :

public class Address {
    @NotNull(groups=ValidationMode.Update.class)
    Long id;
    @NotNull @Size(min=5)
    String street1;
    @Size(min=5)
    String street2;
    @Pattern(regexp="[0-9]{5}") // On accepte les codes postaux du type "01000"
    String postalCode;
    @NotNull @Size(min=1)
    String city;
}

public class Credentials {
    @NotNull @Size(min=1,max=25)
    @Email // Hibernate validator "proprietary" annotation !
    String login;
    @Size(min=3)
    // Password should only be mandatory during create/authent (not in update)
    @NotNull(groups={ValidationMode.Authent.class, ValidationMode.Create.class})
    String password;
}

public class User {
    @NotNull(groups=ValidationMode.Update.class)
    Long id;
    @NotNull @Valid
    Credentials credentials;
    @NotNull @Size(min=1,max=20)
    String firstName;
    @NotNull @Size(min=1,max=20)
    String lastName;
    @Past
    Date birthDate;
    @NotNull @Size(min=1, message="{user.phoneNumbers.notEmpty}")
    List phoneNumbers;
    @Valid
    List</pre>
<address>addresses;
}

Quelques points notables :

  • Bean validation permet de valider un noeud du graphe d’objets à condition d’annoter ce noeud, normalement non primitif, avec l’annotation @Valid. Si l’annotation @Valid est positionnée sur une collection (ex: User.phoneNumbers) ou un array, chaque instance de la collection sera validée (les messages d’erreurs seront donc indexés).
  • Il est possible de faire un premier niveau de validations conditionnelles (nous verrons un second niveau plus tard) en utilisant les groupes de validation. Un groupe de validation permet de dire, de manière typesafe, « cette contrainte sera activée seulement lorsque le groupe de validation XXX est activé » (en l’absence de groupe spécifié, c’est un groupe par défaut -javax.validation.groups.Default-, actif par défaut, qui est attaché à la contrainte… nous y reviendrons plus tard). Dans notre cas, les contraintes @NotNull seront activées sur les id uniquement en Update (la classe ValidationMode est une interface « maison » qui peut s’apparenter à un stereotype : elle ne possède aucune méthode).
  • Les contraintes type @Size ou @Pattern peuvent tout à fait être utilisées en corrélation avec @NotNull : les annotations sont alors complémentaires. L’inverse a également une signification. Par exemple, Address.street2 peut tout à fait être laissée null, en revanche, dès lors que le champ est non null, il doit être d’une taille minimum de 5 !
  • Pendant longtemps, j’ai utilisé l’annotation « propriétaire » @NotEmpty d’hibernate validator pour vérifier qu’une collection était non vide. Cependant, suite à une discussion avec Emmanuel Bernard pendant DevoxxFr, ce dernier m’a fait réaliser qu’utiliser @Size sur une collection permettait de s’affranchir du @NotEmpty (au message d’erreur par défaut prêt) : à vous de voir si vous préférez rester standard et overrider le message par défaut, ou bien utiliser l’annotation propriétaire @NotEmpty qui vient avec son message correctement internationalisé
  • L’utilisation de l’annotation @Email d’Hibernate Validator pourrait être discutable (on se met à dépendre fortement de l’implémentation, rendant la spécification inutile), mais la regexp pour détecter un email étant tellement compliquée (si si !), je me range complètement derrière le choix d’Emmanuel et Hardy (cf le source de la classe EmailValidator dans Hibernate validator) et je trouve bien pratique que le boulot soit déjà fait quelque part !
  • Au sujet de la conversion de type (par exemple sur une Date, un Float ou autre), la configuration Jackson configurée par défaut dans Spring MVC fonctionnera de manière bloquante : si par exemple, pour un Float, vous saisissez « toto », vous aurez une exception de conversion bien avant d’avoir une erreur de validation. Pas cool ! Pour y remédier, jetez un oeil à ce commit qui met en place un ObjectMapper Jackson maison permettant, entre autres, de faire des deserializations non bloquantes.
  • Pour simplifier le code de ce post, j’ai volontairement masqué les getters/setters sur les POJOs. En revanche, Spring et Jackson auront besoin de ces derniers pour pouvoir introspecter les attributs de ces POJOs : ne les oubliez pas 😉

Faire des validations évoluées

Les annotations sont extrêmement lisibles, mais elles présentent l’inconvénient de ne coder les contraintes que de manière « statique ». Or, rapidement, vous aurez besoin de faire des validations conditionnelles entre les champs, par exemple, vérifier qu’une date de fin est bien postérieure à une date de début, vérifier que tel champ doit être alimenté dès que tel autre a été alimenté etc…

Nous avons vu les groupes de validation précédemment, mais cela serait bien évidemment assez rébarbatif de se placer dans le produit cartésien de tous les cas possibles. Or, dans les contraintes de validation standard fournies par bean validation, il y en a une qui paraît peu utile, l’annotation @AssertTrue. Cette annotation se révèle cependant très puissante lorsque placée sur une méthode qui implémente votre règle métier (spécifique, la plupart du temps, au POJO courant que vous manipulez !).

Nous pourrions par exemple garantir que toute instance de User doit être majeur de la manière suivante :

public class User {
    // ...

    @AssertTrue(message="{user.isAdult}")
    @JsonIgnore // Won't expose this boolean as an attribute in JSON deserialization
    public boolean isUserAnAdult(){
        if(this.birthDate == null){ // birthDate is nullable, we shouldn't fail if it is let empty !
            return true;
        }

        Calendar birthCalendar = new GregorianCalendar();
        birthCalendar.setTime(birthDate);
        birthCalendar.add(Calendar.YEAR, 18);
        return birthCalendar.before(new GregorianCalendar());
    }

    // ...
}

Pratique hein ? D’autant que vous pouvez déclarer autant de méthodes (avec autant de messages d’erreurs associés) que vous le souhaitez ! Personnellement, je trouve cette alternative bien plus simple/lisible que de définir un Validator maison pour le POJO ! (Ce qui, de plus, éclate les règles de gestion dans d’autre classes, nuisant ainsi à la lisibilité globale de mon point de vue !)

Validation des beans via Spring MVC

Pour activer la validation au niveau d’un controller Spring, vous aurez besoin de déclarer la balise suivante dans votre configuration XML :

<!--?xml version="1.0" encoding="UTF-8"?-->

    ...

    ...

Une fois fait, tout bean passé en paramètre d’une méthode de votre controller Spring, et annoté de l’annotation bean validation @Valid (javax.validation.Valid), sera validé automatiquement via le Validator de votre implémentation de bean validation.

Par exemple :

package fr.fcamblor.demos.sbjd.web.auth;

import fr.fcamblor.demos.sbjd.models.Credentials;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.validation.Valid;

@Controller
public class AuthenticationController {

    @RequestMapping(value="auth/authenticateQuery", method= RequestMethod.POST)
    public void authenticateInQuery(@Valid Credentials credentials){
        // Do some stuff here to verify given credentials
        // You will never reach this point if either credentials.login or credentials.password field is empty !
    }
}

Creusons un peu plus l’API Bean Validation maintenant : nous avons vu précédemment qu’il était possible de faire des validations conditionnelles en utilisant les « groupes » bean validation.
Dans notre cas, nous aimerions faire en sorte de pouvoir activer (ou non) le groupe ValidationMode.Update lors de la validation de nos beans (pour, par exemple, rendre les champs id obligatoires ou non en fonction de si on est en création/modification).

Problème : l’annotation @Valid de bean validation ne permet pas de spécifier des groupes de validation (j’avais d’ailleurs pesté sur twitter à l’époque où j’avais rencontré ce problème !).
A l’époque, j’avais contourné le problème en faisant un interceptor Spring à la main. D’ailleurs, avec du recul, ce n’était pas vraiment un souci lié à bean validation mais plutôt à Spring : ce dernier avait en fait détourné l’annotation @Valid pour activer la validation sur le bean visé (j’avais mis longtemps avant de comprendre pourquoi « mon annotation maison » nommée @Valid (dans un package différent du @Valid bean validation) était tout de même validée via Spring… en allant voir le source de spring mvc, j’ai compris pourquoi ;)). Après tout, l’annotation @Valid avait pour objectif initial de définir un noeud du graphe d’objet comme étant à valider lors de l’appel du Validator, pas vraiment de déclencher la validation.

… fin de la parenthèse sur ce problème car depuis, la version 3.1 de Spring a été releasée, avec notamment cette issue résolue ! (merci Gildas pour le pointeur ;))
En effet, depuis Spring 3.1, vous pouvez dorénavant utiliser l’annotation Spring (et non bean validation !) @Validated qui, elle, peut prendre des groupes de validation en paramètre pour définir quels groupes seront activés durant l’appel du Validator.

Nous pourrions donc avoir un controleur pour l’inscription, tel que celui-ci :

package fr.fcamblor.demos.sbjd.web.registration;

import fr.fcamblor.demos.sbjd.models.User;
import fr.fcamblor.demos.sbjd.stereotypes.ValidationMode;
import fr.fcamblor.demos.sbjd.web.holders.UserHolder;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Controller
public class RegistrationController {

    @RequestMapping("/")
    public String welcomeView(){
        return "welcome"; // Will forward to welcome.jsp file
    }

    @RequestMapping(value="/users", method=RequestMethod.POST)
    public @ResponseBody User registerUser(@RequestBody @Validated({ Default.class, ValidationMode.Create.class }) User user){
        UserHolder.store(user); // Storing user in session
        return user;
    }

    @RequestMapping(value="/users", method=RequestMethod.GET)
    public @ResponseBody List retrieveStoredUsers(){
        return UserHolder.users();
    }

    @RequestMapping(value="/users", method=RequestMethod.PUT)
    public @ResponseBody User updateRegisteredUser(@RequestBody @Validated({ Default.class, ValidationMode.Update.class }) User user){
        UserHolder.update(user); // Updating stored user infos
        return user;
    }

    @RequestMapping(value="/users/registered", method=RequestMethod.GET)
    public @ResponseBody User registeredUser(){
        return UserHolder.loggedUser(); // Retrieving stored user infos
    }
}

Quelques points à noter là encore :

  • Je ne m’étendrai pas sur les annotations @Controller et @RequestMapping qui sont, je pense, compréhensibles
  • L’annotation @Validated activera « strictement » les groupes qu’on lui passe en paramètre : si je n’avais pas passé le groupe Default.class, toutes les annotations sans groupe défini (et donc attachées par défaut au groupe Default) ne seraient alors pas activées. J’ai récemment pesté sur ce point dans les forums de Spring car je trouve que cela nuit à la lisibilité globale, et ne rend pas « naturelle » la définition des annotations bean validation (lorsqu’on écrit @NotNull « sans groupe », ne vous attendez-vous pas à ce que cette contrainte soit tout le temps activée ?).
    A noter que j’ai du mal à savoir s’il s’agit d’un problème Spring (lié à @Validated) ou un problème bean validation (lié à la « philosophie » des groupes de validation).
    EDIT : Il est possible de facilement contourner ce problème en faisant hériter vos stéréotypes de groupes de validation de la classe javax.validation.groups.Default. Dans ce cas, votre groupe sera considéré comme un groupe déclenchant les validations par défaut.
  • Tel que précisé en commentaire, si une méthode de controller retourne une chaîne (non annotée), il s’agit en fait du nom de la vue (JSP dans le cas du projet sur github)
  • L’annotation @ResponseBody est une annotation qui retranscrit l’objet retourné par la méthode, en un format correspondant avec le header HTTP « Accept » envoyée par le client (par exemple, si ce header est valorisé à « application/json », c’est du JSON qui sera renvoyé, de même pour du XML)
  • L’annotation @RequestBody est le miroir de la précédente : elle permettra à Spring de deserializer le contenu (body) de la requête HTTP (uniquement sur un POST / PUT, car un GET / DELETE ne possède pas de body mais uniquement des query parameters) en fonction du header « Content-Type » (là encore, si le content type est « application/json », le body pourra être envoyé sous la forme d’une chaîne JSON)
  • Pour ceux qui auraient remarqué, le prototype de la méthode AuthenticationController. authenticateInQuery(@Valid Credentials) évoquée précédemment devrait être retravaillée en AuthenticationController. authenticateInQuery(@Validated({Default.class, ValidationMode.Authent.class}) Credentials) pour être consistant avec son Controller voisin, mais également (et surtout !) pour pouvoir activer la contrainte @NotNull sur le password.
  • Si vous comparez le prototype de AuthenticationController.authenticateInQuery() avec celui des méthodes ci-dessus, vous pourrez constater que je n’ai pas déclaré de @RequestBody. Est-ce un oubli ? Et bien non, c’est une omission volontaire pour illustrer que Spring MVC, par défaut, essaiera d’introspecter les paramètres de ses méthodes à l’aide des query parameters (à ne pas confondre avec le body) de la requête : en appelant l’url /auth/authenticateQuery?login=foo&password=bar, l’objet Credentials sera correctement alimenté !
    A noter qu’il s’agit d’un très mauvais exemple : le mot de passe s’affichant en clair dans les paramètres de l’url !

Limitations : valider des collections / arrays racine

Il existe une limitation, aussi bien avec l’annotation @Valid qu’avec l’annotation @Validated : il n’est pas possible de valider des objets de type array ou collection s’ils s’agit de la racine du graphe d’objets.

Par exemple, les méthodes suivantes ne valideront pas le paramètre « addresses » passé (vous pourrez tout à fait passer des Address avec un état invalide, aux méthodes du Controller : ce dernier ne se plaindra pas !) :

@Controller
public class RegistrationController {

    //...

    @RequestMapping(value="/users/{userId}/addresses", method=RequestMethod.PUT)
    public @ResponseBody Address[] addAddresses(@PathVariable Long userId, @RequestBody @Validated Address[] addresses){
        UserHolder.addUserAddresses(userId, Arrays.asList(addresses));
        return addresses;
    }

    @RequestMapping(value="/users/{userId}/addressList", method=RequestMethod.PUT)
    public @ResponseBody List</pre>
<address>addAddresses(@PathVariable Long userId, @RequestBody @Validated List
<address>addresses){
 UserHolder.addUserAddresses(userId, addresses);
 return addresses;
 }

 //...
}

Je n’ai personnellement pas d’explication rationnelle à cela (car la validation de collections / arrays est tout à fait possible sur des attributs d’une classe). En revanche, un workaround, certes un peu lourd, est d’encapsuler vos tableaux / collections au niveau d’une classe (un Wrapper) en les annotant d’un @Valid.

Traiter les erreurs de validation de manière générique coté serveur

Par défaut, lorsque vous aurez une erreur de validation sur un appel de controller, Spring renverra une erreur 400 sans fournir aucune information au client concernant les erreurs de validation survenues (ces infos ne seront disponibles qu’au niveau de la stacktrace affichée coté serveur).

Spring fournit une annotation intéressante pour gérer les exceptions survenues au niveau d’un controller : @ExceptionHandler.
Par exemple, nous pourrions très bien avoir le code suivant :

@Controller
public class AuthenticationController {

    /* ... some @RequestMapping methods here... */

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(value = HttpStatus.PRECONDITION_FAILED)
    public @ResponseBody List handleValidationFailure(MethodArgumentNotValidException exception) {
        return exception.getBindingResult().getAllErrors();
    }
}

Quelques points à noter :

  • Lorsqu’on obtient une erreur de validation (MethodArgumentNotValidException), on renverra un statut 412 « Precondition failed » (ça sera un code erreur bien utile ensuite, coté client, pour « identifier » une erreur de validation facilement : nous verrons cela plus tard). J’avoue détourner un petit peu la signification initiale dans la spec de ce code erreur, cependant, « sémantiquement », le message « Precondition failed » me paraît être plutôt adapté !
  • Il est tout à fait possible d’utiliser l’annotation @ResponseBody pour retourner un objet (au format JSON par exemple) comportant l’ensemble des erreurs bean validation (contenant notamment le champ visé et tous les messages d’erreurs, internationalisés dans la locale de l’utilisateur)
  • Fait troublant : la validation est un rare cas où Spring n’a pas une gestion homogène des exceptions ! Si vous validez un body content (annotation @RequestBody), vous obtiendrez une MethodArgumentNotValidException, à contrario, si vous validez des query parameters (pas d’annotation @RequestBody), vous obtiendrez une BindException. Ces 2 exceptions encapsulent toutefois un BindingResult, contenant les erreurs de validation.
  • Le code présenté est clair et concis, mais il faudra penser à le déclarer dans chacun des controllers spring : pas optimal pour garantir une gestion unifiée des erreurs de validation (à fortiori lorsqu’on doit être conscient de devoir gérer tantôt des MethodArgumentNotValidException et tantôt des BindException)… nous allons voir comment améliorer cela !

Spring fournit un mécanisme de gestion des exceptions via des « exceptionsResolvers » configurés dans la configuration WebMVC. Pour rajouter les vôtres, vous aurez besoin d’overrider la classe WebMvcConfigurationSupport de Spring, qui sert à déclarer la configuration spring MVC « par défaut » de votre application.

package fr.fcamblor.demos.sbjd.web.config;

import fr.fcamblor.demos.sbjd.web.exceptions.GlobalExceptionHandlerMethodExceptionResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.util.List;

@Configuration
public class ProjectWebMvcConfigurationSupport extends WebMvcConfigurationSupport {

    @Override
    protected void configureHandlerExceptionResolvers(List exceptionResolvers) {
        super.configureHandlerExceptionResolvers(exceptionResolvers);

        // Adding GlobalExceptionHandlerMethodExceptionResolver that will allow to provide
        // "global" (not @Controller specific) @ExceptionHandler
        GlobalExceptionHandlerMethodExceptionResolver bindingResultMethodExceptionResolver = new GlobalExceptionHandlerMethodExceptionResolver();
        bindingResultMethodExceptionResolver.setMessageConverters(getMessageConverters());
        bindingResultMethodExceptionResolver.afterPropertiesSet();
        exceptionResolvers.add(bindingResultMethodExceptionResolver);
    }
}

Parmi les « exceptionsResolvers » par défaut, il existe la classe ExceptionHandlerExceptionResolver dont l’objectif est de rechercher toutes les annotations @ExceptionHandler du controller courant, et d’exécuter la méthode adéquat en fonction de l’exception catchée. Dans l’exemple ci-dessous de la classe GlobalExceptionHandlerMethodExceptionResolver, nous allons modifier un peu le comportement de cette implémentation pour faire en sorte de rechercher les @ExceptionHandler dans l’ExceptionResolver (et non dans le controller courant), de manière à permettre la définition d’ExceptionHandlers « globaux ».

/**
 * Exception resolver used to manage globally exception handlers : instead of having
 * several @ExceptionHandler(X.class) in every @Controller,
 */
public class GlobalExceptionHandlerMethodExceptionResolver extends ExceptionHandlerExceptionResolver {

    private static final ExceptionHandlerMethodResolver CURRENT_CLASS_EXCEPTION_HANDLER_RESOLVER =
            new ExceptionHandlerMethodResolver(GlobalExceptionHandlerMethodExceptionResolver.class);

    @Override
    // Instead of looking at handlerMethod.bean instance, looking at current exception resolver instance
    // which should contain @ExceptionHandler annotated methods
    protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
        Method method = CURRENT_CLASS_EXCEPTION_HANDLER_RESOLVER.resolveMethod(exception);
        return (method != null ? new ServletInvocableHandlerMethod(this, method) : null);
    }

    // These @ExceptionHandler methods will be executed with the @Controller ones !
    @ExceptionHandler(BindException.class)
    @ResponseStatus(value = HttpStatus.PRECONDITION_FAILED)
    public @ResponseBody
    List handleBindingFailure(BindException exception) {
        return exception.getBindingResult().getAllErrors();
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(value = HttpStatus.PRECONDITION_FAILED)
    public @ResponseBody
    List handleBindingFailure(MethodArgumentNotValidException exception) {
        return exception.getBindingResult().getAllErrors();
    }
}

A noter que si un @ExceptionHandler(BindException.class) est défini au niveau du @Controller, ce dernier prévaudra sur celui du ExceptionResolver (car le GlobalExceptionHandlerMethodExceptionResolver a été ajouté dans la configuration mvc en fin de liste : le traitement par défaut, qui est de regarder dans le Controller, passera avant le traitement du GlobalExceptionHandlerMethodExceptionResolver)

EDIT : Jean-Baptiste, dans les commentaires, me fait part de l’utilisation d’un @ControllerAdvice qui semble, en effet, être plus simple que d’avoir à déclarer un Exception handler via une WebMvcConfigurationSupport. N’hésitez pas à y jeter un oeil (un exemple par ici).

Tester la validation (et l’API REST en général) avec Rest Assured

Rest-Assured est un framework java créé par Johan Haleby qui permet de tester, via une fluent API, une application REST.

Première étape : créer une @Rule JUnit pour démarrer un serveur tomcat embedded. Une @Rule Junit est en fait un traitement réutilisable qui sera exécuté avant/après chacun de vos @Test (une sorte de @Before / @After, mais réutilisable entre plusieurs classes de test). Rien de bien compliqué grâce à l’artefact org.apache.tomcat.embed:tomcat-embed-core :

package fr.fcamblor.demos.sbjd.test.rules;

import com.jayway.restassured.RestAssured;
import fr.fcamblor.demos.sbjd.web.main.EmbeddedTomcat;
import org.junit.rules.ExternalResource;

/**
 * @author fcamblor
 * JUnit rule which will ensure tomcat embed server is started during the test
 * It will configure rest assured to connect to started tomcat port, too.
 */
public class RequiresRunningEmbeddedTomcat extends ExternalResource {

    static private Object writeTomcatLock = new Object();
    static protected EmbeddedTomcat tomcat = null;

    @Override
    protected void before() throws Throwable {
        super.before();

        synchronized (writeTomcatLock){
            // Starting tomcat embed only once per JVM, sharing static instance across rule instances
            if(tomcat == null){
                tomcat = new EmbeddedTomcat();
                tomcat.start();
                RestAssured.port = tomcat.getWebPort();
            }
        }
    }
}

Vous pouvez consulter l’implémentation (maison) d’EmbeddedTomcat, simple surcouche à la lib tomcat-embed.

Nous allons maintenant écrire nos premiers tests de notre API REST avec Rest-Assured :

package fr.fcamblor.demos.sbjd.web;

import fr.fcamblor.demos.sbjd.test.rules.RequiresDefaultRestAssuredConfiguration;
import fr.fcamblor.demos.sbjd.test.rules.RequiresRunningEmbeddedTomcat;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.http.HttpStatus;

import static com.jayway.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.*;

public class AuthenticationControllerTest {

    private static final int VALIDATION_ERROR_HTTP_STATUS_CODE = HttpStatus.PRECONDITION_FAILED.value();
    private static final int VALIDATION_OK_HTTP_STATUS_CODE = HttpStatus.OK.value();

    @Rule
    // Ensuring a tomcat server will be up and running during test execution !
    public RequiresRunningEmbeddedTomcat tomcat = new RequiresRunningEmbeddedTomcat();

    @Test
    public void nullCredentialsShouldntBeAcceptedInQueryParams(){
        given().
                param("login").
                param("password").
        expect().
                statusCode(VALIDATION_ERROR_HTTP_STATUS_CODE).
        when().
                post("/auth/authenticateQuery");
    }

    @Test
    public void filledCredentialsShouldReturnOk(){
        given().
                param("login", "foo@bar.com").
                param("password", "bar").
        expect().
                statusCode(not(equalTo(VALIDATION_ERROR_HTTP_STATUS_CODE))).
        when().
                post("/auth/authenticate");
    }
}

Comme vous pouvez le constater, c’est clair, lisible et concis ! J’adore ce framework .. je l’aime d’ailleurs tellement que je m’en suis même servi dans du code de production pour consommer des services REST (en attendant JAX-RS 2.0 qui spécifiera ce à quoi devra ressembler une API client REST)

Si vous êtes curieux, vous pouvez aller parcourir d’autres tests unitaires rest assured présents dans la classe RegistrationControllerTest, plus étoffée, qui teste la plupart des cas d’utilisation -plus ou moins tordus- bean validation qu’on peut rencontrer.

Appeler et gérer la validation serveur depuis le client avec JQuery

Préambule : serializer les champs d’un formulaire sous la forme d’un graphe d’objets

Avant toute chose, pour pouvoir valider des paramètres, nous aurons besoin de les envoyer au format JSON afin que Jackson, coté serveur, puisse les deserializer sous la forme d’un graphe d’Objets.

Typiquement, ce qui nous intéresse dans cette section est de serializer les champs suivants :

</pre>
<form><input type="text" name="firstName" value="foo" />
 <input type="text" name="lastName" value="bar" />
 <input type="text" name="credentials.login" value="foo@bar.com" />
 <input type="password" name="credentials.password" value="hello" /></form>
<pre>

Sous la forme du JSON qui suit (ie un JSON « arborescent » basé sur le séparateur « . » dans les noms de champs) qui pourra être deserializé dans un POJO Java par Jackson :

{
    "firstname": "foo",
    "lastname": "bar",
    "credentials": {
        "login": "foo@bar.com",
        "password": "hello"
    }
}

Dans JQuery et la lib JSON, il existe un certain nombre de méthodes utilitaires qui s’approchent plus ou moins près de ce besoin :

  • JQuery.serialize(), qui permet de serializer les champs d’un formulaire sous la forme d’une chaîne de caractère (au format type query parameters HTTP). Pas vraiment utile dans notre cas : on aimerait bien serializer notre formulaire en JSON (et non en query parameters !)
  • JSON.stringify(), qui permet de transformer un objet JS en sa représentation JSON. Pas mal, le problème étant que cette méthode ne se base pas du tout sur un formulaire (il faudrait donc faire le travail de récupérer les infos du formulaire à la main). Je garde toutefois cette méthode de coté car elle me sera sûrement utile si j’arrive à faire une représentation objet de mon formulaire, et que je souhaite l’envoyer en JSON ensuite.
  • JQuery.serializeArray(), qui est sûrement la plus proche de mon besoin car elle permet de serializer un formulaire sous la forme JSON. Elle possède cependant 2 inconvénients :
    • Le JSON est « à plat » : c’est à dire que si vous avez des « . » dans vos noms de champ, ces derniers ne seront pas considérés, représentant le formulaire précédent sous la forme :
      {
          "firstname": "foo",
          "lastname": "bar",
          "credentials.login": "foo@bar.com",
          "credentials.password": "hello"
      }
      
    • Toutes les valeurs seront des chaînes, alors qu’on voudra peut-être faire des traitements particuliers sur des cas particuliers (par exemple, sur les champs date, envoyer cette dernière sous la forme d’une date standardisée telle que préconisé dans la RFC 3339)

Après ces recherches, je me suis donc dit qu’il faudrait plutôt que je code cette conversion moi-même, dans le plugin JQuery suivant :

(function( $ ){

    /**
     * Goal of this method is to retrieve every input fields located inside current selector(s),
     * retrieve their values and create a JSON object with them
     * Note that if input field name has "dots" (example : "foo.bar.baz"), it will be transformed into
     * a hierarchical JSON representation ({ foo: { bar: { baz: "value" } } })
     * If several fields has the same name, a value array will be created
     */
    $.fn.inputsToJSON = function(){
        var jsonObject = {};
        this.each(function(){
            $(this).find(':input').each(function() {
                var name = $(this).attr('name');
                // If name is empty, we shouldn't set anything here ...
                if(!name){
                    return;
                }

                var value = null;
                switch(this.type) {
                    case 'text':
                        if($(this).hasClass("datepicker")){
                            value = $(this).datepicker("getDate");
                            break;
                        }
                    case 'select-multiple':
                    case 'select-one':
                    case 'password':
                    case 'textarea':
                    case 'radio':
                        value = $(this).val();
                        break;
                    case 'checkbox':
                        if($(field).val() === "on"){
                            value = this.checked;
                        } else {
                            value = this.checked?$(this).val():null;
                        }
                        break;
                }

                // Empty string will be considered as null value
                if(value === ""){
                    value = null;
                }

                var pathChunks = name.split(".");
                var currentNode = jsonObject;
                $.each(pathChunks, function(i, pathChunk){
                    // Leaf
                    if(i === pathChunks.length-1){
                        // If leaf already exists...
                        if(currentNode[pathChunk]){
                            // .. and it's not yet an array, we should transform it into an array !
                            if(!$.isArray(currentNode[pathChunk])){
                                currentNode[pathChunk] = [ currentNode[pathChunk] ];
                            }
                            // then append current value to the array's values
                            currentNode[pathChunk].push(value);
                        } else {
                            currentNode[pathChunk] = value;
                        }
                    // Node
                    } else {
                        if(!currentNode[pathChunk]){
                            currentNode[pathChunk] = {};
                        } /* else : pathChunk already created by a previous field, let's traverse it ! */
                        currentNode = currentNode[pathChunk];
                    }
                });
            });
        });
        return jsonObject;
    };

})( jQuery );

//Test if a str is a JSON
function isJSON(str) {
    if (blank(str)) return false;
    str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@');
    str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']');
    str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, '');
    return (/^[\],:{}\s]*$/).test(str);
};

function blank(str) {
    return /^\s*$/.test(this);
};

A partir de maintenant, nous sommes donc en mesure de serializer les données d’un formulaire en JSON pour les envoyer en AJAX au serveur (dans le but de les valider).

Gérer les erreurs de validation de manière générique via l’implémentation d’un handler JQuery ajax

La seconde partie maintenant, est de traiter les erreurs de validation générées coté serveur, pour afficher les messages retournés. Nous avons vu précédemment que ces erreurs pouvaient être facilement identifiables à l’aide du code HTTP 412.
JQuery offre un mécanisme de handlers génériques appelé à chaque erreur d’un appel AJAX. Rien de plus facile, donc :

/**
 * Overriding default behaviour for ajax errors : looking at jquery XMLHttpRequest's status
 * and, if it is the special 412 (Precondition failed) status _and_ response is a JSON representation,
 * then call $.displaySpringErrors()
 */
$(document).ajaxError(function(event, jqXHR, settings, exception){
    if(jqXHR.status === 412 && isJSON(jqXHR.responseText)){
        var springBindingResults = $.parseJSON(jqXHR.responseText);
        $.displaySpringErrors(springBindingResults);
    }
});

Je n’afficherai pas le code de la fonction displaySpringErrors() ici (bien trop verbeux !), mais il est facilement consultable sur github si vous êtes curieux !

J’appellerai donc mon serveur de la manière suivante :

            $("#okButton").live("click", function(){
                var userInfos = $("form").cleanSpringErrors().inputsToJSON();
                $.ajax({
                    'type': "POST",
                    'url': "/users",
                    'cache': false,
                    'contentType': 'application/json',
                    'data': JSON.stringify(userInfos),
                    'dataType': 'json',
                    'success': function(){
                        alert("creation ok");
                    },
                    'error': function(jqXHR, textStatus, errorThrown){
                        alert("erreur (de validation, ou autre) !");
                    }
                });
            });

Si une erreur de validation survient, les champs incriminés seront mis en surbrillance, et les messages d’erreur seront affichés près de ces derniers, le callback d’erreur sera également appelé. Si aucune erreur de validation (puis aucune erreur dans le traitement de la méthode du serveur) ne survient, le callback de success sera appelé.

Vous pouvez constater ce comportement en récupérant les sources sur github et :

  • Soit en l’important dans votre IDE favori, et en lançant la classe fr.fcamblor.demos.sbjd.web.main.Main
  • Soit en lançant la commande maven suivante :
    mvn clean org.mortbay.jetty:jetty-maven-plugin:run
    

Ceci clos cet article, vous devriez maintenant être en mesure de valider, coté client, vos formulaires, en vous basant sur les annotation bean validation du serveur. N’hésitez pas à réagir dans les commentaires si vous pensez que j’ai fait une coquille, ou que certaines parties du code sont simplifiables. Vous pouvez également faire des pull request github sur le projet, je mettrai à jour l’article en conséquence si besoin !

Ressources utilisés durant l’article :
http://doanduyhai.wordpress.com/2012/05/06/spring-mvc-part-v-exception-handling/
A compléter

7 Réponses to “Valider ses POJOs en REST avec Bean validation, Spring MVC et JQuery”

  1. David Pilato (@dadoonet) Says:

    Vraiment excellent ton article !
    Très complet (du back end au front end avec les tests en plus). Cool.

  2. michaelisvy (@michaelisvy) Says:

    Bonjour,
    tout d’abord félicitations pour ce bel article ! Il est très bien écrit et très documenté.
    Il se trouve que je préparais quelque chose de similaire pour le blog de SpringSource. Je vais essayer de ne pas trop m’inspirer :).

  3. Thierry BENDA Says:

    Bonjour,

    Pour serialiser tes formulaires avec JQuery, tu as un excellent plugin form.jquery.plugin.js sur http://jquery.malsup.com/form/

  4. Frédéric Camblor Says:

    J’avais déjà trouvé ce plugin JQuery, cependant, je n’ai pas l’impression qu’il fasse beaucoup plus qu’un JQuery.serialize().

    Entendons-nous bien : ce que je veux, c’est être en mesure de :
    – Mettre dans le body de ma request un contenu au format JSON (et non de la forme « query parameters » : f1=val1&f2=val2…)
    – Je veux que le JSON soit « hiérarchique » et non « à plat », en utilisant le séparateur « . » pour dire que je change de niveau (le champ nommé « foo.bar » doit être représenté { foo: { bar: « value » } })

    Je n’ai pas l’impression que form.jquery.plugin permette de faire cela, en tous cas pas dans les exemples qu’ils présentent ! (je n’ai pas plus creusé que ça).

  5. Jean-Baptiste Nizet Says:

    Super article. Je ne sais pas si ça existait déjà au moment où l’article a été écrit, mais pour gérer une exception gobalement dans Spring MVC, le plus simple est de créer un (ou plusieurs) composant annoté avec @ControllerAdvice, et d’y placer les méthodes @ExceptionHandler désirées. Un @ControllerAdvice est un bean Spring, qui peut être découvert par scanning comme les controllers, et les méthodes @ExceptionHandler s’appliquent à tous les controllers de l’application. Bien plus simple que GlobalExceptionHandlerMethodExceptionResolver.

    • Frédéric Camblor Says:

      En effet, les @ControllerAdvice semblent correspondre exactement à ce cas de figure, il ne me semble pas que ça existait à l’époque (car c’est en effet bien plus concis à mettre en place :-))

      Merci pour l’info, j’ai mis à jour l’article pour y faire référence.


Laisser un commentaire