Struts has support for indexed properties in its form beans. In fact, it has support for both simple and complex indexed properties. Ironically I have been able to find lots of documentation online to explain the more complex situation but none for the simpler one. I have been able to find documentation on using arrays of beans is form beans but not of arrays of simple literals like strings and integers. And I’ve done a lot of googling on the matter. Having the ability to have an array of strings in your form bean is a very handy feature. This is a very handy thing to be able to do and you’d be right to assume that it should be simple, and it is simple, it’s just not documented anywhere I could find (and I spend a lot of time looking). So, to help others who might be stuck with the same problem here is a worked example that should make it all clear.

Defining the Problem

This code example is taken straight form the VTIE Collaborative Writing Environment for use in education which I’m Lead Developer on for the EVE Research Group in NUI Maynooth. This whole project will be GPLed soon (when the code base is stable), so I don’t mind showing un-edited code snippets.

We are going to be looking at a single interface within the environment that allows a teacher to add a new group (or class) of students to the system. This, in the most complex situation, is a three step process, two for gathering data and one for actually doing the work and adding the group to the system. The first step allows the teacher to specify information about the group as a whole and the second step allows the teacher to enter the names of the students. This second step is optional. Teachers can choose between an anonymous group (for minors) and a named group (for adults and older children). If they choose an anonymous group the second step is skipped. Lets have a look at what the forms for the two information gathering steps look like with some screen shots:

Figure 1 - Create Group Form Step 1

Figure 2 - Create Group Form Step 2

The thing to note is that on the second page the number of text areas depends on the number requested on the first page. This makes it impossible to represent these text areas in the form object as regular string elements because you don’t know how many you’ll need. There are a couple of ways of fudging it but trust me, they are all exceptionally bad software engineering! The correct solution is to store the student names as an array, generated after the submission of the first page before the display of the second page and to have one text box mapped to each array element.

Creating the Form Object

In the VTIE project we use dynamic form objects generated from XML in struts-config.xml for our forms because there is no point in wasting time writing a class for each form in your webapp when you don’t have to. To do this we use the class org.apache.struts.validator.DynaValidatorForm for our forms.

Although the data input happens over multiple pages we only use one form object (which will reside in session) because we want all the information together when we finally submit it to the action that will create the group. Below is the XML from struts-config.xml for the form object used for this operation:

  1. <!-- Group creation form -->
  2. <form-bean name="createStudentGroupForm" type="org.apache.struts.validator.DynaValidatorForm">
  3.  <form-property name="groupName" type="java.lang.String" />
  4.  <form-property name="noStudents" type="java.lang.Integer" initial="10" />
  5.  <form-property name="groupType" type="java.lang.String" />
  6.  <form-property name="wikiText" type="java.lang.String" />
  7.  <form-property name="studentNames" type="java.lang.String[]" />
  8.  <form-property name="page" type="java.lang.Integer" />
  9. </form-bean>

I just want to draw your attention to a two things in the above form definition. Firstly, we give the type of the attribute that will hold the student names as java.lang.String[] (an array of strings) but, and this is important, we do not give the array a size. By doing so we allow the array to be any size we want, but we have to initialize it in an action before we try to use it in a JSP page. We will initialize it in the action that the first page is submitted to. The second thing I want to draw your attention to is the property page. This property is used by the Struts Validator to figure out when to validate what form elements. You need this element on all forms that use the validator and collect their data over multiple pages. It is vital that this property have the type java.lang.Integer.

Setting up the Actions

There are three actions involved in this operation. The first one is the action that the first page of the form submits the first lot of data to (including the number of students the group will have). This action is mapped to /mentor/createStudentGroupStep1 and is responsible for the following:

  • Initializing the array of student names in the form object.
  • Deciding where to forward to next, straight to the action to actually add the group if the teacher asked for a anonymous group, or to the jsp for entering names if a named group was requested.

The second Action is called by the second page of the form when it has gathered the student names and is used to create a named group. Finally, the third action is called straight from the first action to create an anonymous group. Both of these actions are actually implemented by the same class because it turns out that there is no real difference between creating a named group and an anonymous group.

Below are the action mapping for these three actions in struts-config.xml:

  1. <action path="/mentor/createStudentGroupStep1" type="vtie.portal.mentor.CreateStudentGroupPrepareAction" name="createStudentGroupForm" scope="session" validate="true" input="/home/mentor/addStudentGroupForm.jsp">
  2.  <forward name="createAnonymous" path="/do/mentor/createAnonymousStudentGroup" />
  3.  <forward name="getStudentNames" path="/home/mentor/getStudentNamesForm.jsp" />
  4.  <forward name="fail" path="/home/mentor/addStudentGroupForm.jsp" />
  5. </action>
  6. <action path="/mentor/createAnonymousStudentGroup" type="vtie.portal.mentor.CreateStudentGroupAction" name="createStudentGroupForm" scope="session" validate="false">
  7.  <forward name="success" path="/do/mentor/showStudentGroup" />
  8.  <forward name="fail" path="/home/mentor/addStudentGroupForm.jsp" />
  9. </action>
  10. <action path="/mentor/createNamedStudentGroup" type="vtie.portal.mentor.CreateStudentGroupAction" name="createStudentGroupForm" scope="session" validate="true" input="/home/mentor/getStudentNamesForm.jsp">
  11.  <forward name="success" path="/do/mentor/showStudentGroup" />
  12.  <forward name="fail" path="/home/mentor/getStudentNamesForm.jsp" />
  13. </action>

The things to note here are that we use the same form in all three actions, that we always call the validator and that we always set the form to session scope. Failing to do these three things will cause the whole lot to stop working.

Implementing The Actions

Now lets have a look inside our two action classes, starting with the one for the action /mentor/createStudentGroupStep1 (vtie.portal.mentor.CreateStudentGroupPrepareAction). Note my actions only implement the function ActionForward so that’s all I’m showing here:

  1. public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
  2.   Logger log = new Logger("Prepare Create Student Group Action", false);
  3.   HttpSession session = request.getSession(true);
  4.  
  5.   // ensure it's a mentor doing this
  6.   SessionHelper.forceMentor(session);
  7.  
  8.   //get the dynamic form
  9.   DynaValidatorForm theForm = (DynaValidatorForm)form;
  10.  
  11.   //get the details from the form
  12.   String groupType = (String)theForm.get("groupType");
  13.   int numStudents = ((Integer)theForm.get("noStudents")).intValue();
  14.  
  15.   //prepare the list of default student names
  16.   String students[] = new String[numStudents];
  17.   for(int i = 0; i < numStudents; i++) {
  18.     students[i] = LoginService.generateStudentUserName(i);
  19.   }
  20.  
  21.   //insert into the form
  22.   theForm.set("studentNames", students);
  23.  
  24.   if(groupType.equals("ADULTS")) {
  25.     //named group
  26.     return mapping.findForward("getStudentNames");
  27.   } else {
  28.     //anonymous group
  29.     return mapping.findForward("createAnonymous");
  30.   }
  31. }

The important thing to note is that we are initializing the array of student names here and inserting it into the form.

Finally lets look at the ActionForward function on the class vtie.portal.mentor.CreateStudentGroupAction which implements both the /mentor/createAnonymousStudentGroup and /mentor/createNamedStudentGroup actions:

  1. public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
  2.   Logger log = new Logger("Create Student Group Action", false);
  3.   LoginService service = new LoginService();
  4.   HttpSession session = request.getSession(true);
  5.   MessageResources messageResources = getResources(request);
  6.  
  7.   // ensure it's a mentor doing this
  8.   SessionHelper.forceMentor(session);
  9.  
  10.   //get the dynamic form
  11.   DynaValidatorForm theForm = (DynaValidatorForm)form;
  12.  
  13.   // instance variables
  14.   String groupId = "";
  15.   String mentorId = SessionHelper.getMentorId(session);
  16.  
  17.   //get the details from the form
  18.   String groupName = (String)theForm.get("groupName");
  19.   int noStudents = ((Integer)theForm.get("noStudents")).intValue();
  20.   String wikiText = (String)theForm.get("wikiText");
  21.   if(wikiText == null) {
  22.     wikiText = "";
  23.   }
  24.   PaperContent wikiContent = PaperContent.fromHtml(wikiText);
  25.   String studentNames[] = (String[])theForm.get("studentNames");
  26.   String groupType = (String)theForm.get("groupType");
  27.  
  28.   //try to create the student group
  29.   try {
  30.     boolean anon = true;
  31.     if(groupType.equals("ADULTS")) {
  32.       anon = false;
  33.     }
  34.     groupId = service.addNewStudentGroup(mentorId, groupName, wikiContent, studentNames, anon);
  35.   } catch(Exception e) {
  36.     log.iLogError("Failed to  create a new Student group of the name " + groupName + ", beloning to the mentor id " + mentorId, e);
  37.     e.printStackTrace();
  38.     RequestHelper.setErrorMessage(request, messageResources.getMessage("mentor.createStudentGroup.fail"));
  39.     return (mapping.findForward("fail"));
  40.   }
  41.  
  42.   try {
  43.     request.setAttribute("groupId", groupId);
  44.     RequestHelper.setMessage(request, messageResources.getMessage("mentor.createStudentGroup.success", groupName));
  45.     log.iLogInfo(mentorId + " created a new student group named " + groupName + " with " + noStudents + " students in it.");
  46.     return (mapping.findForward("success"));
  47.   } catch(Exception e) {
  48.     e.printStackTrace();
  49.     log.iLogError("Failed to create the student group bean and add it to the request");
  50.     RequestHelper.setErrorMessage(request, messageResources.getMessage("mentor.createStudentGroup.fail"));
  51.     return (mapping.findForward("fail"));
  52.   }
  53. }

There’s nothing really special to note about this action, it just does some security checks, grabs the details (including our array of names) from the form and then sends them off to a function in the service class which sticks them in the DB. I really only included the above code for completeness.

Setting up the JSPs for the View

There are two JSPs needed for the view, one to display the first page of the form and one for the second. I’m not going to bore you with the entire JSP for these pages but rather just show you the JSP for the form in each. Those among you who are observant will notice that I have implented a custom tag library for the VTIE project but don’t worry about that, those tags just deal with looking after the layout and aesthetics of the page and don’t do anything that impacts the functionality of the form in any way.

Lets start by having a look at the JSP for rendering the form on the first page of the form (/home/mentor/addStudentGroupForm.jsp):

  1. <vtie:ifLevel level="MENTOR">
  2.  <vtie:formContainer key="mentor.createStudentGroup.pageTile">
  3.   <vtie:systemMessage />
  4.   <p id="html_errors"><html:errors /></p>
  5.  
  6.   <script type="text/javascript">
  7.     function encodeWikiText(){
  8.       tinyMCE.triggerSave();
  9.       this.theForm = document.getElementById('createStudentGroupForm');
  10.       this.theForm.wikiText.value = sanitiseHTML(document.getElementById("wiki_text").value);
  11.       return true;
  12.     }
  13.   </script>
  14.   <html:form action="/mentor/createStudentGroupStep1" styleClass="vtie_form" onsubmit="encodeWikiText()">
  15.   <h2><bean:message key="group.details.label" /></h2>
  16.   <ul class="form_element_list">
  17.    <li><label for="groupName"><bean:message key="mentor.createStudentGroup.groupName.label" /></label> <html:text property="groupName" /></li>
  18.    <li><label for="noStudents"><bean:message key="mentor.createStudentGroup.noStudents.label" /></label> <html:text property="noStudents" /></li>
  19.    <li><label for="groupType"><bean:message key="mentor.createStudentGroup.groupType.label" /></label>
  20.     <html:select property="groupType">
  21.      <html:option value="MINORS"><bean:message key="mentor.createStudentGroup.groupType.minors" /></html:option>
  22.      <html:option value="ADULTS"><bean:message key="mentor.createStudentGroup.groupType.adults" /></html:option>
  23.     </html:select>
  24.    </li>
  25.   </ul>
  26.   <h2><bean:message key="group.description.label" /></h2>
  27.   <html:hidden property="wikiText" />
  28.   <html:hidden property="page" value="1"/>
  29.   <p>
  30.   <textarea id="wiki_text" cols="40" rows="10"></textarea>
  31.   </p>
  32.   <p class="button_bar"><html:submit styleClass="submit" /><vtie:cancel /></p>
  33.   </html:form>
  34.  </vtie:formContainer>
  35. </vtie:ifLevel>

This is just a perfectly normal Struts form. There is some JS funniness to make the nice WYSIWYG HTML editor fit in correctly but nothing more than that. The thing to note here is the hidden form element called page. Leaving this out would break the validator.

Now lets look at the more interesting form, the second one that lets us enter our student names in /home/mentor/getStudentNamesForm.jsp:

  1. <vtie:ifLevel level="MENTOR">
  2.  <vtie:formContainer key="mentor.createStudentGroup.getNames.pageTile">
  3.   <vtie:systemMessage />
  4.   <p id="html_errors"><html:errors /></p>
  5.  
  6.   <html:form action="/mentor/createNamedStudentGroup" styleClass="vtie_form">
  7.  
  8.   <ul class="form_element_list">
  9.    <jsp:useBean id="createStudentGroupForm" scope="session" type="org.apache.struts.validator.DynaValidatorForm" />
  10.    <logic:iterate id="student" name="createStudentGroupForm" property="studentNames" indexId="i">
  11.     <li><label for="student<bean:write name="i" />"><bean:message key="mentor.createStudentGroup.getNames.studentName.label" /></label> <html:text property="studentNames[${i}]" styleId="student${i}" /></li>
  12.    </logic:iterate>
  13.   </ul>
  14.   <html:hidden property="page" value="2" />
  15.   <p class="button_bar"><input type="button" value="<bean:message key="generic.back" />" onclick="location.href=CONTEXT_PATH+'/home/mentor/addStudentGroupForm.jsp'" /><html:submit styleClass="submit" /><vtie:cancel /></p>
  16.   </html:form>
  17.  </vtie:formContainer>
  18. </vtie:ifLevel>

The important thing to note here is how we render the list of textboxes for the names by using the logic:iterate tag from the struts taglibs. Again note the hidden page element in the form and how this time it has a value of 2.

Validating The Form

One of the nicest things about Struts is the Validator that leaves your action code free to just get on with what it has to do safe in the knowledge that the data it gets is valid. This validation is controlled by (you guessed) an XML file (validator.xml is the usual but it can be changed). All our work to get an array into a form would be pointless if the validator could not handle arrays. It can but no one bothered to document that fact (except for arrays of objects). It took me ages to get this figured out mainly because the syntax is rather odd. I did eventually track down the answer in a PowerPoint presentation I found on Google but it took some time. Anyhow, below is the validation form validation.xml for this form.

  1. <form name="createStudentGroupForm">
  2.  <field property="groupName" page="1" depends="required, mask">
  3.   <var><var-name>mask</var-name><var-value>${nameRE}</var-value></var>
  4.   <arg0 key="mentor.createStudentGroup.groupName.label"/>
  5.  </field>
  6.  <field property="noStudents" page="1" depends="required,integer,intRange">
  7.   <arg position="0" key="mentor.createStudentGroup.noStudents.label"/>
  8.   <arg position="1" name="intRange" key="${var:min}" resource="false"/>
  9.   <arg position="2" name="intRange" key="${var:max}" resource="false"/>
  10.   <var><var-name>min</var-name><var-value>1</var-value></var>
  11.   <var><var-name>max</var-name><var-value>99</var-value></var>
  12.  </field>
  13.  <field property="studentNames" indexedListProperty="studentNames" page="2" depends="required, mask, maxlength">
  14.   <arg0 key="mentor.createStudentGroup.studentNames.label"/>
  15.   <arg position="1" name="maxlength" key="${var:maxlength}" resource="false"/>
  16.   <var><var-name>maxlength</var-name><var-value>25</var-value></var>
  17.   <var><var-name>mask</var-name><var-value>${nameRE}</var-value></var>
  18.  </field>
  19. </form>

The thing to note is the strange use of the indexedListProperty in the field element for our array of student names. You have to tell the validator that you are validating the field and that you are validating the same field as an indexed field. Once you do that the validation is applied to all elements in the array and if any one element fails the validation for the form fails and you get sent back to the JSP to get shown the error and asked to try again. The other thing to note is the use of the page attribute of the field elements to tell Struts when to validate what. At any time it validates all fields with a page value less than or equal to the page element it recieved from the form.

Message Resources

In a Struts app all your language specific text should be defined in the ApplicationResources.properties file. This makes internationalization trivial, you just get someone to translate this file for you, name it with the ISO code for the language and add it to your app. In the case of VTIE we have English as our default language but we also have an Irish version and a French version defined in ApplicationResources_ga.properties and ApplicationResources_fr.properties respectively.

For the sake of completeness here are the relevant lines from the default ApplicationResources.properties file:

  1. breadcrumbs.mentorHome=Mentor Home Page
  2. breadcrumbs.mentor.createGroup=Create Student Group
  3.  
  4. mentor.createStudentGroup.pageTile=Creat Student Group
  5. mentor.createStudentGroup.groupName.label=Group Name
  6. mentor.createStudentGroup.noStudents.label=Number of Students
  7. mentor.createStudentGroup.groupType.label=Group Type
  8. mentor.createStudentGroup.groupType.minors=Minors
  9. mentor.createStudentGroup.groupType.adults=Adults
  10. mentor.createStudentGroup.getNames.pageTile=Enter Student Names
  11. mentor.createStudentGroup.getNames.studentName.label=Student Name
  12. mentor.createStudentGroup.studentNames.label=Student Names
  13. mentor.createStudentGroup.success=Created the group '{0}'.
  14. mentor.createStudentGroup.fail=An error occoured while creating the group.
  15.  
  16. group.details.label=Group Details
  17. group.description.label=Group Description
  18.  
  19. generic.ok=OK

Final Thoughts and Some Useful Resources

Something that annoys me about a lot of open source projects is the lack of detailed documentation and Struts suffers from this too. There is a lot of good information on the basics of Struts on the Struts web page so the beginner is very well catered for. However, the more advanced user will often find the documentation is not detailed enough. There is JavaDoc documentation available for all Struts classes but this is useless for the elements that you don't interact with through Java directly. The best examples of this are the Validator, the struts-config.xml file, and the logging features in Struts. The basics of all of these features are well covered in the User's Guide but when you need the full specs this documentation is not sufficient. The Struts taglibs are the exception in this case, there is very good detailed documentation on these. Hopefully the Struts Wiki will begin to address this problem but for the moment developing in Struts can get very frustrating when you start doing non-standard things. The mailing lists are good but they don't have all the answers and I would much prefer to have a manual to go read rather than have to annoy people on a mailing list for things that you should just be able to RTFM.

Anyhow, if you are developing in Struts these are the resources I have found invaluable:

Technorati Tags: , , ,