Multiple Resource Bundles

Multiple Resource Bundles

A lot of projects require more than one resource bundle to keep the localized text organized. This example will show you how to customize Stripes to use any number of resource bundles. For example :

  • application.properties (general messages and form field labels)
  • images.properties (src and alt attributes for s:images)
  • errors.properties (Stripes validation and general error messages)

To make Stripes aware of our bundle names we will have to specify a custom parameter (for our bundle names) and specify a custom LocalizationBundleFactory which will return a custom ResourceBundle which will search our specified bundles.

Configuration

web.xml
 
<filter> 
<display-name>Stripes Filter</display-name> 
<filter-name>StripesFilter</filter-name> 
<filter-class> 
net.sourceforge.stripes.controller.StripesFilter 
</filter-class> 
<init-param> 
<param-name>ActionResolver.Packages</param-name> 
<param-value>action</param-value> 
</init-param> 
<init-param> 
<param-name>Extension.Packages</param-name> 
<param-value>ext</param-value> 
</init-param> 
<init-param> 
<param-name>ResourceBundles.BaseNames</param-name> 
<param-value>application,images,errors</param-value> 
</init-param> 
</filter> 

The first 2 init-param tags belong to Stripes, and the last one ResourceBundles.BaseNames is our custom init-param. The ActionResolver.Packages parameter simply tells Stripes where to find our ActionBeans and the Extension.Packages parameter tells Stripes where to find our custom extension classes.

Configuration Reference

For more information on the configuration of Stripes take a look at the Configuration Reference.

Custom Localization Bundle Factory

ext.CustomLocalizationBundleFactory.class
 
package ext; 

import java.util.*; 

import net.sourceforge.stripes.config.Configuration; 
import net.sourceforge.stripes.localization.LocalizationBundleFactory;
import net.sourceforge.stripes.util.StringUtil; 

/** 
* Permits cycling through multiple named ResourceBundles instead of 
* just StripesResources.properties using the init-param 
* ResourceBundles.BaseNames ( 
* {@link #RESOURCE_BUNDLES_BASE_NAMES}) 
* 
* @author DJDaveMark 
*/ 
public class CustomLocalizationBundleFactory 
implements LocalizationBundleFactory { 
/** 
* The Configuration Key which specifies 
* multiple resource bundles. 
*/ 
public static final String RESOURCE_BUNDLES_BASE_NAMES = 
"ResourceBundles.BaseNames"; 
private String[] bundles; 

public ResourceBundle getFormFieldBundle(Locale locale) { 
return new MultipleResourceBundle(locale, getBundleNames()); 
} 

public ResourceBundle getErrorMessageBundle(Locale locale) { 
return new MultipleResourceBundle(locale, getBundleNames()); 
} 

public void init(Configuration config) { 
String bundleNames = config.getBootstrapPropertyResolver() 
.getProperty(RESOURCE_BUNDLES_BASE_NAMES); 
bundles = StringUtil.standardSplit(bundleNames); 
} 

public List<String> getBundleNames() { 
return Arrays.asList(bundles); 
} 
} 

Here we tell Stripes to use the same custom-multi-bundle-class all the time. For more info see the JavaDoc for LocalizationBundleFactory and DefaultLocalizationBundleFactory The BootstrapPropertyResolver is used to get the param-value associated with our custom param-name ResourceBundles.BaseNames. Then Stripes' StringUtil#standardSplit(String) method is used to break up the separate bundle names.

Custom Resource Bundle

ext.MultipleResourceBundle.class
 
package ext; 

import java.util.*; 
import net.sourceforge.stripes.controller.StripesFilter; 
import net.sourceforge.stripes.localization.DefaultLocalizationBundleFactory;

/** 
* With thanks to Freddy's Stripes Book http://www.pragprog.com/titles/fdstr 
* 
* @author DJDaveMark 
* @author Fred Daoud 
*/ 
public class MultipleResourceBundle extends ResourceBundle { 
private Locale locale; 
private List<String> bundleNames; 

public MultipleResourceBundle(Locale locale, List<String> bundleNames) { 
this.locale = locale; 
this.bundleNames = bundleNames; 
} 

@Override 
public Enumeration<String> getKeys() { 
return null; 
} 

@Override 
protected Object handleGetObject(String key) { 
Object result = null; 
if (bundleNames != null) { 
// Look in each configured bundle 
for (String bundleName : bundleNames) { 
if (bundleName != null) { 
result = getFromBundle(locale, bundleName, key); 
if (result != null) { 
break; 
} 
} 
} 
} 
if (result == null) { 
// Try the application's default bundle 
String bundleName = DefaultLocalizationBundleFactory.BUNDLE_NAME; 
result = getFromBundle(locale, bundleName, key); 
} 
return result; 
} 

/** 
* Returns null if the bundle or key is not found. No exceptions thrown. 
*/ 
private String getFromBundle(Locale loc, String bundleName, String key) { 
String result = null; 
ResourceBundle bundle = ResourceBundle.getBundle(bundleName, loc); 
if (bundle != null) { 
try { 
result = bundle.getString(key); 
} catch (MissingResourceException exc) { 
} 
} 
return result; 
} 
} 


We can now reorganise our resource bundles to contain something like the following :

application.properties
 
app.title=Multiple Resource Bundle Application 
my.label=My Label 

# Resource strings used by the stripes:messages tag 
stripes.messages.header=<ul class="messages"> 
stripes.messages.beforeMessage=<li> 
stripes.messages.afterMessage=</li> 
stripes.messages.footer=</ul> 

... 
images.properties
 
image.logo.src=images/logo.jpg 
image.logo.alt=M.R.B. App Logo 

... 
errors.properties
 
# Resource strings used by the <stripes:errors> tag 
stripes.errors.header=<div style="color:#b72222; font-weight: bold">\ 
Please fix the following errors:</div><ol> 
stripes.errors.beforeError=<li style="color: #b72222;"> 
stripes.errors.afterError=</li> 
stripes.errors.footer=</ol> 

# Validation error messages produced by 
# Stripes' annotation based validations. 
validation.required.valueNotPresent={0} is a required field 
validation.minlength.valueTooShort={0} must be at least {2}\ 
characters long 
validation.maxlength.valueTooLong={0} must be no more than {2}\ 
characters long 

... 

JSTL

The only thing that's missing is to somehow tell the JSTL fmt tags which of our bundles to use. Since JSTL tags don't really accept multiple bundles we can configure our main bundle in the web.xml file :

web.xml
 
<context-param> 
<param-name> 
javax.servlet.jsp.jstl.fmt.localizationContext 
</param-name> 
<param-value>application</param-value> 
</context-param> 

then, just in case we need to use the other bundles outwith the Stripes tags, we can specify the following once in a JSP which we can include in all our other JSPs :

 
<fmt:setBundle var="images" basename="images" scope="application" /> 
<fmt:setBundle var="errors" basename="errors" scope="application" /> 

JSTL fmt

For more information on the JSTL fmt bundles consult the Tag Reference :

Putting It All Together

 
<!-- found in application.properties --> 
<fmt:message key="app.title" /> <br /> 

<!-- found in errors.properties --> 
<s:errors /> 

<s:form beanclass="action.TestAction"> 
<!-- found in application.properties --> 
<s:label for="my.label" /> <br /> 

<!-- found in images.properties --> 
<!-- .src and .alt are added automatically to generate --> 
<!-- <input alt="M.R.B. Logo" name="image.logo" --> 
<!-- src="images/logo.jpg" type="image" /> --> 
<s:image name="image.logo" /> <br /> 

<!-- override the default JSTL bundle --> 
<!-- found in images.properties --> 
The source of this image is : 
<fmt:message bundle="${images}" key="image.logo.src" /> 
</s:form> 

Of course we could just cheat and instead of using <fmt:message key="" bundle="${}" /> we could use <s:label for="" /> which already uses our MultipleResourceBundle class, but alas, that would be cheating!  ;o)