Licence CC BY-NC-SA

Formulaires : à la mode MVC

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

Le modèle MVC est très clair sur ce point : c'est le modèle qui doit s'occuper de traiter les données. Le contrôleur doit avoir pour unique but d'aiguiller les requêtes entrantes et d'appeler les éventuels traitements correspondants. Nous devons donc nous pencher sur la conception que nous venons de mettre en place afin d'en identifier les défauts, et de la rectifier dans le but de suivre les recommandations MVC.

Analyse de notre conception

La base que nous avons réalisée souffre de plusieurs maux :

  • la récupération et le traitement des données sont effectués directement au sein de la servlet. Or nous savons que d'après MVC, la servlet est un contrôleur, et n'est donc pas censée intervenir directement sur les données, elle doit uniquement aiguiller les requêtes entrantes vers les traitements correspondants ;
  • aucun modèle (bean ou objet métier) n'intervient dans le système mis en place ! Pourtant, nous savons que d'après MVC, les données sont représentées dans le modèle par des objets…

Voici à la figure suivante le schéma représentant ce à quoi nous souhaitons parvenir.

Nous allons donc reprendre notre système d'inscription pour y mettre en place un modèle :

  1. création d'un bean qui enregistre les données saisies et validées ;
  2. création d'un objet métier comportant les méthodes de récupération/conversion/validation des contenus des champs du formulaire ;
  3. modification de la servlet pour qu'elle n'intervienne plus directement sur les données de la requête, mais aiguille simplement la requête entrante ;
  4. modification de la JSP pour qu'elle s'adapte au modèle fraîchement créé.

Création du modèle

L'utilisateur

Pour représenter un utilisateur dans notre modèle, nous allons naturellement créer un bean nommé Utilisateur et placé dans le package com.sdzee.beans, contenant trois champs de type String : email, motDePasse et nom. Si vous ne vous souvenez plus des règles à respecter lors de la création d'un bean, n'hésitez pas à relire le chapitre qui y est dédié. Voici le résultat attendu :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.sdzee.beans;

public class Utilisateur {

    private String email;
    private String motDePasse;
    private String nom;

    public void setEmail(String email) {
    this.email = email;
    }
    public String getEmail() {
    return email;
    }

    public void setMotDePasse(String motDePasse) {
    this.motDePasse = motDePasse;
    }
    public String getMotDePasse() {
    return motDePasse;
    }

    public void setNom(String nom) {
    this.nom = nom;
    }
    public String getNom() {
    return nom;
    }
}

com.sdzee.beans.Utilisateur

C'est tout ce dont nous avons besoin pour représenter les données d'un utilisateur dans notre application.

Dans notre formulaire, il y a un quatrième champ : la confirmation du mot de passe. Pourquoi ne stockons-nous pas cette information dans notre bean ?

Tout simplement parce que ce bean ne représente pas le formulaire, il représente un utilisateur. Un utilisateur final possède un mot de passe et point barre : la confirmation est une information temporaire propre à l'étape d'inscription uniquement ; il n'y a par conséquent aucun intérêt à la stocker dans le modèle.

Le formulaire

Maintenant, il nous faut créer dans notre modèle un objet "métier", c'est-à-dire un objet chargé de traiter les données envoyées par le client via le formulaire. Dans notre cas, cet objet va contenir :

  1. les constantes identifiant les champs du formulaire ;
  2. la chaîne resultat et la Map erreurs que nous avions mises en place dans la servlet ;
  3. la logique de validation que nous avions utilisée dans la méthode doPost() de la servlet ;
  4. les trois méthodes de validation que nous avions créées dans la servlet.

Nous allons donc déporter la majorité du code que nous avions écrit dans notre servlet dans cet objet métier, en l'adaptant afin de le faire interagir avec notre bean fraîchement créé.

  • Pour commencer, nous allons nommer ce nouvel objet InscriptionForm, le placer dans un nouveau package com.sdzee.forms, et y inclure les constantes dont nous allons avoir besoin :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.sdzee.forms;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

public final class InscriptionForm {
    private static final String CHAMP_EMAIL  = "email";
    private static final String CHAMP_PASS   = "motdepasse";
    private static final String CHAMP_CONF   = "confirmation";
    private static final String CHAMP_NOM    = "nom";

    ...
}

com.sdzee.forms.InscriptionForm

  • Nous devons ensuite y ajouter la chaîne resultat et la Map erreurs :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
...

private String              resultat;
private Map<String, String> erreurs      = new HashMap<String, String>();

public String getResultat() {
    return resultat;
}

public Map<String, String> getErreurs() {
    return erreurs;
}

...

com.sdzee.forms.InscriptionForm

  • Nous ajoutons alors la méthode principale, contenant la logique de validation :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
...

public Utilisateur inscrireUtilisateur( HttpServletRequest request ) {
    String email = getValeurChamp( request, CHAMP_EMAIL );
    String motDePasse = getValeurChamp( request, CHAMP_PASS );
    String confirmation = getValeurChamp( request, CHAMP_CONF );
    String nom = getValeurChamp( request, CHAMP_NOM );

    Utilisateur utilisateur = new Utilisateur();

    try {
        validationEmail( email );
    } catch ( Exception e ) {
        setErreur( CHAMP_EMAIL, e.getMessage() );
    }
    utilisateur.setEmail( email );

    try {
        validationMotsDePasse( motDePasse, confirmation );
    } catch ( Exception e ) {
        setErreur( CHAMP_PASS, e.getMessage() );
        setErreur( CHAMP_CONF, null );
    }
    utilisateur.setMotDePasse( motDePasse );

    try {
        validationNom( nom );
    } catch ( Exception e ) {
        setErreur( CHAMP_NOM, e.getMessage() );
    }
    utilisateur.setNom( nom );

    if ( erreurs.isEmpty() ) {
        resultat = "Succès de l'inscription.";
    } else {
        resultat = "Échec de l'inscription.";
    }

    return utilisateur;
}

...

com.sdzee.forms.InscriptionForm

  • Pour terminer, nous mettons en place les méthodes de validation et les deux méthodes utilitaires nécessaires au bon fonctionnement de la logique que nous venons d'écrire :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
...

private void validationEmail( String email ) throws Exception {
    if ( email != null ) {
        if ( !email.matches( "([^.@]+)(\\.[^.@]+)*@([^.@]+\\.)+([^.@]+)" ) ) {
            throw new Exception( "Merci de saisir une adresse mail valide." );
        }
    } else {
        throw new Exception( "Merci de saisir une adresse mail." );
    }
}

private void validationMotsDePasse( String motDePasse, String confirmation ) throws Exception {
    if ( motDePasse != null && confirmation != null ) {
        if ( !motDePasse.equals( confirmation ) ) {
            throw new Exception( "Les mots de passe entrés sont différents, merci de les saisir à nouveau." );
        } else if ( motDePasse.length() < 3 ) {
            throw new Exception( "Les mots de passe doivent contenir au moins 3 caractères." );
        }
    } else {
        throw new Exception( "Merci de saisir et confirmer votre mot de passe." );
    }
}

private void validationNom( String nom ) throws Exception {
    if ( nom != null && nom.length() < 3 ) {
        throw new Exception( "Le nom d'utilisateur doit contenir au moins 3 caractères." );
    }
}

/*
 * Ajoute un message correspondant au champ spécifié à la map des erreurs.
 */
private void setErreur( String champ, String message ) {
    erreurs.put( champ, message );
}

/*
 * Méthode utilitaire qui retourne null si un champ est vide, et son contenu
 * sinon.
 */
private static String getValeurChamp( HttpServletRequest request, String nomChamp ) {
    String valeur = request.getParameter( nomChamp );
    if ( valeur == null || valeur.trim().length() == 0 ) {
        return null;
    } else {
        return valeur.trim();
    }
}

com.sdzee.forms.InscriptionForm

Encore une fois, prenez bien le temps d'analyser les ajouts qui ont été effectués. Vous remarquerez qu'au final, il y a très peu de changements :

  • ajout de getters publics pour les attributs privés resultat et erreurs, afin de les rendre accessibles depuis notre JSP via des expressions EL ;
  • la logique de validation a été regroupée dans une méthode inscrireUtilisateur(), qui retourne un bean Utilisateur ;
  • la méthode utilitaire getValeurChamp() se charge désormais de vérifier si le contenu d'un champ est vide ou non, ce qui nous permet aux lignes 4, 14 et 26 du dernier code de ne plus avoir à effectuer la vérification sur la longueur des chaînes, et de simplement vérifier si elles sont à null ;
  • dans les blocs catch du troisième code, englobant la validation de chaque champ du formulaire, nous utilisons désormais une méthode setErreur() qui se charge de mettre à jour la Map erreurs en cas d'envoi d'une exception ;
  • toujours dans le troisième code, après la validation de chaque champ du formulaire, nous procédons dorénavant à l'initialisation de la propriété correspondante dans le bean Utilisateur, peu importe le résultat de la validation (lignes 16, 24 et 31).

Voilà tout ce qu'il est nécessaire de mettre en place dans notre modèle. Prochaine étape : il nous faut nettoyer notre servlet !

Le découpage en méthodes via setErreur() et getValeurChamp() n'est pas une obligation, mais puisque nous avons déplacé notre code dans un objet métier, autant en profiter pour coder un peu plus proprement. :)

Reprise de la servlet

Puisque nous avons déporté la majorité du code présent dans notre servlet vers le modèle, nous pouvons l'épurer grandement ! Il nous suffit d'instancier un objet métier responsable du traitement du formulaire, et de lui passer la requête courante en appelant sa méthode inscrireUtilisateur() :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.sdzee.servlets;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.sdzee.beans.Utilisateur;
import com.sdzee.forms.InscriptionForm;

public class Inscription extends HttpServlet {
    public static final String ATT_USER = "utilisateur";
    public static final String ATT_FORM = "form";
    public static final String VUE = "/WEB-INF/inscription.jsp";

    public void doGet( HttpServletRequest request, HttpServletResponse response ) throws ServletException, IOException{
        /* Affichage de la page d'inscription */
        this.getServletContext().getRequestDispatcher( VUE ).forward( request, response );
    }

    public void doPost( HttpServletRequest request, HttpServletResponse response ) throws ServletException, IOException{
        /* Préparation de l'objet formulaire */
        InscriptionForm form = new InscriptionForm();

        /* Appel au traitement et à la validation de la requête, et récupération du bean en résultant */
        Utilisateur utilisateur = form.inscrireUtilisateur( request );

        /* Stockage du formulaire et du bean dans l'objet request */
        request.setAttribute( ATT_FORM, form );
        request.setAttribute( ATT_USER, utilisateur );

        this.getServletContext().getRequestDispatcher( VUE ).forward( request, response );
    }
}

com.sdzee.servlets.Inscription

Après initialisation de notre objet métier, la seule chose que notre servlet effectue est un appel à la méthode inscrireUtilisateur() qui lui retourne alors un bean Utilisateur. Elle stocke finalement ces deux objets dans l'objet requête afin de rendre accessibles à la JSP les données validées et les messages d'erreur retournés.

Dorénavant, notre servlet joue bien uniquement un rôle d'aiguilleur : elle contrôle les données, en se contentant d'appeler les traitements présents dans le modèle. Elle ne fait que transmettre la requête à un objet métier : à aucun moment elle n'agit directement sur ses données.

doGet() n'est pas doPost(), et vice-versa !

Avant de passer à la suite, je tiens à vous signaler une mauvaise pratique, malheureusement très courante sur le web. Dans énormément d'exemples de servlets, vous pourrez trouver ce genre de code :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ExempleServlet extends HttpServlet {
    public void doGet( HttpServletRequest request, HttpServletResponse response )   throws ServletException, IOException {
        /* Ne fait rien d'autre qu'appeler une JSP */
        this.getServletContext().getRequestDispatcher( "/page.jsp" ).forward( request, response );
    }

    public void doPost( HttpServletRequest request, HttpServletResponse response )  throws ServletException, IOException {
        /*
         * Ici éventuellement des traitements divers, puis au lieu
         * d'appeler tout simplement un forwarding... 
         */
        doGet(request, response);
    }
}

Exemple de mauvaise pratique dans une servlet

Vous comprenez ce qui a été réalisé dans cet exemple ? Puisque la méthode doGet() ne fait rien d'autre qu'appeler la vue, le développeur n'a rien trouvé de mieux que d'appeler doGet() depuis la méthode doPost() pour réaliser le forwarding vers la vue… Eh bien cette manière de faire, dans une application qui respecte MVC, est totalement dénuée de sens ! Si vous souhaitez que votre servlet réalise la même chose, quel que soit le type de la requête HTTP reçue, alors :

  • soit vous surchargez directement la méthode service() de la classe mère HttpServlet, afin qu'elle ne redirige plus les requêtes entrantes vers les différentes méthodes doXXX() de votre servlet. Vous n'aurez ainsi plus à implémenter doPost() et doGet() dans votre servlet, et pourrez directement implémenter un traitement unique dans la méthode service() ;
  • soit vous faites en sorte que vos méthodes doGet() et doPost() appellent une troisième et même méthode, qui effectuera un traitement commun à toutes les requêtes entrantes.

Quel que soit votre choix parmi ces solutions, ce sera toujours mieux que d'écrire que doGet() appelle doPost(), ou vice-versa !

Pour résumer, retenez bien que croiser ainsi les appels est une mauvaise pratique qui complique la lisibilité et la maintenance du code de votre application !

Reprise de la JSP

La dernière étape de notre mise à niveau est la modification des appels aux différents attributs au sein de notre page JSP. En effet, auparavant notre servlet transmettait directement la chaîne resultat et la Map erreurs à notre page, ce qui impliquait que :

  • nous accédions directement à ces attributs via nos expressions EL ;
  • nous accédions aux données saisies par l’utilisateur via l'objet implicite param.

Maintenant, la servlet transmet le bean et l'objet métier à notre page, objets qui à leur tour contiennent les données saisies, le résultat et les erreurs. Ainsi, nous allons devoir modifier nos expressions EL afin qu'elles accèdent aux informations à travers nos deux nouveaux objets :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<%@ page pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Inscription</title>
        <link type="text/css" rel="stylesheet" href="form.css" />
    </head>
    <body>
        <form method="post" action="inscription">
            <fieldset>
                <legend>Inscription</legend>
                <p>Vous pouvez vous inscrire via ce formulaire.</p>

                <label for="email">Adresse email <span class="requis">*</span></label>
                <input type="email" id="email" name="email" value="<c:out value="${utilisateur.email}"/>" size="20" maxlength="60" />
                <span class="erreur">${form.erreurs['email']}</span>
                <br />

                <label for="motdepasse">Mot de passe <span class="requis">*</span></label>
                <input type="password" id="motdepasse" name="motdepasse" value="" size="20" maxlength="20" />
                <span class="erreur">${form.erreurs['motdepasse']}</span>
                <br />

                <label for="confirmation">Confirmation du mot de passe <span class="requis">*</span></label>
                <input type="password" id="confirmation" name="confirmation" value="" size="20" maxlength="20" />
                <span class="erreur">${form.erreurs['confirmation']}</span>
                <br />

                <label for="nom">Nom d'utilisateur</label>
                <input type="text" id="nom" name="nom" value="<c:out value="${utilisateur.nom}"/>" size="20" maxlength="20" />
                <span class="erreur">${form.erreurs['nom']}</span>
                <br />

                <input type="submit" value="Inscription" class="sansLabel" />
                <br />

                <p class="${empty form.erreurs ? 'succes' : 'erreur'}">${form.resultat}</p>
            </fieldset>
        </form>
    </body>
</html>

/WEB-INF/inscription.jsp

Les modifications apportées semblent donc mineures :

  • l'accès aux erreurs et au résultat se fait à travers l'objet form ;
  • l'accès aux données se fait à travers le bean utilisateur.

Mais en réalité, elles reflètent un changement fondamental dans le principe : notre JSP lit désormais directement les données depuis le modèle !

Voici à la figure suivante un schéma de ce que nous avons réalisé.

Nous avons ainsi avec succès mis en place une architecture MVC pour le traitement de notre formulaire :

  1. les données saisies et envoyées par le client arrivent à la méthode doPost() de la servlet ;
  2. celle-ci ordonne alors le contrôle des données reçues en appelant la méthode inscrireUtilisateur() de l'objet métier InscriptionForm ;
  3. l'objet InscriptionForm effectue les traitements de validation de chacune des données reçues ;
  4. il les enregistre par la même occasion dans le bean Utilisateur ;
  5. la méthode doPost() récupère enfin les deux objets du modèle, et les transmet à la JSP via la portée requête ;
  6. la JSP va piocher les données dont elle a besoin grâce aux différentes expressions EL mises en place, qui lui donnent un accès direct aux objets du modèle transmis par la servlet ;
  7. pour finir, la JSP met à jour l'affichage du formulaire en se basant sur les nouvelles données.

  • Il faut utiliser un bean pour stocker les données du formulaire.
  • Il faut déplacer la validation et le traitement des données dans un objet métier.
  • La servlet ne fait alors plus qu'aiguiller les données : contrôle > appels aux divers traitements > renvoi à la JSP.
  • La méthode doGet() s'occupe des requêtes GET, la méthode doPost() des requêtes POST. Tout autre usage est fortement déconseillé.
  • La page JSP accède dorénavant aux données directement à travers les objets du modèle mis en place, et non plus depuis la requête.