http://www.developer.com/

Back to article

Succeeding With Struts: Dynamically Sized Forms


March 4, 2004

In the previous installment of Succeeding with Struts, I alluded to the ability of DynaForms to dynamically size forms at run time.  In other words, the ability to have a form that could be 5 rows long, or 10 rows, or 15 rows as needed.  Perhaps a bit unwisely, I let the actual implementation of such a strategy as an exercise to the reader.  In the following months, I've received dozens of inquires from readers looking for the dirty details, so this month I'll demonstrate not one, but two seperate ways to implement dynamically sizable forms.

The first method is the one I mentioned in passing in the previous column, leaving the size parameter off the form-property attribute of a DynaForm..  To see how this works, let's look at a very simple application that lets the user add comments about various Star Wars actors.  The key fact in this application of interest to us is that the number of actors is dynamically set during the form setup, rather than in the struts-config.xml file.



First, let's look at the struts-config.xml file itself:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
"http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
<struts-config>
<form-beans>
<form-bean name="dynamicArrayForm" type="org.apache.struts.validator.DynaValidatorForm">
<form-property name="people" type="demo.Person[]"/>
</form-bean>
</form-beans>
<action-mappings>
<action path="/setupForm" type="demo.SetupFormAction" name="dynamicArrayForm" scope="session"
validate="false">
<forward name="success" path="/displayForm.jsp"/>
</action>
<action path="/processActorComments" type="demo.ProcessFormAction" name="dynamicArrayForm" scope="session" validate="false">
<forward name="success" path="/displayForm.jsp"/>
</action>
</action-mappings>
</struts-config>


As you can see, this is a fairly simple configuration file with one form and two actions defined.
The first Action, /setupForm, is used to set up the form before the initial display,
and the other action, /processActorComments is what processes the comments entered by the user.

There are two important things to notice in this file that are critical to making things work:

  1. The people form property is defined as type demo.Person[] (meaning an array of demo.Person), but no size parameter is given.  This will result in a placeholder for an array being created, but no actual array being instantiated.
  2. The two actions define the form as session scoped.  This is critical because when the user submits the form after filling in values, the values are populated back into the form before the action is run.  This means that there is no opportunity to manually create the array with the proper number of slots, as you will see is done in the SetupFormAction class before the form is displayed.  In other words, when the form is submitted, there must already be the appropriate slots to accept the form values, and the only way to ensure this is to have the form already instantiated in session scope.
There's basically no value in looking at the Person bean, it's just a vanilla bean with lastName, firstName, dateOfBirth, gender and comment fields.  The source is included in the WAR file.

Now let's take a look at the SetupFormAction class, which is called before the form is first displayed.

package demo;


/**
* Copyright 2004, James M. Turner.
* All Rights Reserved
*
* A Struts action that sets up a DynaForm which is globally scoped
*/

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.*;

import org.apache.struts.action.*;
import org.apache.struts.validator.DynaValidatorForm;

public class SetupFormAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {

DynaValidatorForm df = (DynaValidatorForm) form;
Person[] p = new Person[3];
p[0] = new Person();
p[0].setDateOfBirth("07/13/1942");
p[0].setLastName("Ford");
p[0].setFirstName("Harrison");
p[0].setGender("M");
p[1] = new Person();
p[1].setDateOfBirth("10/21/1956");
p[1].setLastName("Fisher");
p[1].setFirstName("Carrie");
p[1].setGender("F");
p[2] = new Person();
p[2].setDateOfBirth("09/25/1951");
p[2].setLastName("Hamill");
p[2].setFirstName("Mark");
p[2].setGender("M");

df.set("people", p);

return mapping.findForward("success");
}
}

Again, there's not a lot to look at. The first thing the execute method does, as any DynaForm-based action does, is to cast the generic ActionForm class to a DynaValidatorForm. This allows us to use the get and set methods on the form. Next, the method creates a three-element array of type Person. In this method the size is hardwired, in a real application this could be a size based on doing a select from a database. The important thing to consider is that the array is being created in the code here, not by the Struts engine itself. So any arbitrary number of rows could be created by the code in response to application requirements.


Once the array is in place, the method creates three instances of the Person class and populates the values.  Again, in a real application this would more likely be done inside a loop reading rows from a database and populating the form rows.  Lastly, the action returns success, causing Struts to transfer control to the displayForm.jsp page.

<!--
Copyright 2004, James M Turner.
All Rights Reserved

-->

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<head>
<title>Star Wars Actor Fact Page</title>
</head>
<H1><center>Start Wars Actor Fact Page</title>
<html:form action="/processActorComments" >
<table border="1" width="80%">
<tr><th>Last Name</th><th>First Name</th><th>Date of Birth</th><th>Comment</th></tr>
<c:forEach var="people" items="${dynamicArrayForm.map.people}">
<tr><td><c:out value="${people.lastName}"/></td>
<td><c:out value="${people.firstName}"/></td>
<td><c:out value="${people.dateOfBirth}"/></td>
<td><html:text name="people" indexed="true" property="comment"/></td>
</tr>
</c:forEach>
</table>
<P/>
<html:submit value="Update Comments"/>
</html:form>


Again, there's not a huge amount to look at here, it's exactly the same as the code we looked at in the last article when we were looking at fixed-length rows. The page iterates over the rows (remembering that we have to use the map property in JSTL to gain access to DynaForm properties), and displays the last name, first name, and date of birth of the actor, and providing a text field to enter a comment.


When we fire up our browser and request http://localhost:8080/struts/setupForm.do (assuming you put the struts.war file in Tomcat on your local machine), the following page will appear:

Start Wars Actor Fact Page
Last Name First Name Date of Birth Comment
Ford Harrison 07/13/1942
Fisher Carrie 10/21/1956
Hamill Mark 09/25/1951

Once the form is submitted, another simple Struts action processes the results:

package demo;

/**
* Copyright 2004, James M. Turner.
* All Rights Reserved
*
* A Struts action that sends the new comments to the console
*/

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.*;

import org.apache.struts.action.*;
import org.apache.struts.validator.DynaValidatorForm;

public class ProcessFormAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {

DynaValidatorForm df = (DynaValidatorForm) form;
Person[] p = (Person[]) df.get("people");

for (int i = 0; i < p.length; i++) {
System.out.println(p[i].getFirstName() + " " + p[i].getLastName() + ":" + p[i].getComment());
}

return mapping.findForward("success");
}
}

In a real application, this is where data would be written back to the database.  In this case, it just dumps the data to the console so we can see it was correctly received.  Assuming that we filled out appropriate values for each of the actors, we'd see the following on the console:

Harrison Ford:Indiana Jones
Carrie Fisher:Postcards from the Edge
Mark Hamill:Wing Commander
As I mentioned at the beginning of the article, there's another way to handle things however, that doesn't require the use of a session-scoped form.  That way is to use HashMaps to store the rows.  Let's look at the same code, reimplemented using HashMaps.

To start, we'll add a new form to our struts-config.xml:

        <form-bean name="dynamicHashmapForm" type="org.apache.struts.validator.DynaValidatorForm">
<form-property name="people" type="java.util.HashMap"/>
<form-property name="comments" type="java.util.HashMap"/>
</form-bean>
Instead of arrays of beans, now we use a HashMap to store each person's data.  In addition, we need a new HashMap to store the comments, for a reason I'll explain a bit later.  We also need a new action to populate the data:
package demo;

/**
* Copyright 2004, James M. Turner.
* All Rights Reserved
*
* A Struts action that sets up a DynaForm which is globally scoped
*/

import java.io.IOException;
import java.util.HashMap;
import javax.servlet.ServletException;
import javax.servlet.http.*;

import org.apache.struts.action.*;
import org.apache.struts.validator.DynaValidatorForm;

public class SetupHashFormAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {

DynaValidatorForm df = (DynaValidatorForm) form;
HashMap hm = (HashMap) df.get("people");
Person p = new Person();
p = new Person();
p.setDateOfBirth("07/13/1942");
p.setLastName("Ford");
p.setFirstName("Harrison");
p.setGender("M");
hm.put("1", p);
p = new Person();
p.setDateOfBirth("10/21/1956");
p.setLastName("Fisher");
p.setFirstName("Carrie");
p.setGender("F");
hm.put("2", p);
p = new Person();
p.setDateOfBirth("09/25/1951");
p.setLastName("Hamill");
p.setFirstName("Mark");
p.setGender("M");
hm.put("3", p);
return mapping.findForward("success");
}
}
Essentially, this is the same code as before, except we're storing the Person objects into a HashMap instead of an array.  We also don't need to create the HashMap, because it's done automatically as part of the form instantiation.

The tricky part comes in the JSP itself:
<!--
Copyright 2004, James M Turner.
All Rights Reserved

-->

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-html-el.tld" prefix="html-el" %>
<%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib prefix="fmt" uri="/WEB-INF/fmt.tld" %>

<head>
<title>Star Wars Actor Fact Page</title>
</head>
<H1><center>Start Wars Actor Fact Page</title>
<html:form action="/processHashActorComments" >
<table border="1" width="80%">
<tr><th>Last Name</th><th>First Name</th><th>Date of Birth</th><th>Comment</th></tr>
<c:forEach var="people" items="${dynamicHashmapForm.map.people}">
<tr><td><c:out value="${people.value.lastName}"/></td>
<td><c:out value="${people.value.firstName}"/></td>
<td><c:out value="${people.value.dateOfBirth}"/></td>
<td><html-el:text property="comments(${people.value.lastName},${people.value.firstName})" /></td>
</tr>
</c:forEach>
</table>
<P/>
<html:submit value="Update Comments"/>
</html:form>
Remember that the HashMap values populated during the setup will disappear as soon as the form displays, because the form is request-scoped rather than session-scoped.  What this means in particular to us is that all the Person objects go away.  So, if we attached the text field to the comment property of the Person bean, we'd get a null pointer exception when we submitted the form, because the Person object would no longer be in the HashMap (in fact, we will have been given a completely new and empty HashMap.)  So, instead, we need to store the comments in a seperate, parallel HashMap which stores them as simple strings.

There are a few things to notice in the code above.  First, because we're now iterating over HashMap entries, the values available from the c:forEach tag are in fact placeholders for the hash entries, with two properties.  The key property is the value used to reference the hash (the strings "1", "2", "3", etc in our case), and the value property which has the value stored under that key.  So, in this case, we must use the value property to get at the actual properties of the Person bean.

Also, we need to construct a valid Struts property field for the text box.  This is done using the JSTL extensions available in the html-el taglib.  In this case, we store the comments by a string composed of the last name, a comma, and the first name of the actor.

Finally, we need a new action to process the results:

package demo;

/**
* Copyright 2004, James M. Turner.
* All Rights Reserved
*
* A Struts action that sends the new comments to the console
*/

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import javax.servlet.ServletException;
import javax.servlet.http.*;

import org.apache.struts.action.*;
import org.apache.struts.validator.DynaValidatorForm;

public class ProcessHashFormAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {

DynaValidatorForm df = (DynaValidatorForm) form;
HashMap hm = (HashMap) df.get("comments");

Iterator it = hm.keySet().iterator();
while (it.hasNext()) {
String key = (String) it.next();
String comment = (String) hm.get(key);
System.out.println(key + ":" + comment);
}

return mapping.findForward("success");
}
}
Again, the big difference here is that things are being stored as HashMaps.  The code gets the keys (lastname,firstname), and displays the keys and comments on the console, i.e.:

Fisher,Carrie:Leia
Ford,Harrison:Han
Hamill,Mark:Luke

Also notice that when control is returned to the JSP page, a blank table is printed.  This is because the HashMap we created in the setup action is gone now, and we didn't recreate it when processing the results.  You could keep that data around in a session variable, but then you're right back to where you were with the first solution.  Better to choose a key that lets you get at the backend objects when the form is submitted, and always recreate any other form data needed.

Which one is best?  The array-based solution offers the ability to keep everything in one bean, while the hash-based solution avoids any session-scoped data.  Which one works better for you will be the final decision.

NOTE: A WAR file containing all the code and libraries needed to run these examples can be found at http://www.blackbear.com/struts.war.

About the author

James Turner is the Director of Software Development for Benefit Systems, Inc. He is also a contributor to the Apache Struts project. He has two books out on web-facing Java technologies, MySQL and JSP Web Applications, and Struts Kick Start.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date