Testing i18n features of JSF applications – Forcing a refresh of the Resource Bundle(s)

Internationalization, multi-linguality, NLS support for JSF applications – or any Java powered application for that matter – is an important topic. And Resource Bundles are the way to deal with the challenge of presenting elements like titles, prompts, images and error messages in a way best suited to the current end user’s requirements. Resource Bundles allow applications to present themselves in various languages, even regional dialects, as well as per-organization defined business terms without any special facilities in the application code.

Resource Bundles are a fundamental part of the Java programming language. They can be used in Java Client (Swing) applications just as easily as in Web Applications, based on a Servlet architecture. In this article I am concerned with Java Server Faces based applications, but the train of thought and the solution presented applies in only slightly different format to other Java environments as well.

The issue is the following: ResourceBundles are typically implemented using plain text properties files – although various formats are allowed. The translation of strings into values appropriate for the current locale is performed by objects extending abstract class java.util.ResourceBundle. Typically, ResourceBundle instances are instantiated passing in the names of property files. Whenever the local sensitive value of a String is required, the ResourceBundle is called upon to provide a translation. So far so good.

The implementations of ResourceBundle are allowed to – and typically do – make use of internal caches that permit them not to have to reload the property files or other sources of keys and values for each translation request. That makes perfect sense of course: performance would be dreadful if potentially multiple files would have to be read for each and every piece of boilerplate text on a web page. However: there is no way to force a reload of property files, a refresh of ResourceBundles other than by bringing down the JVM (the Web Server for example) and restarting it. 

In 24/7 Application, that is highly undesirable, but even for your development work this is quite unfortunate: changes to resource bundles cannot be made and tested on the fly, the development application server has to be restarted in order to inspect the effect of your changes. In multi-locale applications, it is really cumbersome to work this way.

In this article, I describe a little feature to add to a JSF application to allow the end user – typically the developer (or the administrator in a Production environment) – to force a refresh of the Resource Bundles. Note that in Java 6 – Mustang – there is a proper, built-in solution. While that is a very good thing, it does not help any of us in the short term and our current projects.

I will show solutions specifically for applications developed with either JHeadstart & ADF Faces or just ADF Faces. However, the approach should be pretty much applicable to other JSF implementations as well. I have developed and tested the workaround for refreshing resource bundles with Oracle JDeveloper 10.1.3 and its embedded OC4J Application Server. However, its principle should apply to all file based resource bundles in all development and deployment environments I believe.

Refreshing Resource Bundles in JHeadstart without restarting the application (server)

Let’s take a look at JHeadstart generated applications. JHeadstart has complemented the standard JSF mechanism for handling i18n and Resource Bundles. It introduces two classes, MessageFactory and MessageFactoryMap. The MessageFactory class is the class that loads all bundles specified through the bundleNames managed property. The MessageFactoryMap class is just a wrapper around the MessageFactory class that implements the Map interface, so we can use JSF EL expressions in the page to get entries from the resource bundle.

Instead of generating f:loadBundle tags into the pages, JHeadstart generates a managed bean definition under the key nls which instantiates a class that provides access to all resource bundles of your application. In generated pages, you will often see references to this nls managed bean like this: af:panelPage title=”#{nls[‘TABLE_TITLE_EMPLOYEES’]}” This approach provides you with the flexibility to (re-)organize your resource bundles as you like, without the need to change the references to resource bundle entries in your page. To make this all work, two managed bean definitions are generated into the JhsCommon-beans.xml: for MessageFactory (with key jhsMessageFactory) and for MessageFactoryMap (with key nls).

MessageFactory is the class where the interaction with the standard ResourceBundle mechanism takes place. That is where for the currently active Locale and a given search string or message id all available resource bundles are searched through for the best fit. That too is the place where we have to make the ResourceBundle refresh its cache. From the specification of the ResourceBundle class it follows that previously accessed bundles that have been loaded from property files can be reused from cache without reloading the file. And that is exactly the behavior we have to counter. We can do that by – through the use of reflection since we the ResourceBundle cache is a private member – actively, even aggresively, clearing the cache of the ResourceBundle.

The steps for implementing the ResourceBundle refresh are the following:

  1. extend the JHeadstart MessageFactory to your own MessageFactory with a forceBundleRefresh() method that resets the ResourceBundle cache
  2. extend the JHeadstart MessageFactoryMap to your own MessageFactoryMap with a  public void refreshBundles(ActionEvent ae) JSF action listener that will receive the call to refresh and turn into a request to forceBundleRefresh
  3. update the JhsCommon-beans.xml file to base the managed beans nls and jhsMessageFactory on your own subclasses defined above
  4. add a command button or link to your application that will invoke the refreshBundles action listener

1. Create the extended MessageFactory that does the actual work

The AMISMessageFactory class is rather straightforward:

package nl.amis;

import java.lang.reflect.Field;

import java.util.ArrayList;
import java.util.ResourceBundle;

import oracle.jheadstart.controller.jsf.util.MessageFactory;

import sun.misc.SoftCache;


public class AMISMessageFactory extends MessageFactory {


    public AMISMessageFactory() {
    }


    public void forceBundleRefresh() {
        ArrayList bundles = getBundleNames();
        for (int i = 0; i < bundles.size(); i++) {
            refreshBundle((String)bundles.get(i));
        }
    }


    private void refreshBundle(String bundleName) {
        try {

            Class klass =
                ResourceBundle.getBundle(bundleName).getClass().getSuperclass();
            Field field = null;
            try {
                field = klass.getDeclaredField("cacheList");
            } catch (NoSuchFieldException noSuchFieldEx) {
                System.err.println(this.getClass().getName() + " : " +
                                   noSuchFieldEx.getMessage());
            }
            field.setAccessible(true); // allow ourselves to manipulate the value of the cacheList (private) property in the ResourceBundle class
            SoftCache cache = null;
            try {
                cache = (SoftCache)field.get(null);
            } catch (IllegalAccessException illegalAccessEx) {
                System.err.println("Sys err " + this.getClass().getName() +
                                   " : " + illegalAccessEx.getMessage());
            }
            cache.clear();
            field.setAccessible(false); // Put back the private status on the cacheList property
        } catch (Exception e) {
            System.out.println("Failed to refresh bundle  "+bundleName+" due to "+e.getMessage());
        }
    }

}

The most complex part is the reflection that is going on. The method forceBundleRefresh() iterates over all resource bundles configured with the application. Note: we could restrict the bundle refresh to just a single resource bundle! For each bundle, it calls refreshBundle. This method gets hold of the ResourceBundle for the specified bundle-name. It then acquires a reference to the private member cacheList in the ResourceBundle instance; this member holds the keys and values previously loaded from a property file or some other store of bundle-values. We make the field cacheList accessible, then clear out its contents. Lastly we make the member private again.

2. Create the MessageFactoryMap that contains an Action Listener to execute ResourceBundle refresh

The AMIS MessageFactoryMap subclasses from the default JHeadstart MessageFactortMap, and adds a single method:

package nl.amis;

import javax.faces.event.ActionEvent;

import oracle.jheadstart.controller.jsf.util.MessageFactoryMap;

public class AMISMessageFactoryMap extends MessageFactoryMap {
    public AMISMessageFactoryMap() {
    }

    public void refreshBundles(ActionEvent ae) {
        ((AMISMessageFactory)this.getMessageFactory()).forceBundleRefresh();
    }
}

This method will be invoked from the ADF Faces/JSF user interface to force the ResourceBundle refresh

3. Configure the specialized MessageFactory and MessageFactoryMap classed

Now that we have created our own slightly extended classes, we need to configure them in the JhsCommon-beans.xml file, generated by JHeadstart’s Application Generator:

< >...>

<managed-bean>
<managed-bean-name>nls</managed-bean-name>
<managed-bean-class>nl.amis.AMISMessageFactoryMap</managed-bean-class>
<managed-bean-scope>application</managed-bean-scope>
<managed-property>
<property-name>messageFactory</property-name>
<value>#{jhsMessageFactory}</value>
</managed-property>
</managed-bean>

<managed-bean>
<managed-bean-name>jhsMessageFactory</managed-bean-name>
<managed-bean-class>nl.amis.AMISMessageFactory</managed-bean-class>
<managed-bean-scope>application</managed-bean-scope>
<managed-property>
<property-name>bundleNames</property-name>
<list-entries>
<value>view.ApplicationResources</value>
<value>oracle.jheadstart.exception.JhsUserMessages</value>
<value>javax.faces.Messages</value>
</list-entries>
</managed-property>
</managed-bean>

The changes are minimal – just updating the <managed-bean-class> element for the nls and the jhsMessageFactory bean.

4. Create a Command Link or Command Button in the User Interface to initiate ResourceBundle refresh

Somewhere in the User Interface of the application, there must be a way for the developer or the administrator of the application to get the ResourceBundles refreshed. There are many different ways of going about that requirement. The simplest of them all probably is the following, where I add a Command Button to the Global Menu section of the application – defined in the file public_html\common\regions\menuGlobal.jspx. The advantage of defining it there is that is shows up automatically in every page of the application. Typically the button would only be displayed for users with a specific role or a special privilege. I leave it to the reader to add that additional feature.

  <af:regionDef var="attrs">
    <af:menuButtons>
      <af:commandMenuItem text="Home" action="home"
                          icon="/jheadstart/images/home.gif"/>
    </af:menuButtons>
    <af:objectSpacer height="15" />
    <af:commandButton id="commandlink_refresh"
        actionListener="#{nls.refreshBundles}"
        text="Refresh Bundles"/>
  </af:regionDef>

The command button’s actionListener is linked to the refreshBundles method in the nls managed bean – the MessageFactory. Whenever the button is pressed, the actionListener gets invoked and the ResourceBundles are refreshed.

Proof of the Pudding…

Let’s see if it works as promised. You have to take my word that these are all valid, unmanipulated screenshots of 100% generated application – except for the changes described above.

Testing i18n features of JSF applications - Forcing a refresh of the Resource Bundle(s) resourceBundleRefreshBeforeRefresh

This application has been generated with English as default locale. Other supported languages are Dutch and Spanish. When we look at the file applicationResources.properties – the default name for JHeadstart generated resource bundles – we can see where the titles, prompt and other boilerplate text-items in this application come from:

Testing i18n features of JSF applications - Forcing a refresh of the Resource Bundle(s) resourceBundleUnEdited

It is also clear that in order to change the boilerplate text we do not like, this is the file to do it in. We will change the column headings – some are still Dutch -, change some of the prompts in the Advanced Search section of the page and make some smaller changes as well:

Testing i18n features of JSF applications - Forcing a refresh of the Resource Bundle(s) resourceBundleEdited

Now in order to refresh the application with these new values from the edited resource bundle we need to do three things:

  • Save the ApplicationResources.properties file
  • Make the ApplicationResources.properties file – this will copy the file to the output directory: “Compiling…   converting, through native2ascii, view/ApplicationResources.properties to output directory”
  • Click on the Refresh Bundles button

And we see the following page:

Testing i18n features of JSF applications - Forcing a refresh of the Resource Bundle(s) resourceBundleRefreshAfterRefresh

Resources

Chapter 5, section Internationalization of the JHeadstart 10.1.3 Developer’s Guide

Bug 421439 (logged in 2001) against J2SE – describing the issue, the workaround used in this article and the promise of a fix in Mustang (Java SE 6)

Nice post on the Java Ranch Forum, describing the workaround from the above bug-report, the one I found on Google that got me going

Spring Framework’s solution for reloadable ResourceBundles: http://www.springframework.org/docs/api/org/springframework/context/support/ReloadableResourceBundleMessageSource.html