ResourceBundle inheritance
I’m confused about ResourceBundles.
Usually I write error messages like this:
public void getUserMessage(String pMessageKey, Object[] pArguments, Locale pLocale)
{
ResourceBundle bundle = getCachedResourceBundle(pLocale);
String message = bundle.getString(pMessageKey);
String formattedMessage = MessageFormat.format(message, pArguments);
return message;
}
and then I have a properties file in the class path that contains all the messages. Usually this properties file is in the same package as the classes that require the messages.
There’s a subtle flaw in this method though. It doesn’t pass the ResourceBundle’s name in. As a result, any subclasses are going to be using the same ResourceBundle as the parent.
Also not a problem, you’d think. Except where the subclasses are in another package, or in another project altogether. In this situation, I cannot add more messages to the parent properties file, because it doesn’t (and shouldn’t) know about the subclasses which call it. That behaviour is specific to the extended implementation.
Well, not a problem. In my new project, I’ll just create a new properties file in the same package space as the old one, and then ResourceBundle will look through all the properties files and find the first one that contains the appropriate message key… whoops. ResourceBundle doesn’t work like that.
In fact, ResourceBundle has no concept of “overriding” a message. ResourceBundle works by traversing the ClassLoaders and returning the first properties file it finds in the classpath (modulo the locale parentage).
In English, this means that
classes.jar/foo.bar.baz.Messages firstMessage=foo resources
with another message appended to it in the same package:
src/foo.bar.baz.Message secondMessage=additional message
results in only secondMessage being displayed. There is no firstMessage.
If you want to add a new message and still have the same ResourceBundle, you have to copy all the messages out of the first properties file, and add them to your own properties file, then add your own at the bottom. Which sucks.
It’s also not a solution. For one thing, what happens if a new release comes out and the messages of the subclass get added to? Because ResourceBundle is only looking at your subclassed properties, you’ll be missing those error messages. Worse, everything will continue to work perfectly until an exceptional condition occurs and the system finds out it doesn’t have the error messages to describe it.
The immediate solution would be to use a different ResourceBundle for each subclass, and setup a series of fallbacks to handle message key failures. This would look something like:
public class ResourceUtils
{
public void doStuff()
{
String msg = getUserMessage(this, "firstMessage");
System.out.println("firstMessage: " + msg);
msg = getUserMessage(this, "secondMessage");
System.out.println("secondMessage: " + msg);
}
public String getUserMessage(Object pCaller, String messageKey) {
Package packageObj = pCaller.getClass().getPackage();
String msg = null;
while (packageObj != null)
{
try
{
ResourceBundle bundle = getBundle(packageObj, "Messages");
msg = bundle.getString(messageKey);
} catch (MissingResourceException mre)
{
// do nothing.
}
String parentPackageName = getParentPackage(packageObj.getName());
packageObj = Package.getPackage(parentPackageName);
}
return null;
}
private String getParentPackage(String pPackageName)
{
int lastDotPos = pPackageName.lastIndexOf('.');
if (lastDotPos == -1)
{
return "";
}
String name = pPackageName.substring(0, lastDotPos);
return name;
}
public ResourceBundle getBundle(Package pPackage, String pName)
{
return getBundle(pPackage, Locale.getDefault(), pName);
}
public ResourceBundle getBundle(Package pPackage, Locale pLocale,
String pName)
{
String packageName = pPackage.getName();
String bundleName = packageName + "." + pName;
ResourceBundle bundle = ResourceBundle.getBundle(bundleName, pLocale);
return bundle;
}
}
Now that’s just plain silly. Catching exceptions inside a loop, doing bizarre stuff with Package parsing… ugh. I could go through the loop once and then cache the results, but Package.getPackage() returns null, probably because the “root” packages don’t have any classes in them.
Plus the essential problem that this just adds another level of complexity to something that should be simple and unbreakable, means that this approach is a washout.
No, the really bright idea would be to bite the bullet and have a different ResourceBundle for each subclass, and then any messages have to be in that ResourceBundle. So it’s a ResourceBundle per class, or package.
But that sucks. I have to pass in a bundle name parameter for each class, my messages are all split up. And I have to pass in a name, because different requests can have different locales. So just using ResourceBundle.getBundle(bundleName) isn’t good enough.
If I want to use an error message from a “super” ResourceBundle, I have to explicitly get that one (and that means I can’t fallback if that key is taken out… hmm, maybe that’s a bad idea). I have another problem in that I literally don’t know where my error messages are coming from. If I don’t have access to the logs, I have to bounce through the files to find out which class is responsible.
It also doesn’t solve the problem of what to do when I legitimately need to change an error message in a subclass in a different project. I don’t want to completely replace the file, but I think I can use the locale inheritence to make my error message take precedence, that is:
foo.bar.baz.Messages firstMessage=foo resourcessecondMessage=second resources
can be overridden by
foo.bar.baz.Message_en firstMessage=foo src secondMessage=additional message
but that only works on one level. If there’s another subclass or project that needs to do that over that, I can’t pull the same trick again. Plus, it’s a subversion of the original intent of the design.
I’ve toyed with the idea of subclassing ResourceBundle, but ResourceBundle.getBundle() is a static method. I’ve thought about throwing it away altogether, but it does too much to make it worthwhile to write a replacement. And I really don’t want to cross my fingers and hope one of the Jakarta libraries works. And this doesn’t even get into the limitations of MessageFormat – five years after PropertyDescriptors and we’re still stuck with {date,time,number,choice}.
Like I said, I’m confused.
Update: It seems like having a central class for each package is the best idea. Eclipse and Dynamo do the same thing – there’s a UserMessages class which accesses the resource bundle, and then messages are passed through that.