Dashboard > Cookbook > Home > Forms - Multiple forms on the same page
  Cookbook Log In View a printable version of the current page.  
  Forms - Multiple forms on the same page
Added by Derek Clarkson, last edited by Derek Clarkson on May 22, 2006  (view change)
Labels: 

If you've ever tried to put multiple forms on the same web page and bind to their form bean (command) properties, then hook various controllers to them you will have rapidly run into a variety of issues. most especially if you are dealing with controllers (such as the SimpleFormController) which are derived from the AbstractFormController. The basic issues is that these controllers auto populate the model with a form bean so that your web page input fields can bind to it's properties. The basic flow is that the controller is called without paramters which triggers it to load the bean and redirect to the web page. The user does data entry and then submits, this time the controller sees parameters and executes it's functionality. In addition, if the controller has been told to, it manages the storage of the form bean in the users session.

This system works just fine if you only have a single form on a web page. But what if you have more ? For example you might want (as I did when sorting this) to have the top part of a web page as a search critiera form and the bottom of the page to display the results data. As well as the results though, there is a form which allows you to select records via a set of checkbox and link links and buttons to perform further processing on those records. Thats the second form. Google has a similar layout but doesn't let you manipulate the records. In my case this was for a financial program so selecting records for processing was part of the target. Spring cannot handle this because it was designed so that you can only specify one form to access a web page.

For example we would first access the web page by calling /orders/ordersearch.form. Spring will then call and load the form bean for that form. This works fine, there are no results in the model so the second form is not rendered. But when the user submits the search form, and there are results in the model we have a different situtation because now we are going to render the second form. But because we have not come in through our second controller (/orders/ordergroupaction.form) Spring has not had the chance to create the necessary form bean and place it in the model (or session). So the data binders fall over because of lack of data.

What I needed was a way to be able to make a call to any controller to render a web page, and get all form beans required for that page instantiated regardless of which controllers they needed so that the page could be rendered correctly. Eventually I designed an HandlerInterceptorAdapter which I called a MultiFormBeanCheckingInterceptor.

What the ? is a HandlerInterceptorAdaptor ?

This is discussed in the manual in Section 13.4.3 Adding handlerInterceptors (PDF Page 161) of the manual. But to put it basically you register one of these classes in the interceptor property of your mapper. They provide a way to "intercept" the mapping of calls to controllers so you can performs specific actions before or after the mapping has taken place.

In my case I wanted to intercept after the mapping had occured and the controller been called, but before the view was rendered. At that point I wanted to inject the missing form beans into the model (and session) so that the view would render with all the data it needed.

How does it work ?

My interceptor contains a list of the views in the system which require multiple controllers and therefore multiple beans. Every time the mapper makes a call to a controller,  my interceptor looks up this list and compares it to the requested view. If there is a match, the interceptor then scans the list of associated controllers for the view checks the session and model for the beans required by each one. If missing it creates them. problem solved.

 Registering the Interceptor

Programmatically I could not see any way to link controllers which specific forms when registering the controller beans so I decided to have a property that does this as part of the interceptors initialisation. Here's my xml file that I used to register the mapper and the interceptor:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
 <!--
           Handdler mappings intercept requests coming in and "handle them. In this case
               we are using a URL handler to decide where to route requests based on the URLs
              requested. THe values associated with each url are the bean names of the controllers
                which are to process the request.
   -->
      <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
                <property name="mappings">
                    <props>
                               <prop key="/accounts/accountsearch.form">accountSearch</prop>
                           <prop key="/accounts/accountdetails.form">accountDetails</prop>
                         <prop key="/orders/ordersearch.form">orderSearch</prop>
                         <prop key="/orders/ordergroupaction.form">groupOrderAction</prop>
                       </props>
              </property>
           <property name="interceptors">
                        <list>
                                <ref bean="sessionBeanCreator" />
                             <ref bean="lpiSessionInViewInterceptor" />
                            <ref bean="commissionsSessionInViewInterceptor" />
                    </list>
               </property>
   </bean>

   <!-- Bean which intercepts controllers and after they have created the view
              checks to see if any beans need to be created in the session. This handles
          situations where multiple forms exist on the same document.
        -->
      <bean name="sessionBeanCreator"
          class="commissions.spring.interceptors.MultiFormBeanCheckingInterceptor">
                <property name="controllers">
                 <map>
                         <entry key-ref="orderSearch" value="formView" />
                              <entry key-ref="groupOrderAction" value="formView" />
                 </map>
                </property>
   </bean>

</beans>

Firslty you can see that the mapper has my custom interceptor listed along with two others (thats a different discussion ). The second thing you will see is the interceptor bean itself. Note the Map of controllers stored as a property called (logically enough) controllers. When the bean is initialised this list is scanned and each controller is interrogated regardling the form it uses for data entry. The urls are stripped of redirect: and extension to derive a list of the raw view names. The controller bean references are used as keys in this list and the value is the property on that controller that will return the view name. You could of couse make this work differently if you like.

And now for the multiFormBeanCheckingInterceptor code:

package commissions.spring.interceptors;

import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.mvc.AbstractFormController;

import com.lp.beans.BeanUtils;

/**
 * This interceptor checks all request for controllers. When a view is asked for
 * and that view is in it's list, it checks the controllers listed against the
 * controllers and ensures that their form beans have been created in session
 * and in the model before the view is rendered. This handles situations where
 * multiple forms exist on a single view.
 * <p>
 * The controllers property should contain a map where the controller beans are
 * the keys and the property which returns the view name are the values. Each
 * controller is then interrogated to find out what type of form bean is needed
 * and the resulting data is used to generate the session name of the bean so
 * that it can be correctly created when necessary.
 * <p>
 * This interceptor assumes that the controllers are using the default session
 * attribute naming system for the beans. I.e. <b><i>controllerclassname</i>.FORM.<i>formname</i></b>.
 * This is set in the
 * {@link org.springframework.web.servlet.mvc.AbstractFormController
 * AbstractFormController} class. But it can be overriden so if you are, you
 * will need to create a modified version of this class.
 * <p>
 * Note the use of generics to allows anything that is derived from a
 * AbstractFormController to be used.
 *
 * @author DerekC
 *
 */

public class MultiFormBeanCheckingInterceptor<K extends AbstractFormController> extends
             HandlerInterceptorAdapter {

     private Log logger = LogFactory.getLog(this.getClass());

        /**
  * Stores the references between controllers and classes to create. Use a
    * Hashmap containing a string key and vector of the commands.
       */
 private HashMap<String, Vector<K>> controllers = new HashMap<String, Vector<K>>();

      @Override
   public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                    ModelAndView modelAndView) throws Exception {

           // at this point the controller has executed and returned a model so lets
           // go sort out what we need to do.
          String viewName = this.getViewName(modelAndView.getViewName());
             this.logger.debug("Post processing view: " + viewName);

         // Now get the vector for the view.
         Vector<K> controllerList = this.controllers.get(viewName);
            if (controllerList == null) {
                       this.logger.debug("View is not in list. Exiting.");
                 return;
             }

               // Loop and create the beans.
               String beanKey;
             String cmdName;
             Object cmdBean;
             Class cmdClass;
             boolean sessionBeanFound;
           boolean modelBeanFound;
             for (K c : controllerList) {

                    cmdName = c.getCommandName();
                       cmdClass = c.getCommandClass();
                     beanKey = c.getClass().getName() + ".FORM." + cmdName;

                  Map<String, Object> model = modelAndView.getModel();

                      //Look for an establish bean we can use.
                    sessionBeanFound = request.getSession().getAttribute(beanKey) != null;
                      modelBeanFound = model.containsKey(cmdName);

                    //Try and get a reference to an established bean using either
                      //the session bean or model bean. Otherwise create a new one.
                       if (sessionBeanFound) {
                             this.logger.debug("Bean present under id:" + beanKey);
                              cmdBean = request.getSession().getAttribute(beanKey);
                       } else if (modelBeanFound) {
                                this.logger.debug("Bean present in model.");
                                cmdBean = model.get(cmdName);
                       } else {
                            this.logger.debug("Creating new bean.");
                            cmdBean = cmdClass.newInstance();
                   }

                       //Now put the bean in session and model if not already there.
                       request.getSession().setAttribute(beanKey, cmdBean);
                        model.put(cmdName, cmdBean);

            }

               // Done, call the parent.
           super.postHandle(request, response, handler, modelAndView);
 }

       /**
  * Set to store the data.
    *
  * @param controllers
        *           A Map containing the revelant controllers as keys and name of
   *           the property on then that returns the view name as the value.
   */
 public void setControllers(Map<K, String> controllers) {

          // Loop through the passed controllers and assemble the data.
               String viewName;
            for (K c : controllers.keySet()) {

                      this.logger.debug("Setting controller form bean for " + c.getCommandName());

                    if (!c.isSessionForm()) {
                           throw new BeanInitializationException("Controller is not set for session beans.");
                  }

                       // get the view name.
                       try {
                               viewName = this.getViewName((String) BeanUtils.getValue(controllers.get(c), c));
                            this.logger.debug(viewName + " => bean " + c.getCommandClass().getName());
                       } catch (NoSuchMethodException nsme) {
                              throw new BeanInitializationException("Could not find getter method for "
                                   + controllers.get(c));
                      } catch (Exception e) {
                             throw new BeanInitializationException("Could not extract view name from controller", e);
                    }

                       // Check and create a vector for it .
                       if (!this.controllers.containsKey(viewName)) {
                              this.controllers.put(viewName, new Vector<K>());
                      }

                       // Now add the controller to the list,
                      this.controllers.get(viewName).add(c);
              }
   }

       /**
  * Internal routine used to strip redirect: off the front of the view names
         * and suffixes off the back so
     * we can find them correctly regardless of whether they are doing
  * redirects.
        */
 private String getViewName(String viewName) {
               String view[] = viewName.split("[:\\.]");
           switch (view.length) {
                      case 1:
                             return viewName;
                    case 2:
                             return view[0].equalsIgnoreCase("redirect") ? view[1] : view[0];
                    case 3:
                             return view[1];
                     default:
                            throw new IllegalArgumentException(viewName + " does not appear to be a valid view.");
              }

    }

}

It's a bit long winded and I've used Java 5 generics and autoboxing to help. Basically there are two important aspects to this interceptor. Firstly the code in the setControllers method which accepts the list of controllers during bean initialisation. This code not only checks that you haven't accidently registered a controller that doesn't support data binding, it also does the heavy work of sorting out which controllers are to be associated with which views.

The second aspect of the interceptor is the Posthandle method. This is the method that is called after the mapper has decided on a controller for any given function and called it. In other words this is where we do the work of looking for missing beans, finding or creating them and ensuring they are placed where the forms can find them. 

I hope you find this useful.

Hi Derek, thanks a lot. Your approach helped me a lot. Also it showed me how malleable spring code is for customizations. thanks - vinay

Posted by Anonymous at Jul 12, 2006 09:51

I m still new to spring in terms of customizing it. but I can relate to a similar problem.

I have a form which has about 30 fileds. I dont want to define a single big command object to hold all thse 30 fields and be available in my controller. Instead I want to define about 5 different commands and somehow have spring bind these 30 request params into these 5 different command objects. And then these 5 be made available to me in my controller. I can then run different validation logic onto these or even have them map into a domain objects directly.

I cant exactly understand if this solution is intended to do the same. I will re-read it, but please comment on my problem.

Thanks

- Shawn

Posted by Anonymous at Jul 27, 2006 12:56

There is one thing that I don't understand. What is com.lp.beans.BeanUtils for? I couldn't find the source code for it. Any idea?

Thanks,

Leon 

Posted by Anonymous at Nov 29, 2006 22:31

Will this work even for Spring Portlets, Can you please give me the changes that I may need to do.

Thanks

Rajesh

Posted by Anonymous at Jul 01, 2008 21:54
Site running on a free Atlassian Confluence Open Source Project License granted to Spring Framework. Evaluate Confluence today.
Powered by Atlassian Confluence, the Enterprise Wiki. (Version: 2.5.5 Build:#811 Jul 25, 2007) - Bug/feature request - Contact Administrators