14 mai 2010
Imprimer ce billet

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<? extends Enum> clazz;

    public PrintableEnumerationEditor(Class<? extends Enum> clazz) {
        this(null, clazz);
    }

    public PrintableEnumerationEditor(MessageSource messageSource, Class<? extends Enum> 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 :

<form:form method="post" commandName="user" action="create.do">
   <div>
      <label><fmt:message key="user.creation.infos.title" />:</label><form:select path="title" items="${titles}" />
      <form:errors path="title" cssClass="error"/>
   </div>
   <input type="submit" value="<fmt:message key="user.creation.buttons.submit" />"/>
</form:form>

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...

Mots-clefs :, , ,