Quantcast
Channel: techscouting through the java news » Thomas Asel
Viewing all articles
Browse latest Browse all 25

A comprehensive example of JSF’s Faces Flow

0
0

Though introduced as a “big ticket feature” in JSF 2.2, there are surprisingly few examples available for the new Faces Flow feature. Those that are existing seem to be more or less variations of the example given in section 7.5.1 of the spec. I was not able to find any examples on how to use e.g. switch nodes, so I decided to play around a bit and share this example along with my findings. Feel free to add to this example by leaving a comment. While parts of the example are discussed in this article, the whole thing can be found on GitHub.

Working with flows

Basically, a flow is a state graph formed by a set of nodes. Nodes are defined explicitly, there is exactly one entry point (the start node) and an arbitrary number of exit points. Every instance of a flow has exclusive access to an isolated scope, the flow scope. A flow node represents a certain state and thus moving between nodes means transitioning between states. A flow node may be something like a view, so navigating between the pages of a website is a transition of state.

A Faces Flow is tied to a certain browser tab or window, so from a developers view, Faces Flows are one possibility to leverage a window specific scope. The flow scope may be used as a scope that spreads over several pages. It is created once a certain view is reached (the start node) and destroyed if either an endpoint is reached or if navigation to a view that is not part of the flow happened. These features make Faces Flow a suitable tool to implement more or less complex “wizard-like” structures.

Nodes in Faces Flow are not restricted to views, currently there are 5 types of nodes:

  1. View node
    This is the most common and basic type. A view node represents a certain view that is part of a flow.
  2. Method call node
    These nodes represent the call to a method. The called method may return an outcome that determines which node needs to be visited next.
  3. Switch node
    Switch nodes consist of an arbitrary number of statements. Every statement is associated with an outcome that will be used if the statement evaluates to Boolean.TRUE
  4. Return node
    These nodes form the explicitly defined exit nodes of a flow graph.
  5. Flow call node
    A flow may call another flow, effectively chaining or nesting them. Flow call nodes are used to transition between flows. The original flow may be continued once the called flow has finished.

Defining Flows

Flows may be defined either in XML or Java-based, using a fluent API. The XML-based approach looks a bit tedious to me so I will stick to the fluent FlowBuilder API for the remainder of this article. You can find an example on how to define flows through XML in the Java EE 7 tutorial. A number of conventions exists to make definition of flows easier. It is possible to package defined flows either in directories as an integral part of an application or in a separate JAR file, allowing to reuse defined flows (including views and logic) in applications. More on packaging flows later.

The runtime scans the application for flow definitions during startup. You can define Flows using the fluent FlowBuilder API by providing a CDI producer method annotated with a predefined qualifier (@FlowDefinition). That said, it is clear that Faces Flows are dependant on CDI (not only for flow definitions, the whole Faces Flow feature requires CDI, e.g. the Flow scope is essentially a CDI scope). Lets illustrate this with some code:

public class FlowFactory implements Serializable {

    @Produces @FlowDefinition
    public Flow defineFlow( @FlowBuilderParameter FlowBuilder flowBuilder) {
        flowBuilder.id("", "myFlow");
        flowBuilder.viewNode("viewNode", "/myFlow/someFlowNode.xhtml");

        return flowBuilder.getFlow();
    }
}

When an appropriate producer for @FlowDefinition is found, the annotated method is called during application startup. The method returns a FlowDefinition that will be used by the runtime. The runtime will inject an instance of FlowBuilder into the method. The flow is defined using the fluent interface of FlowBuilder.

Flows are identified by their Flow-Id, which is set by calling FlowBuilder#id(definingDocumentId, flowId). The definingDocumentId param may be a blank String but not null. It is used to disambiguate Flows with the same name but defined in different documents. This may be the case in larger projects where a lot of JAR-packaged Flows are used but this is unnecessary for this example. Therefore the parameter is left blank in line 5 of the example. The second param defines the id of the flow. We may associate a Flow scoped bean with this scope by using the Flow-Id with the @FlowScoped annotation:

@Named
@FlowScoped("myFlow")
public class FlowScopedBean implements Serializable {
    // code omitted
}

The FlowBuilder API

As already mentioned, the fluent FlowBuilder API allows to create flows and to set certain properties on them. You can see the big picture here.

Every node needs to be identified by an id that is unique within this flow. Every method of FlowBuilder that creates a flow node takes a flow node id as first argument. Flow node methods return an instance of NodeBuilder which offers further configuration methods depending on the node type.

View Nodes

FlowBuilder#viewNode(viewNodeId, vdlDocumentId) is used to create a view node in the current flow. The viewNodeId argument is the node id, vdlDocumentId is a path to the facelet file representing this node. The path may be relative to the WAR-root or just the name of a file placed in the flows directory, if directory packaging is used. This enables a flow to reuse existing pages or to use a single page in more than one flow. View nodes are probably the node type that will be used most often. The returned ViewBuilder offers only a single method - markAsStartNode() that may be used to define the view node as the entrypoint to the flow.

Method Call Nodes

Method call nodes are created using FlowBuilder#methodCallNode(methodCallNodeId). The method takes the flow node id as its only argument and returns an instance of
MethodCallBuilder which is used for further configuration of the node.

flowBuilder.methodCallNode("myMethodCallNode")  // create a method call node
        .defaultOutcome("anOutcome")            // default outcome
        .expression("#{bean.someMethod()}");    // method expression to invoke

The example above demonstrates the usage of MethodCallBuilder. The EL expression is invoked once the node is reached. If the method produces an outcome by returning a String, this outcome is used to navigate to the next page or node in the flow. A default outcome may be defined if the method does not return any. MethodCallBuilder#parameters(parameters) can optionally be used to set parameters for the method call. These are passed as List<Parameter>. Unfortunately the standardized JSF API comes with the abstract class javax.faces.flow.Parameter. So, to create parameters you either have to introduce a dependency to the implementation (e.g. use com.sun.faces.flow.ParameterImpl) or to create an own subclass of javax.faces.flow.Parameter. The latter is advised, since dependencies to a specific JSF implementation makes it hard to switch to the other one.

Switch Node

Switch nodes are the equivalent to the switch-case construct in Java. Several statements may be defined using an EL expression and an outcome. The outcome of the first statement thats expression evaluates to Boolean.TRUE will be used to navigate in the flow. The following is an example of a switch node:

flowBuilder.switchNode("switch-node")   // create a switch node
        .defaultOutcome("home")         // exit flow to homepage, if none of the statements evaluates to true
        // several switch statements
        .switchCase()
            .condition("#{someBean.value==1}").fromOutcome("next-node")
        .switchCase()
            .condition("#{someBean.methodWithReturnTypeBool}").fromOutcome("any-node")
        .switchCase()
            .condition("#{'a' eq 'b'}").fromOutcome("exit-node");

Return Node

Return nodes represent the endpoint of a flow. While there can only be one start node (marked by invoking the markAsStartNode() method of the appropriate node builder) an arbitrary number of return nodes may exist. When a return node is reached, a flows finalizer is invoked and its flow scope destroyed. A return node may define an outcome which is used for navigation to a page outside of the flow, or, in case of a flow call node, for navigation within the calling flow. The following example demonstrates the creation of a return node:

flowBuilder.returnNode("exit-flow").fromOutcome("home");    // exit flow and navigata to homepage

Flow Call Node

Flow call nodes are the last and probably most complex type of flow nodes. These nodes are used to enter one flow from another. The initial flow stays active while the called flow is executed and will be continued, when the called flow has reached a return node. The API allows to define in- and outbound parameters. These may be used to pass data from one flow to another but there is no such thing as a shared scope. The following example illustrates how to create flow call nodes.

public class FlowFactory implements Serializable {

// Definition for calling flow
@Produces @FlowDefinition
public Flow defineFlowA( @FlowBuilderParameter FlowBuilder flowBuilder) {

    String flowId = "flow-a";   // id for this flow
    flowBuilder.id("", flowId);

    flowBuilder.flowCallNode("call-flow-b")
        .flowReference("", "flow-b")                     // define the flow to call
        .outboundParameter("param", "#{bean.value}");    // pass a parameter to the flow

    // ...
    // definition of other nodes ommited

    return flowBuilder.getFlow();
}

// Definition for called flow
@Produces @FlowDefinition
public Flow defineFlowB( @FlowBuilderParameter FlowBuilder flowBuilder) {

    String flowId = "flow-b";   // id for this flow
    flowBuilder.id("", flowId);

    flowBuilder.
        inboundParameter("param", "#{flowBean.inboundParam}");   // define the inbound parameter

    flowBuilder.returnNode("return-to-a")    // define exit point for this flow
        .fromOutcome("node-in-flow-a");      // navigation outcome to use when the flow is terminated

    // ...
    // definition of other nodes ommited

    return flowBuilder.getFlow();
}
}

The flow call node is defined in lines 10-12. An outbound paramter with name “param” is defined for the calling flow and an inbound parameter with the same name is defined for the called flow.
There is no restriction on the number of in- and outbound parameters that may be defined for a flow or flow call node.

Initializer and Finalizer

FlowBuilder provides lifecycle callbacks that may be used to invoke methods when a flow is created and before it is destroyed. As the name suggests, FlowBuilder#initializer(methodExpression) is invoked when the flow is created and may be used to execute initialization steps. The method expression may be passed as a String containing a valid EL expression or as an instance of MessageExpression. The same applies to FlowBuilder#finalizer(methodExpression) which is called when a flow is exited. It is not possible to define more than one initializer or finalizer, so a delegate method should by referenced, if needed.

<

h3>Navigation within the flow graph

A flow is entered when a navigation outcome is used that matches a valid flow id. For a flow with id “flow-a”, the following snippet shows how to provide a button to enter the flow:

<h:form>
    <h1>Welcome - Choose a flow </h1>
    <h:commandButton value="Enter the flow" action="flow-a" />
</h:form>

The same applies for navigation between flow nodes, but in this case the flow node id (not the flow id) is used as outcome. When in a flow, only nodes within the same flow and views outside of the flow are reachable.
As with normal outcomes, the navigation target (view or flow node) does not need to be defined statically in the facelet. Instead an action method may be referenced. Section 7.4 of the spec defines the NavigationHandler algorithm when navigation to or within a flow is involved.

Using the Flow Scope

Maybe the most useful feature of Faces Flow is the new flow scope. A bean that is annotated with @FlowScoped(“flow-id”) is tied to the lifecycle of that particular flow. The bean will be created when the flow is entered and it will be destroyed when the flow is exited, either by reaching a return node or by navigating to a view that is not part of the flow. This effectively gives us a scope that lasts longer as @RequestScoped, shorter than @SessionScoped and is not tied to staying at the same page like @ViewScoped. @FlowScoped is capable of solving a lot of problems that arise when @SessionScoped is overly used. Additionally, since a flow is tied to a specific browser tab or window, @FlowScoped allows to cleanly separate the state of these tabs or windows. The flow scope may be accessed via EL using #{flowScope}, a flow scoped bean is accessible through its EL name like any other managed bean. The following example demonstrates the usage of the flow scope and a flow scoped bean using EL.

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html">
    <h:body>
        <h:form>
            Put something into flow scope:
            <h:inputText value="#{flowScope.someValue}" />
            <h:commandButton value="store" /> <br />

            Store something a flow scoped bean:
            <h:inputText value="#{flowBean.someMessage}" />
            <h:commandButton value="store" /> <br />
        </h:form>
    </h:body>
</html>

Packaging Flows

Flows may optionally be packaged either in a JAR file or in directories. Packaging flows in directories within the application is a convenient approach since section 11.4.3.3. of the spec defines some conventions for this case:

  • Flows may be defined in directories directly under the web app root. The name of the directory is the name of flow
  • Every vdl file in that directory is a view node of that flow.
  • The start node of the flow is the view whose name is the same as the name of the flow.
  • Navigation among any of the views in the directory is considered to be within the flow.
  • The flow defining document id is the empty string.

The packaged flow may be further configured with an XML configuration file named -flow.xml. For every directory packaged flow, an instance of javax.faces.flow.Flow is created by the runtime during startup.

While directory packaged flows offer convenience, JAR packaged flows may be reused in several applications. Other than with directory packaging, JAR packaging requires explicit configuration since none of the conventions are applied. Section 11.4.3.2 defines the measures to be taken if JAR packaging shall be used. However, it is totally valid to forego the benefits of packaging flows at all, if desired.

Conclusion

I would like to conclude this example with a short summary of my findings:

  • Though CDI’s @ConversationScoped may be sufficient for a lot / most cases, @FlowScoped adds a lot of value to JSF, especially when a defined state graph is needed.
  • Additionally, the programming model does not require the application developer to deal with conversation demarcation, once the flows are defined
  • Since flows may span multiple views, @FlowScoped may be very helpful to avoid session pollution.
  • Faces Flow offers a solution for window-dependant scopes out-of-the-box. However, if this feature is required throughout the whole application, every view needs to be part of flow. This may make the application hard to maintain.
  • The XML configuration is very verbose, so the Java-based API is preferable
  • Outcomes become more ambiguous as they may be interpreted as:
    1. Normal navigation outcomes
    2. Implicit navigation targets
    3. Flow ids or flow node ids
  • Transition between nodes is only possible using the outcome of CommandComponents, thus Flows can only be used with HTTP POST.
  • Navigation using GET (quite common, right?) has no or an unwanted effect on flows.
  • Faces Flow depends on CDI, so applications running in a servlet container need to provide a CDI implementation. However, this is not a big deal.

Useful links:
[1] example on GitHub: https://github.com/tasel/facesflow-example
[2] https://blogs.oracle.com/arungupta/entry/jsf_2_2_faces_flow
[3] http://balusc.blogspot.de/2013/10/how-to-install-cdi-in-tomcat.html
[4] http://docs.oracle.com/javaee/7/tutorial/doc/jsf-configure003.htm

The post A comprehensive example of JSF’s Faces Flow appeared first on techscouting through the java news.


Viewing all articles
Browse latest Browse all 25

Latest Images

Trending Articles





Latest Images