Parameterized Typesafe Constants — Charles Owen
Conventional methods of accessing externalized text do not provide adequate guarantees that application code is rigorously correct. This lack of rigor can lead to significant maintenance problems, particularly in large applications where the consequences of loose APIs are accentuated. By using parameterized typesafe constants, one can provide such guarantees while exploiting conventional APIs.

Consider a simplified example. Let's say our problem involves aggregating error messages during request processing then delivering the collection of messages to the client, perhaps translated into the user's preferred language. Each anomaly encountered in the request prompts a call to our API that carries sufficient information to characterize the problem. In what form should information be passed?

For larger applications, a String-based API is a bad idea. The semantics are ambiguous, and to prove that all calls to the API throughout the code base are correct would be unnecessarily difficult. We'll examine this more closely in what follows; first take a look at a couple of ways in which this might be implemented:

    // API method: String argument
    public void addMessage(String s) {
        ...
    }
    
    // Example (1) client code: String argument is actual message
    if ((name == null) || (name.length() == 0))
        msg = ResourceBundle.getBundle(...).getMessage(NAME_REQUIRED);
        errors.addMessage(msg);
    }

    // Example (2) client code: String argument is key to message
    if ((name == null) || (name.length() == 0))
        errors.addMessage(NAME_REQUIRED);
    }
Example (1) is obviously a problem. In a system of any size and complexity, this approach would be disastrous. The idiom is weak, enforceable only by code inspection. Enough said.

Example (2) is only slightly better. In this case the API requires that clients supply a key indicating which message to use; it also hides the critical implementation details. All of this represents improvement but ambiguities remain. The compiler cannot distinguish misspelled keys, for example. And to prove that the code is correct still depends essentially on careful inspection.

A substantial solution involves more specific types. The approach presented here will take advantage of existing APIs when possible, and will behave much like a typesafe constant. In the following code snippets, we use Message instances; in practice one would use types that have more specific meaning. For example, ErrorMessage, Warning, or FieldLabel might make sense.

    // API method
    public void addMessage(Message msg) { ... }
    
    // client code
    if ((name == null) || (name.length() == 0)) {
        errors.addMessage(Message.NAME REQUIRED);
    }
Valid instances of a more specific type must be used; the compiler will balk otherwise. Moreover, careful design can eliminate any remaining doubt that the indirectly-specified text exists.

There are certainly several reasonable ways in which one might parameterize Message instances with localized text. I'll offer one such approach here. Message text in our hypothetical application will be externalized in properties files named according to the conventions specified by the ResourceBundle API. ResourceBundle will also be used to initially retrieve the text. In addition, we will require that names of Message instances be identical to the keys specified in the locale-specific properties files. Our design will enforce this simple, but obviously quite useful constraint. (For more detail, download the code and have a look. Do look at the unit tests as well.)

Examine first the source for Message. Developers who adopt this approach would create classes like this one, with meaningful names to suit their needs. It looks like an ordinary typesafe constant — but with a twist. As one might expect, the constructor is private; the only instances of Message are the static final members. There is a static initializer that calls init, defined in the base class, which handles the heavier work of retrieving text from its external location and parameterizing subclasses. As input it requires a class to parameterize, and a bundle name.

    public class Message extends Externalized
    {
        public static Message NAME_REQUIRED = new Message();
        public static Message INVALID_DATE  = new Message();
    
        private Message() {}
    
        static { 
            init(Message.class, "Messages"); 
        }
    }
The implementation of Externalized is also straightforward. Each instance has a map associating a Locale with a String. Access to localized text is provided through the toString() method. Importantly, if one of the static Externalized fields (like one of our Message fields, for example) has a name that is not associated with a message, an exception is thrown while the class is being initialized. Successfully loading and initializing Message is enough to prove that every declared Message instance is in fact associated with some text. This should be done as part of a system's unit tests, and could be done at system startup. Contrast this with the problem noted earlier: if messages are found in the conventional manner by using a String key, then it may be very complicated to attain a similar level of confidence in the correctness of the system.
    public abstract class Externalized
    {
        /**
         * Initialize specified class using the named bundle.
         * Equivalent to init(clazz, bundleName, null).
         */
        protected static final void init(Class clazz, String bundleName) {
            init(clazz, bundleName, null);
        }
    
        /**
         * Initialize specified class using the named bundle and locales.
         */
        protected static final void init(
            Class clazz, 
            String bundleName, 
            Locale[] supportedLocales) {
            try {
                Field[] fields = clazz.getFields();
                ResourceBundle bundle = ResourceBundle.getBundle(bundleName);
                populate(fields, Locale.getDefault(), bundle);
    
                if (supportedLocales == null) return;
    
                for (Locale locale : supportedLocales) {
                    bundle = ResourceBundle.getBundle(bundleName, locale);
                    populate(fields, locale, bundle);
                }
            } catch (Throwable ex) {
                ex.printStackTrace();
                String msg = ex.getMessage();
                throw new IllegalStateException(msg);
            }
        }
    
        private static void populate(
            Field[] fields, 
            Locale locale, 
            ResourceBundle bundle)
            throws IllegalAccessException
        {
            for (Field f : fields) {
                if (Externalized.class.isAssignableFrom(f.getType())) {
                    Externalized ext = (Externalized) f.get(null);
                    ext.localeValues.put(locale, bundle.getString(f.getName()));
                }
            }
        }
    
        /**
         * Collection of messages, one for each supported Locale.
         * The default locale is always represented.
         */
        protected Map localeValues = new HashMap();
    
        protected Externalized() {}
    
        /**
         * Return value for default Locale.
         */
        public final String toString() {
            return this.localeValues.get(Locale.getDefault());
        }
    
        /**
         * Return value for specified Locale.
         */
        public final String toString(Locale locale) {
            String msg = this.localeValues.get(locale);
            return (msg != null) ? msg : toString();
        }
    }
This approach provides rigorous controls not present in String-based APIs. The compiler exercises most of the control by guaranteeing that APIs designed to deliver specific types of externalized text will do exactly that: if the user is supposed to see an error message, a field label, or any other sort of externalized text, then application developers must use a Message or a FieldLabel or some other specific type. Hard-coded text mixed with source code becomes impossible. And here is perhaps the most important point: using this approach automates the task of verifying that every attempt to access externalized text will meet with success — clearly helpful in large applications. Another advantage is that all of the guesswork and reliance on documentation that often characterizes looser APIs becomes unnecessary.