• 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

Créez des convertisseurs de type pour vos objets

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

Dans la première partie, on a vu que Struts convertissait automatiquement les paramètres de la requête HTTP (de type String) dans les types Java des attributs qu'il renseigne dans les classes d'action.

Et inversement, il convertit la valeur de ces attributs en String dans les JSP.

Ceci est valable pour les types de bases de Java :

  • String

  • boolean/Boolean

  • char/Character

  • int/Integerlong/Longfloat/Floatdouble/Double

  • BigDecimalBigInteger

  • les énumérations

  • les dates (en utilisant le format SHORT de la Locale associé à la requête)

  • les tableaux et collections des types ci-dessus

Sachez que vous pouvez aussi ajouter vos propres convertisseurs soit pour modifier le comportement de base, soit prendre en compte de nouveaux types.

Créer un convertisseur de type simple

Commençons avec un exemple simple.

DemoConvertAction.java

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

import org.apache.commons.lang3.math.Fraction;
import com.opensymphony.xwork2.ActionSupport;


/**
 * Action de démo pour les StrutsTypeConverter
 */
public class DemoConvertAction extends ActionSupport {


    // ==================== Attributs ====================
    private Fraction fraction;


    // ==================== Getters/Setters ====================
    public Fraction getFraction() {
        return fraction;
    }
    public void setFraction(Fraction pFraction) {
        fraction = pFraction;
    }


    // ==================== Méthodes ====================
    @Override
    public String execute() throws Exception {
        if (this.fraction != null) {
            this.addActionMessage(String.format("La fraction est : %d sur %d",
                                                this.fraction.getNumerator(),
                                                this.fraction.getDenominator()));
        }

        return ActionSupport.INPUT;
    }
}

struts.xml :

<action name="demo_convert" class="org.example.demo.ticket.webapp.action.DemoConvertAction">
    <result name="input">/jsp/demo/convert.jsp</result>
</action>

jsp/demo/convert.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 - Converter</h2>

    <s:form action="demo_convert">
        <s:textfield name="fraction" label="Fraction (format : #/#)" />

        <s:submit value="OK"/>
    </s:form>
</body>
</html>

Je voudrais récupérer une fraction saisie dans un formulaire. Pour cela je souhaite utiliser, dans la classe action, un attribut de type org.apache.commons.lang3.math.Fraction.

Si vous testez en saisissant une fraction dans le formulaire et en cliquant sur le bouton OK, vous verrez un message d'erreur :

Invalid field value for field "fraction".

En effet, la classe org.apache.commons.lang3.math.Fraction de l'attribut fraction n'est pas gérée par Struts. Il faut donc créer un convertisseur de type pour étendre les capacités de Struts.

Implémenter un Type Converter

Pour implémenter un convertisseur de type, il faut créer une classe implémentant l'interface com.opensymphony.xwork2.conversion.TypeConverter.

Afin de simplifier les choses et bénéficier de quelques mécanismes déjà codés, il est préférable d'hériter de la classe StrutsTypeConverter.

Je crée donc une classe FractionConverter implémentant les deux méthodes abstraites de StrutsTypeConverter :

  • convertFromString(Map pContext, String[] pValues, Class pToClass) pour convertir de String vers le type concerné (ici Fraction)

  • convertToString(Map pContext, Object pObject) pour convertir le type concerné (ici Fraction) en String

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

import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.Fraction;
import org.apache.struts2.util.StrutsTypeConverter;
import com.opensymphony.xwork2.conversion.TypeConversionException;


/**
 * Converter {@link org.apache.commons.lang3.math.Fraction} / {@link String}.
 */
public class FractionConverter extends StrutsTypeConverter {


    @Override
    public Object convertFromString(Map pContext, String[] pValues, Class pToClass) {
        Object vRetour = null;

        if (pValues != null) {
            if (pValues.length == 1) {
                String vValue = pValues[0];
                try {
                    vRetour = StringUtils.isEmpty(vValue) ? null : Fraction.getFraction(vValue);
                } catch (NumberFormatException pEx) {
                    throw new TypeConversionException("Format de fraction invalide", pEx);
                }
            } else {
                vRetour = performFallbackConversion(pContext, pValues, pToClass);
            }
        }

        return vRetour;
    }


    @Override
    public String convertToString(Map pContext, Object pObject) {
        String vString;
        if (pObject instanceof Fraction) {
            Fraction vFraction = (Fraction) pObject;
            vString = String.format("%d/%d",
                                    vFraction.getNumerator(),
                                    vFraction.getDenominator());
        } else {
            vString = "";
        }
        return vString;
    }
}

Vous remarquez que convertFromString(...) prend en paramètre un tableau de String. Je vous rappelle que dans l'interface ServletRequest, les paramètres HTTP de la requête sont accessibles via une Map de type Map<String, String[]>. En effet, il est possible d'envoyer une requête contenant plusieurs paramêtres de même nom pour traduire une sélection multiple dans un champ select d'un formulaire par exemple.

Comme FractionConverter hérite de StrutsTypeConverter, les tableaux de valeurs avec plus d'un élément sont déjà gérés par Struts : la méthode convertFromString sera appelée pour chaque élément du tableau et non en passant le tableau complet.

Pour plus de sécurité, si le tableau ne contient pas exactement 1 élément, je demande à Struts de faire une conversion par défaut : c'est l'objet de l'appel à la méthode performFallbackConversion(...).

Enfin, si une erreur de conversion survient (ici, si le format de la fraction est incorrect), il faut lever une TypeConversionException.

Déclarer un TypeConverter

Une fois le Type Converter implémenté, il faut le déclarer auprès de Struts pour qu'il soit utilisé.

Celui-ci peut être déclarer à plusieurs niveaux :

  • au niveau de l'action, pour l'appliquer à un attribut spécifique de l'action (ou sous-attribut)

  • au niveau du modèle, pour l'appliquer à un attribut (ou sous-attribut) d'un objet

  • au niveau de l'application, pour l'appliquer à un type d'objet

Les deux premiers cas étant assez spécifiques, je ne m'attarderai pas dessus. Si vous voulez tout de même voir de quoi il en retourne, je vous renvoie vers la documentation officielle.

Le troisième en revanche est pour moi le plus commun : mettre en place un convertisseur de type au niveau de l'application pour qu'il soit utilisé pour tous les attributs de ce type.

Pour cela, il faut le déclarer dans un fichier xwork-conversion.properties placé à la racine du classpath, en utilisant le format : <type> = <converter>.

Je crée donc le fichier src/main/resources/xwork-conversion.properties, et y ajoute le contenu suivant :

# Convertisseur pour le type Fraction
org.apache.commons.lang3.math.Fraction=org.example.demo.ticket.webapp.converter.FractionConverter

Et c'est tout. Si vous re-testez maintenant la soumission du formulaire en saisissant une fraction (dans le bon format), ça devrait parfaitement fonctionner.

Faire un bean locator

Vous avez compris le principe du Type Converter : faire une conversion entre un String et un type Java quelconque.

Je vous propose d'aller un peu plus loin en créant un bean locator. Le principe serait que ce Type Converter aille récupérer un objet métier à partir de son identifiant en String reçu en entrée.

Le but étant, lorsqu'on veut affecter un utilisateur, de ne plus remplir l'attribut id de l'utilisateur, comme c'est le cas actuellement par exemple dans l'écran de création de projet pour le responsable de projet, mais de directement assigner l'utilisateur.

Voici tout de suite une mise en application pour clarifier tout ça.

Impact sur les actions

Dans la méthode doCreate de l'action GestionProjetAction, la partie récupérant l'utilisateur depuis son identifiant pour l'affecter en tant que responsable du projet devient inutile :

    public String doCreate() {
        // ...

        // ===== Validation de l'ajout de projet (projet != null)
        if (this.projet != null) {
            // =====> [DEBUT] Partie devenant inutile
            // Récupération du responsable
            if (this.projet.getResponsable() == null
                || this.projet.getResponsable().getId() == null) {
                this.addFieldError("projet.responsable.id", "ne doit pas être vide");
            } else {
                try {
                    Utilisateur vResponsable
                        = managerFactory.getUtilisateurManager()
                                        .getUtilisateur(this.projet.getResponsable().getId());
                    this.projet.setResponsable(vResponsable);
                } catch (NotFoundException pEx) {
                    this.addFieldError("projet.responsable.id", pEx.getMessage());
                }
            }
            // <===== [FIN] Partie devenant inutile

            // ...
        }

        //...
    }
}

Impact dans les JSP

Dans la JSP jsp/projet/new.jsp, le champ de sélection du responsable du projet va pointer directement sur l'attribut responsable du projet, et plus sur l'attribut id du responsable du projet :

<%-- name="projet.responsable.id" est remplacé par name="projet.responsable" --%>
<s:select name="projet.responsable" label="Responsable"
        list="listUtilisateur" listKey="id" listValue="prenom"
        emptyOption="true"
        requiredLabel="true"/>

Le Locator lui-même

Une fois n'est pas coutûme, j'ai commencé par les impacts du changement avant de vous montrer comment faire le Locator, mais j'y viens tout de suite.

Comme je le disais plus haut, il va s'agir de créer un Type Converter (donc une classe implémentant l'interface TypeConverter) qui va permettre de convertir :

  • un String contenant l'identifiant de l'utilisateur, en une instance d'Utilisateur qui sera récupérée grâce à l'UtilisateurManager à partir de cet identifiant.

  • une instance d'Utilisateur en String contenant l'identifiant de cet utilisateur.

Alors c'est parti, je crée une classe UtilisateurLocator héritant comme d'habitude de la classe StrutsTypeConverter :

package org.example.demo.ticket.webapp.converter.locator;

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

import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.util.StrutsTypeConverter;
import com.opensymphony.xwork2.conversion.TypeConversionException;

import org.example.demo.ticket.business.contract.ManagerFactory;
import org.example.demo.ticket.model.bean.utilisateur.Utilisateur;
import org.example.demo.ticket.model.exception.NotFoundException;


/**
 * Locator d'{@link Utilisateur} via son identifiant.
 */
public class UtilisateurLocator extends StrutsTypeConverter {

    @Inject
    private ManagerFactory managerFactory;


    @Override
    public Object convertFromString(Map pContext, String[] pValues, Class pToClass) {
        Object vRetour = null;

        if (pValues != null) {
            if (pValues.length == 1) {
                String vValue = pValues[0];
                try {
                    vRetour
                        = StringUtils.isEmpty(vValue)
                        ? null
                        :  managerFactory.getUtilisateurManager().getUtilisateur(new Integer(vValue));
                } catch (NumberFormatException pEx) {
                    throw new TypeConversionException("Format d'identifiant utilisateur invalide", pEx);
                } catch (NotFoundException pEx) {
                    throw new TypeConversionException("Utilisateur introuvable", pEx);
                }
            } else {
                vRetour = performFallbackConversion(pContext, pValues, pToClass);
            }
        }

        return vRetour;
    }


    @Override
    public String convertToString(Map pContext, Object pObject) {
        String vString;
        if (pObject instanceof Utilisateur) {
            Utilisateur vUtilisateur = (Utilisateur) pObject;
            vString
                = vUtilisateur.getId() != null
                ? vUtilisateur.getId().toString()
                : "";
        } else {
            vString = "";
        }
        return vString;
    }
}

Il ne reste plus qu'à déclarer ce nouveau Type Converter dans le fichier xwork-conversion.properties :

# ===== Converters =====

# Convertisseur pour le type Fraction
org.apache.commons.lang3.math.Fraction=org.example.demo.ticket.webapp.converter.FractionConverter


# ===== Locators =====

# Locator pour le bean Utilisateur
org.example.demo.ticket.model.bean.utilisateur.Utilisateur=org.example.demo.ticket.webapp.converter.locator.UtilisateurLocator

Pour résumer

Créer des convertisseurs de type n'est pas très compliqué, du moment qu'ils héritent de la classe StrutsTypeConverter. Il suffit alors d'implémenter les méthodes :

  • convertFromString(Map pContext, String[] pValues, Class pToClass) pour convertir de String vers le type concerné,

  • convertToString(Map pContext, Object pObject) pour convertir le type concerné en String.

Il est très pratique de créer des Type Converters particuliers : les Locators. Ceux-ci permettent de récupérer une instance d'un objet (généralement un objet métier) à partir d'un identifiant en String en entrée. La gestion de cette récupération est ainsi centralisée et n'est plus à faire dans chaque Action.

Dans le chapitre suivant, je vais vous montrer comment affiner le flot de traitement des requêtes HTTP grâce aux piles d'Interceptor.

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