Skip to content

7. Application QuiEst

Nous décrivons ici une application Struts un peu plus sophistiquée que celles qui ont précédé et qui se devaient d'être simples pour être pédagogiques.

7.1. La classe users

On dispose d'une classe Java mémorisant les informations sur les utilisateurs d'une machine Unix. Celles-ci sont enregistrées dans trois fichiers particuliers :

  • /etc/passwd : liste des utilisateurs
  • /etc/group : liste des groupes
  • /etc/aliases : liste des alias pour le courrier

Le contenu de ces trois fichiers est le suivant :

- /etc/passwd

Les lignes de ce fichier ont la forme suivante :


    login:pwd:uid:gid:id:dir:shell

avec

login

login de l'utilisateur

pwd

son mot de passe crypté

uid

son n° d'utilisateur

gid

son n° de groupe

id

son identité

dir

son répertoire de connexion

shell

son interpréteur de commandes

Ainsi la ligne d'un utilisateur pourrait être la suivante :

dupond:xg675SDFEkl09:110:57:Guillaume Dupond:/home/iup2-auto/dupond:/bin/bash

L'utilisateur précédent porte le n° 110 et appartient au groupe 57. C'est dans le fichier /etc/group qu'on trouvera la définition du groupe 57.

- /etc/group

Les lignes de ce fichier ont la forme suivante :


    nomGroupe:pwd:gid:membre1,membre2,....

avec

nomGroupe

nom du groupe

pwd

son mot de passe crypté - le plus souvent ce champ est vide

gid

le n° du groupe

membrei

logins d'utilisateurs - ce champ peut être vide

Ainsi la ligne du groupe 57 précédent pourrait être la suivante :

iup2-auto::57:

qui indique que le groupe 57 porte le nom iup2-auto.

- /etc/aliases

Les lignes de ce fichier ont la forme suivante :

alias:[tab]login

avec

alias

alias

[tab]

une ou plusieurs tabulations

login

login de l'utilisateur auquel appartient l'alias

Ainsi la ligne


    guillaume.dupond:    dupond

signifie que l'alias guillaume.dupond appartient à l'utilisateur de login dupond. Rappelons que les alias sont utilisés dans les adresses électroniques. Ainsi si dans l'exemple précédent, la machine Unix s'appelle shiva.istia.univ-angers.fr, un courrier adressé à guillaume.dupond@shiva.istia.univ-angers.fr sera délivré dans la boîte à lettres de l'utilisateur de login dupond de cette machine.

Nous ne nous intéresserons pas ici à la totalité de l'interface de la classe users mais simplement à son constructeur et quelques méthodes :

import java.io.*;
import java.util.*;

public class users{


  // attributs
  private Hashtable usersByLogin=new Hashtable();       // login --> login, pwd, ..., dir
     private ArrayList erreurs=new ArrayList();             // liste de messages d'erreur

....

  // constructeur
  public users(String usersFileName, String groupsFileName, String aliasesFileName) throws Exception {
        // usersFileName : nom du fichier des utilisateurs avec des lignes de la forme
        // login:pwd:uid:gid:id:dir:shell
        // groupsFileName : nom du fichier des groupes avec des lignes de la forme
        // nom:pwd:numéro:membre1,membre2,..
        // aliasesFileName : nom du fichier des alias avec des lignes de la forme
        // alias:[tab]login
        // construit le dictionnaire usersByLogin
....
    }// constructeur

    // liste des utilisateurs
  public Hashtable getUsersByLogin(){
    return usersByLogin;
  }

  // erreurs
  public ArrayList getErreurs(){
    return erreurs;
  }
usersByLogin

dictionnaire (Hashtable) dont les clés sont les logins du fichier passwd. La valeur associée à la clé est un tableau de chaînes (String [7]) dont les éléments sont les 7 champs de la ligne du fichier passwd associée au login. Certains champs peuvent être vides si la ligne a moins de 7 champs.

erreurs

liste de messages d'erreurs - vide si pas d'erreurs

7.2. L'application web quiest

On se propose de construire l'application web suivante (page formulaire) :

nom

type HTML

rôle

1

cmbLogins

<select ...>...</select>

présente la liste de tous les logins à propos desquels on peut demander de l'information

2

btnChercher

<input type="submit" ...>

pour lancer la recherche

Lorsque l'utilisateur appuie sur le bouton [Chercher] (2), le login de (1) est demandé à un objet U de type users. Si le login existe, on a la réponse suivante (page infos) :

Comme l'URL du navigateur le montre ci-dessus, les paramètres du formulaire sont envoyés au serveur par un GET. On peut donc donner directement au navigateur cette URL paramétrée. C'est ce que nous faisons ici, pour introduire un login qui n'existe pas. On a la réponse suivante (page erreurs) :

7.3. L'architecture de l'application

Nous trouvons dans cette architecture les composants suivants :

  • les vues :
    • logins.jsp, utilisée pour afficher la liste des logins (vue 1)
    • infos.jsp utilisée pour afficher les informations liées à un login (vue 2)
    • erreurs.jsp utilisée pour afficher une liste d'erreurs (vue 3)
  • les formulaires de type ActionForm utilisés par les actions :
    • formLogins utilisé pour collecter les données du formulaire logins.jsp
  • les actions :
    • SetupLoginAction qui prépare le contenu de formulaire.jsp puis affiche cette vue
    • InfosLoginAction qui traite le contenu de logins.jsp une fois celui-ci envoyé au serveur
    • ForwardAction qui traite le lien [Retour vers le formulaire] des vues infos.jsp et erreurs.jsp
  • la classe métier users utilisée par les actions pour obtenir leurs données
  • le modèle assuré par les trois fichiers plats passwd, group et aliases

7.4. Les fichiers de configuration de l'application web

7.4.1. Le fichier server.xml

Le contexte de l'application s'appellera / strutsquiest2. On ajoutera donc la ligne suivante dans le fichier server.xml de Tomcat :

    <Context path="/strutsquiest2" docBase="..." />

Ceci fait, nous relançons éventuellement Tomcat afin qu'il prenne en compte le nouveau contexte. Nous pouvons vérifier la validité de celui-ci en demandant l'URL http://localhost:8080/strutsquiest2.

7.4.2. Le fichier web.xml

Le fichier de configuration web.xml de l'application sera le suivant :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
<web-app>
    <servlet>
        <servlet-name>strutsquiest2</servlet-name>
        <servlet-class>istia.st.struts.quiest.Quiest2ActionServlet</servlet-class>
        <init-param>
            <param-name>config</param-name>
            <param-value>/WEB-INF/struts-config.xml</param-value>            
        </init-param>
        <init-param>
            <param-name>passwdFileName</param-name>
            <param-value>data/passwd</param-value>            
        </init-param>
        <init-param>
            <param-name>groupFileName</param-name>
            <param-value>data/group</param-value>            
        </init-param>        
    </servlet>

    <servlet-mapping>
        <servlet-name>strutsquiest2</servlet-name>
        <url-pattern>*.do</url-pattern>        
    </servlet-mapping>
</web-app>

Ce fichier web.xml apporte une nouveauté. Le contrôleur Struts n'est plus org.apache.struts.action.ActionServlet mais une classe dérivée qu'on a appelée ici istia.st.struts.quiest.Quiest2ActionServlet. Cela va nous permettre de récupérer les deux paramètres d'initialisation que sont passwdFileName (localisation du fichier passwd) et groupFileName (localisation du fichier group). Le fichier aliases est inutile dans cette application.

7.4.3. Le fichier struts-config.xml

Le fichier struts-config.xml sera le suivant :

<?xml version="1.0" encoding="ISO-8859-1" ?>

<!DOCTYPE struts-config PUBLIC
          "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
          "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">

<struts-config>
    <form-beans>
        <form-bean name="formLogins" type="org.apache.struts.action.DynaActionForm">
            <form-property name="cmbLogins" type="java.lang.String" initial=""/>
            <form-property name="tLogins" type="java.lang.String[]"/>            
        </form-bean>            
    </form-beans>

    <action-mappings>

      <action
          path="/init"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.SetupLoginsAction"
      >
            <forward name="afficherLogins" path="/vues/logins.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>            
        </action>

      <action
          path="/infosLogin"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.InfosLoginAction"
      >
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
            <forward name="afficherInfos" path="/vues/infos.jsp"/>            
        </action>

      <action
          path="/retourLogins"
            parameter="/vues/logins.jsp"
          type="org.apache.struts.actions.ForwardAction"
      />

    </action-mappings>

        <message-resources 
          parameter="istia.st.struts.quiest.ApplicationResources"
    null="false"
  />    

</struts-config>

On y retrouve les trois grandes sections :

  • la déclaration des formulaires dans la section <form-beans>
  • la déclaration des actions dans la section <action-mappings>
  • la déclaration du fichier de ressources dans <message-ressources>

7.4.4. Les objets (beans) formulaires de l'application

    <form-beans>
        <form-bean name="formLogins" type="org.apache.struts.action.DynaActionForm">
            <form-property name="cmbLogins" type="java.lang.String" initial=""/>
            <form-property name="tLogins" type="java.lang.String[]"/>            
        </form-bean>            
    </form-beans>    

Il n'y a qu'un bean formulaire dans notre application., appelé formLogins et de type dérivé de DynaActionForm. Il sera utilisé dans les situations suivantes :

  • contenir les données nécessaires à l'affichage de la vue n° 1
  • récupérer les valeurs du formulaire de la vue n°1 lorsque l'utilisateur va le valider (submit)

La structure du bean formLogins est liée au formulaire de la vue n° 1. Etudions celui-ci :

nom

type HTML

rôle

1

cmbLogins

<select ...>...</select>

présente la liste de tous les logins à propos desquels on peut demander de l'information

2

btnChercher

<input type="submit" ...>

pour lancer la recherche

Distinguons plusieurs cas :

  • du client vers le serveur, l'objet formLogins est utilisé pour contenir les valeurs du formulaire HTML ci-dessus qui sera posté par le bouton [Envoyer]. Il lui faut donc un champ cmbLogins qui recevra la valeur du champ HTML cmbLogins, c.a.d le login choisi par l'utilisateur.
  • du serveur vers le client, l'objet formLogins est utilisé pour donner le contenu initial de la vue n° 1. Son champ tLogins servira de contenu à la liste 1. Son champ cmbLogins permettra de fixer l'élément de la liste 1 à sélectionner.

7.4.5. Les actions de l'application

Les actions sont assurées par des objets de type Action ou dérivés. La configuration des actions est faite à l'intérieur des balises <action-mappings> :

    <action-mappings>

      <action
          path="/init"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.SetupLoginsAction"
      >
            <forward name="afficherLogins" path="/vues/logins.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>            
        </action>

      <action
          path="/infosLogin"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.InfosLoginAction"
      >
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
            <forward name="afficherInfos" path="/vues/infos.jsp"/>            
        </action>

      <action
          path="/retourLogins"
            parameter="/vues/logins.jsp"
          type="org.apache.struts.actions.ForwardAction"
      />

    </action-mappings>

L'action /init

      <action
          path="/init"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.SetupLoginsAction"
      >
            <forward name="afficherLogins" path="/vues/logins.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>            
        </action>

Décrivons le fonctionnement de l'action /init :

  • l'action /init a lieu normalement une unique fois lors du 1er cycle demande-réponse où l'utilisateur demande l'URL http://localhost:8080/strutsquiest2/init.do
  • l'objet formsLogins est créé ou recyclé. Il est récupéré (recyclage) ou placé (création) dans la session comme le demande l'attribut scope.
  • sa méthode reset est appelée. Rappelons que cette méthode ne fait rien par défaut dans les classes ActionForm et dérivées. Elle est appelée juste avant la recopie des données de la requête du client dans l'objet ActionForm et sert à nettoyer l'objet avant cette recopie. Quelle est ici, la requête du client ? L'action /init est déclenchée lorsque l'url demandée est http://localhost:8080/strutsquiest2/init.do. Cette URL peut être demandée par un GET ou un POST. Il suffit que l'on mette dans cette requête des paramètres portant le nom de champs de formLogins pour que ceux-ci soient initialisés comme le montre l'exemple suivant :

Image

  • la requête contient le paramètre cmbLogins (afterpak). Le contrôleur Struts a donc recopié la valeur de ce paramètre dans le champ cmbLogins de formLogins. L'action SetupLoginsAction s'est ensuite déroulée et s'est achevée par l'affichage de la vue logins.jsp. Cette vue a un formulaire dont certains champs reçoivent leur valeur de formLogins. Ainsi le champ HTML select appelé cmbLogins a reçu sa valeur du champ cmbLogins (=afterpak) de formLogins. C'est pourquoi, la liste de logins apparaît positionnée sur le login afterpak.
  • on pourrait s'amuser à passer également un paramètre tLogins de la façon suivante :
http://localhost:8080/strutsquiest2/init.do?cmbLogins=afterpak&tLogins=login1&tLogins=login2

Cela aurait pour effet d'initialiser le champ tLogins de formLogins avec un tableau {"login1","login2"}. Seulement, nous allons voir plus loin, que l'action SetupLoginsAction affecte une valeur au champ tLogins et remplace le tableau ainsi créé par un nouveau tableau. C'est ce dernier qui apparaît donc dans la vue logins.jsp.

  • la discussion précédente, bien qu'un peu complexe, a le mérite de montrer qu'on ne peut pas faire l'hypothèse que l'action /init sera déclenchée sans paramètres provenant du client. Il peut donc être utile d'utiliser la méthode reset pour nettoyer formLogins. Dans ce cas, il nous faudrait dériver la classe DynaActionForm. Nous ne l'avons pas fait ici.
    • une fois la méthode reset de formLogins appelée, le contrôleur recopie les données de la requête du client dans les champs de même nom de formLogins. Normalement, l'action /init est appelée sans paramètres du client mais nous avons montré précédemment que rien n'empêchait le client d'invoquer l'action /init avec des paramètres arbitraires. A l'issue de cette phase, les champs cmbLogins et tLogins peuvent donc très bien avoir une valeur. Nous avons vu que le champ cmbLogins conserverait cette valeur mais pas le champ tLogins.
    • le contrôleur regarde ensuite l'attribut validate de l'action. Ici, il a la valeur "false". La méthode validate de formLogins ne sera pas appelée. Nous ne l'écrirons donc pas.
    • l'objet SetupLoginsAction est créé ou recyclé s'il existait déjà et sa méthode execute lancée. Son seul rôle est d'affecter une valeur au champ tLogins de formLogins. Cette valeur est le tableau des logins, tableau qui sera demandé à la classe métier users. Cette opération peut échouer. C'est pourquoi l'action /init peut être suivie de deux vues :
      • la vue erreurs.jsp si la classe users n'a pu fournir le tableau des logins
      • la vue logins.jsp sinon
    • le contrôleur fera afficher l'une de ces deux vues
    • le cycle demande-réponse de l'action /init est terminé.

L'action /infosLogin

      <action
          path="/infosLogin"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.InfosLoginAction"
      >
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
            <forward name="afficherInfos" path="/vues/infos.jsp"/>            
        </action>

Décrivons le fonctionnement de l'action / infosLogin :

  • l'action /infosLogin a lieu normalement lorsque l'utilisateur clique sur le bouton [Chercher] de la vue logins.jsp. Une requête est alors faite au serveur, fixée par la balise HTML <form> de la vue :
<html:form name="formLogins" method="get" action="/infosLogin" type="org.apache.struts.action.DynaActionForm">
  • on voit que la requête est envoyée au serveur par la méthode get. L'utilisateur peut donc la taper à la main :

Image

  • l'objet formsLogins est créé ou recyclé. Il est récupéré (recyclage) ou placé (création) dans la session comme le demande l'attribut scope.
  • sa méthode reset est appelée juste avant la recopie des données de la requête du client dans l'objet ActionForm. Elle est normalement de la forme http://localhost:8080/strutsquiest2/infosLogin.do?cmbLogins=xxxx est un login choisi dans la liste des logins. Mais ce peut être aussi n'importe quoi si l'utilisateur a utilisé l'URL précédente en passant des paramètres arbitraires. Considérons la séquence de pages suivantes :

Image

  • l'action /infosLogin a été appelée avec la chaîne de paramètres cmbLogins=xx&tLogins=login1&tLogins=login2. Les champs cmbLogins et tLogins de formLogins vont donc recevoir respectivement les valeurs "xx" et {"login1","login2"}. L'action /infosLogin va demander à la classe métier users, les informations associées au login "xx". La classe users va lui répondre que ce login n'existe pas. D'où la vue envoyée ci-dessus. Maintenant, utilisons le lien [Retour au formulaire] ci-dessus :

Image

  • C'est l'action /retourLogins qui est déclenchée par le lien [Retour au formulaire]. Cette action se contente d'afficher la vue logins.jsp sans action intermédiaire. On se rappelle que le champ tLogins sert à alimenter la liste des logins de la vue logins.jsp. Comme l'utilisateur a changé cette valeur en {"login1","login2"}, ce sont ces deux logins qui apparaissent maintenant dans la liste. De nouveau, on ne peut qu'insister sur la nécessité absolue de prendre en compte dans le fonctionnement d'une application le cas des paramètres arbitraires fixés par un utilisateur ou un programme. La solution au problème posé ici serait que le lien [Retour au formulaire] vise l'action /init. Ainsi on serait sûr d'obtenir la liste correcte des logins.
  • Revenons à une requête normale à l'action /infosLogin du genre :

http://localhost:8080/strutsquiest2/infosLogin.do?cmbLogins=afterpak

  • Le contrôleur Struts va affecter une valeur au champ cmbLogins de l'objet ActionForm. Le champ tLogins lui ne sera pas affecté (pas de champ correspondant dans la requête envoyée). Ce fonctionnement nous convient. Nous n'aurons donc pas à écrire de méthode reset personnalisée pour formLogins.
    • une fois la méthode reset de formLogins appelée, le contrôleur recopie les données de la requête du client dans les champs de même nom de formLogins. Le champ cmbLogins va recevoir une valeur, le login choisi par l'utilisateur (afterpak).
    • le contrôleur regarde ensuite l'attribut validate de l'action. Ici, il a la valeur "false". La méthode validate de formLogins ne sera pas appelée.
    • l'objet InfosLoginAction est créé ou recyclé s'il existait déjà et sa méthode execute lancée. Son rôle est d'obtenir les informations associées au login cmbLogins. Ces informations seront demandées à la classe métier users. Cette opération peut échouer (login inexistant par exemple). C'est pourquoi l'action /infosLogin peut être suivie de deux vues :
      • la vue erreurs.jsp si la classe users n'a pu fournir les renseignements demandés
      • la vue infos.jsp sinon
    • le contrôleur fera afficher l'une de ces deux vues
    • le cycle demande-réponse de l'action /infosLogin est terminé.

L'action /retourLogins

      <action
          path="/retourLogins"
            parameter="/vues/logins.jsp"
          type="org.apache.struts.actions.ForwardAction"
      />
  • l'action /retourLogins est déclenchée par activation du lien [Retour au formulaire] des vues erreurs.jsp et infos.jsp.
  • ici il n'y a pas de formulaire associé à l'action. On passe donc tout de suite à l'exécution de la méthode execute d'un objet ForwardAction qui va rendre un objet ActionForward pointant sur la vue /vues/logins.jsp.

7.4.6. Le fichier de messages de l'application

La troisième section du fichier struts-config.xml est celui du fichier des messages :

        <message-resources 
      parameter="istia.st.struts.quiest.ApplicationResources"
    null="false"
  />        

Le fichier ApplicationResources.properties est placé dans WEB-INF/classes/istia/st/struts/quiest. Son contenu est le suivant :

errors.header=<ul>
errors.footer=</ul>
parametreManquant=<li>Le paramètre [{0}] n'a pas été initialisé</li>
usersException=<li>Erreur d'initialisation de l'application : {0}</li>
loginInconnu=<li>Le login [{0}] n'existe pas</li>

7.5. Le code des vues

Le lecteur est invité à relire la leçon sur la gestion des formulaires s'il ne comprend pas le code des vues présentées ci-dessous.

7.5.1. La vue logins.jsp

Rappelons que cette vue est affichée dans deux cas :

  • à l'appel de l'action /init lors du 1er cycle demande-réponse
  • à l'appel de l'action /retourLogins lors des cycles suivants

Le code de la vue logins.jsp est le suivant :

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>

<html>
    <head>
      <title>Quiest - formulaire</title>
  </head>
  <body background="<html:rewrite page="/images/standard.jpg"/>">
      <center>
        <h2>Application QuiEst</h2>
      <hr>
      <html:form name="formLogins" method="get" action="/infosLogin" type="org.apache.struts.action.DynaActionForm">
          <table>
            <tr>
              <td>Login cherché</td>
            <td>
                <html:select name="formLogins" property="cmbLogins">
                      <html:options name="formLogins" property="tLogins"/>
                  </html:select>
            </td>
            <td>
                <html:submit value="Chercher"/>
            </td>
          </tr>
        </table>
      </html:form>
    </center>
  </body>
</html>

7.5.2. La vue infos.jsp

Cette vue est affichée lors d'un appel réussi à l'action /infosLogin. Son code est le suivant :

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>

<html>
    <head>
      <title><bean:write name="infosLoginBean" scope="request" property="titre"/></title>
  </head>
  <body background="<html:rewrite page="/images/standard.jpg"/>">
      <h2><bean:write name="infosLoginBean" scope="request" property="titre"/></h2>
    <hr>
    <table border="1">
            <tr>
                <th>login</th><th>pwd</th><th>uid</th><th>gid</th><th>id</th><th>dir</th><th>shell</th>
            </tr>
            <tr>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[0]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[1]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[2]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[3]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[4]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[5]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[6]"/></td>            
            </tr>                                                                                                                                                                                                                
    </table>                                      
    <br>
    <html:link page="/retourLogins.do">
            Retour au formulaire
        </html:link>        
  </body>
</html>

Cette vue utilise un objet appelé infosLoginBean et placé dans la requête par l'action /infosLogin. Cet objet a deux champs :

String titre;                        // titre à afficher dans la vue
String[] infosLogin;        // tableau des informations à afficher dans la vue

Nous détaillerons cette classe lorsque nous aborderons le code de la classe InfosLoginAction.

7.5.3. La vue erreurs.jsp

Cette vue est affichée lorsque les actions /init ou /infosLogin se terminent par une erreur. Son code est le suivant :

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>

<html>
    <head>
      <title>Application QuiEst - erreurs</title>
  </head>
  <body background="<html:rewrite page="/images/standard.jpg"/>">
        <h2 align="center">Application QuiEst - Erreurs</h2>
        <hr>
      <h2>Les erreurs suivantes se sont produites</h2>
        <html:errors/>
    <html:link page="/retourLogins.do">
            Retour au formulaire
        </html:link>    
  </body>
</html>

7.6. Les classes Java

Le fichier web.xml référence une classe Java :

<web-app>
    <servlet>
      <servlet-name>strutsquiest2</servlet-name>
    <servlet-class>istia.st.struts.quiest.Quiest2ActionServlet</servlet-class>
....
  </servlet>

...
</web-app>

Le fichier de configuration struts-config.xml référence lui deux classes Java :

      <action
          path="/infosLogin"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.InfosLoginAction"
      >
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
            <forward name="afficherInfos" path="/vues/infos.jsp"/>            
        </action>

      <action
          path="/init"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.SetupLoginsAction"
      >
            <forward name="afficherLogins" path="/vues/logins.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>            
        </action>

7.6.1. La classe Quiest2ActionServlet

La classe Quiest2ActionServlet est dérivée de la classe ActionServlet, la classe du contrôleur Struts. Nous dérivons la classe ActionServlet pour personnaliser sa méthode init. En effet, cette méthode, exécutée une seule fois au moment du chargement initial de la servlet, va nous permettre de construire un objet métier de type users. Cet objet n'a en effet besoin d'être construit qu'une seule fois et la méthode init est un bon endroit pour faire cette construction. L'objet users a besoin de deux fichiers pour se construire, les fichiers passwd et group. La localisation de ces deux fichiers est passée en paramètres à la servlet dans le fichier web.xml de l'application :

    <servlet>
      <servlet-name>strutsquiest2</servlet-name>
    <servlet-class>istia.st.struts.quiest.Quiest2ActionServlet</servlet-class>
    <init-param>
        <param-name>config</param-name>
      <param-value>/WEB-INF/struts-config.xml</param-value>
    </init-param>
    <init-param>
        <param-name>passwdFileName</param-name>
      <param-value>data/passwd</param-value>
    </init-param>
    <init-param>
        <param-name>groupFileName</param-name>
      <param-value>data/group</param-value>
    </init-param>        
  </servlet>

Le code de la servlet est le suivant :

package istia.st.struts.quiest;

import java.util.*;
import javax.servlet.*;
import org.apache.struts.action.*;
import istia.st.users.*;

public class Quiest2ActionServlet
  extends ActionServlet {

  // attributs de la servlet
  private users u = null;
  private ActionErrors erreurs = new ActionErrors();
  private String[] tLogins;

  //init
  public void init() throws ServletException {

    // ne pas oublier d'initialiser la classe parent
    super.init();

    // variables locales
    final String[] initParams = {"passwdFileName", "groupFileName"};
    Properties params = new Properties();

    // on récupère les paramètres d'initialisation de la servlet
    ServletConfig config = getServletConfig();
    String servletPath = config.getServletContext().getRealPath("/");
    for (int i = 0; i < initParams.length; i++) {
      String valeur = config.getInitParameter(initParams[i]);
      if (valeur == null) {
        erreurs.add(ActionErrors.GLOBAL_ERROR, new ActionError("parametreManquant", initParams[i]));
        valeur = "";
      }
      // on mémorise le paramètre
      params.setProperty(initParams[i], valeur);
    } //for
    // retour s'il y a eu des erreurs d'initialisation
    if (erreurs.size() != 0) {
      return;
    }
    // on crée un objet users
    try {
      u = new users(servletPath + "/" + params.getProperty("passwdFileName"),
                    servletPath + "/" + params.getProperty("groupFileName"), null);
    }
    catch (Exception ex) {
      erreurs.add(ActionErrors.GLOBAL_ERROR, new ActionError("usersException", ex.getMessage()));
      return;
    } //catch
    // on récupère la liste des logins
    tLogins = new String[u.getUsersByLogin().size()];
    Enumeration eLogins = u.getUsersByLogin().keys();
    for (int i = 0; i < tLogins.length; i++) {
      tLogins[i] = (String) eLogins.nextElement();
    }
    // on trie les logins
    Arrays.sort(tLogins);
  } //init

  // méthode d'accès aux infos privées de la servlet
  public Object[] getInfos() {
    return new Object[] {erreurs, u, tLogins};
  }
}

Sommairement, le fonctionnement de la méthode init est le suivant :

  • tout d'abord, il y a appel à la méthode init de la classe parent (ActionServlet) pour que celle-ci s'initialise correctement
  • puis il y a lecture des paramètres d'initialisation. S'il en manque, l'attribut privé ActionErrors erreurs est renseigné.
  • si les paramètres d'initialisation sont bien là, un objet users est construit. Cette construction peut lancer une exception. Dans ce cas, l'attribut ActionErrors erreurs est renseigné.
  • si la construction s'est bien passée, on tire de l'objet créé la liste de tous les logins et on trie celle-ci dans un tableau que l'on met dans l'attribut privé String[] tLogins.
  • l'objet users construit est mémorisé dans l'attribut privé users u.
  • la méthode publique getInfos permet d'obtenir les trois attributs privés (u, erreurs, tLogins) dans un tableau d'objets.

7.6.2. La classe SetupLoginsAction

Cette action a pour but d'initialiser l'objet DynaActionForm formLogins. Cet objet placé dans la session n'aura plus besoin d'être réinitialisé par la suite. L'action SetupLoginsAction n'a donc lieu qu'une fois. Son code est le suivant :

package istia.st.struts.quiest;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.struts.action.*;

public class SetupLoginsAction
  extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
                               HttpServletRequest request, HttpServletResponse response) throws IOException,ServletException {

    // prépare le formulaire à afficher
    // on récupère les infos de la servlet contrôleur
    // infos=(ActionErrors erreurs, users u, String[] tLogins)
    Object[] infos = ( (Quiest2ActionServlet)this.getServlet()).getInfos();

    // y-a-t-il eu des erreurs d'initialisations ?
    ActionErrors erreurs = (ActionErrors) infos[0];
    if (!erreurs.isEmpty()) {
      this.saveErrors(request, erreurs);
      return mapping.findForward("afficherErreurs");
    }

    // on met les logins dans le formulaire
    DynaActionForm formLogins=(DynaActionForm) form;
    formLogins.set("tLogins",infos[2]);
    return mapping.findForward("afficherLogins");
  }
}

Comme pour toutes les actions Struts, le code se trouve dans la méthode execute. Celle-ci :

  • récupère auprès du contrôleur Struts les informations que celui-ci a mémorisées grâce à sa méthode init. C'est la méthode getServlet() de la classe Action qui permet cela.
  • parmi celles-ci se trouve l'attribut ActionErrors erreurs du contrôleur. Si cette liste d'erreurs est non vide, elle est placée dans la requête et on demande l'affichage de la vue erreurs.jsp.
  • si la liste des erreurs est vide, alors on affecte au champ tLogins du bean formLogins, la liste des logins créée initialement par le contrôleur. On demande ensuite l'affichage de la vue logins.jsp qui va afficher la liste des logins.

7.6.3. Les classes InfosLoginBean et InfosLoginAction

L'action InfosLoginAction a pour but de récupérer les informations associées au login choisi par l'utilisateur et de les présenter à celui-ci. Les informations seront rassemblées dans un objet de type InfosLoginBean :

package istia.st.struts.quiest;

public class InfosLoginBean implements java.io.Serializable{

  // bean détenant l'info nécessaire à la page d'infos
  private String titre;
  private String[] infosLogin;

  // constructeur
  public InfosLoginBean(String titre, String[] infosLogin){
    this.titre=titre;
    this.infosLogin=infosLogin;
  }

  // getters
  public String getTitre(){
    return this.titre;
  }
  public String[] getInfosLogin(){
    return this.infosLogin;
  }
  public String getInfosLogin(int i){
    return this.infosLogin[i];
  }
}

La classe précédente est un bean, c.a.d. une classe java où un attribut privé T unAttribut est automatiquement associé à deux méthodes privées :

  • void setUnAttribut(T valeur){unAttribut=valeur;}
  • T getUnAttribut(){ return unAttribut;}

On notera la syntaxe spéciale des méthodes get et set. Si l'attribut est un tableau T[] unAttribut, on peut créer des méthodes get et set pour les éléments du tableau :

  • void setUnAttribut(T valeur, int i){unAttribut[i]=valeur;}
  • T getUnAttribut(int i){ return unAttribut[i];}

Pour mieux comprendre, reprenons le code de la vue infos.jsp qui doit être envoyée à la suite de l'action InfosLoginAction :

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>

<html>
    <head>
      <title><bean:write name="infosLoginBean" scope="request" property="titre"/></title>
  </head>
  <body background="<html:rewrite page="/images/standard.jpg"/>">
      <h2><bean:write name="infosLoginBean" scope="request" property="titre"/></h2>
    <hr>
    <table border="1">
            <tr>
                <th>login</th><th>pwd</th><th>uid</th><th>gid</th><th>id</th><th>dir</th><th>shell</th>
            </tr>
            <tr>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[0]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[1]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[2]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[3]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[4]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[5]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[6]"/></td>            
            </tr>                                                                                                                                                                                                                
    </table>                                      
    <br>
    <html:link page="/retourLogins.do">
            Retour au formulaire
        </html:link>        
  </body>
</html>

Prenons la balise suivante :

<bean:write name="infosLoginBean" scope="request" property="titre"/>

Elle demande à écrire la valeur du champ titre (property) de l'objet infosLoginBean (name) mis dans la requête (scope). La valeur à écrire sera obtenue par request.getAttribute("infosLoginBean").getTitre(). Il faut donc que la méthode getTitre existe dans la classe InfosLoginBean. C'est le cas. La balise

<bean:write name="infosLoginBean" scope="request" property="infosLogin[0]"/>

demande à écrire la valeur de l'élément infosLogin[0] de l'objet infosLoginBean mis dans la requête. La valeur à écrire sera obtenue par request.getAttribute("infosLoginBean").getInfosLogin(0). Il faut donc que la méthode getInfosLogin(int i) existe dans la classe InfosLoginBean. C'est le cas.

La classe InfosLoginAction a pour but de construire l'objet InfosLoginBean précédent à partir d'un login choisi par l'utilisateur. Son code est le suivant :

package istia.st.struts.quiest;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import istia.st.users.*;

public class InfosLoginAction
  extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
                               HttpServletRequest request, HttpServletResponse response) throws IOException,ServletException {

    // doit afficher les infos liées à un login

    // on récupère les infos de la servlet contrôleur
    // infos=(ActionErrors erreurs, users u, LoginBean[] tLogins)
    Object[] infos = ( (Quiest2ActionServlet)this.getServlet()).getInfos();

    // y-a-t-il eu des erreurs d'initialisations ?
    ActionErrors erreurs = (ActionErrors) infos[0];
    if (!erreurs.isEmpty()) {
      this.saveErrors(request, erreurs);
      return mapping.findForward("afficherErreurs");
    }

    // d'abord récupérer ce login
    String login = (String) ( (DynaActionForm) form).get("cmbLogins");

    // a-t-on qq chose ?
    if (login == null) {
      // pas normal - on renvoie le formulaire des logins
            DynaActionForm formLogins=(DynaActionForm) form;
            formLogins.set("tLogins",infos[2]);
            return mapping.findForward("afficherLogins");
    }

    // on a un login - on le cherche
    String[] infosLogin = (String[]) ( (users) infos[1]).getUsersByLogin().get(login);

    // a-t-on trouvé ?
    if (infosLogin == null) {
      // le login n'a pas été trouvé - on affiche la page d'erreurs
      ActionErrors erreurs2=new ActionErrors();
      erreurs2.add(ActionErrors.GLOBAL_ERROR, new ActionError("loginInconnu", login));
      this.saveErrors(request, erreurs2);
      return mapping.findForward("afficherErreurs");
    }

    // le login a été trouvé - on met les infos trouvées dans la requête
    String titre="Application QuiEst - login["+login+"]";
    InfosLoginBean infosLoginBean= new InfosLoginBean(titre,infosLogin);
    request.setAttribute("infosLoginBean",infosLoginBean);
    return mapping.findForward("afficherInfos");
  }

}

Le fonctionnement de la méthode execute est le suivant :

  • on récupère les informations collectées par le contrôleur Struts lors de son initialisation. Si celui-ci avait noté des erreurs, l'exécution s'arrête là avec demande d'affichage de ces erreurs.
  • on vérifie qu'il y a bien un login. Si l'utilisateur est passé par le formulaire de choix d'un login, le login est bien présent. Mais l'utilisateur peut très bien taper l'url de l'action directement dans son navigateur sans passer de paramètres. S'il n'y a pas de login, on fait réafficher la liste des logins.
  • si on un login, on demande les informations associées à la classe métier users. Si celle-ci ne trouve pas le login cherché, la page d'erreurs est affichée. Sinon, un objet InfosLoginBean est construit pour y mettre les informations dont a besoin la vue infos.jsp. Cet objet est mis dans la requête et on fait afficher la page infos.jsp.

7.7. Déploiement

L'arborescence de l'application est la suivante :

 

7.8. Conclusion

Nous avons utilisé Struts dans une application réaliste utilisant une classe métier. Nous avons par ailleurs montré qu'il fallait prêter particulièrement attention à la requête envoyée par un client et ne faire aucune hypothèse sur la nature de celle-ci. Une requête peut être quelconque et toute application doit commencer par vérifier la validité de celle-ci.