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 "- "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 {
String viewName = this.getViewName(modelAndView.getViewName());
this.logger.debug("Post processing view: " + viewName);
Vector<K> controllerList = this.controllers.get(viewName);
if (controllerList == null) {
this.logger.debug("View is not in list. Exiting.");
return;
}
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();
sessionBeanFound = request.getSession().getAttribute(beanKey) != null;
modelBeanFound = model.containsKey(cmdName);
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();
}
request.getSession().setAttribute(beanKey, cmdBean);
model.put(cmdName, cmdBean);
}
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) {
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.");
}
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);
}
if (!this.controllers.containsKey(viewName)) {
this.controllers.put(viewName, new Vector<K>());
}
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