http://www.developer.com/

Back to article

Succeeding with Struts: Dynamically Allocated Forms


December 5, 2005

The peril of ending an article with a item left as a problem for the reader is that it inevitably comes back to haunt you. In my last article, Succeeding With Struts: Dynamically Sized Forms, I mentioned casually that it was possible to set up a form with indexed properties such that it could have a dynamically initialized array of values that varied depending on the number of items submitted. And, of course, I've gotten a ton of letters since then asking exactly how to go about it.

The classic example of this type of problem is a checkout form for a shopping cart. The action that displays the form knows exactly how many items are in the cart, and therefore can create the appropriate size array to service the page. But what happens when you submit? Remember that form populate happens very early in the Struts request processor, before validation or action processing. Even if you wrote a magic hidden field with the number of lines on the page, how could you get the array allocated before the form was populated, because the action runs after the form is populated? The answer lies in the one thing that does run before form population, the reset method for the ActionForm.

As always, the best way to illustrate this is with a practical example, and what better example than an actual shopping cart check out screen? To begin with, we need a bean to hold the individual items in the cart:

package com.blackbear.forms;

public class Product {

   /**
    * An individual product items
    */

   private String productNumber;
   private String productPrice;
   private String productQuantity;
   private String total;

   /**
    * @return Returns the productQuantity.
    */
   public String getProductQuantity() {
      return productQuantity;
   }
   /**
    * @param productQuantity The productQuantity to set.
    */
   public void setProductQuantity(String productQuantity) {
      this.productQuantity = productQuantity;
   }
   /**
    * @return Returns the productNumber.
    */
   public String getProductNumber() {
      return productNumber;
   }
   /**
    * @param productNumber The productNumber to set.
    */
   public void setProductNumber(String productNumber) {
      this.productNumber = productNumber;
   }
   /**
    * @return Returns the productPrice.
    */
   public String getProductPrice() {
      return productPrice;
   }
   /**
    * @param productPrice The productPrice to set.
    */
   public void setProductPrice(String productPrice) {
      this.productPrice = productPrice;
   }
   /**
    * @return Returns the total.
    */
   public String getTotal() {
      return total;
   }
   /**
    * @param total The total to set.
    */
   public void setTotal(String total) {
      this.total = total;
   }
}

Just your standard Javabean, with four properties: the product number, product quantity, product price, and subtotal. Next, you set up an ActionForm, which in this case consists of nothing but an array of Product beans. The form also has a validate method that makes sure all the quantities are parsable integers.

package com.blackbear.forms;

import java.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.apache.struts.action.ActionError;
import org.apache.struts.action.ActionErrors;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionMapping;

public class ListOfProductsForm extends ActionForm {


   /** FormItems property */
   private Product[] formItems;

   /** 
    * Method validate
    * @param mapping          The Struts Action Mapping
    * @param request          The incoming request
    * @return ActionErrors    The resulting errors
    */
   public ActionErrors validate(
      ActionMapping mapping,
      HttpServletRequest request) {

      ActionErrors errors = new ActionErrors();

      for (int i = 0; i < formItems.length; i++)
      {
         String quantity = formItems[i].getProductQuantity();
         if ((quantity != null) && (quantity.length() > 0))
         {
            try
            {
               Integer.parseInt(quantity);
            }
            catch (Exception ex)
            {
               errors.add("formItems[" + i + "].productQuantity",
                          new ActionError(
                             "com.blackbear.badquantity",
                             quantity));
            }
         }
      }
      return errors;
   }

   /**
    * Returns the FormItems.
    * @return Product
    */
   public Product[] getFormItems() {
      return formItems;
   }

   /**
    * Set the FormItems.
    * @param FormItems The FormItems to set
    */
   public void setFormItems(Product[] FormItems) {
      this.formItems = FormItems;
   }
}

The display action for the page simply creates an array of Product beans (of a random size) and populates it with random products:

package com.blackbear.action;

import java.util.Random;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

import com.blackbear.forms.ListOfProductsForm;
import com.blackbear.forms.Product;

public class DisplayRandomListOfProductsAction extends Action {

/**
  * Create a random list of shopping cart items
  *
  * @param mapping           The Struts Action Mapping
  * @param form              The Struts Form (ListOfProductsForm)
  * @param request           The request
  * @param response          The response
  * @return ActionForward    The resulting page
  */
   public ActionForward execute(ActionMapping mapping,
                                ActionForm form,
                                HttpServletRequest request,
                                HttpServletResponse response) {

      ListOfProductsForm productForm = (ListOfProductsForm) form;

      Random rand = new Random();
      int numberOfProducts = rand.nextInt(20);

      Product[] products = new Product[numberOfProducts];
      productForm.setFormItems(products);

      for (int i = 0; i < numberOfProducts; i++) {
         products[i] = new Product();
         products[i].setProductNumber("1" + rand.nextInt(10000));
         products[i].setProductPrice(String
            .valueOf(rand.nextInt(10000) / 100.0));
         products[i].setProductQuantity(
            String.valueOf(rand.nextInt(5) + 1));
      }

      return mapping.findForward("success");
   }

}

And finally, the JSP for the page uses a JSTL c:forEach loop to render a table with editable fields for the quantities:

Dynamic Product Input Page






    
    


Dynamic Product Input Page

Product Number Product Price Quantity Total

Dynamic Product Input Page

Product NumberProduct PriceQuantity

So far, so good. You have a display action that creates an array of products, sets the initially null formItems property to the newly submitted array, and a JSP page to render the form. But what happens when the form is submitted? The first thing that the request processor is going to do is to try to take all those formItems[34].productNumber fields and populate them with the parameters from the request. Only, because the form is in request scope, the one you used to render the form has traveled to the bit bucket, and a new one with a nulled out formItems is going to be used to accept the values. This, of course, is not going to work, and will lead to our yold friend, the Null Pointer Exception.

To make this work, you need to look at the incoming request, figure out the number index value for formItems you will have to deal with, and create an array to handle it. As I mentioned, the place to do this is in the reset method of the form, which is called each time before form population from the request (unless the form is in session scope).

Why not put it in the form's instantiator? Two reasons. First, because Struts is allowed to pool form objects. So, the form you get may not be newly instantiated; it may have been allocated from the pool, which means the instatiantor won't be run. Also, the instantiator doesn't take any arguments, so there's no good handle on the request to fetch the parameters from. On the other hand, the reset method does get handed the request as one of the arguments, so it's readily available. Here's your reset method:

/**
 * Pattern to match request parameters
 */
private Pattern itemPattern =
   Pattern.compile("formItems\[(\d+)\].*");

/** 
 * Method reset
 * Dynamically creates the appropriate product array based on the
 * request
 * 
 * @param mapping    The Struts Action mapping
 * @param request    The incoming request
 */
public void reset(ActionMapping mapping,
                  HttpServletRequest request) {

   Enumeration paramNames = request.getParameterNames();
   int maxSize = 0;
   while (paramNames.hasMoreElements())
   {
      String paramName = (String) paramNames.nextElement();
      Matcher itemMatcher = itemPattern.matcher(paramName);
      if (itemMatcher.matches())
      {
         String index = itemMatcher.group(1);
         if (Integer.parseInt(index) > maxSize)
         {
            maxSize = Integer.parseInt(index);
         }
      }
  }
  formItems = new Product[maxSize + 1];
  for (int i = 0; i <= maxSize; i++)
  {
      formItems[i] = new Product();
   }

}

A fairly simple piece of code: The reset method iterates over the parameters in the request looking for ones that match the pattern, which only matches against formItems[nn] parameters. The matcher extracts the index values. You look to see whether the value is greater than then largest value you've seen so far, and if so remember it. After you find the largest index value, you use it to create an array of products and populate the array with newly created Product objects.

By using this reset method, you're assured that you'll always have enough (but no more than you need) of the Product array slots. This is much superior to the "let's just create an array of 100; no one will ever need more than that" approach, which only assures that some day, at 3 AM, when you're vacationing in Mexico, someone will try to enter 101 items.

Finally, your process action can just do the trivial subtotaling:

package com.blackbear.action;

import java.util.Vector;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

import com.blackbear.forms.ListOfProductsForm;
import com.blackbear.forms.Product;

public class ProcessRandomListOfProductsAction extends Action {

   /** 
    * Subtotal the list of products
    *
    * @param mapping           The Struts Action Mapping
    * @param form              The Struts Form (ListOfProductsForm)
    * @param request           The request
    * @param response          The response
    * @return ActionForward    The resulting page
    */

   public ActionForward execute(
      ActionMapping mapping,
      ActionForm form,
      HttpServletRequest request,
      HttpServletResponse response) {
      ListOfProductsForm listOfProductsForm =
         (ListOfProductsForm) form;

      Vector newProducts = new Vector();
      for (int i = 0;
           i < listOfProductsForm.getFormItems().length; i++)
      {
         Product p = listOfProductsForm.getFormItems()[i];
         if ((p.getProductQuantity() != null) &&
             (p.getProductQuantity().length() > 0))
         {
            int quantity = Integer.parseInt(p.getProductQuantity());
            if (quantity < 1)
            {
               continue;
            }
            float total =
               quantity * Float.parseFloat(p.getProductPrice());
            p.setTotal(String.valueOf(total));
            newProducts.add(p);
         }
      }
      listOfProductsForm.setFormItems((Product[])
         newProducts.toArray(new Product[0]));
      return mapping.findForward("success");
   }

}
Product Number Product Price QuantityTotal
12877 9.66 1 9.66
17443 84.55 2 169.1
1501 57.3 4 229.2
17076 57.97 3 173.91
15176 68.84 4 275.36
13281 66.03 1 66.03

Download

Click here for a jar file with the complete sources to this sample application.

About the Author

James Turner is a Senior Developer at Axis Technology, LLC. He is also a freelance technology journalist who has written two books on Open Source Java development (MySQL and JSP Web Applications and Struts Kick Start, both from SAMS) and currently serves as Products Editor for Linux Journal.

Sitemap | Contact Us

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