Advanced Struts

Given the nature of applications developed using the Struts framework, you'll likely soon find that you need to use some of the more advanced capabilities. As usual, the documentation is sparse and you might find yourself learning by trial-and-error. This is a time-consuming process so I'm documenting my experiences in order to reduce the learning curve for those who are travelling the same road.

I ran into a common requirement when putting together an expense reporting system. Since consultants could conceivably travel to different countries, I needed a mechanism to use a database lookup to obtain a list of permissible currencies and use them to populate a select element. Of course they also had to be able to enter data on multiple lines.

A number of elements are connected so we'll start with the premise that I've added a session attribute (object) named expenses which is an instance of com.onlinetrs.ers.Expenses. The object encapsulates a java.util.Vector which contains elements of type com.onlinetrs.ers.Expense, exposed via the getExpenses method.

First the JSP source:

<logic:iterate id="expense" name="expenses" property="expenses">
<tr>
<td><html:select name="expense" indexed="true" property="currency">
<html:options name="expenses" property="currencies" filter="false"/>
</html:select></td>
</tr>
</logic:iterate>

The iterate tag directs the framework to invoke getExpenses() on the bean named expenses and, for each element returned from the java.util.Collection, give it a name of expense within the tag body. The select tag pulls in the property called currency which results in a call to getCurrency(). Setting the indexed attribute to true results in HTML code like the following being generated:

<select name="expense[11].currency">

This is exactly what we need, as we'll see in a minute. Finally, the options tag uses the expenses attribute (NOT the expense element) and specifies the property name, so getCurrencies() is invoked on expenses. Finally, some currencies (euro, yen, pound) need to be represented as special sequences. If I didn't set the filter attribute to false then the sequences would get "corrected" on output, i.e. &euro; would be converted to &amp;euro;.

So here's the struts-config.xml file:

        <form-beans>
                <form-bean name="expensesForm"
                  type="com.onlinetrs.ers.ExpensesForm">
                </form-bean>
        </form-beans>

Now here we're using an indexed property, which is why we specified the indexed attribute on the select tag. When the ActionForm is populated, the indexed getter will be invoked, i.e. getExpense( int index ). Here's a snippet from ExpensesForm.java:

        private Vector	expenses;
	...
	public void reset() {
		expenses = new Vector();
	}
	...
        public Expense getExpense( int index ) {
                for( int i = expenses.size(); i <= index; i++ )
                        expenses.add( new Expense() );
                return( (Expense) expenses.elementAt( index ) );
        }

This is a very useful piece of code! The reason is that the getExpense calls can arrive in any order. If the first call was made with an index of 17 then we would ensure that there were 18 elements in the Vector before returning element index 17. As part of the reset method (overriding the one in ActionForm) I create a new java.util.Vector of expenses, hence it has an initial size of 0.

Prior to that, the individual com.onlinetrs.ers.Expense elements are populated. Remember the select tag? The property attribute tells Struts that we have to invoke setCurrency on the com.onlinetrs.ers.Expense object returned by getExpense on the ActionForm. So here's that snippet:

	private String	currency;
	...
        public void setCurrency( String val ) {
                currency = val;
        }

So let's go over the flow when the form is submitted:

The only piece of the puzzle still missing is the Expenses.java source. Here's the relevant snippet:

	public Vector getExpenses() {
		// return Vector of com.onlinetrs.ers.Expense elements
	}

        public Vector getCurrencies() {
		// return Vector of currencies from database (cached, of course)
	}

Finally, in the servlet which extends org.apache.struts.action.Action, I pull the data out of the org.apache.struts.action.ActionForm and update the expenses session object. Some people were curious about that approach. Why not just specify scope="session" in the struts-config.xml file? Every time the servlet is entered I have to update the database and blank elements are ignored so I eliminate empty records as a matter of course. Thus when the screen is redisplayed the populated elements will all be at the top.

Then again, you have to store the information somewhere. I typically use objects which I maintain in session scope. I just have to remove them when navigating to a different page which doesn't require access to the contents. You can also use your class which extends org.apache.struts.action.ActionForm and specify session scope. You just have to implement a mechanism which recognizes the initial invocation such that you can populate the necessary elements.

Finally, you can always populate a bean in application scope which contains the options. Use a load-on-startup tag within a servlet tag in your $APP_HOME/WEB_INF/web.xml file. In the init method you can read the options from a variety of sources (properties file, database, etc.) and use ServletContext.setAttribute to make the object available to the entire web application.

So you can see that there are a lot of interrelated elements at work here. It took me some time and effort to get it all working properly but it does work.

UPDATE:

Based on the number of questions which come my way, there is still considerable confusion insofar as applying these techniques. People think that it won't work with radio buttons, checkboxes, etc. Truth be told, it extends to handle those additional elements without a great deal of effort. You just have to keep in mind what is happening "under the covers". Sometimes the best way to track things down is to display the HTML page source.

So let's take a more complex example. We combine a simple text field, a couple of radio button and some checkboxes in two classes. The challenge is in displaying the fields in a JSP and capturing the data in a class which extends org.apache.struts.action.ActionForm. This example also demonstrates how we use different objects to model the data and collect the input.

Here's the code for our class:

package	com.onlinetrs.ers;

import	java.util.Vector;

public class ComplexObject {

	private String	value;
	private String	status;
	private Vector	checkList;

	public ComplexObject( String s ) {
		value = s;
		checkList = new Vector();
	}

	public String getValue() {
		return( value );
	}

	public void setValue( String s ) {
		value = s;
	}

	public String getStatus() {
		return( status );
	}

	public void setStatus( String s ) {
		status = s;
	}

	public Vector getCheckList() {
		return( checkList );
	}

	public void setCheckList( Vector v ) {
		checkList = v;
	}
}

This is a fairly standard bean implementation. We also need to encapsulate the information for our checkboxes. Here's that class:

package	com.onlinetrs.ers;

import	java.util.Vector;

public class CheckItem {

	private String	title;
	private String	checked;

	public String getTitle() {
		return( title );
	}

	public void setTitle( String s ) {
		title = s;
	}

	public String getChecked() {
		return( checked );
	}

	public void setChecked( String s ) {
		checked = s;
	}
}

The checkList in com.onlinetrs.ers.ComplexObject contains objects of this class. Here's some code (from com.onlinetrs.ers.Testing) which populates some test data and sets a request attribute:

        ComplexObject   co = null;
        CheckItem       ci = null;
        Vector          items = null;
        Vector          list = new Vector();

        ...

        for( int i = 0; i < 5; i++ ) {
                co = new ComplexObject( "testing " + i );
                co.setStatus( ( ( i % 2 ) == 0 ) ? "accept" : "reject" );
                items = new Vector();
                for( int j = 0; j < 3; j++ ) {
                        ci = new CheckItem();
                        ci.setTitle( "checkbox " + ( i + 1 ) + ":" +
                          ( j + 1 ) );
                        if( ( ( j % 2 ) == 1 ) || ( i == 3 ) )
                                ci.setChecked( i + "" );
                        items.add( ci );
                }
                co.setCheckList( items );
                list.add( co );
        }
        req.setAttribute( "ITEMS", list );

Here is a screen shot of the rendered form (we'll get to the JSP in just a minute):

Nothing ground-breaking here. Now we need to look at the JSP which generated the page:

<%@ taglib uri="logic" prefix="logic" %>
<%@ taglib uri="html" prefix="html" %>
<%@ taglib uri="bean" prefix="bean" %>
<%@ taglib uri="nested" prefix="nested" %>
<head>
<title>Testing Page</title>
</head>
<body>
<form action="/ers/testing.do" method="GET">
<table>
<logic:iterate id="stringField" name="ITEMS" indexId="index">
<tr>
<td>
<html:text name="stringField" indexed="true" maxlength="15" property="value"/>
</td>
<td>
<html:radio name="stringField" property="status" value="accept" indexed="true">
Accept
</html:radio>
<br>
<html:radio name="stringField" property="status" value="reject" indexed="true">
Reject
</html:radio>
</td>
<td>
<logic:iterate id="checkItem" name="stringField" property="checkList"
  type="com.onlinetrs.ers.CheckItem">
<html:checkbox name="checkItem" property="checked" indexed="true"
value="<%= index.toString() %>"/>
<bean:write name="checkItem" property="title"/>
</html:checkbox>
<br>
</logic:iterate>
</td>
</tr>
</logic:iterate>
</table>
<p>
<html:submit value="Submit"/>
</form>
</body>

There are a couple of things worth noting here. First, we use the logic:iterate tag to step through our com.onlinetrs.ers.ComplexObject objects. Our initialization code added a request parameter of type java.util.Vector so we don't have to specify the property. But note that we do specify the indexId. We use it to specify the value attribute of the html:checkbox tag which is nested inside another logic:iterate tag. We specify the indexed="true" attribute in both logic:iterate tags but the behaviour is not exactly what you might expect.

Remember that the id attribute in the logic:iterate tag pulls double duty. In addition to providing a "handle" for the bean, when we use it in the name attribute of the html:checkbox or html:radio tags then it also becomes the field name. This name is used to select the appropriate accessor and mutator methods when it comes time to populate the form bean. So here's part of the HTML source for the form generated by the JSP above:

<head>
<title>Testing Page</title>
</head>
<body>
<form action="/ers/testing.do" method="GET">
<table>

<tr>
<td>
<input type="text" name="stringField[0].value" maxlength="15" value="testing 0">
</td>
<td>
<input type="radio" name="stringField[0].status" value="accept" checked="checked">Accept
<br>
<input type="radio" name="stringField[0].status" value="reject">Reject
</td>
<td>

<input type="checkbox" name="checkItem[0].checked" value="0">
checkbox 1:1
</html:checkbox>
<br>

<input type="checkbox" name="checkItem[1].checked" value="0" checked="checked">
checkbox 1:2
</html:checkbox>
<br>

<input type="checkbox" name="checkItem[2].checked" value="0">
checkbox 1:3
</html:checkbox>
<br>

</td>
</tr>

<tr>
<td>
<input type="text" name="stringField[1].value" maxlength="15" value="testing 1">
</td>
<td>
<input type="radio" name="stringField[1].status" value="accept">Accept
<br>
<input type="radio" name="stringField[1].status" value="reject" checked="checked">Reject
</td>
<td>

<input type="checkbox" name="checkItem[0].checked" value="1">
checkbox 2:1
</html:checkbox>
<br>

<input type="checkbox" name="checkItem[1].checked" value="1" checked="checked">
checkbox 2:2
</html:checkbox>
<br>

<input type="checkbox" name="checkItem[2].checked" value="1">
checkbox 2:3
</html:checkbox>
<br>
   ...

</td>
</tr>

</table>
<p>
<input type="submit" value="Submit">
</form>
</body>

Examine carefully the indexed attributes and the subscript values assigned. The checkbox elements take their indexes from the inner loop, not the outer. That means we have five checkboxes named checkItem[0].checked, five named checkItem[1].checked, etc. And this is where some people get bogged down. They lament that Struts can't handle multiple entries on a form when they have the same name. I emphatically disagree!

Now I'll be the first to admit that the Struts documentation is somewhat "thin". On the order hand, Struts is an incredibly rich framework with some powerful capabilities. I downloaded the source some time ago when I was trying to discover how certain operations were actually performed. It was through reading the source code that I discovered some priceless nuggets. One was the fact that you can specify arrays of arguments, not just scalars. So let's take a look at the class which extends org.apache.struts.action.ActionForm:

package	com.onlinetrs.ers;

import	org.apache.struts.action.ActionForm;
import	org.apache.struts.action.ActionMapping;
import	org.apache.struts.action.ActionError;
import	org.apache.struts.action.ActionErrors;
import	org.apache.struts.action.ActionServlet;
import	javax.servlet.http.HttpServletRequest;
import	java.util.Vector;

public class TestingForm extends ActionForm {

	private Vector	strings;
	private Vector	checkItems;

	public void reset( ActionMapping map, HttpServletRequest req ) {
		strings = new Vector();
		checkItems = new Vector();
	}

	public ComplexObject getStringField( int index ) {
		while( strings.size() <= index )
			strings.add( new ComplexObject( null ) );
		return( (ComplexObject) strings.elementAt( index ) );
	}

	public FormCheckItem getCheckItem( int index ) {
		while( checkItems.size() <= index )
			checkItems.add( new FormCheckItem() );
		return( (FormCheckItem) checkItems.elementAt( index ) );
	}

	public Vector getStringFields() {
		return( strings );
	}

	public Vector getCheckItems() {
		return( checkItems );
	}
}

There are again a number of things worth noting. First, I use my favorite construct to create objects as necessary in order to return to the Struts populate method. You don't need indexed mutators when you're dealing with complex objects: the mutator methods are invoked directly on the objects returned by the getXXX methods. Also note that we introduce a new object class here, namely com.onelinetrs.ers.FormCheckItem.

I mentioned earlier that we have to use different objects in the model and the form. Due to the way that Struts tries to populate the form attributes, we can't reuse the com.onlinetrs.ers.CheckItem class here. And we shouldn't in any case. The title field, while essential for display purposes, is not an entry field. Also, the checked item is stored in the object instance as a java.lang.String scalar in com.onlinetrs.ers.CheckItem instances. So here's the checkbox class which we use for the form:

package	com.onlinetrs.ers;

import	java.util.Vector;

public class FormCheckItem {

	private Vector	checked = new Vector();

	public String[] getChecked() {
		String	result[] = new String[checked.size()];

		for( int i = 0; i < checked.size(); i++ )
			result[i] = (String) checked.elementAt( i );
		return( result );
	}

	public void setChecked( String s[] ) {
		for( int i = 0; i < s.length; i++ ) {
			if( checked.contains( s ) )
				continue;
			checked.add( s );
		}
	}

	public Vector getCheckedItems() {
		return( checked );
	}
}

Note that the argument to the mutator and the return from the accessor are arrays of java.lang.String. The content of the value attributes of html:checkbox tags are combined into an array and a single invocation of setChecked contains one entry for each checkbox which was selected when the form was submitted. Now it's merely a matter of extracting the data in the execute method of the action:

        ComplexObject   co = null;
        FormCheckItem   fci = null;
        Vector          items = null;
        Vector          selections = null;
        String          selectedItems[] = null;
	TestingForm	form = null;
        ...

	// cast form from the ActionForm argument

        items = form.getCheckItems();
        for( int i = 0; i < items.size(); i++ ) {
                fci = (FormCheckItem) items.elementAt( i );
                selections = fci.getCheckedItems();
                if( selections == null )
                        continue;
                for( int j = 0; j < selections.size(); j++ ) {
                        selectedItems =
                          (String []) selections.elementAt( j );
                        for( int k = 0; k < selectedItems.length; k++ ) {
				// update the model where selectedItems[k]
				// contains the index of the ComplexObject
				// and i is the index of the checkbox within
				// the ComplexObject
			}
                }
        }

I haven't included the code for extracting the data from the com.onlinetrs.ers.ComplexObject because it's fairly trivial. What I've shown here is the tricky part.

As you can see, despite claims to the contrary it is possible to generate complex forms using multiple display elements in Struts. Uncovering the details isn't always easy but the results can be very rewarding. I hope that you'll be able to use some of this code, or at least the underlying concepts, in your own applications.

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 by Phil Selby