Friday, May 20, 2005

XML or database-backed ResourceBundle

Ever wanted to store your localized messages in an XML file or database? You have probably found that java.util.ResourceBundle was designed in such a way that it is impossible to provide a clean solution to this problem. This is tracked as bug 4403721 and it seems that some solution may be available in Mustang.

For those who need it now I managed to put together a couple of classes that address this issue. Not as clean as I'd like it to be, but it works fine with anything that uses ResourceBundle.getBundle(), such as JSTL. In other words it should work in any situation where .properties file would normally work.

The "not as clean as I'd like" part is that you'll need a new class for each supported locale. The good news is that this class is almost empty.

First you need to create some sort of MessageProvider. Its role is to read localized messages from the database or load them from XML file. This object would be a signleton instance shared by all bundles. It would also provide a method like getString(String localeName, String key) used by all the bundles to retrieve the messages.

All localized resource bundle classes would inherit from the same class. Let's call it AbstractResourceBundle. Its code could look like:

public abstract class AbstractResourceBundle extends ResourceBundle {
protected String mLocaleStr;
private MessageProvider mMessageProvider =
MessageProvider.getSharedInstance();

protected AbstractResourceBundle(String localeStr) {
mLocaleStr = localeStr;
}

protected AbstractResourceBundle() {
String className = getClass().getName();
int pos = className.indexOf('_');
if (pos == -1 || pos >= className.length()-1) {
throw new RuntimeException("Class name must contain locale suffix");
}
mLocaleStr = className.substring(pos+1);
}

public Enumeration getKeys() { return null; }

protected Object handleGetObject(String key) {
assert mLocaleStr != null : "mLocaleStr not set";
return mMessageProvider.getString(mLocaleStr, key);
}
}


There are two important things here. First is the fact that handleGetObject method delegates the call to the shared instance of MessageProvider.

The second is that the constructor AbstractMessageProvider() will figure out the locale from the inheriting class name. It's required that the class name contains locale suffix, otherwise ResourceBundle.getBundle(...) will not be able to load it.

An implementation for a supported locale looks like this:
public class MyBundle_fr_FR extends AbstractResourceBundle {}

Finally, you would use it like:
ResourceBundle rb = ResourceBundle.getBundle("MyBundle", new Locale("fr", "FR"));
String msg = rb.getString("label.hello");

If anyone managed to solve it in a better way, please share it.