Liferay diaries: Multiple spring form controllers in a portlet
Now there comes a moment in the life of every portlet author, be it a seasoned veteran (like jstam) or a fresh starter like myself when you need to do implement something out of the ordinary for which portlets were not intended.
Last week we had to scetch up an all-in-one portlet that could display several different forms, perform queries and display the results. Since these forms are complicated, we decided to use Spring's FormControllers to handle validation, error messages and of course binding of HTTP parameters to our form backing beans.
The problem
As it turns out, this is not trivial as there are some technical hickhups to silence first.
Since the same portlet is to process all GET and POST requests, we can't differentiate the views by different URL(at least not the path part, i.e. /myapp/portlet1, /myapp/portlet2 etc).
Instinctively I'm looking for other ways to differentiate portlet views and I arrive at view modes. Unfortunatelly it turns out that custom view modes are optional and not implemented [1]
The solution
What we need is a clever portlet handler that can read the requested URL and decide which form controller to invoke.
My solution looks like this:
public class PortletHandlerMapping extends AbstractMapBasedHandlerMapping implements InitializingBean{ private Map<String, Object> mappings = new HashMap<String, Object>(); public void setMappings(Map<String, Object> mappings){ this.mappings = mappings; } @Override protected Object getLookupKey(PortletRequest request) throws Exception { Object key = request.getParameter("jspPage"); return key; } @Override public void afterPropertiesSet() throws Exception { for (String key:mappings.keySet()) registerHandler(key, mappings.get(key)); } }
The contract of this handler assumes that there is a request parameter named "jspPage" which contains the (short, thus without the package) class name of the FormController to invoke.
A controller would look like this:
@Controller @RequestMapping("SearchDeliveriesController") public class SearchDeliveriesController { @ActionMapping public void findDeliveries(SearchDeliveryQuery query, BindingResult result, @CookieValue("JSESSIONID") String jsessionid, PortletSession session, ModelMap modelMap) { if (!result.hasErrors()) { modelMap.put("msg", "Ok, submit completed"); List<Delivery> rs = deliveriesDao.search(query); modelMap.put("result", rs); } else{ modelMap.put("msg", "Has errors"); } // we need that, otherwise the next form will forget current values modelMap.put("searchDeliveryQuery", query); } @ResourceMapping public String viewAsText(ResourceResponse response) { response.setContentType("text/plain"); return "searchDeliveriesResults"; } @RenderMapping public String view() { return "searchDeliveries"; } // the model name must match the class name, otherwise error handling won't work @ModelAttribute("searchDeliveryQuery") public SearchDeliveryQuery loadModel() { SearchDeliveryQuery query = new SearchDeliveryQuery(); return query; } }
There is one last thing: because of the two phase request lifecycle for portlets (see my previous post Help! My post form parameters are empty) we need a mechanism that preserves HTTP parameters from the action phase to the render phase:
public class ParameterForwardingInterceptor implements HandlerInterceptor{ @Override public boolean preHandleAction(ActionRequest request, ActionResponse response, Object handler) throws Exception { response.getRenderParameterMap().putAll(request.getParameterMap()); return true; } ... } Since we've not implemented any clever context scanning, this approach will not discover controllers on its own (despite them being annotated), thus we have to declare them in the portlet.
<bean id="dashboardController" class="gr.open.acs.customersarea.web.DashboardController"/> <bean id="searchDeliveriesController" class="gr.open.acs.customersarea.web.SearchDeliveriesController"/> <bean id="portletModeHandlerMapping" class="gr.open.acs.customersarea.web.PortletHandlerMapping"> <property name="defaultHandler" ref="dashboardController"/> <property name="interceptors"> <bean class="gr.open.acs.customersarea.web.ParameterForwardingInterceptor"/> </property> <property name="mappings"> <map> <entry key="view" value-ref="dashboardController"/> <entry key="DashboardController" value-ref="dashboardController"/> <entry key="SearchDeliveriesController" value-ref="searchDeliveriesController"/> </map> </property> </bean> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" /> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean>
[1] Discussion on Stackoverflow
http://stackoverflow.com/questions/5434349/liferay-6-0-5-and-spring-mvc-3-question