Comment lines by Scott Johnson: Loading Java resource bundles via HTTP

Learn how to load a Java™ resource bundle that resides in a different Web application. Before Java Platform Standard Edition (Java SE) 6, support for accomplishing this was not part of the JDK. But with the addition of the ResourceBundle.Control class in Java SE 6, loading a "remote" resource bundle became possible by simply extending the Control class and adding some custom code. This content is part of the IBM WebSphere Developer Technical Journal.

Share:

Scott Johnson (scottjoh@us.ibm.com), Software Engineer, WebSphere Business Monitor, IBM

author photoScott Johnson has been a software developer for 26 years. Before joining IBM he worked in the areas of banking, cash management, survey research, and healthcare scheduling and staffing. He joined IBM in 2000 as the JavaServer Pages component lead for WebSphere Application Server. He currently uses Java, Javascript, Dojo and other technologies to develop widgets for WebSphere Business Monitor.


developerWorks Contributing author
        level

30 September 2009

Also available in Chinese Portuguese

Another way to use resource bundles

Imagine this scenario: You must provide a display widget that pulls message keys and message replacement parameters from a database, looks up the keys in Java resource bundles, and then formats and displays the messages. The only problem is that the resource bundles reside in Web applications other than the one in which your widget lives, and, in fact, the Web applications live on different servers.

This is not the typical use-case for Java resource bundles and it could be kind of a nightmare. How do you accomplish this? Rather than explain this out of the gate, it will be easier to show you, and to help with that, I have included a sample application for you to download and run.


Sample apps and source files

Working samples for this situation are provided for IBM® WebSphere® Application Server V7 and Apache Tomcat 6 running with Java Platform Standard Edition (Java SE) 6. Java SE 6 is required because the sample uses the Java class java.util.ResourceBundle.Control to accomplish bundle loading via HTTP, and that class is not available before Java SE 6.

Installing the sample application

The sample material included with this article for download include:

  • sj_Tomcat.zip contains
    • RemoteResourceBundle.war
    • AutoParts.war
    • AutoSales.war
  • sj_WAS.zip contains:
    • RemoteResourceBundleEAR.ear
    • AutoPartsEAR.ear
    • AutoSalesEAR.ear
  • sj_Source.zip contains the Java source and other files that comprise the sample. Sources used in RemoteResourceBundle.war:
    • src/remote/bundle/example/RemoteResourceBundleLoader.java
    • src/remote/bundle/example/ProductContextRoots.java
    • src/remote/bundle/example/MessagesBundle.java
    • src/remote/bundle/example/MessagesBean.java
    • src/remote/bundle/example/productContextRoots.properties
    • WebContent/RemoteBundleDisplay.jsp

Sources used in AutoParts.war: WebContent/remote/bundle/example/autoparts.properties

Sources used in AutoSales.war: WebContent/remote/bundle/example/autosales.properties

To install the sample materials:

  1. Deploy the Tomcat WAR files

    Unzip sj_Tomcat.zip to the root of your Tomcat installation’s webapps directory. If your Tomcat server is started, it will deploy the applications automatically. If your Tomcat server is not started, start it now and the applications will be automatically deployed.

  2. Deploy the WebSphere Application Server EAR files
    1. Unzip sj_WAS.zip to a convenient temp directory.
    2. Log in to the Integrated Solutions Console, and from the menu, select Applications => New Application => New Enterprise Application.
    3. Click the Browse button and locate the temp directory where you extracted sj_WAS.zip and choose RemoteResourceBundleEAR.ear.
    4. Click the Next button until you are presented with the Summary page (accept all defaults on all the panels) and then click the Finish button to start installing the application.
    5. When the application has been installed, click the Save link to save the application to the master configuration.
    6. Repeat these steps for AutoPartsEAR.ear and AutoSalesEAR.ear.
    7. Once the three applications are installed, navigate to Applications => Application Types => WebSphere enterprise applications.
    8. In the list of applications you should see the three applications you just installed, with a red X in the Application Status column next to each application. Select the checkbox to the left of each of the three applications and click the Start button.
    9. When the applications have started, you will see a green arrow in the Application Status column next to each application, indicating the applications have been started.
  3. Run the sample

    In your browser’s address bar, type http://<host>:<port>/ RemoteResourceBundle/RemoteBundleDisplay.jsp, where <host>:<port> are the host and port of the running server in which you installed the sample applications. For example, in WebSphere Application Server this might look like:

    http://localhost:9080/RemoteResourceBundle/RemoteBundleDisplay.jsp

    or in Tomcat this might look like:

    http://localhost:8080/RemoteResourceBundle/RemoteBundleDisplay.jsp

    The sample’s very simple browser output is shown in Figure 1.

    Figure 1. The sample’s display output
    Figure 1. The sample’s display output

The interesting thing about this output is that RemoteBundleDisplay.jsp, which lives in the RemoteResourceBundle Web application, was able to retrieve resource bundles from two totally separate Web applications -- AutoParts and AutoSales -- and format messages from those bundles for display.


How the sample application works

Using Figure 2 for reference, below is an overview of how the sample application works:

Figure 2. Overview of the sample
Figure 2. Overview of the sample
  1. RemoteBundleDisplay.jsp is requested. It instantiates the class MessagesBean and accesses the MessageBean’s property, called messagesMap, which will return formatted messages for the JSP to display.
  2. MessagesBean’s method getMessagesMap() iterates over some hardcoded message keys, and for each key it calls the private method getString().
  3. MessagesBean’s getString() method calls the static Java method ResourceBundle.getBundle() in order to retrieve the resource bundle for the current message key. One of the arguments to ResourceBundle.getBundle() is an instance of the sample class RemoteResourceBundleLoader, which extends the Java class ResourceBundle.Control and implements the method newBundle().
  4. RemoteResourceBundleLoader.newBundle() creates an HttpURLConnection for retrieving the required resource bundle from a Web application. The resource bundle is located and is loaded into a new instance of the sample class called MessagesBundle. This MessagesBundle instance is returned by RemoteResourceBundleLoader. (If the bundle was retrieved by a previous call to ResourceBundle.getBundle(), the Java ResourceBundle class will return a cached version of the bundle.)
  5. The MessagesBundle instance is used by MessagesBean’s getString() method to retrieve the string for the message key being processed. MessagesBean formats the string and stores it in a map which, when all messages have been retrieved, is returned to the JSP.
  6. The JSP displays the formatted messages.

How bundle loading via HTTP works

The key is the Java class ResourceBundle.Control. The class java.util.ResourceBundle.Control was introduced in Java SE 6. The Control class gives you more control over resource bundle loading than was available in any previous Java release.

Look at the sample class RemoteResourceBundleLoader, which is found in the downloadable sj_Source.zip file in the package remote.bundle.example (Listing 1).

The getFormats() method is used by ResourceBundle to determine what format(s) will be used for the resource bundles. In the sample application, you only want to load bundles of type properties; for example, autoparts.properties.

Listing 1. Specifying the format for the resource bundle
private static String propertiesType = "properties";

// Only "properties" files are used (e.g., autoparts.properties)
   public List<String> getFormats(String baseName) {
   return Collections.singletonList(propertiesType);

The newBundle() method in RemoteResourceBundleLoader (Listing 2) is called by the factory method ResourceBundle.getBundle() to instantiate a ResourceBundle for the base bundle name, a locale, and a format. (See the Javadoc for ResourceBundle for a complete description of the bundle loading process.)

Listing 2. The newBundle() method in RemoteResourceBundleLoader
public ResourceBundle newBundle(String baseName, 
                                    Locale locale, 
                                    String format,
                                    ClassLoader loader, 
                                    boolean reload) 
                   throws IllegalAccessException, InstantiationException, 
                            IOException {

        ResourceBundle bundle = null;

        if ((baseName == null) || 
            (locale == null) || 
            (format == null) || 
            (loader == null)) {
            return null;
        }
        
        // format must be '.properties'
        if (!format.equals(propertiesType)) {
            return null;
        }

        // Create bundle name from baseName and locale (e.g., "autoparts" 
        //  (no locale), autoparts_fr (French)
        String bundleName = toBundleName(baseName, locale);
        
        // Create resource name (e.g., "autoparts.properties", 
        //                             "autoparts_fr.properties"
        String resourceName = toResourceName(bundleName, format);
        
        // get product context roots, if not already obtained
        Properties productContextRoots=ProductContextRoots.getProductContextRoots();
        if (productContextRoots==null) {
            ProductContextRoots pe = new ProductContextRoots();
            pe.loadProductContextRoots();
            productContextRoots = ProductContextRoots.getProductContextRoots();
            if (productContextRoots==null) {
                return null;
            }
        }
        // The last segment of the baseName indicates product 
        // (i.e., 'autosales', 'autoparts').
        // Use this string to find the product context root in productContextRoots.
        int dotIndex = baseName.lastIndexOf('.');
        if (dotIndex==-1) {
            return null;
        }
        String productName= baseName.substring(dotIndex+1);
        
        // Find product context root using productName
        String productContextRoot = productContextRoots.getProperty(productName);
        if (productContextRoot==null) {
            return null;
        }

	// Create the full name for this resource 
        String fullResourceName;
        if (!productContextRoot.startsWith("http")) {
        	if (this.refererHeader != null && this.refererHeader.length()>0) {
            	    // Create full resource name using Referer, 
                   //  context root and resource name:
            	    fullResourceName=this.refererHeader+
                   productContextRoot+resourceName;
        	} else {
	            // Create full resource name using scheme, 
                    // host, port, context root and resource name:
	            fullResourceName=this.scheme+"://"+
                    this.host+":"+this.port+productContextRoot+resourceName;
        	}
        } else {
        	// Create full resource name using context root and resource name:
        	fullResourceName=productContextRoot+resourceName;
        }

        // Create HttpURLConnection for the resource file
        URL proxy=new URL(fullResourceName);
        HttpURLConnection httpProxy = (HttpURLConnection)proxy.openConnection();
        if (httpProxy == null) {
          return null;
        }
        if (reload) {
          httpProxy.setUseCaches(false);
        }

        // Instantiate the input stream
        InputStream stream = httpProxy.getInputStream();
        if (stream == null) {
          return null;
        }
        BufferedInputStream bis = null;

        // Instantiate the bundle with the stream.
        try {
            bis = new BufferedInputStream(stream);
            bundle = new MessagesBundle(bis);
            bis.close();
        }finally{
            if (bis != null) try { bis.close (); } catch (Throwable ignore) {}
        }
        return bundle;
}

Creating a URL from a message key and base bundle name

How do you know which Web application contains the resource bundle for a particular message key? The sample code in Listing 2 implements a simple scheme for building the URLs it needs for retrieving resource bundles from Web applications. The scheme is:

  • Message keys contain product identifiers

    The message keys contain a product identifier in the first segment of the key name. For example: autoparts.parts.received. In this message key, autoparts is the Web application that contains the resource bundle.

  • Base bundle names include a fixed string plus a product identifier

    All the sample resource bundles use the same base string for their names: remote.bundle.example

    When the MessagesBean calls ResourceBundle.getBundle(), it passes in this fixed string appended with the product identifier from the message key, for example:

    remote.bundle.example.autoparts

    This string is known as the baseName argument to ResourceBundle.getBundle().

  • Product context roots

    A properties file called productContextRoots.properties contains the context roots of the Web applications that contain the resource bundles that will be loaded. The contents of the properties file are:

    autoparts = /AutoParts/
    autosales = /AutoSales/

    The method RemoteResourceBundleLoader.newBundle() reads this properties file one time, the first time the method is invoked.

  • Mapping a bundle baseName to a product context root

    When the method RemoteResourceBundleLoader.newBundle() is called to load a new resource bundle, it takes the last segment of the baseName argument and uses it as a key to finding the context root of the Web application where the bundle resides.

    For example, the last segment of the bundle baseName:

    remote.bundle.example.autoparts

    will be used to find the context root for the autoparts Web application in productContextRoots.properties. In this example, the context root will be:

    /AutoParts/


Filling in the gaps

As you recall from the overview of the sample application, RemoteBundleDisplay.jsp instantiates the class MessagesBean. Listing 3 shows the JSP code.

Listing 3. RemoteBundleDisplay.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1" import="remote.bundle.example.MessagesBean"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
    
<jsp:useBean id="messagesBean" scope="page" 
	class="remote.bundle.example.MessagesBean"></jsp:useBean> 
<jsp:setProperty name="messagesBean" property="locale" 
	value="${pageContext.request.locale}"></jsp:setProperty>    
<jsp:setProperty name="messagesBean" property="scheme" 
	value="${pageContext.request.scheme}"></jsp:setProperty>    
<jsp:setProperty name="messagesBean" property="port" 
	value="${pageContext.request.serverPort}"></jsp:setProperty>    
<jsp:setProperty name="messagesBean" property="host" 
	value="${pageContext.request.serverName}"></jsp:setProperty>    
<jsp:setProperty name="messagesBean" property="refererHeader" 
	value='${header["Referer"]}'></jsp:setProperty>    
<html>  
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> 
<title>Displays messages from resource bundles located in other Web 
Applications</title>
</head>
<body>
<p>The Messages:</p>

<ul>
<c:forEach var="msg" items="${messagesBean.messagesMap}">
  <li>${msg.value}</li>
</c:forEach>
</ul>
</body>
</html>

The JSP sets several properties on the MessagesBean instance, and then loops over the messagesMap created by MessagesBean and displays the formatted messages (Listing 4).

Listing 4. MessagesBean.java
public Map<String,String> getMessagesMap(){
        messages = new HashMap<String, Object[]>();
        // The message keys and the arguments are stored in
        // a HashMap for the purposes of this sample.

        // In the real world, both the message keys and their arguments could be
        // persisted to a database by event emitters living in many different
        // Web Applications which could be deployed on different hosts.
        // A single, centralized display widget, living on any server anywhere,
        // could retrieve the message keys and their arguments from the database, 
        // get the messages from the appropriate Web Application resource bundles, 
        // and then format and display the messages.
        messages.put("autoparts.parts.received", 
            new Object[]{"Detroit"});
        messages.put("autoparts.parts.shipped", 
            new Object[]{"Cleveland"});
        messages.put("autosales.invoice.received", 
            new Object[]{"TopNotch Motors", 10});
        messages.put("autosales.shipment.received", 
            new Object[]{"TopNotch Motors", "Lamborghini"});

        Set<String> messagesKeys = messages.keySet();
        Iterator<String> messagesIter = messagesKeys.iterator();
        messagesMap = new HashMap<String,String>();
        while(messagesIter.hasNext()) {
            String key = messagesIter.next();
            Object[] values = messages.get(key);
            int dotIndex = key.indexOf('.');
            if (dotIndex>1) {
                String productName= key.substring(0, dotIndex);
                String msg = getString(key,productName, this.locale );
                String formattedParams = MessageFormat.format(msg, values);

                messagesMap.put(key, formattedParams);
            }
        }
        return messagesMap;
    }

    /**
     * Returns the string in a resource bundle for key, product and locale
     *
     * @param messageKey a key
     * @param product the identifier for a specific resource bundle
     * @param locale the locale of the client
     * @return String a value which is assigned to the key
     */
    public String getString(String messageKey, String product, String locale) {
        try {
            // Load bundle using RemoteResourceBundleLoader.
            ResourceBundle bundle = ResourceBundle.getBundle(
                    BASE_BUNDLE_PACKAGE+"."+product,
                    new Locale(locale),
                    new RemoteResourceBundleLoader(this.scheme, this.host, 
                        this.port, this.refererHeader));
            String messageString=bundle.getString(messageKey);
            return messageString;
        } catch (MissingResourceException e) {
            return null;
        }
}

When the JSP accesses the bean’s messagesMap property, the bean calls getString() for each of the hardcoded message keys.

In JDK 6, the ResourceBundle.getBundle() method accepts a ResourceBundle.Control class as an argument. In the getString() method in Listing 4, you pass a new instance of the class RemoteResourceBundleLoader as the third argument to ResourceBundle.getBundle(). RemoteResourceBundleLoader extends ResourceBundle.Control. The call to ResourceBundle.getBundle() returns a resource bundle. That bundle is used to get the message for the message key by calling bundle.getString(messageKey).

After you’ve gotten the message, you use MessageFormat.format() to format the message, and then you put the formatted message in the messagesMap, which is used by the JSP.


Summary

This article showed you how the loading of Java resource bundles can be done via HTTP. In a real-world scenario, both the message keys and their arguments could be persisted to a database by event emitters living in many different Web applications, which could be deployed on different hosts. A single, centralized display widget, living on any server anywhere, could retrieve the message keys and their arguments from the database, get the messages from the appropriate Web application resource bundles, and then format and display the messages.


Acknowledgements

The author thanks Varad Ramamoorthy for his technical review of this article.


Downloads

DescriptionNameSize
Code samplesj_Source.zip17 KB
Code samplesj_Tomcat.zip289 KB
Code samplesj_WAS.zip12 KB

Resources

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into WebSphere on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=WebSphere, Java technology
ArticleID=431146
ArticleTitle=Comment lines by Scott Johnson: Loading Java resource bundles via HTTP
publish-date=09302009