Java EJB Succeeding With Struts: Dynamically Sized Forms

Succeeding With Struts: Dynamically Sized Forms

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.

Latest Posts

Related Stories