Sunday, December 14, 2008

Java Resources Support

One of the programming philosophies that I try to follow in order to encourage myself always to do my best work is to assume that every program I write will eventually become a "production" program, distributed to customers all over the world. One detail that falls into that "all over the world" part is internationalization. In other words, I design internationalization into my programs from the start, even when the program is primarily for internal or personal use.

Java makes the string-replacement part of this pretty easy to do with its ResourceBundle class. If you use ResourceBundle consistently, you can easily ship localizations for multiple languages bundled with your application, and other people can take your program and resource files and localize them for other languages without having to recompile your program.

If you have thousands of strings being translated into dozens of languages, you might want to get yourself a sophisticated translation management system or ship around XLIFF files (or perhaps you would like to help write an open source localization repository). But for smaller projects such as I have worked on, I have found that some relatively simple support methods and a bit of convention have served me pretty well.

Contents

Building The Resources

To simplify loading resources, I find it easiest to put them all into one properties file per locale. But to improve maintainability, it is best to keep the resources in multiple files close to the source files in which they are used. I bridge these two conflicting goals by keeping my resources in multiple properties files, then concatenating them all together into a single properties file when I build my application.

To avoid confusion and emphasize the fact that my properties source files are not used in their source file form, I use the extension .props for those files. My concatenation program collects all of the .props files for one locale into one .properties file. I run it once for each locale for which I have files. The concatenation program also adds separators and source filenames in comments between the contents of each source file, and converts non-ascii characters to backslash-u notation. While I am at it, I also stuff some other information into the generated properties files, such as version and date information, so that I can easily access those details at runtime.

In conformance with the standard per-locale naming convention for resource bundle properties files, for each .props base file there can also be locale-specific translations. For example, for the base file Foo.props there could be Foo_hu.props with Hungarian translations, Foo_fr.props with French translations, and Foo_fr_CA.props with French-Canadian translations.

The properties file with the collected resources goes into a file in the same directory as the class files and gets packaged up in the jar file along with the class files. For example, I might name the collected resources file net/jimmc/app/Resources.properties, so it goes in the directory with all of the files in the net.jimmc.app package. When I load it, I specify its location as net.jimmc.app.Resources and the ResourceBundle code automatically finds it from my jar file, as shown in the initResources method below.

The Basic Interface

In order to be able to use the simplest possible code when calling to retrieve resources in my Java apps, I use support code, shown below, to do the following:
  • Encapsulate my ResourceBundle in the support code so that the caller does not need to worry about where the resources come from.
  • Make my resource retrieval functions so that, when a key is not found in the resources, it returns the key as the value of the method rather than throwing an exception. The key then appears in my GUI, where I can see it and correct it later when I want, rather than causing an exception that stops the program and forces me to correct the string immediately before I proceed.
  • Retrieve a resource string and format it with one or more arguments in one call.
In my Java apps, I define a ResourceSource interface with definitions for methods to implement the above functionality:
public interface ResourceSource { /** Get a string from resources. * @param name The resource name. * @return The value of the resource, * or the name if no value is found. */ public String getResourceString(String name); /** Get a string from resources, or null if not found. * @param name The resource name. * @return The value of the resource, * or null if no value is found. */ public String getResourceStringOrNull(String name); /** Get a string from a resource and pass it to MessageFormat.format * with the specified arguments, returning the result. * @param name The resource name. * @param args The args to MessageFormat.format. * @return The formatted resource string. */ public String getResourceFormatted(String name, Object[] args); /** Get a string from a resource and pass it to MessageFormat.format * with the specified one argument put into a new Object[1], * returning the result. * @param name The resource name. * @param arg The argument to MessageFormat.format. * @return The formatted resource string. */ public String getResourceFormatted(String name, Object arg); }

The Basic Implementation

My Application object, a singleton, implements my ResourceSource interface and is the source for resource values for the application. (This is conceptually the same as the IResources interface I mentioned in a previous post.) It contains an internal reference to my resources, so that the caller does not need to worry about that:
import java.text.MessageFormat; import java.util.ResourceBundle; private ResourceBundle resources;
During initialization of my Application object, I call a simple initResources method to load my resources from that one file containing the concatenated contents of all of the props files:
private void initResources() { resources = ResourceBundle.getBundle("net.jimmc.app.Resources"); }
My Application object has a private method that accesses my resources, which all of my other Application resource methods call (to simplify adding other functionality later):
private String getResourceValue(String name) throws MissingResourceException { return resources.getString(name); }
The methods in the Application object that implement the ResourceSource interface all call getResourceValue to get the resource value:
public String getResourceString(String name) { try { return getResourceValue(name); } catch (MissingResourceException ex) { return name; //use the resource name } } public String getResourceStringOrNull(String name) { try { return getResourceValue(name); } catch (MissingResourceException ex) { return null; } } public String getResourceFormatted(String name, Object[] args){ String fmt = getResourceString(name); return MessageFormat.format(fmt,args); } public String getResourceFormatted(String name, Object arg) { String fmt = getResourceString(name); Object[] args = { arg }; return MessageFormat.format(fmt,args); }

Field Names In Format Strings

When using the formatting methods in java.text.MessageFormat you specify the locations of replacement values using numbers in braces, so your formatting string might contain phrases such as {1} or {2,date,medium}. You have to ensure that these index numbers correspond properly to the arguments in the source code that uses that format string. Since these format strings are in a different file (the resource properties file) than the code that uses them, extra care is required to avoid mixing up the numbers, such as when adding or modifying the argument list. To minimize this problem, I add a version of getResourceFormatted that allows using field names rather than numbers in the format strings, so they contain phrases such as {count} and {modtime,date,medium}. When calling these methods, the fieldNames array contains names that correspond to the args of the same index.
/** Get a string from the resource file, map fields names to numbers, * and format it with the given arguments. */ public String getResourceFormatted(String name, Object[] args, String[] fieldNames){ String fmt = getResourceFormat(name,fieldNames); return MessageFormat.format(fmt,args); } /** Get a resource string and map field names to numbers. */ public String getResourceFormat(String name, String[] fieldNames) { String fmt = getResourceString(name); return mapFieldNamesToNumbers(fmt, fieldNames); } /** Replace named MessageFormat field references by field numbers. * The named field reference looks just like a regular format * segment, but with a string in place of the number. */ public String mapFieldNamesToNumbers(String s, String[] fieldNames) { for (int i=0; i<fieldNames.length; i++) { String fieldName = fieldNames[i]; if (fieldName==null || fieldName.equals("")) continue; //skip blanks in the array String p = "\\{"+fieldName+"\\}"; String q = "{"+i+"}"; s = s.replaceAll(p,q); p = "\\{"+fieldName+"\\,"; q = "{"+i+","; s = s.replaceAll(p,q); } return s; }

Runtime Overriding Of Resources

In order to allow loading additional resources at run time, for debugging, I add an additional variable to store my override resources:
private Properties resourceProperties;
I then add a method, which gets called from a debugging command to which I have specified a resources properties file name, to load values from the specified properties file into my override resources:
public void loadResourceProperties(String filename) { if (resourceProperties==null) resourceProperties = new Properties(); try { FileInputStream ifile = new FileInputStream(filename); resourceProperties.load(ifile); String msg = getResourceFormatted("info.LoadedResourceProperties", filename); startupMessage(msg); } catch (Exception ex) { String msg = getResourceFormatted( "error.LoadingResourceProperties",filename); throw new RuntimeException(msg,ex); } }
I modify my private getResourceValue method to look in my override properties before using the regular properties:
private String getResourceValue(String name) throws MissingResourceException { if (resourceProperties!=null) { String v = resourceProperties.getProperty(name); if (v!=null) return v; //found an override property value } return resources.getString(name); }

Recursive Resources

For some applications I want to be able to define resource strings that reference other resource strings or context values provided by the application. The standard replacement text in a message format starts with an open brace followed by a number. In my resource files, after implementing my Field Name extension above, I can also follow an open brace by a field name. With the extension in this section, I can follow an open brace by the "@" character and a context name or the name of another resource. The values for the context names are passed to the formatting methods in a Map.

For example, if I have these resources:
Sample.abc.foo=A formatted string with {@Sample.abc.bar} for {@User} Sample.abc.bar=a nested property
and I create a context Map with User=Jim, then using the methods in this section I can call getResourceFormattedRecurse passing name as "Sample.abc.foo" along with my context and get back "A formatted string with a nested property for Jim".

To implement this capability, I add the following to my ResourceSource interface:
/** Get a string from resources, recursing to do replacement of * special patterns with other resources. */ public String getResourceStringRecurse(String name); /** Get a formatted string from the resource file, including * recursive replacement of special strings. */ public String getResourceFormattedRecurse(String name, Object[] args, String[] fieldNames, Map ctx, int maxRecurse);
I add the corresponding code to my Application object:
public final static int DEFAULT_MAX_RESOURCE_RECURSE = 20; /** Get the value of a resource, replacing the special tags. * @see #replaceResourceRecurse */ public String getResourceStringRecurse(String name) { String value = getResourceStringOrNull(name); if (value==null) return name; return replaceResourceRecurse(value); } /** Get a formatted string from the resource file, including * recursive replacement of special strings. */ public String getResourceFormattedRecurse(String name, Object[] args, String[] fieldNames, Map ctx, int maxRecurse) { String fmt = getResourceString(name); if (maxRecurse>0) fmt = replaceResourceRecurse(fmt, ctx, maxRecurse); if (fieldNames!=null) fmt = mapFieldNamesToNumbers(fmt, fieldNames); return MessageFormat.format(fmt,args); } /** Given a string, look for occurrences of our special replacement * syntax and replace with the referenced value, with a * default recursion limit. * @param s The string through which we look for our special * replacement tags. The replacement tags are of the * form {@XXX} where XXX is the name of the field with * which to replace that tag. * @throws RecursionLimitException if the maximum recursion depth * is exceeded. * @see #replaceResourceRecurse(String,Items,int) */ public String replaceResourceRecurse(String s) { return replaceResourceRecurse(s, null, DEFAULT_MAX_RESOURCE_RECURSE); } /** Given a string, look for occurrences of our special replacement * syntax and replace with the referenced value. * @param s The string through which we look for our special * replacement tags. The replacement tags are of the * form {@XXX} where XXX is the name of the field with * which to replace that tag. * @param ctx The context dictionary for replacement. We first look * for XXX in this dictionary; if not found, we look for * a resource named XXX. May be null. * @param maxRecurse The maximum recursion depth. * @throws RecursionLimitException if the maximum recursion depth * is exceeded. */ public String replaceResourceRecurse(String s, Map ctx, int maxRecurse){ if (s==null) return s; int x = s.indexOf("{@"); //look for our replacements if (x<0) return s; //no replacements StringBuffer sb = new StringBuffer(); int a = 0; while (x>=0 && x<s.length()) { sb.append(s.substring(a,x)); int y = s.indexOf("}",x); if (y<0) { String eMsg = "Missing close brace "+ "in resource after "+ s.substring(x,s.length()); //TBD i18n throw new RuntimeException(eMsg); } String name = s.substring(x+2,y); String v = null; if (ctx!=null) //try the context if we have one v = (String)ctx.get(name); if (v==null) { //If no context property, try resource v = getResourceStringOrNull(name); } if (v!=null) { if (maxRecurse<=0) { throw new RecursionLimitException(name); } //Found the specified resource, recurse on it and //replace specials in it before we plug it into our string. try { v = replaceResourceRecurse(v,ctx,maxRecurse-1); } catch (RecursionLimitException ex) { //If we exceeded the recursion limit, we will be popping //back through here many times; we add on the name of //each item in the recursion chain to add in debugging. ex.setMessage(name+"->"+ex.getMessage()); throw ex; } sb.append(v); } a = y+1; x = s.indexOf("{@",a); } if (a<s.length()) sb.append(s.substring(a)); return sb.toString(); }
RecursionLimitException is a simple exception class that also allows setting the error message:
public class RecursionLimitException extends RuntimeException { private String ourMessage; /** Create a RecursionLimitException. */ public RecursionLimitException(String message) { super(message); } public void setMessage(String msg) { this.ourMessage = msg; } public String getMessage() { if (ourMessage!=null) return ourMessage; else return super.getMessage(); } }

No comments: