Thursday, September 24, 2009

Spring Web Flow Menus

Overview

Spring Web Flow allows you to manage real conversational state on the web.
In general, for a given user you allow one flow (i.e. conversation) at a time. This flow may launch sub-flows, but technically, your user is still running one flow at a time.
Let’s address a more complex scenario where you would like to let your user start several independent flows in parallel and allow your user to switch from one flow to another.
Nothing prevents you from starting multiple flows. You just have to provide one or several links pointing to a flow start. Your user may click on them several times and launch several flows in parallel. But,how do you allow the user to switch from one flow to another?
The first trivial solution is to start each flow in a separate window. A more user-friendly solution is to provide a flow menu, listing all the active flows.
This feature is not built-in in Spring Web Flow, but the Spring Web Flow API is flexible enough to let us implement it.
Note that the code shown below was generated by SpringFuse

Here is the Java code

You first need to create a FlowExecutionListener to capture for each running flow a menu entry, that is a URL and a Label.
package com.jaxio.example.web.flow;

/**
 * Captures a relevant URL and Label to be displayed in a flow Menu.
 */
public class FlowMenuExecutionListener extends FlowExecutionListenerAdapter {

    @Autowired
    private FlowMenu flowMenu;

    @Override
    public void viewRendering(RequestContext context, View view, StateDefinition viewState) {
        // we ignore non view state and popups
        if (!viewState.isViewState() || ((ViewState) viewState).getPopup()) {
            return;
        }

        flowMenu.updateFlowMenuItem(context.getFlowExecutionUrl(), getFlowMenuItemLabel(context));
    }

    @Override
    public void stateEntering(RequestContext context, StateDefinition state) throws EnterStateVetoException {
        if (state instanceof EndState) {
            flowMenu.removeFlowMenuItem(context.getFlowExecutionUrl());
        }
    }

    @Override
    public void sessionEnding(RequestContext context, FlowSession session, String outcome, MutableAttributeMap output) {
        flowMenu.removeFlowMenuItem(context.getFlowExecutionUrl());
    }

    private String getFlowMenuItemLabel(RequestContext context) {
        return (String) context.getFlowScope().get("menuLabel");
    }
}
The code above relies on the following bean below:
package com.jaxio.example.web.flow;

// this bean has a session scope (see conf file)
/**
 * A session scope bean (see conf file) that holds the
 * user's active.
 */
public class FlowMenu implements Serializable {
    static final private long serialVersionUID = 1L;

    private Map<String, FlowMenuItem> menu = new LinkedHashMap<String, FlowMenuItem>();

    /**
     * Create or update a menu entry.
     */
    public void updateFlowMenuItem(String flowExecutionUrl, String label) {
        menu.put(getFlowExecutionId(flowExecutionUrl), new FlowMenuItem(flowExecutionUrl, label));
    }

    /**
     * Remove a menu entry.
     */
    public void removeFlowMenuItem(String flowExecutionUrl) {
        menu.remove(getFlowExecutionId(flowExecutionUrl));
    }

    /**
     * Extract string that represent the flow execution id, for example:
     * http://localhost:8080/flow/myflow?execution=e12s2 becomes
     * http://localhost:8080/flow/myflow?execution=e12
     */
    private String getFlowExecutionId(String flowExecutionUrl) {
        return flowExecutionUrl.substring(0, flowExecutionUrl.lastIndexOf('s'));
    }

    /**
     * In flow end-state, instead of redirecting to a fixed page,
     * you can call this method to redirect the user
     * to a flow that is not yet ended.
     */
    public String getEndStateRedirect() {
        if (menu.isEmpty()) {
            return defaultExternalRedirect;
        } else {
            return "serverRelative:" + menu.values().iterator().next().getUrl();
        }
    }

    /**
     * Called from the view in charge of displaying the menu.
     */
    public List<FlowMenuItem> getFlowMenuAsList() {
        return new ArrayList<FlowMenuItem>(menu.values());
    }

    /**
     * Holds the label/url.
     */
    public class FlowMenuItem implements Serializable {
        static final private long serialVersionUID = 1L;

        String url;
        String label;

        public FlowMenuItem(String url, String label) {
            this.url = url;
            this.label = label != null ? label : url;
        }

        public String getUrl() {
            return url;
        }

        public String getLabel() {
            return label;
        }

        @Override
        public String toString() {
            return url + ":" + label;
        }
    }

    //---------------------------------------------------
    // Configuration
    //---------------------------------------------------

    private String defaultExternalRedirect = "contextRelative:index.action";
    public void setDefaultExternalRedirect(String defaultExternalRedirect) {
        this.defaultExternalRedirect = defaultExternalRedirect;
    }
Note that the listener is a singleton while the flowMenu bean must be a session bean. You must declare your flowMenu bean in your configuration file as a scoped-proxy.
Please read Spring documentation for more information.
Beside that, if you are familiar with spring Web flow and Spring MVC, the configuration is trivial.
Here is how the configuration file should look like.
    

    
        
    

    
        
            
        
        
            
            
        
    

How do we get the label?

The label string must be set in the flow as a flowScope variable and be named ‘menuLabel’.
You can set it inside an on-start tag and update it at will in view-state if you want your label to vary during the flow execution. Here is an example (again from a project generated with SpringFuse):


    

    
        
    
    

Redirection on flow ending

Would not it be nice to redirect the user to one of the other active flows when he/she ends a flow?
This is already implemented in the flowMenu bean, all you have to do is set the view attribute of your end-state as illustrated below:


    

    

How to access the menu bean from the view?

Now you need to access the menu from the view, in a JSP you can do this:
    
  • Active Flows
  • The only trick is that the flowMenu must be somehow set as a request attribute. In SpringFuse, we use a BeanInViewInterceptor, here it is:
    
    import com.jaxio.example.web.flow.FlowMenu;
    
    /**
     * This interceptor is responsible for binding useful beans on the ModelMap so they can be used
     * from the view.
     */
    @Service
    public class BeanInViewInterceptor implements HandlerInterceptor {
    
        @Autowired
        private FlowMenu flowMenu;
    
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException {
            // Note: using the modelAndView in the postHandle would not work
            //       view returned by Spring Web Flow
            request.setAttribute("flowMenu", flowMenu);
    
            // proceed
            return true;
        }
    
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        }
    
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        }
    }
    
    Don't forget to declare this interceptor... in your interceptor chain.

    0 comments:

    Post a Comment