Spring Web Flow console integration
And I finally did it. Wrote a numberguess application that is backed by Spring Web Flow (SWF). Spring has done a great job decoupleing SWF from other APIs, there really is no connection or dependencies to servlet API for example. But still the documentation isn’t very clear concerning integration with other view layers. So how did I do it?
So first up let’s look some of the main method I’m executing all this with:
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application-config.xml");
ConsoleFlowController flowController = (ConsoleFlowController) context.getBean("flowController");
log.debug("Flow controller obtained " + flowController);
Scanner in = new Scanner(System.in);
String inpt;
StringWriter enterFlow = new StringWriter();
//Must do one simple handle to force entering the flow execution
flowController.handleRequest(null, null, enterFlow);
enterFlow.flush();
// Initial output
System.out.println(enterFlow.getBuffer().toString());
while (!(inpt = in.nextLine()).equals("")) {
StringWriter response = new StringWriter();
// Can be used for a bit programmatic input eventId:paramname=paramvalue:paramname2=paramvalue2
// ConsoleInputResolver inputResolver = new NumberGuessInputResolver(inpt);
// Just requires input
ConsoleInputResolver inputResolver = new SimpleInputResolver(inpt);
flowController.handleRequest(inputResolver.getEventId(), inputResolver.getParameterMap(), response);
response.flush();
// Output for every "request"
System.out.println(response.getBuffer().toString());
}
System.out.println("You entered nothing. Exiting");
in.close();
}
First up I create context for Spring and obtain my flowController from there. After that I’m reading in user input. I’m not using java 6 console class because Eclipse console does not support that as far as I know. So more interesting is what I’m doing with the input.
flowController.handleRequest(inputResolver.getEventId(), inputResolver.getParameterMap(), response)
I extract the event and parameters from it and try to handle that request providing also a response writer that is simple StringWriter.
So what does that configuration contain that I’m loading (headers removed. Download the zip for whole files.):
<!-- My entry point to SWF.
With this class I'm controlling flow execution -->
<bean id="flowController"
class="ee.swf.console.ConsoleFlowController">
<property name="flowExecutor" ref="flowExecutor"/>
</bean>
<webflow:flow-executor id="flowExecutor" flow-registry="flowRegistry" />
<!-- The registry of executable flow definitions -->
<webflow:flow-registry id="flowRegistry" flow-builder-services="flowBuilderServices">
<!-- My one and only flow definition. -->
<webflow:flow-location path="higherlower-flow.xml"/>
</webflow:flow-registry>
<!-- Plugs in a custom creator for Web Flow views -->
<webflow:flow-builder-services id="flowBuilderServices" view-factory-creator="consoleViewFactoryCreator"/>
<bean id="consoleViewFactoryCreator" class="ee.swf.console.ConsoleViewFactoryCreator" />
I think that is pretty well commented what’s going on there. Now flowController is created by Spring and user console input is parsed and eventId + parameters extracted I call handleRequest method. Let’s look at that. It is pretty long and sloppy but bear with me I tried to comment it as thoroughly as I could.
ConsoleExternalContext context;
public void handleRequest(String eventId, Map<String, String> paramMap, StringWriter response) {
ConsoleExternalContext newContext = new ConsoleExternalContext(response);
newContext.setRequestParameterMap(new MockParameterMap());
newContext.setRequestMap(new LocalAttributeMap());
if (context != null) {
newContext.setSessionMap(context.getSessionMap());
newContext.setApplicationMap(context.getApplicationMap());
}
context = newContext;
External context is a facade that provides normalized access to an external system that has called into the Spring Web Flow system. For example ServletExternalContext – uses session to store its data and request to initialize. For every request some parts of the ExternalContext gets reset/cleared. Currently I’m clearing everything but session and application parameters.
if (eventId != null) context.setEventId(eventId);
if (paramMap != null) {
for (Iterator<Entry<String, String>> iterator = paramMap.entrySet().iterator(); iterator.hasNext();) {
Entry<String, String> type = iterator.next();
context.putRequestParameter(type.getKey(), type.getValue());
}
}
Next up I’m setting all required request parameters. eventId is also stored in request parameters map.
To not split up my code too much I used comments for following code. Now comes the real execution of the flow.
FlowExecutionResult result = null;
// Execution key for current flow execution is in threadlocal
String flowExecutionKey = flowExecutionContext.get();
if (flowExecutionKey == null) {
log.debug("Launching new flow");
// If no execution key yet then launch a new flow
result = flowExecutor.launchExecution("higherlower-flow", new LocalAttributeMap(), context);
log.debug("Launched new flow: " + result.getFlowId());
} else {
log.debug("Resuming flow: " + flowExecutionKey);
// If execution key found then use it to resume flow
result = flowExecutor.resumeExecution(flowExecutionKey, context);
}
// FlowExecutionResult can be in two possible states paused or ended
if (result.isPaused()) {
log.debug("Flow execution is paused: " + result.getPausedKey());
// If flow is paused store the execution key to threadlocal
// so it could be resumed.
flowExecutionContext.set(result.getPausedKey());
// Here comes a bit tricky part.
// After handling event SWF tries to redirect user
// to avoid reposting data.
// But with console redirect is pointless.
if (context.getFlowExecutionRedirectRequested()) {
log.debug("Redirecting user to continue flow: " + result.getPausedKey());
// Simple redirect with no events nor parameters but using the same responsewriter
handleRequest(null, null, response);
}
} else if (result.isEnded()) {
// If result indicates that flow has completed
// then remove execution key so that new flow is started
// upon next request.
flowExecutionContext.remove();
} else {
throw new IllegalStateException("Execution result should have been one of [paused] or [ended]");
}
// After handling clear the responses generated so far just in case
context.clearResponseWriter();
A bit about that redirect. You can take a look at ViewState class doEnter method. If transition with event is done to the same view then redirect is requested and no view gets rendered. After the redirect where no eventId is specified view is obtained and rendered. It makes perfect sense in web but not using console. So flow is resumed twice but I’m fine with that. I haven’t found a way how to remove that redirect in my console app but I have no need to.
And finally my flow definition:
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd" start-state="enterGuess">
<var name="game" class="ee.swf.console.game.HigherLowerGame"/>
<view-state id="enterGuess" view="higherlower.standard" redirect="false">
<transition on="submit" to="makeGuess"/>
</view-state>
<action-state id="makeGuess">
<evaluate expression="flowScope.game.makeGuess(requestParameters.guess)" result="flashScope.guessResult"/>
<transition on="CORRECT" to="showAnswer"/>
<transition on="*" to="enterGuess"/>
<transition on-exception="java.lang.NumberFormatException" to="enterGuess"/>
</action-state>
<end-state id="showAnswer" view="higherlower.standard"/>
</flow>
Please download the source swf-console.zip
In case you get any questions or comments feel free to email me or just post a reply here.
RSS