Jun 192007
 

Als Vertreter des deutschen Sprachraums musste ich mich schon in mehr als einem Projekt mit der Problematik von Umlauten und der unterschiedliche Kodierung in Windows und Unix und der Internationalisierung von Java-Systemen auseinander setzen. Die im folgenden beschriebene Problematik bezieht sich lediglich auf die Internationalisierung von Texten in einem System – Internationalisierung ist ein weites Feld, welches sich auf Layout, Benutzerführung, Bilder, etc. ausdehnt.

Um Internationalisierung von Texten in Java zu ermöglichen, werden Resource-Bundles eingesetzt. Eh klar.

Oft wird an Java-Server-Systemen unter Windows entwickelt und das fertige System wird dann unter Unix betrieben. Wenn Euch dieses Setup (Windows, Unix, Java, Resource-Bundles) bekannt vorkommt, so habt Ihr Euch sicher schon mal so richtig darüber aufgeregt, dass die Klasse java.util.PropertyResourceBundle des JDK lediglich den Zeichensatz ISO-8859-1 unterstützt? Nein?? Ich schon. Aber jedes Problem hat seine (mehr oder weniger elegante) Lösung.

Wenn ich eine Property-Datei unter Windows bearbeite und dabei zum Beispiel einen Umlaut einfüge, so liegt die zugeordnete Bitfolge nicht mehr im definierten Bereich von 7-Bit ASCII. Wird die Datei im Windows-Zeichensatz CP-1252 bearbeitet, so wird der Umlaut in ISO-8859-1 anders interpretiert. Der Zeichensatz ISO-8859-1 ist in der Regel in Unix-Systemen eingesellt.
Wird die Property-Datei von der Klasse PropertyResourceBundle unter Unix geladen, so werden die Bitfolgen in andere Zeichen interpretiert und in der GUI werden seltsame Symbole angezeigt.

Die reguläre Lösung für dieses Problem ist das Konvertieren der Property-Datei mittels Ant-Task “native2ascii”. Dieser wandelt Sonderzeichen, außerhalb des ASCII-Bereichs (7 Bit) in Unicode-Sequenzen der Form “u0815″ um. Die Klasse PropertyResourceBundle weis mit diesen Sequenzen umzugehen und wird daraus die korrekten Zeichen ermitteln. Wichtig ist, sofern die Original-Proprty-Dateien weiterhin in CP-1252 bearbeitet werden, dass Entwicklung und Build auf demselben Betriebssystem (Windows, Unix) ablaufen.

In Eclipse ist mittlerweile ISO-8859-1 die vorgegebene (aber konfigurierbare) Kodierung für Text-Dateien. Damit gibt es in der Regel diese Probleme nicht, jedoch ist damit die Abbildung von Sonderzeichen außerhalb des ISO-8859-1 Zeichensatzes (Osteuropäische Sprachen, Chinesisch, etc.) nicht möglich.

Um eine hohe Flexibilität zu erreichen und unabhängig von Betriebssystem-spezifischen Zeichensätzen zu sein, wird als Kodierung der Original-Property-Dateien ein Unicode-Zeichensatz, vorzugsweise UTF-8 gewählt. Dies kann im Eclipse-Workspace (leider noch nicht in den Projekteinstellungen) für alle “*.properties”-Dateien definiert werden.

Dennoch muss dann zwingend eine Konvertierung der Property-Dateien beim Build erfolgen, wenn mit der JDK-Klasse PropertyResourceBundle gearbeitet werden soll.

Da die Einbindung eines derartigen spezifischen Build-Schrittes zum Beispiel bei der Verwendung des Eclipse Web Tools Projekt und dessen integrierte Deploy-Funktion hinderlich ist, wäre eine schöne Lösung die Property-Dateien zur Laufzeit in Unicode lesen zu können.

Meine Lösung ist das Erstellen einer UTF-8 fähigen PropertyResourceBundle-Klasse. Da ich mit meiner gewählten Lösung gleichzeitig das wiederholte Laden und Interpretieren der Property-Datei verhindern will, enthält die nachvollgend gelistete Lösung zusätzlich einen Caching-Mechanismus. Der Kern der Lösung ist das Laden und Interpretieren (Parsen) der Property-Dateien weiterhin durch PropertyResourceBundle durchführen zu lassen und lediglich die Behandlung von UTF-8 und das Caching zu übernehmen:

  1 import java.io.UnsupportedEncodingException;
  2 import java.util.Collections;
  3 import java.util.Enumeration;
  4 import java.util.HashMap;
  5 import java.util.Locale;
  6 import java.util.Map;
  7 import java.util.PropertyResourceBundle;
  8 import java.util.ResourceBundle;
  9 
 10 /**
 11  * Same as {@link ResourceBundle} but is target for UTF-8 property file resources.
 12  * May not be used with 8-bit ASCII - only with UTF-8.
 13  *
 14  * @author Marc Neumann
 15  */
 16 public class ResourceBundleUtf8 {
 17 
 18   private static class PropertyResourceBundleUtf8 extends ResourceBundle {
 19 
 20     private final Map<String, String> valueByKey = new HashMap<String, String>();
 21 
 22     private PropertyResourceBundleUtf8(PropertyResourceBundle pBundle) {
 23       loadEntries(pBundle, valueByKey);
 24     }
 25 
 26     /**
 27      * @see java.util.ResourceBundle#getKeys()
 28      */
 29     public Enumeration<String> getKeys() {
 30       return Collections.enumeration(valueByKey.keySet());
 31     }
 32 
 33     private void loadEntries(PropertyResourceBundle pBundle, Map<String, String> pValueByKey) {
 34       for (Enumeration<String> keys = pBundle.getKeys(); keys.hasMoreElements();) {
 35         String key = keys.nextElement();
 36         String valueRaw = pBundle.getString(key);
 37         String value;
 38 
 39         try {
 40           value = new String(valueRaw.getBytes("ISO-8859-1"), "UTF-8");
 41         }
 42         catch (UnsupportedEncodingException e) {
 43           throw AssertUtil.fail("could not load UTF-8 property resource bundle [" + pBundle + "]", e);
 44         }
 45 
 46         if (pValueByKey.put(key, value) != null) {
 47           throw AssertUtil.fail("duplicate key [" + key + "] in UTF-8 property resource bundle [" + pBundle + "]");
 48         }
 49       }
 50     }
 51 
 52     /**
 53      * @see java.util.ResourceBundle#handleGetObject(java.lang.String)
 54      */
 55     protected Object handleGetObject(String pKey) {
 56       return valueByKey.get(pKey);
 57     }
 58   }
 59 
 60   private static Map<ClassLoader, Map<String,Map<Locale,ResourceBundle>>> bundleByClassLoaderByBaseNameByLocale =
 61     new HashMap<ClassLoader, Map<String,Map<Locale,ResourceBundle>>>();
 62 
 63   /**
 64    * @see ResourceBundle#getBundle(String)
 65    */
 66   public static final ResourceBundle getBundle(String pBaseName) {
 67     ResourceBundle bundle = ResourceBundle.getBundle(pBaseName);
 68     return createUtf8PropertyResourceBundle(bundle);
 69   }
 70 
 71   /**
 72    * @see ResourceBundle#getBundle(String, Locale)
 73    */
 74   public static final ResourceBundle getBundle(String pBaseName, Locale pLocale) {
 75     ResourceBundle bundle = ResourceBundle.getBundle(pBaseName, pLocale);
 76     return createUtf8PropertyResourceBundle(bundle);
 77   }
 78 
 79   /**
 80    * @see ResourceBundle#getBundle(String, Locale, ClassLoader)
 81    */
 82   public static ResourceBundle getBundle(String pBaseName, Locale pLocale, ClassLoader pLoader) {
 83 
 84     Map<String,Map<Locale,ResourceBundle>> bundleByBaseNameByLocale;
 85     Map<Locale,ResourceBundle> bundleByLocale = null;
 86     ResourceBundle bundle = null;
 87 
 88     synchronized (bundleByClassLoaderByBaseNameByLocale) {
 89       bundleByBaseNameByLocale = bundleByClassLoaderByBaseNameByLocale.get(pLoader);
 90       if (bundleByBaseNameByLocale ==  null) {
 91         bundleByBaseNameByLocale = new HashMap<String, Map<Locale,ResourceBundle>>();
 92         bundleByClassLoaderByBaseNameByLocale.put(pLoader, bundleByBaseNameByLocale);
 93       }
 94     }
 95 
 96     synchronized (bundleByBaseNameByLocale) {
 97       bundleByLocale = bundleByBaseNameByLocale.get(pBaseName);
 98       if (bundleByLocale == null) {
 99         bundleByLocale = new HashMap<Locale, ResourceBundle>();
100         bundleByBaseNameByLocale.put(pBaseName, bundleByLocale);
101       }
102     }
103 
104     synchronized (bundleByLocale) {
105       bundle = bundleByLocale.get(pLocale);
106       if (bundle == null) {
107         bundle = ResourceBundle.getBundle(pBaseName, pLocale);
108         bundle = createUtf8PropertyResourceBundle(bundle);
109         bundleByLocale.put(pLocale, bundle);
110       }
111     }
112 
113     return bundle;
114   }
115 
116   private static ResourceBundle createUtf8PropertyResourceBundle(ResourceBundle pBundle) {
117     if (!(pBundle instanceof PropertyResourceBundle)) {
118       throw AssertUtil.fail("only UTF-8 property files are supported");
119     }
120 
121     return new PropertyResourceBundleUtf8((PropertyResourceBundle) pBundle);
122   }
123 
124 }
125 

Wir erfolgt dann zum Beispiel die Einbindung diese Lösung in Java Server Faces? Man braucht zunächst einen ersatz für das JSF-Standard-Tag “f:loadBundle” und eine Klasse, welche den Zugriff auf das Resource-Bundle als java.util.Map zulässt.

  1 import java.util.Collection;
  2 import java.util.Map;
  3 import java.util.ResourceBundle;
  4 import java.util.Set;
  5 
  6 /**
  7  * For making a resource bundle accessible like a map.
  8  * Only some reading operations are supported.
  9  * This class is used to make the message resources available to jsf pages.
 10  *
 11  * @author Marc Neumann
 12  */
 13 public class ResourceBundleMap implements Map {
 14 
 15   private ResourceBundle bundle;
 16 
 17   /**
 18    * Creates a new map for which uses the given bundle.
 19    * @param pBundle Around this bundle will the map wrap.
 20    */
 21   public ResourceBundleMap(ResourceBundle pBundle) {
 22     bundle = pBundle;
 23   }
 24 
 25   /**
 26    * {@inheritDoc}
 27    */
 28   public void clear() {
 29     throw AssertUtil.failOperationNotSupported();
 30   }
 31 
 32   /**
 33    * {@inheritDoc}
 34    */
 35   public boolean containsKey(Object pKey) {
 36     throw AssertUtil.failOperationNotSupported();
 37   }
 38 
 39   /**
 40    * {@inheritDoc}
 41    */
 42   public boolean containsValue(Object pValue) {
 43     throw AssertUtil.failOperationNotSupported();
 44   }
 45 
 46   /**
 47    * {@inheritDoc}
 48    */
 49   public Set entrySet() {
 50     throw AssertUtil.failOperationNotSupported();
 51   }
 52 
 53   /**
 54    * {@inheritDoc}
 55    */
 56   public Object get(Object pKey) {
 57     return bundle.getString((String)pKey);
 58   }
 59 
 60   /**
 61    * {@inheritDoc}
 62    */
 63   public boolean isEmpty() {
 64     throw AssertUtil.failOperationNotSupported();
 65   }
 66 
 67   /**
 68    * {@inheritDoc}
 69    */
 70   public Set keySet() {
 71     throw AssertUtil.failOperationNotSupported();
 72   }
 73 
 74   /**
 75    * {@inheritDoc}
 76    */
 77   public Object put(Object pKey, Object pValue) {
 78     throw AssertUtil.failOperationNotSupported();
 79   }
 80 
 81   /**
 82    * {@inheritDoc}
 83    */
 84   public void putAll(Map pT) {
 85     throw AssertUtil.failOperationNotSupported();
 86   }
 87 
 88   /**
 89    * {@inheritDoc}
 90    */
 91   public Object remove(Object pKey) {
 92     throw AssertUtil.failOperationNotSupported();
 93   }
 94 
 95   /**
 96    * {@inheritDoc}
 97    */
 98   public int size() {
 99     throw AssertUtil.failOperationNotSupported();
100   }
101 
102   /**
103    * {@inheritDoc}
104    */
105   public Collection values() {
106     throw AssertUtil.failOperationNotSupported();
107   }
108 
109 }
110 

Der folgende Facelets Tag-Handler dient mir um das Resource-Bundle zu laden und ein Objekt im Page-Scope abzulegen, welches mir den Zugriff auf das ResourceBundle erlaubt:

 1 import java.io.IOException;

 2 import java.util.Locale;

 3 import java.util.MissingResourceException;

 4 import java.util.ResourceBundle;

 5 

 6 import javax.el.ELException;

 7 import javax.faces.FacesException;

 8 import javax.faces.component.UIComponent;

 9 import javax.faces.component.UIViewRoot;

10 import javax.faces.context.FacesContext;

11 

12 import org.apache.commons.logging.Log;

13 import org.apache.commons.logging.LogFactory;

14 

15 import com.openwishes.util.ResourceBundleMap;

16 import com.openwishes.util.ResourceBundleUtf8;

17 import com.sun.facelets.FaceletContext;

18 import com.sun.facelets.FaceletException;

19 import com.sun.facelets.tag.TagConfig;

20 import com.sun.facelets.tag.TagHandler;

21 

22 

23 /**

24  * For loading a resource bundle and assigning it to a variable in the request scope.

25  *

26  * @author Marc Neumann

27  */

28 public class LoadBundleTag extends TagHandler {

29 

30   private static final long serialVersionUID = 1L;

31 

32   private static Log log = LogFactory.getLog(LoadBundleTag.class);

33 

34   private String basename;

35 

36   private String var;

37 

38   /**

39    * Creates a tag instance.

40    * @param pConfig

41    */

42   public LoadBundleTag(TagConfig pConfig) {

43     super(pConfig);

44 

45     basename = getRequiredAttribute("basename").getValue();

46     var = getRequiredAttribute("var").getValue();

47   }

48 

49   /**

50    * {@inheritDoc}

51    */

52   public void apply(FaceletContext pFaceletsContext, UIComponent pUICompParent) throws IOException, FacesException,

53     FaceletException, ELException {

54 

55     FacesContext facesContext = pFaceletsContext.getFacesContext();

56 

57     try {

58       resolveBundleAndSetVar(facesContext);

59     }

60     catch (IllegalStateException e) {

61       throw new FacesException(e);

62     }

63   }

64 

65   @SuppressWarnings("unchecked")

66   private void resolveBundleAndSetVar(FacesContext pFacesContext) {

67     UIViewRoot viewRoot = pFacesContext.getViewRoot();

68 

69     if (viewRoot == null) {

70       throw new IllegalStateException("No view root! LoadBundle must be nested inside <f:view> action.");

71     }

72 

73     Locale locale = viewRoot.getLocale();

74     if (locale == null) {

75       locale = pFacesContext.getApplication().getDefaultLocale();

76     }

77 

78     try {

79       ResourceBundle bundle = ResourceBundleUtf8.getBundle(basename, locale, Thread.currentThread()

80         .getContextClassLoader());

81       pFacesContext.getExternalContext().getRequestMap().put(var, new ResourceBundleMap(bundle));

82     }

83     catch (MissingResourceException e) {

84       log.error("Resource bundle '" + basename + "' could not be found.");

85     }

86   }

87 

88 }

89 

In einer JSF-Seite wird das eigene loadBundle-Tag verwendet um den Schlüssel zu definieren, über den auf das Resource-Bundle zugegriffen wird. Angenommen die Variable heisst “msg”, so lautet ein Value-Binding-Ausdruck um den Wert für den Schlüssel “key1″ zu ermitteln: “#{msg.key1}”.

Die hier vorgestellte Lösung ist sicherlich nicht perfekt und möglicherweise nicht frei von Fehlern. Verbesserungsvorschläge und Kritik sind immer Willkommen. Für mich funktioniert die Lösung sehr gut, ist recht überschaubar und verbessert zudem die Performance des Systems in welchem ich die Lösung einsetze (wenn auch marginal).

  5 Responses to “UTF-8 Unterstützung für Property Resource Bundles in Java-Systemen und speziell für JSF”

  1. Hallo Marc,

    danke für deinen informativen Artikel. Leider komme ich als Java Umsteiger nicht klar mit den fehlenden Paketen, besonders in der “LoadBundleTag” Klasse. Könntest Du evt. erklären, wo und wie ich an die notwendigen Pakete komme?

    Vielen Dank und beste Grüße,
    Andi

  2. Hallo Andi,

    die Klassen ResourceBundleUtf8 und ResourceBundleMap verwenden ausschließlich Klassen des JDK – die müsstest du also haben. Die Klasse LoadBundleTag verwendet tatsächlich Java Server Faces spezifische Klassen. Diese brauchst Du aber wirklich nur, wenn Du das UTF-8 Resource Bundle in JSF benötigst (also nicht in anderen Web-Frameworks wie Struts, etc.):

    javax.el – Java Expression Language API: Sollte bei Facelets dabei sein.

    javax.faces – Java Server Faces API: http://java.sun.com/javaee/javaserverfaces/download.html

    com.sun.facelets – Faclets Aufsatz für JSF: https://facelets.dev.java.net/

    org.apache.commons.logging – Apache Commons Logging: http://commons.apache.org/logging/

    Von welcher (en) Programmiersprachen steigst Du denn auf Java um?

  3. Hi Marc,

    die letzte Frage ist etwas peinlich: von PHP :| aber egal… Kenne Java vom Studium und brauche nun nur etwas Routine um wieder rein zu kommen.

    Danke für Deine Tips, ich werde am Wochenende alles ausprobieren.

    Andi

  4. Hi,

    Unter welcher Lizenz steht Ihre Klasse? Darf ich sie in meiner GPL Software verwenden?

    Vielen Dank,

    A. Weber

  5. Hallo Andreas,
    Du kannst den Quellcode uneingeschränkt verwenden.

    Grüße,
    Marc

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>