Struts MessageResources

This article came about primarily as the result of some questions which were posted to the comp.lang.java.programmer newsgroup. An e-mail query sent to me off-line cemented my resolve to address this issue in more detail. As I've noted previously, the Struts documentation available on-line is not particularly voluminous. My research into the behaviour of the underpinnings of the framework frequently requires reference to the source code. Since that can be a time consuming task, I'm sharing my discoveries with others who might encounter similar challenges.

The basics

Struts permits the specification of multiple message resource bundles in the struts-config.xml file. Here's the general format:

<message-resource parameter="classname" [null="true|false"] [key="name"]>

Attribute Usage
parameter This is the parameter passed to the message factory. In general terms it identifies the properties file(s) located under the $APP_HOME/classes directory. A parameter value of com.myorg.app would search for files named $APP_HOME/classes/com/myorg/app[_locale].properties.
null The optional null attribute controls what will be returned by a message get in the case of an unrecognized key. If set to "true" then an empty string will be returned. If set to "false" then a string of the form ???locale.key??? (e.g. ???en_US.ccard.invalid???) will be returned. This can be very useful for debugging.
key There can be only one default (no key attribute) bundle and zero or more uniquely identified bundles. Don't try to specify multiple resources with the same key; the result will be that no messages will be located for that key.

So now that we've specified the message resources, how and where do we use them?

Struts tags

Struts tags are the primary, but not the exclusive, message consumers. The utilization of message resources also differs according to the tag type. The following sections detail those tags which use messages.

bean:message

This is the mechanism designed to be utilized by the application developer. The key and (optionally) bundle attribute values are used to retrieve the desired message. The messages can also be internationalized (see next section). This tag can be useful if you use the same labels or text throughout your application.

html:errors and html:messages

While errors are typically generated by the validation process (see below) messages can be readily added to a page by the execute method of an Action. Here's some sample code:

    public ActionForward execute( ActionMapping mapping, ActionForm actionForm,
      HttpServletRequest req, HttpServletResponse resp ) throws Exception {
        ActionMessages msgs = new ActionMessages();
	...
        msgs.add( new ActionMessage( "ccard.expiration", expireMonth ) );
        saveMessages( req, msgs );
        ...

In our hypothetical application, we could use a message like this to suggest to a customer that their credit card was soon to expire.

It's important to note that only the message key, not the message text, is stored in the ActionMessage. The errors and messages tags perform the resolution of key/bundle to message text. The arguments to the ActionMessage, however, are fully resolved at the time that the message is created. Comprehending this is vital to understanding how validation messages are built. Here's a diagram of the flow:

Here's a code example:

    // obtain a reference to an ActionMessage called msg
    // and assume that bundle and locale have been set
    String key = msg.getKey();
    Object args[] = msg.getValues();
    String format = RequestUtils.message( pageContext, bundle, locale, key );
    String message = MessageFormat.format( format, args );

html:option

The key and bundle attributes specify the label string associated with the option. Processing is otherwise the same as the errors and messages tags.

html:img and html:image

These tags use message resources quite differently but at least the bundle attribute is supported so you can partition the messages. The pageKey and srcKey attributes are resolved to provide the relative or absolute (respectively) path to the image location. The altKey and titleKey attributes are resolved to provide the alt and title attributes of the HTML tags.

Internationalization (I18N)

As alluded to above, the Struts framework will attempt to use the locale of the client in order to render messages in the appropriate language. The primary key in these cases is the information sent by the browser. Maintaining the locale in the framework is accomplished by including the following at the head of every page in the application:

<html:html locale="true">

When rendered, this will result in something akin to the following being generated at the top of the document:

<html lang="en">

It's important to note that only the language is rendered. This is essential to understanding the naming of the properties files, among other things. If I configure my browser to use the ‘fr_CA’ locale (French Canadian, as per the standards) then Struts doesn't concern itself with the ISO country code. To use the example mentioned previously, Struts would be looking for a file named $APP_HOME/classes/com/myorg/app_fr.properties when resolving message keys. The same rules apply to all message resource bundles. See the validation section for some extra twists.

Of course you can also access MessageResources in your classes which extend Action. This can be very useful for retrieving parameters which are dependant on the user's locale. Here's a typical invocation:

        public ActionForward execute( ActionMapping mapping,
          ActionForm actionForm, HttpServletRequest req,
          HttpServletResponse resp ) {
            ...
            String param = getResources().getMessage( getLocale( req ),
              "parameter.key" );

The getResources method (from Action) returns a MessageResources object. We then invoke getMessage using the appropriate locale and key. We extract the locale from the HttpServletRequest passed to the execute method. Note that for this to work properly you must either use the html:html tag as defined above or set the locale attribute of the controller tag to true in the struts-config.xml application configuration file.

Validation

Struts validation is a very convenient mechanism for ensuring that certain input conditions are met. There are a set of predefined message keys and values which must be included in the bundle used to render the messages. Assume that we have the following bundles defined:

<message-resources parameter="ApplicationResources"/>
<message-resources parameter="ValidationMessages" key="validator"/>

If we were to render the validation messages using the <html:errors/> tag then the message keys would have to be defined in the $APP_HOME/class/ApplicationResource.properties file. If we were to use <html:errors bundle="validator"/> then the keys would have to be defined in the $APP_HOME/class/ValidationMessages.properties file.

But we're not limited to using the standard validation messages. We can override messages for validators by using the msg tag within the field tag in our validation XML configuration file. Here's an example:

    <field property="customer.name" depends="required,minlength">
            <msg name="minlength" key="custom.errors.minlength"/>
            <arg0 key="customer.name" resource="true"/>
            <arg1 key="${var:minlength}" resource="false"/>
            <var>
                    <var-name>minlength</var-name>
                    <var-value>10</var-value>
            </var>
    </field>

This example reveals an important point. Since we've specified a key value for arg0 with the resource attribute set to "true", the message will be resolved using the default message bundle! There's no mechanism for specifying an alternate bundle. The message key we specify in the msg tag is a different kettle of fish. As mentioned earlier, the ActionMessage object encapsulates the message key. It's only when we render the message using html:errors that the lookup occurs. As such, we can use the bundle attribute to select any available message resource bundle.

This can lead to confusion since the keys are resolved using separate mechanisms and at different times during the processing. It's not at all obvious that this should be the case and so people can be excused for thinking that these elements are defined in the same file. That might or might not be the case.

Combining validation with I18N adds some additional complexity. You should now be able to understand why all message resource bundles need to be translated and stored in files with the appropriate extension. The twist comes in the form of a requirement that your validation configuration file requires a formset for every language your application supports. You end up with a file which looks something like this:

<!DOCTYPE form-validation PUBLIC
  "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0//EN"
  "http://jakarta.apache.org/commons/dtds/validator_1_0.dtd">
<form-validation>
    <formset>
        <form name="PurchaseOrderForm">
            <field property="customer.name" depends="required,minlength">
                <msg name="minlength" key="custom.errors.minlength"/>
                <arg0 key="customer.name" resource="true"/>
                <arg1 key="${var:minlength}" resource="false"/>
                <var>
                    <var-name>minlength</var-name>
                    <var-value>10</var-value>
                </var>
            </field>
            ...
        </form>
        ...
    </formset>
    <formset language="fr">
        <form name="PurchaseOrderForm">
            <field property="customer.name" depends="required,minlength">
                <msg name="minlength" key="custom.errors.minlength"/>
                <arg0 key="customer.name" resource="true"/>
                <arg1 key="${var:minlength}" resource="false"/>
                <var>
                    <var-name>minlength</var-name>
                    <var-value>10</var-value>
                </var>
            </field>
            ...
        </form>
        ...
    </formset>
    ...
</form-validation>

This can lead to sizeable configuration files yet the requirement makes some sense. Considering the different syntax of languages, arg0 and arg1 might have to be reversed in some locales. It's just important that you make all the changes required or you might discover that messages aren't being displayed properly, if at all.

Enhancing functionality

A couple of posters wanted to know how to perform manipulations of message resources in custom tags. I've always performed such tasks in my Action servlets and stored the results in the appropriate scope. I suggested to one poster that they could always store the MessageResources in application scope in order to access them via custom tags but it seemed to run counter to portability concerns.

It turns out that the MessageResources are already stored in application scope. If you're willing to introduce a struts dependency on your custom tags, you can readily access resources and fold, spindle and mutilate them at will. Here's the source for a tag which will create a TreeMap of messages and values when supplied with a Collection. The messages serve as the keys in the TreeMap and so are sorted lexically.

/*
 * author: pselby
 * date: Dec 19, 2003
 * Copyright © 2003 by Phil Selby
 * All rights reserved.
 * Please contact the author for licensing information.
 */
package com.myorg.tags;

import java.text.MessageFormat;
import java.util.Collection;
import java.util.Iterator;
import java.util.TreeMap;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.TagSupport;

import org.apache.struts.Globals;
import org.apache.struts.taglib.html.Constants;
import org.apache.struts.util.MessageResources;
import org.apache.struts.util.RequestUtils;

/**
 * @jsp.tag name="SortedMap"
 *          body-content="empty"
 *          tei-class="com.myorg.tags.SortedMapTEI"
 * @author: pselby
 */
public class SortedMapTag extends TagSupport {

	private static MessageResources messages =
	  MessageResources.getMessageResources( Constants.Package + ".LocalStrings" );
	private String id = null;
	private String name = null;
	private String property = null;
	private String formatString = null;
	private String bundle = null;
	private String locale = Globals.LOCALE_KEY;
	private String scope = null;

	/**
	 * @jsp.attribute required="true"
	 *                rtexprvalue="true"
	 *                type="java.lang.String"
	 */
	public String getId() {
		return( id );
	}

	public void setId( String s ) {
		id = s;
	}

	/**
	 * @jsp.attribute required="true"
	 *                rtexprvalue="true"
	 *                type="java.lang.String"
	 */
	public String getName() {
		return( name );
	}

	public void setName( String s ) {
		name = s;
	}

	/**
	 * @jsp.attribute required="false"
	 *                rtexprvalue="true"
	 *                type="java.lang.String"
	 */
	public String getProperty() {
		return( property );
	}

	public void setProperty( String s ) {
		property = s;
	}

	/**
	 * @jsp.attribute required="false"
	 *                rtexprvalue="true"
	 *                type="java.lang.String"
	 */
	public String getFormat() {
		return( formatString );
	}

	public void setFormat( String s ) {
		formatString = s;
	}

	/**
	 * @jsp.attribute required="false"
	 *                rtexprvalue="true"
	 *                type="java.lang.String"
	 */
	public String getBundle() {
		return( bundle );
	}

	public void setBundle( String s ) {
		bundle = s;
	}

	/**
	 * @jsp.attribute required="false"
	 *                rtexprvalue="true"
	 *                type="java.lang.String"
	 */
	public String getLocale() {
		return( locale );
	}

	public void setLocale( String s ) {
		locale = s;
	}

	/**
	 * @jsp.attribute required="false"
	 *                rtexprvalue="true"
	 *                type="java.lang.String"
	 */
	public String getScope() {
		return( scope );
	}

	public void setScope( String s ) {
		scope = s;
	}

	/* (non-Javadoc)
	 * @see javax.servlet.jsp.tagext.Tag#doStartTag()
	 */
	public int doStartTag() throws JspException {
		Object		collection = null;
		Iterator	iter = null;
		String		key = null;
		String		value = null;
		TreeMap		tm = new TreeMap();
		
		if( ( collection = RequestUtils.lookup( pageContext, name, property, scope ) ) == null )
			throw( new JspException( "Cannot find collection" ) );
		if( collection instanceof Collection )
			iter = ( (Collection) collection ).iterator();
		if( iter == null )
			throw( new JspException( "Cannot create iterator") );
		while( iter.hasNext() ) {
			try {
				key = (String) iter.next();
			}
			catch( ClassCastException e ) {
				throw( new JspException( "Values must be of type java.lang.String" ) );
			}
			value = RequestUtils.message( pageContext, bundle, locale, formatValue( key ) );
			tm.put( value, key );
		}
		pageContext.setAttribute( id, tm );
		return( TagSupport.SKIP_BODY );
	}

	private String formatValue( String s ) {
		if( formatString == null )
			return( s );
		Object	args[] = { s };
		return( MessageFormat.format( formatString, args ) );
	}
}

Excuse the XDoclet tags. Since we're creating an object in page scope we also need the TagExtraInfo class. Here's that code.

/*
 * author: pselby
 * date: Dec 19, 2003
 * Copyright © 2003 by Phil Selby
 * All rights reserved.
 */
package com.myorg.tags;

import javax.servlet.jsp.tagext.TagData;
import javax.servlet.jsp.tagext.TagExtraInfo;
import javax.servlet.jsp.tagext.VariableInfo;

/**
 * @author: pselby
 */
public class SortedMapTEI extends TagExtraInfo {
	public VariableInfo[] getVariableInfo( TagData data ) {
		return( new VariableInfo[] {
			new VariableInfo( data.getId(), "java.util.TreeMap", true, VariableInfo.AT_END )
		} );
	}
}

We also require a TLD. Here's an edited version of the file generated by XDoclet using the tags in the source.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE taglib PUBLIC
  "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
  "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">
<taglib>
   <tlib-version>1.0</tlib-version>
   <jsp-version>1.2</jsp-version>
   <short-name>Utilities</short-name>
   <tag>
      <name>SortedMap</name>
      <tag-class>org.yours.tags.SortedMapTag</tag-class>
      <tei-class>org.yours.tags.SortedMapTEI</tei-class>
      <body-content>empty</body-content>
      <attribute>
         <name>id</name>
         <required>true</required>
         <rtexprvalue>true</rtexprvalue>
         <type>java.lang.String</type>
      </attribute>
      <attribute>
         <name>name</name>
         <required>true</required>
         <rtexprvalue>true</rtexprvalue>
         <type>java.lang.String</type>
      </attribute>
      <attribute>
         <name>property</name>
         <required>false</required>
         <rtexprvalue>true</rtexprvalue>
         <type>java.lang.String</type>
      </attribute>
      <attribute>
         <name>format</name>
         <required>false</required>
         <rtexprvalue>true</rtexprvalue>
         <type>java.lang.String</type>
      </attribute>
      <attribute>
         <name>bundle</name>
         <required>false</required>
         <rtexprvalue>true</rtexprvalue>
         <type>java.lang.String</type>
      </attribute>
      <attribute>
         <name>locale</name>
         <required>false</required>
         <rtexprvalue>true</rtexprvalue>
         <type>java.lang.String</type>
      </attribute>
      <attribute>
         <name>scope</name>
         <required>false</required>
         <rtexprvalue>true</rtexprvalue>
         <type>java.lang.String</type>
      </attribute>
   </tag>
</taglib>

Finally, here's some JSP code which utilizes this tag.

<%@ taglib uri="html" prefix="html" %>
<%@ taglib uri="bean" prefix="bean" %>
<%@ taglib uri="logic" prefix="logic" %>
<%@ taglib uri="utils" prefix="utils" %>
<html:html locale="true">
<head>
<title>SortedMap Demo</title>
</head>
<body>
<utils:SortedMap id="sortedList" name="itemList" format="item.{0}"/>
<html:form action="OptionTest.do" method="POST">
<html:select property="item">
	<logic:iterate id="currentItem" name="sortedList">
		<bean:define id="itemValue" name="currentItem" property="value" type="java.lang.String"/>
		<html:option value="<%= itemValue %>"><bean:write name="currentItem" property="key"/></html:option>
	</logic:iterate>
</html:select>
<br>
<html:submit/>
</html:form>
</body>
</html:html>

NOTE: The author is available for short- or long-term contract work. While we are unable to guarantee a reply due to the volume of e-mail received, you are most welcome to posit additional questions to pselby@selbyinc.com.

Copyright © 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015 by Phil Selby. All rights reserved.