Integrating AJAX within Spring Web Flow Portlets
It is likely that using ResourceRequests from the JSR-286 Portlet 2.0 spec at least partially addresses this use case a bit simpler and more like normal portlet development in terms of the url request. However some aspects of this page are still applicable so it is retained for historic and additional information.
Preface
This document is intended to provide tips and techniques for integrating AJAX within Spring Web Flow portlets, which is not typically straightforward.
At time of writing, Spring Web Flow 2.0 is the target version, as it is the last to have support for Portlet 1.0/JSR-168 (Spring Web Flow 2.1 depends on Spring 3 and as a result, Portlet 2.0/JSR-286).
Spring Web Flow 2.0 reference manual: http://static.springsource.org/spring-webflow/docs/2.0.x/reference/html/index.html
Limitations of Web Flow
Spring Web Flow allows you to describe essentially a state machine for your web application in an XML document called the flow definition. The key elements of the flow are:
- view-states
- action-states
- end-states
- the transitions between the states.
Each view-state has in-bound transitions and out-bound transitions. there aren't any view-states that don't have outbound transitions.
Common goals
You are likely viewing this page because you have a view-state within a flow that could be enhanced by using AJAX to retrieve some JSON data. For that JSON data, we're going to need a unique url or view.
We can't use a view-state within the flow because this JSON url isn't going to have any out-bound transitions.
Even though they don't have out-bound transitions, we can't use end-states either as they "end" the flow and typically remove any state.
Example approach
We can provide this JSON producing URL by creating an Servlet-style Spring @Controller
external to the flow and integrating Spring MVC within the portlet.
Steps:
- Add spring-webmvc as a dependency to your portlet project, if it isn't already.
- Add the Spring DispatcherServlet to your web.xml and bind a url-pattern to use for your AJAX
@Controller
(s) - Implement servlet
@Controller
(s) for returning your JSON - Configure Spring Web MVC appropriately
- Modify your webflow view to make AJAX calls to your JSON
@Controller
(s)
Step 1: Project Dependencies
If you are using maven, add the following dependency to your pom.xml:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>2.5.6.SEC01</version> </dependency>
If you already have spring-webmvc-portlet as a dependency, which you likely already do, you don't need to add this (spring-webmvc is a dependency of spring-webmvc-portlet).
While editing the pom, add the Spring 2.5.6 compatible JSONView:
<dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib-ext-spring</artifactId> <version>1.0.2</version> </dependency>
Step 2: Add Spring DispatcherServlet to web.xml
Open up your web.xml, and add the following elements:
<!-- You should already have the ViewRendererServlet for proper Spring Web Flow integration --> <servlet> <servlet-name>ViewRendererServlet</servlet-name> <servlet-class>org.springframework.web.servlet.ViewRendererServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <!-- Add the following DispatcherServlet for your AJAX controllers --> <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>2</load-on-startup> </servlet> ... <!-- You should already have this ViewRendererServlet servlet-mapping for proper Spring Web Flow integration --> <servlet-mapping> <servlet-name>ViewRendererServlet</servlet-name> <url-pattern>/WEB-INF/servlet/view</url-pattern> </servlet-mapping> <!-- Add a new servlet-mapping to bind urls like "/ajax/*" to the Spring DispatcherServlet --> <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/ajax/*</url-pattern> </servlet-mapping>
Step 3: Implement a @Controller
to return JSON
Example:
package org.jasig.portlet.example.web; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.View; @Controller public class WidgetController { private WidgetService widgetService; @Autowired public void setWidgetService(WidgetService widgetService) { this.widgetService = widgetService; } @RequestMapping("/ajax/widgets") public String getWidgetsAsJson(@RequestParam("query") String searchString, final ModelMap model) { List<Widget> widgets = this.widgetService.searchForWidgets(searchString); model.addAttribute("widgets", widgets); return "jsonView"; } }
Step 4: Configure Spring Web MVC
The Spring DispatcherServlet is going to look by default for a Spring XML ApplicationContext in /WEB-INF with the name 'servlet-name-servlet.xml'. Following the example
web.xml above, create a file named 'spring-servlet.xml' in your portlet's /WEB-INF directory that contains the following:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <context:component-scan base-package="org.jasig.portlet.example.web"/> <context:annotation-config/> <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"> <property name="alwaysUseFullPath" value="true"/> </bean> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="alwaysUseFullPath" value="true"/> </bean> <bean class="org.springframework.web.servlet.view.XmlViewResolver" p:order="0" p:location="/WEB-INF/views.xml"/> </beans>
Important things to note in the example application context:
- The base-package attribute of the context:component-scan element refers to the package that contains our Spring {{@Controller}(s).
- alwaysUseFullPath is an absolute necessity.
- The XmlViewResolver defines another Spring XML Application Context in /WEB-INF/views.xml.
/WEB-INF/views.xml should include (at a minimum):
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="jsonView" class="net.sf.json.spring.web.servlet.view.JsonView"> <property name="contentType" value="application/json" /> </bean> </beans>
The return value of the example @Controller
will resolve to the JsonView class.
Step 5: Modify your webflow view to make AJAX calls
Now your JSON url should be accessible to the views in your flow:
<c:set var="n"><portlet:namespace/></c:set> <c:url value="/ajax/widgets" var="widgetsJsonUrl"/> <script type="text/javascript"> var ${n} = ${n} || {}; ${n}.jQuery = jQuery.noConflict(); ${n}.jQuery(function(){ var $ = ${n}.jQuery; var widgetSearchText = ""; //extract this from user interaction, e.g. text input $.ajax({ url: '${widgetsJsonUrl}', data: { query: widgetSearchText}, type: "GET", dataType: "json", async: false, success: function(data) { if(data.widgets) { //show our widgets! } } }); }); </script>
Advanced topic: making Web Flow state data available to your AJAX @Controllers
You may need to access information garnered during the Web Flow in your AJAX Controllers that either is inappropriate or not serializable as a query parameter.
One example: current authentication information.
You don't want someone to be able to spoof authentication, hence this type is information is inappropriate for a query parameter for an AJAX @Controller
.
You can store this information in the session during the Web Flow using the APPLICATION_SCOPE, and it can be safely retrieved later in the AJAX @Controller
.
Storing data in session with APPLICATION_SCOPE
You likely already have a @Service
that your flow regularly interacts with via 'evaluate' expressions in the flow definition.
Here is an example of a method that stores some data gathered during one of the evaluations and stores the data in the APPLICATION_SCOPE:
@Service public class FlowHelper { public static final String CURRENT_USER_ATTR = FlowHelper.class.getName() + ".CURRENT_USER"; public String getRemoteUser() { RequestContext requestContext = RequestContextHolder.getRequestContext(); PortletRequest request = (PortletRequest) requestContext.getExternalContext().getNativeRequest(); final String remoteUser = request.getRemoteUser(); PortletSession portletSession = request.getPortletSession(); portletSession.setAttribute(CURRENT_USER_ATTR, remoteUser, PortletSession.APPLICATION_SCOPE); return remoteUser; } }
Later in the flow, this data can be retrieved from the session in your AJAX @Controller
:
import org.springframework.web.context.request.WebRequest; ..... @RequestMapping("/ajax/something") public String doSomeControllerMethod(WebRequest request) { final String remoteUser = (String) request.getAttribute(FlowHelper.CURRENT_USER_ATTR, PortletSession.APPLICATION_SCOPE);
Note: it is very important to use Spring's WebRequest
here rather than HttpServletRequest.