Friday, January 25, 2013

Spring Webflow: Embedding a flow in a modal JSF dialog

Introduction

The latest version of Spring Webflow (2.3.1) provides us with a new and very interesting functionality that I have really missed in my current project: embedded flows. By default, Webflow applies the POST/REDIRECT/GET pattern every time it enters a view state. The reason to do this is to prevent duplicate form submissions, but this also prevents us from having two different view states on the same page.

With embedded flows, we have the possibility to execute transitions and load view states via Ajax requests, avoiding a full page render. Thanks to the Spring samples repository, I've had the opportunity to see how to embed a flow on a page. In this tutorial, I will explain how to apply the example within a modal dialog.

You can get this tutorial source code at github: https://github.com/xpadro/spring-webflow


The flow

The demo application consists of a main flow called 'sales-flow', which shows the sales made by a specified employee. The employee information view contains an access to a subflow called 'employee-flow' that loads the employee data. This subflow will fully execute within a modal dialog.




How it works

In order to execute a flow in embedded mode, you simply need to add a parameter to the request url. For example:


                http://localhost:8080/myApp/myFlow?mode=embedded


As we need to call the subflow from the main sales flow, we can also do it by defining an input attribute.

In this example, the main flow calls the subflow using an input attribute:

      <view-state id="main">
            <transition on="start" to="embedded-flow"/>
            <transition on="next" to="sales"/>
      </view-state>
     
      <view-state id="sales">
            <transition on="back" to="main"/>
      </view-state>

      <subflow-state id="embedded-flow" subflow="sales-flow/employee-flow">
            <input name="mode" value="'embedded'"/>
            <transition on="final" to="main"/>
      </subflow-state>



And the embedded flow:

      <action-state id="getDataAction">
            <evaluate expression="getDataController"/>
            <transition on="yes" to="confirmation"/>
      </action-state>
     
      <view-state id="confirmation">
            <transition on="reset" to="reset"/>
            <transition on="confirm" to="final"/>
      </view-state>

      <view-state id="reset">
            <transition on="getData" to="getDataAction"/>
      </view-state>
     
      <end-state id="final"/>


To load the embedded flow on the main page of the sales flow, I use the dialog component from the Primefaces library:

<p:dialog widgetVar="employeeSearch" header="Search employee" modal="true" width="580">
   <p:outputPanel>
      <h:form id="mainForm">
         Insert employee name and surname: <br /><br />
         Name: <h:inputText value="#{userBean.name}" />
         Surname: <h:inputText value="#{userBean.surname}" /><br /><br />
         <p style="text-align:center">
            <p:commandButton value="Get data" action="start" update="mainForm"/>
         </p>
      </h:form>  
   </p:outputPanel>
</p:dialog>

The commandButton component will execute the action 'start' that will start the embedded flow as a subflow. The main view of the embedded flow is as follows:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                xmlns:h="http://java.sun.com/jsf/html"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:p="http://primefaces.org/ui"
                template="/WEB-INF/layouts/standard.xhtml">

<ui:define name="content">
   <h:form id="mainForm">
      Employee data:<br />
      <p:fieldset>
        <h:outputLabel style="font-family:bold" for="name">Name: </h:outputLabel>
        <h:outputText id="name" value="#{userBean.name}"/><br/>
        <h:outputLabel style="font-family:bold" for="surname">Surname: </h:outputLabel>
        <h:outputText id="surname" value="#{userBean.surname}"/>
      </p:fieldset>
           
      <br/>
      Additional info:<br />
      <p:fieldset>
        <h:outputLabel style="font-family:bold" for="code">Code: </h:outputLabel>
        <h:outputText id="code" value="#{userBean.code}"/><br/>
        <h:outputLabel style="font-family:bold" for="city">City: </h:outputLabel>
        <h:outputText id="city" value="#{userBean.city}"/>
      </p:fieldset>

      <p>
        Confirm selection?
      </p>
      <p style="text-align:center">
        <p:commandButton value="Reset" action="reset" update="mainForm"/>
        <p:commandButton value="Confirm" action="confirm" update="mainForm" />  
      </p>
   </h:form>
</ui:define>
</ui:composition>

Keep in mind that the form must have the same id attribute in all the embedded flow views. Otherwise, it won't be possible to execute the embedded flow.



Saturday, January 12, 2013

Testing webflow 2 with inheritance

This blog entry shows how to test a flow with inheritance in Spring Webflow 2. The flow to be tested consists of a simple navigation which starts with a view state and ends getting to another view state that will depend on the result of the execution of a controller. This flow extends another flow which basically contains a redirection to a common page in case of error.

Introduction 


The test flow (main.xml) is as follows:

<?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"
      parent="parent">

      <view-state id="startPage" view="main/startPage.jsp">
            <transition on="next" to="generateError"/>
      </view-state>
     
      <action-state id="generateError">
            <evaluate expression="checkParameterController"/>
            <transition on="ok" to="OkView"/>
            <transition on="ko" to="KoView"/>
      </action-state>



      <end-state id="OkView" view="main/finalOkView.jsp"/>
      <end-state id="KoView" view="main/finalKoView.jsp"/>
</flow>



And the parent flow (parent.xml):

 <?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"
      abstract="true">

      <end-state id="errorState" view="commonErrorPage.jsp"/>

      <global-transitions>
            <transition on-exception="java.lang.Exception" to="errorState"/>
      </global-transitions>
</flow>

When executing tests, Spring provides us with a quite useful class: AbstractXmlFlowExecutionTests, in the org.springframework.webflow.test.execution package. This class has a variety of methods that will help us test the flow. The most interesting:
FlowDefinitionResource getResource(FlowDefinitionResourceFactory resourceFactory)
 Here, we specify where the flow which we want to test is located.

FlowDefinitionResource[] getModelResources(FlowDefinitionResourceFactory resourceFactory)
 If the flow uses inheritance, we define the parent flow here.

void configureFlowBuilderContext(MockFlowBuilderContext builderContext)
Lets us customize the builder context. I use it for registering the beans that will use in the test as this is the way it's indicated at the class javadoc.

void registerMockFlowBeans(ConfigurableBeanFactory flowBeanFactory)
Allows registering the beans that will be used by the flow to be tested. For example:

     flowBeanFactory.registerSingleton("myService", new MyMockService());


Testing the flow

I've divided it in two classes in order to separate configuration from tests cases:

public class BaseTestFlow  extends AbstractXmlFlowExecutionTests {
      protected ApplicationContext applicationContext;

      /**
       * This method returns the flow to be tested.
       */
      protected FlowDefinitionResource getResource(FlowDefinitionResourceFactory resourceFactory) {
            return resourceFactory.createFileResource("src/test/resources/flows/main.xml");
      }

      /**
       * Needs to be overridden if the flow to be tested inherits from another flow. In this case, we register its parent flow.
       */
      protected FlowDefinitionResource[]
       getModelResources(FlowDefinitionResourceFactory resourceFactory) {
        FlowDefinitionResource[] flowDefinitionResources = new FlowDefinitionResource[1];
        flowDefinitionResources[0] = resourceFactory.createFileResource("src/test/resources/flows/parent.xml");
        return flowDefinitionResources;
      }

      /**
       * Registers beans used by the tested flow.
       */
      protected void configureFlowBuilderContext(MockFlowBuilderContext builderContext) {
            applicationContext = new ClassPathXmlApplicationContext(new String[] {
                  "classpath:xpadro/spring/test/configuration/root-context.xml",
                  "classpath:xpadro/spring/test/configuration/app-context.xml"
            });
            builderContext.registerBean("checkParameterController", applicationContext.getBean("checkParameterController"));
      }

      /*
      protected void registerMockFlowBeans(ConfigurableBeanFactory flowBeanFactory) {
            flowBeanFactory.registerSingleton("checkParameterController", new CheckParameterController());
      }
      */
}

You could simply delete configureFlowBuilderContext method and use registerMockFlowBeans method instead if you don't want/need to start your own test context.
public class TestFlow extends BaseTestFlow {
      public void testFlowStarts() {
            MockExternalContext externalContext = new MockExternalContext();
            startFlow(externalContext);
            assertFlowExecutionActive();
            assertCurrentStateEquals("startPage");
      }

      public void testNavigation() {
            MockExternalContext externalContext = new MockExternalContext();
            setCurrentState("startPage");
            externalContext.setEventId("next");
            externalContext.getMockRequestParameterMap().put("inputField", "yes");
            getFlowScope().put("testFlowVar", "testValue");
            resumeFlow(externalContext);
            assertCurrentStateEquals("OkView");
            assertEquals("testValue", getFlowScope().get("testFlowVar"));
      }

      public void testGlobalTransition() {
            MockExternalContext externalContext = new MockExternalContext();
            setCurrentState("startPage");
            externalContext.setEventId("next");
            externalContext.getMockRequestParameterMap().put("inputField", "error");
            resumeFlow(externalContext);
            assertFlowExecutionOutcomeEquals("errorState");
      }
}