• 10 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 03/09/2019

Implémentez des actions asynchrones

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Ça y est, on arrive enfin au dernier chapitre de ce cours !

Je vous propose, pour terminer, de voir comment implémenter des actions asynchrones avec Struts. J'utiliserai AJAX avec jQuery dans la partie client, et la sérialization en JSON côté serveur pour envoyer les réponses.

Pour illustrer tout ça, je vais implémenter une page avec 2 exemples :

  • un bouton permet d'obtenir/rafraichîr la liste des projets sans recharger la page ;

  • 2 listes de sélection en cascade : après sélection d'un projet, la liste de sélection de la version est peuplée avec les versions du projet sélectionné.

Obtenir la liste des projets en AJAX

Dans mon premier exemple, je souhaite appeler une action en AJAX afin d'obtenir une liste des projets en JSON et ainsi mettre à jour la liste des projets dans la page à partir de ces données au format JSON, sans recharger cette page.

Un plugin Struts pour le JSON

Afin de gérer le format JSON et pouvoir automatiquement sérialiser des objets Java en JSON, Struts fournit un plugin... le JSON plugin !

J'ajoute donc ce plugin dans mes dépendances Maven du module ticket-webapp :

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-json-plugin</artifactId>
</dependency>

Création de l'action

Du point de vue de Struts, il n'y a aucune différence entre une action appelée "normalement" et une action appelée en AJAX. Il s'agit toujours d'une requête HTTP classique en GET ou POST.

Je vais donc pouvoir créer une seule classe d'action qui gèrera :

  • l'arrivée sur la page de démonstration (avec la méthode execute()) ;

  • et des méthodes pour les actions AJAX (méthodes doAjax*()).

package org.example.demo.ticket.webapp.action;

import java.util.List;
import javax.inject.Inject;

import com.opensymphony.xwork2.ActionSupport;

import org.example.demo.ticket.business.contract.ManagerFactory;
import org.example.demo.ticket.model.bean.projet.Projet;


/**
 * Action de démo pour les appels AJAX
 */
public class DemoAjaxAction extends ActionSupport {

    // ==================== Attributs ====================
    @Inject
    private ManagerFactory managerFactory;

    // ----- Eléments en sortie
    private List<Projet> listProjet;


    // ==================== Getters/Setters ====================
    public List<Projet> getListProjet() {
        return listProjet;
    }


    // ==================== Méthodes ====================
    public String execute() {
        return ActionSupport.SUCCESS;
    }


    /**
     * Action "AJAX" renvoyant la liste des projets
     * @return success
     */
    public String doAjaxGetListProjet() {
        listProjet = managerFactory.getProjetManager().getListProjet();
        return ActionSupport.SUCCESS;
    }
}

OK, jusque là, rien de bien compliqué. ;)

Dans la méthode execute() je renvoie simplement un result"success".

Dans la méthode doAjaxGetListProjet() je peuple l'attribut listProjet et renvoie également un result"success".

L'idée maintenant est que le result"success" de l'action AJAX renvoie un JSON qui serait la sérialisation de l'attribut listProjet de l'action.

Configuration du mapping

Je commence par créer le mapping de base de l'action avec la JSP de démonstration :

<struts>
    <!-- ===== Package pour les actions publiques ===== -->
    <package name="public" extends="base">
        ...

        <!-- Action de démonstration pour l'AJAX -->
        <action name="demo_ajax" class="org.example.demo.ticket.webapp.action.DemoAjaxAction">
            <result name="success">/jsp/demo/ajax.jsp</result>
        </action>
    </package>

    ...
</struts>

Ensuite je vais créer un package dédié aux actions AJAX, que je nommerai ajax. En effet, il va falloir que j'étende en plus du package base, le package json-default. Celui-ci est fourni par le JSON Plugin et apporte des types de result supplémentaires liés au format JSON.

<struts>
    ...

    <!-- ===== Package pour les actions AJAX ===== -->
    <package name="ajax" extends="base, json-default">

        <!-- Action renvoyant la liste des Projets -->
        <action name="demo_ajax_getListProjet"
                class="org.example.demo.ticket.webapp.action.DemoAjaxAction"
                method="doAjaxGetListProjet">
            <result name="success" type="json">
                <param name="root">listProjet</param>
            </result>
        </action>
    </package>
</struts>

Comme vous pouvez le voir :

  • J'ai créé le packageajax qui étend les packages base et json-default.

  • J'ai déclaré l'actiondemo_ajax_getListProjet pointant sur la méthode doAjaxGetListProjet() de la classe DemoAjaxAction.

  • Je lui ai ajouté un resultsuccess de type json. Ce type de result permet de renvoyer un document JSON en réponse au lieu d'une page HTML ou un redirect.

Par défaut, le document JSON renvoyé est généré en sérialisant en JSON l'ensemble des attributs accessibles (attributs avec avec un getter) de la classe d'action. Ici, seul le contenu de l'attribut listProjet m'intéresse. Je suis donc descendu d'un niveau (avec le paramètre root) en demandant au plugin de sérialiser en JSON en partant de l'attribut listProjet et non plus en partant de l'action.

En appelant l'action dans mon navigateur (http://127.0.0.1:8080/ticket/demo_ajax_getListProjet), le résultat obtenu est donc quelque chose qui ressemble à ceci :

[
  {
    "cloture": false,
    "dateCreation": "2018-01-31T17:17:06",
    "id": 0,
    "nom": "Projet n°0",
    "responsable": {
      "id": 0,
      "nom": "Dalton",
      "prenom": "Joe"
    }
  },
  {
    "cloture": false,
    "dateCreation": "2018-01-31T17:17:06",
    "id": 1,
    "nom": "Projet n°1",
    "responsable": {
      "id": 1,
      "nom": "Dalton",
      "prenom": "William"
    }
  },
  ...
]

Création de la vue

Pour la vue, je vais faire quelque chose de simple.

Je crée la page src/main/webapp/jsp/demo/ajax.jsp :

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<!DOCTYPE html>
<html>
<head>
    <%@ include file="../_include/head.jsp" %>
</head>

<body>
    <%@ include file="../_include/header.jsp" %>

    <h2>Démo - AJAX</h2>


    <h3>Liste des projets</h3>
    <ul id="listProjet">
        <li><em>à charger...</em></li>
    </ul>
    <button onclick="reloadListProjet()">Recharger</button>

    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    <script>
        function reloadListProjet() {
            // URL de l'action AJAX
            var url = "<s:url action="demo_ajax_getListProjet"/>";

            // Action AJAX en POST
            jQuery.post(
                url,
                function (data) {
                    var $listProjet = jQuery("#listProjet");
                    $listProjet.empty();
                    jQuery.each(data, function (key, val) {
                        $listProjet.append(
                            jQuery("<li>")
                                .append(val.nom)
                                .append(" - Responsable : ")
                                .append(val.responsable.prenom)
                        );
                    });
                })
                .fail(function () {
                    alert("Une erreur s'est produite.");
                });
        }
    </script>
</body>
</html>

Dans cette page :

  • j'ai une liste de projets : <ul id="listProjet">...</ul> ;

  • j'ai un bouton qui permet de rafraichir la liste en appelant la fonction javascriptreloadListProjet() ;

  • l'importation de jQuery ;

  • la fonction javascript reloadListProjet().

Listes de sélection en cascades

Maintenant que vous avez vu le fonctionnement de base, voyons un autre exemple avec 2 listes de sélection en cascade. Je voudrais avoir une liste de sélection de projet et quand un projet est sélectionné, la liste de sélection de version est mise à jour avec la liste des versions du projet sélectionné.

La classe d'action

Dans la classe DemoAjaxAction je vais ajouter :

  • un attribut projet pour récupérer, dans la requête, le projet sélectionné,

  • un attribut listVersion qui sera sérialisé en JSON pour renvoyer la liste des versions,

  • une méthode doAjaxGetListVersion() qui sera appelée pour l'action AJAX de récupération des versions.

public class DemoAjaxAction extends ActionSupport {

    // ==================== Attributs ====================

    // ----- Eléments en entrée
    private Projet projet;

    // ----- Eléments en sortie
    private List<Version> listVersion;


    // ==================== Getters/Setters ====================
    public Projet getProjet() {
        return projet;
    }
    public void setProjet(Projet pProjet) {
        projet = pProjet;
    }
    public List<Version> getListVersion() {
        return listVersion;
    }


    // ==================== Méthodes ====================
    /**
     * Action "AJAX" renvoyant la liste des versions d'un projet
     * @return success / error
     */
    public String doAjaxGetListVersion() {
        if (projet == null) {
            addActionError("Le projet doit être précisé !");
        } else {
            listVersion = managerFactory.getProjetManager().getListVersion(projet);
        }

        return hasErrors() ? ActionSupport.ERROR : ActionSupport.SUCCESS;
    }

    ...
}

Il existe déjà un attribut listProjet qui servait pour renvoyer la liste des projets en JSON. Je vais m'en servir pour remplir la liste de sélection des projets à la construction de la page.

public class DemoAjaxAction extends ActionSupport {

    // ==================== Méthodes ====================
    public String execute() {
        listProjet = managerFactory.getProjetManager().getListProjet();
        return ActionSupport.SUCCESS;
    }

    ...
}

Le mapping

Au niveau du mapping, je vais :

  • déclarer l'actiondemo_ajax_getListVersion et son resultsuccess de type JSON ;

  • déclarer un global resulterror. Celui-ci renverra un code HTTP 400 et sérialisera en JSON seulement les attributs actionErrors et fieldErrors (présents dans la classe mère ActionSupport et alimentés par les méthodes addActionError et addFieldError).

<struts>
    <!-- ===== Package pour les actions AJAX ===== -->
    <package name="ajax" extends="base, json-default">

        <global-results>
            <result name="error" type="json">
                <param name="statusCode">400</param>
                <param name="includeProperties">actionErrors.*,fieldErrors.*</param>
                <!-- Pour accéder au attributs des classes parentes à la classe d'action -->
                <param name="ignoreHierarchy">false</param>
            </result>
        </global-results>

        <!-- Action renvoyant la liste des Versions d'un Projet -->
        <action name="demo_ajax_getListVersion"
                class="org.example.demo.ticket.webapp.action.DemoAjaxAction"
                method="doAjaxGetListVersion">
            <result name="success" type="json">
                <param name="root">listVersion</param>
            </result>
        </action>

        ...
    </package>

    ...
</struts>

La vue

Ajout de 2 selects
<h3>Selects en cascade</h3>
<s:form>
    <s:select id="selectProjet" name="projet" label="Projet"
              list="listProjet" listKey="id" listValue="nom"
              onchange="onSelectProjetChange()"/>

    <s:select id="selectVersion" label="Version" list="{}"/>
</s:form>
Fonction Javascript pour l'AJAX

Enfin, en ce qui concerne l'action AJAX, cette fois ci, il faut passer en paramètre de la requête le projet sélectionné, et en cas d'erreur, je log dans la console la réponse du serveur.

function onSelectProjetChange() {
    // URL de l'action AJAX
    var url = "<s:url action="demo_ajax_getListVersion"/>";

    // Paramètres de la requête AJAX
    var params = {
        projet: jQuery("#selectProjet").val()
    };

    // Action AJAX en POST
    jQuery.post(
        url,
        params,
        function (data) {
            var $selectVersion = jQuery("#selectVersion");
            $selectVersion.empty();
            jQuery.each(data, function (key, val) {
                $selectVersion.append(
                    jQuery("<option>")
                        .text(val.numero)
                        .val(val.numero)
                );
            });
        })
        .fail(function (data) {
            if (typeof data.responseJSON === 'object') {
                console.log(data.responseJSON);
            } else {
                console.log(data);
            }
            alert("Une erreur s'est produite.");
        });
}

Et voilà, j'ai maintenant 2 listes de sélection chainées !

Résumé

Comme vous avez pu le constater, gérer des requêtes AJAX avec Struts n'est pas très compliqué surtout quand il s'agit de simplement sérialiser en JSON des attributs des classes d'action.

Pour cela, il faut ajouter une dépendance vers le plugin JSON plugin dans votre webapp et étendre le package json-default.

Ce plugin apporte d'autres fonctionnalités et d'autres paramètrages notamment au niveau des results. N'hésitez pas à vous tourner vers la documentation du plugin si vous voulez en apprendre d'avantage.

De plus, comme je vous le disais au début de ce chapitre, vous pouvez faire de l'AJAX sur une application Struts. Mais ce n'est pas sa vocation première. Si le besoin est ponctuel comme chaîner 2 select, ça va. Mais si votre application fait beaucoup d'actions asynchrones, il se peut que vous ne soyez pas sur la bonne approche avec un framework tel que Struts et d'autres architectures pourraient être mieux adaptés. Par exemple, en se basant sur des API REST et en utilisant des frameworks comme AngularJS ou Vue.js.

Encore une chose...

Félicitations, vous êtes arrivé·e au bout de ce cours ! ;)

Vous avez pu vous rendre compte que Struts est un framework assez riche. Reposant sur le modèle MVC (Modèle Vue Contrôleur), il permet de grandement faciliter le développement d'applications web en évitant d'écrire beaucoup de code de « plomberie » comme l'implémentation de servlets à la main !

Je vous encourage, comme toujours, à pratiquer. N'hésitez pas à refaire les implémentations vues dans ce cours et à étoffer cette application de gestion de tickets pour vous exercer.

Exemple de certificat de réussite
Exemple de certificat de réussite