@Flow – accessing Webflow data in Spring MVC controllers

At the company I work at we are presently in the middle of replacing JSF with Spring Webflow + a yet undecided templating technology. We have grown tired of the problems associated with JSF and are ready to replace them with a different set of problems :) (as every technology has).

Since we do not have a full analysis on a suitable templating technology we decided to use JSF for rendering of HTML but use Spring Webflow to handle the postback. Using Facelets to handle HTML templating and leaving out JSF components produces nice and plain HTML ideal for jQuery (or any other JavaScript framework) on the client.

Using Webflow to handle the postback proved to hold much less surprises than when we used JSF. Most of the tasks required just work. But there is one thing that was missing. There was no way of accessing flow scoped (or view scoped) variables in Spring MVC controller methods handling ajax calls.

Why not use the Webflow’s ajax abstraction? Webflow’s ajax abstraction is based on re-rendering page fragments which is not suitable for retrieving pure data from the server. On the other hand Spring 3 introduced a very appealing ajax simplification (as described here) which significantly cuts down server and client side ajax handling code.

Problem statement

I want to have all data related to the process defined in (and handled by) Webflow conveniently accessible from my MVC controller methods handling ajax calls. While I do not want to define the flow data outside the flow definition xml.

Example: Imagine a user registering an account. The process of creating an account spans several screens and it is defined in a flow. Inside this flow there is a flow scoped variable of type Account. On one of the screens you need to provide a list of available products using an ajax call. The available products depend on data entered on previous screens (this means they are already stored in flow scope) and on data on the current screen (sent via ajax).

Solution proposal

A prefered solution would be to have Spring MVC inject data stored in flow scope as handler method parameters. You could denote these parameters by adding a @Flow annotation to them.

@RequestMapping("/data/availableProducts")
public @ResponseBody List getProducts(@Flow Account account, @RequestParam("query") String query) {
    List<Product> products = // filter using a query and data from the account being created
    return products;
}

Solution implementation

The concept behind the implementation is to expose the current flow execution to the AnnotationMethodHandlerAdapter (adapter handling our ajax requests) in similar fashion as it is done in FlowHandlerAdapter (adapter handling flow requests) and resolve arguments using a custom WebArgumentResolver.

For the @Flow annotation to work we 3 classes:

  1. The @Flow annotation itself
  2. A custom implementation of the WebArgumentResolver interface (a Spring SPI interface). Implementations of this interface are responsible for resolving arguments of handler methods
  3. A custom implementation of the HandlerInterceptor interface (another Spring SPI interface)

@Flow

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Flow {

    /**
     * Name under which the annotated parameter is registered in Flow scope.
     */
    String value() default "";
}

FlowArgumentResolver

This class does all the work. It contains one “hack” without which all of this would not work. It accesses FlowExecutorImpl the implementation of the FlowExecutor to retrieve the execution repository. If you cannot stomach this, tread no further :).

public class FlowArgumentResolver implements WebArgumentResolver, InitializingBean {

    private FlowExecutor flowExecutor;
    private FlowExecutionRepository executionRepository;

    @Override
    public void afterPropertiesSet() throws Exception {
        executionRepository = ((FlowExecutorImpl) flowExecutor).getExecutionRepository();
        //...
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception {
        if(isFlowParameter(methodParameter)) {
            return resolveFlowArgument(methodParameter, webRequest);
        }

        return UNRESOLVED;
    }

    private boolean isFlowParameter(MethodParameter methodParameter) {
        return getParameterAnnotation(methodParameter) != null;
    }

    private Flow getParameterAnnotation(MethodParameter methodParameter) {
        return methodParameter.getParameterAnnotation(Flow.class);
    }

    private Object resolveFlowArgument(MethodParameter methodParameter, NativeWebRequest webRequest) {
        FlowExecution flowExecution = getFlowExecution(executionRepository, (HttpServletRequest) webRequest.getNativeRequest());

        Flow parameterAnnotation = getParameterAnnotation(methodParameter);
        if("".equals(parameterAnnotation.value())) {
            return resolveByType(methodParameter, flowExecution);
        } else {
            return resolveByName(methodParameter, parameterAnnotation.value(), flowExecution);
        }
    }

    private Object resolveByName(MethodParameter methodParameter, String name, FlowExecution flowExecution) {
        MutableAttributeMap flowAttributes = flowExecution.getActiveSession().getScope();
        return flowAttributes.get(name);
    }

    private Object resolveByType(MethodParameter methodParameter, FlowExecution flowExecution) {
        Map flowAttributes = filterValues(flowExecution.getActiveSession().getScope().asMap(), instanceOf(methodParameter.getParameterType()));
        return ((Map.Entry) flowAttributes.entrySet().iterator().next()).getValue();
    }

    private FlowExecution getFlowExecution(FlowExecutionRepository executionRepository, HttpServletRequest request) {
        String flowExecutionKeyParameter = // from flowUrlHandler
        FlowExecutionKey executionKey = executionRepository.parseFlowExecutionKey(flowExecutionKeyParameter);
        return executionRepository.getFlowExecution(executionKey);
    }

    // ...
}

FlowHandlerInterceptor

This is only a helper class that initializes ExternalContextHolder. Without this the FlowArgumentResolver would not work.

public class FlowHandlerInterceptor extends HandlerInterceptorAdapter implements ServletContextAware, InitializingBean {

    private ServletContext servletContext;
    private FlowUrlHandler flowUrlHandler;

    @Override
    public void afterPropertiesSet() throws Exception {
        if (flowUrlHandler == null) {
            flowUrlHandler = new DefaultFlowUrlHandler();
        }
        //...
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        ExternalContextHolder.setExternalContext(createServletExternalContext(request, response));
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        ExternalContextHolder.setExternalContext(null);
    }
    private ExternalContext createServletExternalContext(HttpServletRequest request, HttpServletResponse response) {
        ServletExternalContext context = new MvcExternalContext(servletContext, request, response, flowUrlHandler);
        context.setAjaxRequest(ajaxHandler.isAjaxRequest(request, response));
        return context;
    }

    // ...
}

Configuration

After we have these three things we register the FlowArgumentResolver and FlowHandlerInterceptor and arguments annotated with @Flow are automagically resolved from the current flow execution.

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
    <property name="interceptors">
        <list>
            <ref local="flowHandlerInterceptor"/>
        </list>
    </property>
</bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="customArgumentResolvers">
        <list>
            <bean class="com.company.flow.FlowArgumentResolver">
                <property name="flowExecutor" ref="flowExecutor" />
            </bean>
        </list>
    </property>
</bean>

<bean id="flowHandlerInterceptor" class="com.company.flow.FlowHandlerInterceptor"/>

Conclusion

There is one (IMHO fortunate) side-effect here. Since Webflow stores snapshots of flow scoped objects when flow pauses in view states you always access a deserialized copy of your data in Spring MVC handlers. The consequence of this is that you cannot really change flow scoped variables in MVC ajax handlers. And since changing server-side state using ajax calls can get really tricky really fast this is a not as big of a problem as it might seem. (Actions that change data should be recorded in the flow definition as event handlers if needed)

Using these 3 rather short classes and couple of lines of configuration we are able to access flow scoped variables very conveniently just by a simple annotation. Furthermore a similar approach can be adopted to access view scoped variables as well.

/Enjoy

Edit: the full code and a showcase can be found at http://code.google.com/p/webflow-mvc-bridge/

About these ads
7 comments
  1. Steve said:

    Brilliant! Exactly my problem as well. I have to “lazy” populate pages inside a couple of flows and AJAX backed by MVC controllers seems to do the trick quite nicely. As I don’t want to “leak” the identifier key – using a parameter is not an option and access to a flow scoped variable inside the @Controller seems the easiest way of getting this going. Your solution is therefore ideal for this purpose. Any chance to post the complete source of both the Interceptor and the Resolver. Thanks

    • ytoh said:

      I am thinking about creating a project on googlecode or some other site and release the code that way. But this will take a few days as I am currently on a business trip. Are you able to wait? Or do you need it urgently?

      • Steve said:

        Thanks for the quick reply – very much appreciated. Sooner would be awesome – but only if it is no hassle for you. Thanks again!

      • ytoh said:

        I have appended a link to the google code project site. Please feel free to take the implementation for a spin.

  2. Xiul said:

    Great, just what I was looking for…. Thanks.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: