Das übliche Verfahren, lokalisierte Meldungen zu erzeugen, ist die – meist implizite – Nutzung von PropertyResourceBundle. So bezieht bspw. Bean Validation seine Meldungen aus einem Resource Bundle mit Namen ValidationMessages. Dieses materialisiert sich üblicherweise aus Classpath-Ressourcen namens ValidationMessages.properties, ValidationMessages_de.properties etc.
Problematisch ist bei diesem Verfahren, dass z. B. nur eine Datei namens ValidationMessages.properties verwendet wird, auch wenn es davon mehrere im Classpath gibt. Damit ist leider der Weg verbaut, in einer Utility-Bibliothek bspw. Bean Validation Constraints inkl. der zugehörigen Meldungen auf einfache Weise zu definieren:
– Anwendung definiert Validierungsmeldungen als Classpath-Ressource-Bundle „ValidationMessages“ (1),
– von der Anwendung genutzte Bibliothek enthält ebenfalls ein Classpath-Ressource-Bundle „ValidationMessages“ (2).
– Validierungsfehler nutzen die Meldungen aus (1), aber nicht (2)!
Abhilfe ist hier leicht zu schaffen: Statt der impliziten Nutzung eines PropertyResourceBundle lässt sich eine Lokalisierung von Texten auch durch explizite Bereitstellung eines Resource-Bundles in Form passend benannter Klassen erreichen, d. h. im Beispielszenario können die Klassen ValidationMessages, ValidationMessages_de etc. zur Verfügung gestellt werden (alle im Default-Paket). Die Klassen müssen von java.util.ResourceBundle abgeleitet sein. In ihrem Konstruktor suchen sie nach allen passenden Classpath-Ressourcen und bauen damit einen interne Lookup-Tabelle auf. Sinnigerweise implementiert man dieses Verhalten in einer entsprechenden Basisklasse:
public class ValidationMessages extends MultiPropertiesResourceBundle { } public class ValidationMessages_de extends MultiPropertiesResourceBundle { } ... public class MultiPropertiesResourceBundle extends ResourceBundle { private Map messageMap = new HashMap(); } public MultiPropertiesResourceBundle() { loadProperties(this.getClass().getName().replace('.', '/') + ".properties"); } public MultiPropertiesResourceBundle(String bundleName) { loadProperties(bundleName + ".properties"); } private void loadProperties(String resourceName) { try { Enumeration found = Thread.currentThread().getContextClassLoader().getResources(resourceName); while (found.hasMoreElements()) { URL url = found.nextElement(); loadMessages(url); } } catch (IOException e) { // ignore } } private void loadMessages(URL url) { Reader reader = null; try { reader = new InputStreamReader(url.openStream(), "UTF-8"); Properties prop = new Properties(); prop.load(reader); for (Entry entry : prop.entrySet()) { String key = (String) entry.getKey(); if (!this.messageMap.containsKey(key)) { String value = (String) entry.getValue(); this.messageMap.put(key, value); } } } catch (IOException e) { // ignore } finally { try { reader.close(); } catch (Exception e) { // ignore } } } @Override public Enumeration getKeys() { ResourceBundle parent = this.parent; return new ResourceBundleEnumeration(this.messageMap.keySet(), (parent != null) ? parent.getKeys() : null); } @Override protected Object handleGetObject(String key) { return this.messageMap.get(key); } }