(including all source code left out of the article in IOUG Collaborate 2009 Select Journal)
Web applications, like any type of application, contain a great deal of boilerplate text. This includes button labels, field prompts, hint text, error messages, page titles and display label for domain values. The boilerplate text is typically specified by functional analysts and is frequently refined during testing or even later in the lifecycle of an application. Having this text embedded, hard-coded, all through the application in page definitions, backing beans, JavaScript validation functions and model classes is not a good idea. It makes efficient management of the boilerplate text virtually impossible.
Additionally, it may very well be that the boilerplate text is not static. The text may have to be aligned with whoever is accessing the application. Users from different departments or organizations may use different terminology. What for one user is a customer could be a client or patient to another user. Of course, users may speak different languages; depending on their personal preference, such as the language setting in the browser so they may desire the text to be presented in English, Spanish, or French.
This arrticle discusses an interesting challenge: A JSF application should display boilerplate text, namely titles, button labels, prompt, error messages, tool tips, etc. in a context sensitive way. This must be done not just by language, region and variant, which are the well known dimensions along which the standard JSF and Java mechanism works with Resource Bundles. Beyond this simple “locale” sensitivity, which was also needed, a more specialized context dependency was required along several dimensions.
For example when a user of younger age category accesses the web application, the text presented should be (or at least could be) different from whatever is shown to senior users. Also, when the application is accessed in the context of a certain brand or company, the text may need to be different from other brand or company contexts. In addition, the marketing department came up with the ability to present some text tailored to the time of year (e.g. Winter or Summer, Holiday Season or no Christmas in sight, or the day of the week, working day or weekend). Good old marketing department – if they were to rule the world….
How can we cater to these various context dependencies, along various mutually orthogonal dimensions. Start with a close look at the default mechanism in JSF 1.2.
Default ResourceBundle facilities in JavaServer Faces 1.2
The default facilities in JSF are pretty simple:
ResourceBundles are configured, either per page or for the application as a whole (as of JSF 1.2). The latter is usually preferable, since specifying a resource bundle for every page is quite a task.
The ResourceBundle configuration in the faces-config.xml looks like this:
<faces -config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee"> <application> <resource -bundle> <base -name>nl.amis.appBundle</base> <var>msg</var> </resource> </application>
Here the base-name refers to a properties file on the file system. Note that instead of a properties file, you can also use a class. This can provide some control over the encoding and special characters in messages and/or where to retrieve the messages from (for example a database).
The properties file in this case is extremely simple:
title=The Interesting World of Internationalization formSubmit=Apply Changes if you like ageCategorySelector=Select Age Category
It includes just three keys with corresponding messages. At this point, no locale-specific versions of the bundle, so no appBundle_en_us.,properties or appBundle_nl.properties files. What has already been achieved here is storing the boilerplate text outside of the code in a central store where it can easily be maintained. Note that many utilities exist for maintaining the contents of resource bundles with global search and replace for example, and support for easy translation.
With the configuration in faces-config.xml and the properties file with the keys and messages in place, you can create a JSF page that uses messages from the bundle. Note that JSF takes care of applying the proper locale, read from the ViewRoot (FacesContext.getCurrentInstance().getViewRoot().getLocale()). The locale value found here is first of all derived from the Browser locale, but can be programmatically set to a value derived, for example, from a user preference.
The very simple JSF page looks like this:
... <f:view> <html> ... <body> <h:form> <h:panelgrid columns="1" > <h:outputtext value="#{msg.title}"/> <h:commandbutton value="#{msg.formSubmit}"/> </h:panelgrid> </h:form> </body> </html> </f:view>
Note the references to i18n-ed messages using EL expressions of the format #{msg[‘key’]} or the equivalent #{msg.key}. The resource bundle was registered with a variable called msg that you can refer to in these expressions.
When you run the page, it will display an extremely ugly page that takes its boilerplate text from the properties file.
Setting the stage for context sensitive resource processing – Intercepting the ResourceBundle requests
So far this is nothing special. But you have to start preparing for the influence exerted by the context. Ask yourself: How can you let the context influence the way in which messages are retrieved from the resource bundle? Surely the default JSF and Java resource bundle mechanism have no knowledge beyond plain old Locale. It is up to you to preprocess a message-request to the resource bundle in order to handle the context sensitivity. So you have to intercept the request from the application before the resource bundle mechanism is invoked.
Doing that is easy enough. The resource bundle calls are specified through EL Expressions like #{msg.key}. If you make sure that msg is no longer the resource bundle itself but your very own managed bean that implements the Map interface, in order to handle the .key request, you have achieved the interception.
The bean should subsequently at some point call upon the real Resource Bundle to handle the request. But now you can fiddle with the request in two ways: you can manipulate the key and you can decide to call something other than the default resource bundle.
You can start with the plain interception, which requires no fiddling at all.
The following is the configuration of the managed beans. Note that there are two beans, one to implement Map and intercept the message request (MessageProvider) and the other one to implement the interception and apply context sensitivity logic:
<managed -bean> </managed><managed -bean-name>msgMgr</managed> <managed -bean-class>nl.amis.MessageManager</managed> <managed -bean-scope>session</managed> <managed -bean> </managed><managed -bean-name>msg</managed> <managed -bean-class>nl.amis.MessageProvider</managed> <managed -bean-scope>request</managed> <managed -property> <property -name>msgMgr</property> <value>#{msgMgr}</value> </managed>
Now that you have hijacked the msg name for your MessageProvider, you should register the Resource Bundle itself under a different name.
<application> <resource -bundle> <base -name>nl.amis.appBundle</base> <var>msgbundle</var> </resource> </application>
The implementation of the MessageProvider class is very simple. It passes the request onwards to the MessageManager:
package nl.amis; import java.util.HashMap; public class MessageProvider extends HashMap{ private MessageManager msgMgr; public MessageProvider() { } @Override public Object get(Object key) { return msgMgr.getMessage((String)key); } public void setMsgMgr(MessageManager msgMgr) { this.msgMgr = msgMgr; } public MessageManager getMsgMgr() { return msgMgr; } }
The references in the JSF pages can all stay the same. It makes no difference whether the EL expression in the page references the Resource Bundle registered with JSF directly or a managed bean as shown here.
For the MessageManager, the initial implementation that proves the interception mechanism works, but does no manipulation, looks like this:
public class MessageManager { public String getMessage(String key) { // use standard JSF Resource Bundle mechanism return getMessageFromJSFBundle(key); // use the default Java ResourceBund;e mechanism // return getMessageFromResourceBundle(key); } private String getMessageFromResourceBundle(String key) { ResourceBundle bundle = null; String bundleName = "nl.amis.appBundle"; String message = ""; Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale(); try { bundle = ResourceBundle.getBundle(bundleName, locale, getCurrentLoader(bundleName)); } catch (MissingResourceException e) { // bundle with this name not found; } if (bundle == null) return null; try { message = bundle.getString(key); } catch (Exception e) { } return message; } private String getMessageFromJSFBundle(String key) { return (String)resolveExpression("#{msgbundle['" + key + "']}"); } public static ClassLoader getCurrentLoader(Object fallbackClass) { ClassLoader loader = Thread.currentThread().getContextClassLoader(); if (loader == null) loader = fallbackClass.getClass().getClassLoader(); return loader; } public static Object resolveExpression(String expression) { FacesContext facesContext = FacesContext.getCurrentInstance(); Application app = facesContext.getApplication(); ExpressionFactory elFactory = app.getExpressionFactory(); ELContext elContext = facesContext.getELContext(); ValueExpression valueExp = elFactory.createValueExpression(elContext, expression, Object.class); return valueExp.getValue(elContext); } }
You can choose one of two ways to access the Resource Bundle.
One is using the JSF mechanism directly, which will only work for Resource Bundles that are explicitly registered with JSF in faces-config.xml files. See method getMessageFromJSFBundle.
The other one goes around whatever facilities JSF offers and uses the standard Java ResourceBundle library directly. With this approach, you have to find out the Locale yourself. This approach can be used with any resource bundle file on the classpath, without them having been explicitly registered in faces-config.xml. This approach is used in getMessageFromResourceBundle().
Enter the Context that should influence the Resource Bundle results
At this point the extra context enters the picture. For simplicity’s sake, assume that the context will be indicated by a property on a session scope managed bean. The context can be set using a List control in the user interface (typically this would be handled in a more subtle way).
The MessageManager bean is extended with the ageCategory property and a getter and setter method.
The JSF page is extended with the list control, which for some weird reason does not get its labels from the resource bundle:
<h:selectonelistbox value="#{msgMgr.ageCategory}" label="#{msg.ageCategorySelector}"> <f:selectitem itemLabel="Junior" itemValue="junior" itemDescription="Age category Under 35"/> <f:selectitem itemLabel="Senior" itemValue="senior" itemDescription="Age category 35+"/> </h:selectonelistbox>
Now you can set the context, but the question is: what difference does it make? Or better yet, how can you have it make a difference? When the user toggles from Senior to Junior age category, what should be the effect on the text in the page?
Basically there are two ways to handle this. One is to add additional keys to the resource bundle. These keys are composed of the original (base) key and an identification of the context in which the key applies. For example:
title=The Interesting World of Internationalization formSubmit=Apply Changes if you like ageCategorySelector=Select Age Category title_senior=The never ending wonders of the World of Internationalization formSubmit_senior=Notify the application of your desires by pressing this button title_junior=Speaking your own language ageCategorySelector_junior=Pick your own age peer group
Here the keys to the resource bundle properties file that was already being used have been added. Note that you do not need all keys to be included for every context that you want to support. Only when the message should be context specific for a certain context is it necessary to provide an extra entry.
The change required in the code of the MessageManager is minimal. First try to find the message for the key composed of the base key (passed in from the page) and the current context. When no message is found, try again, this time with only the base key.
public String getMessage(String key) { // add the current context to the key and dive into the large resource bundle with all keys, simple and composed with context // use the JSF bundle mechanism, that requires <application><resource -bundle> elements in the faces-config.xml // String msg = getMessageFromJSFBundle(key+"_"+ageCategory); // if (msg==null || msg.startsWith("???")) // msg = getMessageFromJSFBundle(key); // use the default Java ResourceBund;e mechanism String msg = getMessageFromResourceBundle(key+"_"+ageCategory); if (msg==null || msg.startsWith("???")) msg = getMessageFromResourceBundle(key); return msg; }
Again, two approaches are possible, one going through JSF and the other going straight at the ResourceBundles.
Note: The complex keys could have some fancy hierarchical context scheme if you like: junior_christmas_male_brandX_page1Title, christmas_male_brandX_page1Title, male_brandX_page1Title, brandX_page1Title, pageTitle1.
Run the page; select Senior and press the button:
Then select Junior and press the button:
Resource Bundle per context value
Instead of adding all of the context-specific keys and messages to the same big old resource bundle (file), you can also create specific files for each context that you want to support.
Doing this gives the context precedence of the locale: first check the context-specific resource bundles for the specific locale, the less specific locale, and the default locale. Only when the context specific bundles for all applicable locales have been exhausted, will the not context specific bundle be checked, for all the locales. The end result is a message that is specific to the context but not to the language. The approach with a single resource bundle and composite keys gives precedence to the locale over the context.
Summary
You have seen how the standard Resource Bundle mechanism in JSF can be used. A way was suggested to intercept a boilerplate text request in a managed bean that can manipulate the request, taking the current context (other than the standard Locale) into account. What was not shown is how the Resource Bundle can be implemented by a class that either specifies the key/message pairs in a hard-coded way or gets them from an external source (such as a database) which would allow for runtime management of the resource bundle contents.