Wednesday, October 14, 2009

Sorting internationalized lists in JSP

One topic which seems to keep coming up for me lately is sorting values for a web application which have been extracted into a resource bundle, for internationalization. Everything I've read on the web says the preferred place to do the sorting is in the servlet / controller class, with the view (in this article, JSP) merely responsible for iterating over that sorted list and displaying it in its native order.

This is all well and good for basic, non-internationalized applications, but seems to break down when dealing with resource bundles and keys. The problem, in my opinion, is that the controller should not have to worry about extracting messages from the resource bundle in order to properly sort, especially when most of the other logic for displaying resource bundle messages is already at the JSP level:

<fmt:message key="${my.label.key}">

Since the view already has a dependency on the resource bundle resolver, resolving those messages in the controller for the purpose of sorting feels wrong. The code I usually see shoe-horned into controllers is ugly, and in the worst cases, I've seen it even thrown in the service layer (*gasp*). It requires knowledge of the current locale, language, bundle, etc.

In an effort to maintain the purity of controllers everywhere, I propose some kind of "sort by resolved message" functionality in the view layer, and will present an example implementation below, as an adaptation of the JSTL c:forEach tag construct.

package com.abstractbits.web.taglib;

import java.util.Comparator;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.jstl.fmt.LocaleSupport;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.taglibs.standard.tag.rt.core.ForEachTag;

/**
* Implementation of a loop tag construct which iterates over a given collection
* of items which have been sorted by the resolved message from a resource bundle
* for a given bundle key.
*
* http://abstractbits.blogspot.com
*
* @author Jared Stehler
*/
public class SortedForEachTag extends ForEachTag {

private enum SortOrder {asc, desc};

private String bundleKeySortProperty;

private SortOrder order = SortOrder.asc;

/**
* Property path notation to get at the bundle key in each object in the given collection
*
* @param bundleKeyProperty
*/
public void setBundleKeySortProperty(String bundleKeyProperty) {
this.bundleKeySortProperty = bundleKeyProperty;
}

/**
* The order in which to sort the bundle messages. {"asc", "desc"}
*
* @param sortOrder
*/
public void setSortOrder(String sortOrder) {
this.order = SortOrder.valueOf( sortOrder.toLowerCase() );
}

/* (non-Javadoc)
* @see javax.servlet.jsp.jstl.core.LoopTagSupport#prepare()
*/
@Override
protected void prepare() throws JspTagException {
super.prepare();

try {
SortedMap<String, Object> sortedValues = new TreeMap<String, Object>(new Comparator<String>() {
public int compare(String o1, String o2) {
int result = o1.compareTo(o2);
return SortedForEachTag.this.order == SortOrder.asc ? result : -result;
}
});

while( this.items.hasNext() ){
Object nextItem = this.items.next();

String bundleKey = (bundleKeySortProperty == null) ? (String) nextItem : BeanUtils.getProperty(nextItem, bundleKeySortProperty);
String resolvedMessage = LocaleSupport.getLocalizedMessage(this.pageContext, bundleKey);

sortedValues.put(resolvedMessage, nextItem);
}

this.items = toForEachIterator(sortedValues.values());

} catch( Exception e ){
throw new JspTagException("Error sorting items", e);
}
}
}

I extended the ForEachTag class from the JSTL standard implementation, and found that I only needed to override the prepare() method to implement my custom sorting. This custom tag uses the standard method of resolving resource messages from bundle keys, and then sorts them in ascending / descending fashion, depending on the order specified. If the bundleKeyProperty is not set, I assume the given collection is a list of Strings, each of which is a bundle key.

Here is the corresponding TLD definition snippet for this tag (I copied this from c.tld, found within the standard taglib implementation jar):

<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">

<description>
JSTL extension tag library
</description>

<tlib-version>1.0</tlib-version>
<short-name>x</short-name>

<tag>
<description>
Adaptation of the classic iteration tag, supporting the notion of sorting based
on the resolved message values of the given objects' bundle keys.
</description>
<name>sortedForEach</name>
<tag-class>com.abstractbits.web.taglib.SortedForEachTag</tag-class>
<tei-class>org.apache.taglibs.standard.tei.ForEachTEI</tei-class>
<body-content>JSP</body-content>
<attribute>
<name>bundleKeySortProperty</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
<type>boolean</type>
</attribute>
<attribute>
<name>sortOrder</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<description>
Collection of items to iterate over.
</description>
<name>items</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
<type>java.lang.Object</type>
</attribute>
<attribute>
<description>
If items specified:
Iteration begins at the item located at the
specified index. First item of the collection has
index 0.
If items not specified:
Iteration begins with index set at the value
specified.
</description>
<name>begin</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
<type>int</type>
</attribute>
<attribute>
<description>
If items specified:
Iteration ends at the item located at the
specified index (inclusive).
If items not specified:
Iteration ends when index reaches the value
specified.
</description>
<name>end</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
<type>int</type>
</attribute>
<attribute>
<description>
Iteration will only process every step items of
the collection, starting with the first one.
</description>
<name>step</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
<type>int</type>
</attribute>
<attribute>
<description>
Name of the exported scoped variable for the
current item of the iteration. This scoped
variable has nested visibility. Its type depends
on the object of the underlying collection.
</description>
<name>var</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<description>
Name of the exported scoped variable for the
status of the iteration. Object exported is of type
javax.servlet.jsp.jstl.core.LoopTagStatus. This scoped variable has nested
visibility.
</description>
<name>varStatus</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>

</taglib>

That's all there is to it! I like this tag because it simplifies sorting resolved resource bundle messages, which is something I tend to need to do often, and would otherwise require an unnecessary dependency of the controller class on the resource bundle resolver. Controllers are cleaner when they only deal with bundle keys, in my opinion. I do feel like the name of the tag could be better; any suggestions? Does this already exist somewhere, and I've simply missed it?

No comments: