Archive

Monthly Archives: February 2012

In Spring Webflow passing attributes between flows is made a little more difficult because you have to do a client side redirect when switching flows. When you take into account that you usually pass attributes between flows when one flow ends and another starts that leaves only two options how to pass information around:

  1. request parameters
  2. a longer living scope (because one flow scope is ending and another hasn’t been created yet)

Passing information around in request attributes may reveal too much information to the client or might not be suitable for certain information (e.g. high volume data, binary data). Most of the solutions to this problem I have found are revolving around creating a parent/child or a sibling relationship between flows and either using a shared scope (conversation scope) or passing shared attributes as inputs to subflow-states.

This approach has an unpleasant disadvantage. You have to have a single flow definition that wraps all flows that need to share information. A side effect of this is that you will have one URL for all of your flows and you will not see the URL changing when you enter a subflow-state.

Also (and this is IMHO a bigger problem) defining a flow as a subflow of another implies that the subflow is a subprocess of a larger process. Creating a parent/child relationship just to share information can therefore be misleading and unnecessary.

Problem definition

I want to be able to pass output attributes from an ending flow to a starting flow as input without sending the data to the client.

Ideal solution

The prefered solution would be to have an end state in one flow be essentially a “forward” to a start state of another flow:

<end-state id="reservation-successful" view="flowRedirect:payment-process">
    <output name="reservationInformation" value="flowScope.reservation"/>
</end-state>

And to have all the output forwarded and picked up as the input to the starting flow (by naming convention):

<input name="reservationInformation" />

Solution implementation

The concept of the solution is to briefly store the output of the ending flow in a longer living scope – session scope then when the second flow starts retrieve the stored output from the session and pass it as input to the starting flow. To identify the stored output in session scope we pass an identifier identifying the correct output from the ending flow to the starting flow as a request parameter.

The whole implementation consists of only one class. It is a custom FlowExecutionListener that takes care of the whole output storing/retrieving process. All the other parts are provided by Spring Webflow itself:

As the Webflow documentation states each flow has a well-defined input/output contract:

FlowOutcome flowId(Map inputAttributes);

with the FlowOutcome having the following signature:

public interface FlowOutcome {
   public String getName();
   public Map getOutputAttributes();
}

Notice how the flow outputAttributes have the same signature as the flow input attributes.

Also the flowRedirect: view prefix used in the solution example is something already present in Spring Webflow. Even though it is not documented in the 2.3 version this prefix causes a redirect to be sent to a flow with the id following this prefix. So when the client enters a state where the view is set to flowRedirect:payment-process the client gets a redirect and ends up starting the payment-process flow.

The custom FlowExecutionListener called OutputExposingListener listens for both ending flow sessions and starting flow sessions and does the following things:

  1. in sessionEndedit check whether the ending flow requested a flow redirect
    1. if it did generates an identifier for the ending flow’s output and stores it in session scope under this generated identifier
    2. adds this generated identifier to the redirect parameters
  2. in sessionStartingchecked whether there is any exposed output from a previously ended flow
    1. if there is an exposed output it retrieves the output (based on the generated identifier passed as a request parameter) and adds the output to the input attributes of the starting flow
    2. removes the stored output from session scope

public class OutputExposingListener extends FlowExecutionListenerAdapter {

    private static final String SESSION_MAP_KEY = OutputExposingListener.class.getName() + ".SESSION_KEY";

    private static final String EXPOSED_OUTPUT_ID = "_outputId";

//--------------------------------------------------------------------------------------------------------------------
// public methods
//--------------------------------------------------------------------------------------------------------------------

    @Override
    public void sessionStarting(RequestContext context, FlowSession session, MutableAttributeMap input) {
        if(hasExposedOutput(context) && session.isRoot()) {
            exposePreviousFlowOutput(context, input);
        }
    }

    @Override
    public void sessionEnded(RequestContext context, FlowSession session, String outcome, AttributeMap output) {
        if(shouldExposeOutput((ServletExternalContext) context.getExternalContext())  && session.isRoot()) {
            String exposedOutputId = exposeOutput(output, context.getExternalContext().getSessionMap());
            addIdentificationParameter((ServletExternalContext) context.getExternalContext(), exposedOutputId);
        }
    }

//--------------------------------------------------------------------------------------------------------------------
// private methods
//--------------------------------------------------------------------------------------------------------------------

    private boolean hasExposedOutput(RequestContext context) {
        return context.getExternalContext().getRequestParameterMap().contains(EXPOSED_OUTPUT_ID);
    }

    private void exposePreviousFlowOutput(RequestContext context, MutableAttributeMap input) {
        String exposedOutputId = context.getExternalContext().getRequestParameterMap().get(EXPOSED_OUTPUT_ID);
        SharedAttributeMap session = context.getExternalContext().getSessionMap();

        input.putAll(retrieveOutputFromSession(session, exposedOutputId));
    }

    @SuppressWarnings("unchecked")
    private AttributeMap retrieveOutputFromSession(SharedAttributeMap session, String exposedOutputId) {
        synchronized (session.getMutex()) {
            checkState(session.contains(SESSION_MAP_KEY), "no exposed output");

            HashMapAttributeMap> outputMap = (HashMap) session.get(SESSION_MAP_KEY);

            checkState(outputMap.containsKey(exposedOutputId), "no exposed output under [" + exposedOutputId + "]");

            return outputMap.remove(exposedOutputId);
        }
    }

    private boolean shouldExposeOutput(ServletExternalContext context) {
        return context.getFlowDefinitionRedirectRequested();
    }

    @SuppressWarnings("unchecked")
    private String exposeOutput(AttributeMap output, SharedAttributeMap session) {
        synchronized(session.getMutex()) {
            if(!session.contains(SESSION_MAP_KEY)) {
                session.put(SESSION_MAP_KEY, new HashMap<String, AttributeMap>());
            }

            String outputId = UUID.randomUUID().toString();

            ((HashMap) session.get(SESSION_MAP_KEY)).put(outputId, output);

            return outputId;
        }
    }

    private void addIdentificationParameter(ServletExternalContext servletContext, String id) {
        servletContext.getFlowRedirectFlowInput().put(EXPOSED_OUTPUT_ID, id);
    }
}

The final step is just to register this listener as a Webflow execution listener:

<webflow:flow-executor id="..." flow-registry="...">
    <webflow:flow-execution-listeners>
        <webflow:listener ref="outputExposingListener" />
    </webflow:flow-execution-listeners>
</webflow:flow-executor>

<bean id="outputExposingListener" class="com.company.OutputExposingListener" />

Conclusion

This one FlowExecutionListener in combination with some of existing Webflow features allows for flow composition based on convention. You can have multiple flows chained one after another and just by naming the output attribute of one flow the same as an input attribute of the next this attribute will be passed from the first flow to the second. You do not need one monolithic flow definition file to pass attributes around and you can add pre- or post-processing flows to existing flows without touching their definition (more or less :) ).

/Enjoy

Follow

Get every new post delivered to your Inbox.