Publié par
Il y a 7 années · 2 minutes · Craft

Afficher une énumération internationalisée avec Spring MVC 3.0, pas si simple !

Je considère Spring MVC comme l’un des framework web action-based les plus conviviaux du moment. Pourtant, il faut avouer que pour répondre à certaines problématiques simples, il nous oblige à inventer des solutions alambiquées, en voici un exemple. Je suis preneur de toute autre meilleure solution :)

Le besoin : afficher une liste de civilité internationalisée dans un formulaire
La solution : utiliser un custom property editor

Spring offre une fonctionnalité intéressante : le DataBinder. Celui-ci permet entre autre d’enregistrer des PropertyEditors personnalisés vous permettant de convertir des String dans n’importe quel autre type (et vice versa). Ainsi, vous disposez d’un contrôle complet sur la manière dont vos objets sont représentés.

Cependant, bizarrement, je n’ai pas trouvé de moyen simple pour convertir les valeurs d’une énumération. Rien ne permet out-of-the-box de convertir un code d’une énumération en son instance typée. Si de plus votre application est internationalisée, ce qui reste une demande très répandue, rien n’est prévu.

Voici, en détail les différentes étapes de la solution que je vous propose :

Ajout d’un code i18n à mon énumération

Je rajoute à mon énumération une propriété message qui contient le code du message internationalisé des différents éléments de mon enum. C’est ce nouveau champ qui sera utilisé pour représenter les différentes instances de mon enum dans la liste de mon formulaire. Si vous n’êtes pas familier avec cette syntaxe, je vous renvoie vers cet article que j’ai écrit il y a quelque temps : Enumérations – Utilisation avancée. Un peu de publicité au passage :)

public enum Title implements PrintableEnumeration {
  Mister("titles.mister"), Madam("titles.madam"), Miss("titles.miss");
  
  private final String message;
  private Title(String message) {
    this.message = message;
  }
  
  public String getMessage() {
    return message;
  }
}

Notez par la même occasion que mon énumération étend l’interface PrintableEnumeration dont voici le contrat. Elle se contente d’exposer la méthode getMessage() à qui le veut. Elle permettra ainsi à d’autres énumérations d’utiliser le même mécanisme que nous allons développer dans la suite de cet article.

public interface PrintableEnumeration {
  String getMessage();
}

Création d’un éditeur spécifique

Continuons par créer un éditeur spécifique permettant de convertir une énumération en fonction de la locale. Nous verrons dans la partie suivante comment utiliser celui-ci pour modifier la représentation de notre énumération précédemment développée. Notez donc que vous pouvez réutiliser cet éditeur sur n’importe quelle énumération possédant le flag PrintableEnumeration.

public class PrintableEnumerationEditor extends PropertyEditorSupport {

  private MessageSource messageSource;
  private Class clazz;

  public PrintableEnumerationEditor(Class clazz) {
    this(null, clazz);
  }

  public PrintableEnumerationEditor(MessageSource messageSource, Class clazz) {
    this.messageSource = messageSource;
    this.clazz = clazz;
  }

  public String getAsText() {
    if (getValue() == null) {
      return "";
    }
    if (!(getValue() instanceof PrintableEnumeration)) {
      return ((Enum) getValue()).name(); 
    }
    PrintableEnumeration value = (PrintableEnumeration) getValue();
    String code = value.getMessage();
    if (code == null || messageSource == null) {
      return ((Enum) getValue()).name();
    }
    String message = messageSource.getMessage(code, new  String[] { }, LocaleContextHolder.getLocale());
    return message;
  }

  public void setAsText(String text) throws IllegalArgumentException {
    setValue(Enum.valueOf(clazz, text));
  }
}

Cet éditeur possède deux constructeurs, l’un pour gérer les transformations simples des Enum en String (et inversement), l’autre utilisant l’internationalisation.
C’est le MessageSource passé en paramètre du second constructeur qui permet de gérer la représentation multi-langues.

Enregistrement de l’éditeur dans le binder

Dernière étape, il nous faut encore enregistrer ce nouvel éditeur dans le WebDataBinder pour que le contrôleur puisse l’utiliser. Pour cela, utilisons l’annotation @InitBinder :

@Controller
public class UserController {

  @Resource(name = "myMessageRessourceBundle")
  private ReloadableResourceBundleMessageSource resourceBundleMessageSource;
  
  @InitBinder
  public void initBinder(WebDataBinder binder) {
    binder.registerCustomEditor(Title.class, new PrintableEnumerationEditor(resourceBundleMessageSource, Title.class));
  }

   ...
}

Comment utiliser tout cela ?

Nous avons maintenant toutes les briques à notre disposition pour enfin afficher notre liste de civilité.
Il ne vous reste plus qu’à fournir la liste des civilités à la vue …

  @RequestMapping(value="/user/create.do", method = RequestMethod.GET)
  public String createUser(ModelMap model) {
    model.addAttribute("user", new User());
    model.addAttribute("titles", Title.values());
    return "edit";
  }

… et à créer le formulaire dans celle-ci :


   
"/>

Pour terminer, notez le code utilisé pour récupérer les valeurs de ce formulaire, il devrait vous réconcilier avec Spring MVC. Difficile de faire plus simple.

  @RequestMapping(value="/user/create.do", method = RequestMethod.POST)
  public String createUser(@ModelAttribute("user") @Valid User user, BindingResult result) {
    if (result.hasErrors()) {
      return "edit";
    }
    userService.createUser(user);
    return "editConfirm";
  }

J’utilise ici l’intégration Beans Validation offerte par Spring MVC. Nul besoin de tester tous les cas d’erreur, il suffit de rajouter les annotations qui vont bien sur mon bean User pour que la magie opère. Ce sera peut-être le sujet d’un prochain article…

Erwan Alliaume
Passionné par les technologies Java/JEE depuis sa sortie de l'EPITA, Erwan Alliaume est aujourd'hui consultant chez Xebia. Il intervient depuis 2 ans sur des missions de développement et d'architecture pour l’un des leaders mondiaux de la production et de la distribution de contenus multimédia. Expert technique polyvalent, Erwan a été amené très tôt à prendre des responsabilités sur des projets de taille significative. Il a notamment développé des compétences clé dans la mise en place de socle de développement, la métrologie et les audits de performance et de qualité. Erwan participe également activement au blog Xebia ou il traite aussi bien de sujets d’actualités que de problématiques techniques.

11 réflexions au sujet de « Afficher une énumération internationalisée avec Spring MVC 3.0, pas si simple ! »

  1. Publié par Michael Isvy, Il y a 7 années

    Salut Erwan,
    si ce n’est déjà fait, n’hésite pas à ouvrir une issue sur notre jira. Il y a plein d’améliorations de prévues sur Spring @MVC pour la version 3.1, et ce que tu avances pourrait en faire partie.
    Michael.

  2. Publié par Piwaï, Il y a 7 années

    Très intéressant comme article !

    J’ai une question : dans PrintableEnumerationEditor.getAsText(), lorsque la value est une PrintableEnumeration et qu’un code existe, c’est le message traduit qui est retourné. Dans le cas contraire, c’est le nom de l’élément dans l’enum.

    Cependant, dans PrintableEnumarationEditor.setAsText(), c’est systématiquement le nom de l’élément dans l’enum qui permet de retrouver l’instance (Enum.valueOf(clazz, text))

    Il semble que le comportement est asymétrique. Quand la valeur dans le formulaire est « Monsieur », comment fais-tu pour retrouver Title.Mister après un POST ?

    Est-ce que j’ai loupé quelque chose ? Etant donné qu’un champ HTML option peut comporter à la fois un nom et une valeur, c’est peut-être là l’explication ?

    Je m’en vais regarder la doc sur les PropertyEditor / comment ils sont utilisés pour rendre un form:select … Mais si quelqu’un en sait plus, n’hésitez pas !

    Plus anecdotique, à propos du nom de variable « clazz », je suis en train de lire « Clean code », et, je cite : « Les programmeurs se créent des problèmes lorsqu’ils écrivent du code uniquement pour satisfaire le compilateur ou l’interpréteur. […] C’est par exemple le cas de la pratique véritablement horrible qui consiste à créer une variable nommée klass [ou clazz] simplement parce que le nom class est employé pour autre chose ».

    Je ne fais pas là une critique, j’emploie moi même « clazz » très couramment, sans réfléchir, notamment après l’avoir vu utilisé dans nombre de frameworks Open Source. Mais après réflexion, « clazz » est peut-être un mot clé apportant peu de sens…

  3. Publié par Erwan Alliaume, Il y a 7 années

    Le tag ‘select’ présenté dans cet article est rendu sous cette forme :

    <select id="title" name="title">
      <option value="Mister">Monsieur</option>
      <option value="Madam">Madame</option>
      <option value="Miss">Mademoiselle</option>
    </select> 
    

    La valeur soumise par le formulaire correspondra au nom de l’élément de l’énumération. Il est vrai que le code semble asymétrique, mais fonctionne.

    Je n’ai pas trouvé de moyen plus élégant pour répondre à cette problématique simple, la solution du property editor présente l’avantage d’être générique. Pour paraphraser l’introduction : « Je suis preneur de toute autre meilleure solution » :)

    Concernant la seconde remarque, je ne suis pas persuadé que de remplacer « clazz » par « enumerationClass » apporte une meilleure lisibilité du code. L’autodocumentation n’est à mon avis pas suffisante ici…

    Erwan (Xebia)

  4. Publié par philippe voncken, Il y a 7 années

    Bonjour,

    Avec Java de base on peut déjà afficher une énumération internationalisée.

    (Je vous file un code de tête donc il est possible qu’il faille corriger des fautes d’ortho)

    public enum Toto {
    PREMIER(ResourceBundle.getBundle(« MyResource »).getObject(« PREMIER »)),
    SECOND(ResourceBundle.getBundle(« MyResource »).getObject(« SECOND »));
    private String name;
    private Toto(String s) {
    name = s;
    }
    }

    Et voilà, grâce au resource bundle on bénéficie de l’internationalisation sur l’enum.

    Je pense que c’est plus simple et plus élégant que l’exemple de ce billet.

    Cordialement,
    Philippe

  5. Publié par philippe voncken, Il y a 7 années

    Oups, j’ai oublié de rajouter la méthode toString à mon enum pour obtenir un affichage transparent :/

    Voici le code donc :
    public enum Toto {
    PREMIER(ResourceBundle.getBundle(« MyResource »).getObject(« PREMIER »)),
    SECOND(ResourceBundle.getBundle(« MyResource »).getObject(« SECOND »));
    private String name;
    private Toto(String s) {
    name = s;
    }
    public String toString() {
    return this.name;
    }
    }

  6. Publié par Piwaï, Il y a 7 années

    Huuh ??

    Arrêtez moi si je me trompe, mais les éléments d’une enum sont construits au chargement de la classe.

    Autrement dit, au chargement de la classe (l’enum), PREMIER et SECOND vont être instanciés. Et le paramètre de constructeur va être valorisé. Ce qui signifie que Toto, dans ton exemple, aura des valeurs qui dépendront de la locale AU MOMENT DU CHARGEMENT DE LA CLASSE, et non modifiables par la suite.

    Ici, c’est tout le contraire : la locale va dépendre de la langue du client web… Donc cela peut varier à chaque requête.

    Faut pas oublier qu’une enum, c’est grosso modo une classe portant plusieurs instances statiques d’elle même.

    Je réagis peut-être un peu vivement, mais j’aimerai éviter des erreurs au futurs lecteurs de cet article ;-) . Encore une fois, si je fais erreur, tell me :-)

  7. Publié par Maxime Picque, Il y a 7 années

    @Piwai

    Pour rentrer plus dans les détails, les valeurs des enums sont instanciées au chargement de la classe et non modifiable. J’entends par « non modifiable » que l’instance ne peut être remplacée par une autre.

    Par contre il est possible de modifier la valeur des champs d’une enum (champ « message » dans l’exemple). Mais il faut à tout prix éviter ce genre de comportement, surtout dans une application distribuée/multi-utilisateur (j’ai des mauvais souvenirs de ces erreurs lors d’une de mes missions…).

    Par convention, il faut éviter de modifier les valeurs des champs d’une enum à la volée.

    @Philippe Voncken

    Ce que vous écrivez peut fonctionner dans un environnement Java standalone mono-utilisateur. Mais ceci ne colle pas du tout avec la problématique expliquée dans cet article (Spring MVC est utilisé pour faire du Web => application multi-utilisateur).

  8. Publié par philippe voncken, Il y a 7 années

    Il est tout a fait possible d’obtenir une instance d’enum par utilisateur et de sélectionner la langue à ce moment.

    Je ne vois pas où est le problème. Je pense que ca fait beaucoup moins de code à écrire et ca n’est pas moins optimisé il me semble.

  9. Publié par Piwaï, Il y a 7 années

    @Philippe Voncken

    Non.

    En tout cas, pas dans l’exemple de code que vous donnez. Prenons un autre exemple :

    public enum EnumWithField {

    A(System.currentTimeMillis());

    private long value;

    private EnumWithField(long value) {
    this.value = value;
    }

    public long getValue() {
    return value;
    }
    }

    Dans cette première enum, le champ value est fixé à la construction de l’enum, qui a lieu au chargement de la classe de l’enum.

    Alors que :

    public enum EnumDynamic {
    A;
    public long getValue() {
    return System.currentTimeMillis();
    }
    }

    Ici, la valeur renvoyée value suivant le moment de l’appel. Il suffit de tester :

    public static void main(String[] args) throws Exception {
    System.out.println(« Field 1:  » + EnumWithField.A.getValue());
    System.out.println(« Dynamic 1:  » + EnumDynamic.A.getValue());
    Thread.sleep(1337);
    System.out.println(« Field 2:  » + EnumWithField.A.getValue());
    System.out.println(« Dynamic 2:  » + EnumDynamic.A.getValue());
    }

    Cela affiche :

    Field 1: 1275041432914
    Dynamic 1: 1275041432919
    Field 2: 1275041432914
    Dynamic 2: 1275041434256

    Cependant, votre idée de base reste intéressante, à condition de stocker la clé et de déterminer le bon message dynamiquement au moment de l’appel :

    public enum Toto {
    PREMIER(« PREMIER »), SECOND(« SECOND »);
    private String name;

    private Toto(String s) {
    name = s;
    }

    public String toString() {
    return ResourceBundle.getBundle(« MyResource »).getObject(name).toString();
    }
    }

  10. Publié par philippe voncken, Il y a 7 années

    @piwaï

    Oui effectivement, j’ai posé mes exemples de tête sans aller vérifier dans mes vieilles source comment j’avais fait la dernière fois.

    Je suis content qu’on ai cette solution qui me parait plus simple quand même.

  11. Publié par aphilippe, Il y a 6 années

    Merci Erwan pour ce billet. J’aime beaucoup la stratégie que tu as utilisé meme si elle très alembiqué : le code est clair et c’est vraiment Spring MVC qui est mise en oeuvre.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *