Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Sessions intra-entreprises sur demande : contact[at]mokatech.net.
Inscrivez-vous vite !

Internationalisation des Enums avec Wicket

Note: Ce billet a été mis à jour le 28/09/2009 pour présenter une conception alternative de l'EnumModel.

Les enums sont très pratiques pour représenter des collections finies d'éléments : saisons, jours de la semaine... Il est donc fréquent de devoir saisir ou afficher de telles valeurs dans les applications web - et de manière internationalisée si possible.
Voyons comment Wicket permet de répondre à ce besoin.

Internationaliser les Enums

Pour notre exemple, nous utiliserons un Enum représentant les saisons :

  1. public enum Season {
  2. SPRING, SUMMER, AUTUMN, WINTER;
  3. }

Wicket propose un puissant mécanisme d'internationalisation hiérarchique : en fonction d'une clé logique, une traduction correspondante est recherchée au niveau du composant courant, puis de son composant parent, etc. jusqu'à l'Application elle-même si nécessaire.

La question est donc : comment générer une clé unique pour chaque constante d'un Enum ?
La solution la plus évidente est d'utiliser le nom complet (FQN, ex: "net.thecodersbreakfast.wickettips.model.WINTER") de chaque constante de l'Enum, ce qui éviterait tout conflit de nommage.

Le code suivant permet de déterminer le FQN d'un Enum donné :

  1. Season enumValue = Season.WINTER;
  2. String messageKey = enumValue.getDeclaringClass().getCanonicalName() + "." + enumValue.name();

Afin d'éviter de copier/coller ce code partout où l'internationalisation d'un Enum est requise, et afin de pouvoir changer d'algorithme si nécessaire, nous allons utiliser le pattern Strategy et l'encapsuler dans une classe.
Mais afin d'éviter que Wicket ne la sérialise en même temps que les composants l'utilisant, nous mettrons en place une architecture inspirée de la classe java.util.Locale :

  1. public abstract class EnumMessageKeyProvider {
  2. private static EnumMessageKeyProvider provider = new DefaultEnumResourceKeyProvider();
  3.  
  4. public static EnumMessageKeyProvider getDefault() {
  5. return EnumMessageKeyProvider.provider;
  6. }
  7.  
  8. public static void setDefault(EnumMessageKeyProvider provider) {
  9. EnumMessageKeyProvider.provider = provider;
  10. }
  11.  
  12. public abstract <T extends Enum<T>> String computeMessageKey(T enumValue);
  13.  
  14. public static <T extends Enum<T>> String getMessageKey(T enumValue) {
  15. return EnumMessageKeyProvider.provider.computeMessageKey(enumValue);
  16. }
  17. }
  1. public class DefaultEnumResourceKeyProvider extends EnumMessageKeyProvider {
  2. @Override
  3. public <T extends Enum<T>> String computeMessageKey(T enumValue) {
  4. return enumValue.getDeclaringClass().getCanonicalName() + "." + enumValue.name();
  5. }
  6. }

L'internationalisation d'un Enum est alors très simple :

  1. Season enumValue = Season.WINTER;
  2. String key = EnumMessageKeyProvider.getMessageKey(enumValue);

Et pour changer d'algorithme, il suffit d'écrire une Stratégie différente...

  1. public class SimpleNameEnumResourceKeyProvider extends EnumMessageKeyProvider {
  2. @Override
  3. public <T extends Enum<T>> String computeMessageKey(T enumValue) {
  4. return enumValue.getDeclaringClass().getCanonicalName() + "#" + enumValue.name();
  5. }
  6. }

... et de l'enregistrer auprès de l'EnumMessageKeyProvider, par exemple au cours du processus d'initialisation de l'application Wicket :

  1. public class WicketTipsApplication extends WebApplication {
  2. @Override
  3. protected void init() {
  4. EnumMessageKeyProvider.setDefault(new TestEnumResourceKeyProvider());
  5. }
  6. (...)
  7. }

Afficher un Enum internationalisé

Le composant le plus fréquemment utilisé pour afficher une information est le Label. Ce qu'il affiche est déterminé par le Model qui lui est passé en paramètre.
Voyons donc comment développer un Modèle spécialisé dans l'affichage (internationalisé évidemment) d'un Enum.

Approche par sous-classement

Nous partirons ici sur la base d'un PropertyModel. En plus des deux paramètres standard (l'objet cible et sa propriété à afficher), nous aurons besoin d'un Component qui servira à lancer le processus d'internationalisation.

  1. public class EnumPropertyModel<T extends Enum<T>> extends PropertyModel<String> {
  2.  
  3. private Component component;
  4.  
  5. public EnumPropertyModel(Object modelObject, String expression, Component resourceProvider) {
  6. super(modelObject, expression);
  7. this.component = resourceProvider;
  8. }
  9.  
  10. @Override
  11. public String getObject() {
  12. final String expression = propertyExpression();
  13. final Object target = getTarget();
  14. if (target != null) {
  15. T enumValue = (T) PropertyResolver.getValue(expression, target);
  16. String key = EnumMessageKeyProvider.getMessageKey(enumValue);
  17. return component.getString(key);
  18. }
  19. return null;
  20. }
  21. }

Ce modèle peut ensuite être utilisé avec un Label :

  1. public class HomePage extends WebPage {
  2. private Season season = Season.SPRING;
  3. public HomePage() {
  4. add(new Label("label", new EnumPropertyModel<Season>(this, "season", this)));
  5. }
  6. }

Approche par composition

Il est encore plus intéressant d'adopter une approche par composition, c'est-à-dire de concevoir notre modèle comme le décorateur d'un modèle existant. Par exemple, notre Modèle pourra internationaliser un Enum préalablement extrait d'un objet métier par un PropertyModel.

Notre Modèle effectuant une transformation de type unidirectionnelle (Enum vers String), nous le ferons étendre AbstractReadOnlyModel. Comme précédemment, il nécessite le passage en paramètre d'un Component pour lancer le processus d'internationalisation :

  1. public class EnumModel<T extends Enum<T>> extends AbstractReadOnlyModel<String> {
  2.  
  3. private IModel<T> model;
  4. private Component component;
  5.  
  6. public EnumModel(IModel<T> model, Component component) {
  7. this.model = model;
  8. this.component = component;
  9. }
  10.  
  11. @Override
  12. public String getObject() {
  13. String key = EnumMessageKeyProvider.getMessageKey(model.getObject());
  14. return component.getString(key);
  15. }
  16. }

Il s'utilise de la façon suivante :

  1. public class HomePage extends WebPage {
  2. private Season season = Season.SPRING;
  3. public HomePage() {
  4. // En décomposant :
  5. IModel<Season> seasonEnumModel = new PropertyModel<Season>(this, "season");
  6. IModel<String> seasonStringModel = new EnumModel<Season>(seasonEnumModel, this);
  7. add(new Label("label", seasonStringModel));
  8.  
  9. // Ou en une seule ligne :
  10. add(new Label("label", new EnumModel<Season>(new PropertyModel<Season>(this, "season"), this)));
  11. }
  12. }

Notez que cette approche est très souple, elle permet d'internationaliser un Enum quelle que soit sa source : PropertyModel, LoadableDetachableModel, ou même un simple IModel...

Sélection d'un Enum avec un DropDownChoice

Maintenant que nous savons afficher un Enum internationalisé, voyons comment le sélectionner à l'aide d'un DropDownChoice personnalisé.

Le composant DropDownChoice respecte le modèle MVC :

  • DropDownChoice est le Contrôleur.
  • Le modèle qu'il prend en paramètre représente sans surprise le Modèle,
  • Un ChoiceRenderer est utilisé pour générer les balises <option> en HTML (valeur et label)

Habituellement, c'est le Modèle qui fournit les différents éléments sélectionnables. Comme nous manipulons un Enum, nous les connaissons déjà : nous allons donc redéfinir la méthode "getChoices()" du composant DropDownChoice :

  1. public class EnumDropDownChoice<T extends Enum<T>> extends DropDownChoice<T> {
  2.  
  3. public EnumDropDownChoice(String id, IModel<T> model) {
  4. super(id);
  5. setModel(model);
  6. setChoiceRenderer(new EnumChoiceRenderer<T>(this));
  7. }
  8.  
  9. public EnumDropDownChoice(String id, IModel<T> model, EnumChoiceRenderer<T> choiceRenderer) {
  10. super(id);
  11. setModel(model);
  12. setChoiceRenderer(choiceRenderer);
  13. }
  14.  
  15. @Override
  16. public List<? extends T> getChoices() {
  17. return Arrays.asList(getModelObject().getDeclaringClass().getEnumConstants());
  18. }
  19. }

Quant au ChoiceRenderer, il utilisera évidemment l'identifiant de l'Enum comme identifiant HTML, et sa valeur internationalisée comme label :

  1. public class EnumChoiceRenderer<T extends Enum<T>> implements IChoiceRenderer<T> {
  2.  
  3. /** The Component used a the root of the I18N search process */
  4. private final Component resourceProvider;
  5.  
  6. public EnumChoiceRenderer(final Component resourceProvider) {
  7. this.resourceProvider = resourceProvider;
  8. }
  9.  
  10. @Override
  11. public Object getDisplayValue(final T value) {
  12. final String key = EnumMessageKeyProvider.getMessageKey(value);
  13. return resourceProvider.getString(key);
  14. }
  15.  
  16. @Override
  17. public String getIdValue(final T object, final int index) {
  18. final Enum<?> enumValue = object;
  19. return enumValue.name();
  20. }
  21. }

Notre composant personnalisé s'utilise strictement comme un DropDownChoice normal :

  1. public class HomePage extends WebPage {
  2. private Season season = Season.SPRING;
  3. public HomePage() {
  4. Form<Void> form = new Form<Void>("form");
  5. form.add(new EnumDropDownChoice<Season>("season", new PropertyModel<Season>(this, "season")));
  6. add(form);
  7. add(new Label("label", new EnumPropertyModel<Season>(this, "season", this)));
  8. }
  9. }

En action

Le code source complet est disponible en annexe de ce billet. Je vous encourage à jouer avec pour voir les composants décrits ici en action. Deux liens ont également été rajoutés pour permettre de choisir une Locale (française ou anglaise) et constater que l'internationalisation a bien lieu.

Note : l'exemple utilise Gradle comme système de build ; exécutez simplement la commande suivante pour compiler et lancer l'application web (Jetty inside) :

  1. gradle jettyRun

Puis ouvrez votre navigateur à l'adresse suivante :

http://localhost:8080/Wicket-tips/

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.