JSF 2.2: Flow
JSF 2.2 introduces a standard flow concept. The flow concept have been existed in several web solutions for several years, such as Spring Web Flow and Seam 2 page flow.Introduction
The flow definition follows the convention over configuration.For example, in order to define a flow named flow1, you could create a folder named flow1 in the web root to group all views in this flow. By default, you must provides a view which name is same to the flow name as start node. In this example, it is flow1.xhtml. In order to start the flow, you should specify the value of h:commandButton or h:commandLink as the flow name, it will activate the Window Context support(by default it is disabled) and search the matched flow and navigate the start node of the flow.
/flow1 /flow1.xhtml /flow1a.xhtml /flow1b.xhtmlBesides the view files, another flow description file is required.
There are two approaches to declare a flow.
-
Create a CDI @Produces to declare the flow.
@Produces @FlowDefinition public Flow defineFlow(@FlowBuilderParameter FlowBuilder flowBuilder) { String flowId = "flow1"; flowBuilder.id("", flowId); flowBuilder.viewNode(flowId, "/" + flowId + "/" + flowId + ".xhtml").markAsStartNode(); flowBuilder.returnNode("taskFlowReturn1"). fromOutcome("#{flow1Bean.returnValue}"); flowBuilder.inboundParameter("param1FromFlow2", "#{flowScope.param1Value}"); flowBuilder.inboundParameter("param2FromFlow2", "#{flowScope.param2Value}"); flowBuilder.flowCallNode("call2").flowReference("", "flow2"). outboundParameter("param1FromFlow1", "param1 flow1 value"). outboundParameter("param2FromFlow1", "param2 flow1 value"); return flowBuilder.getFlow(); }
Personally, I can not understand why I have to add a @FlowDefintion with the @Produces on the method and a @FlowBuilderParameter on the argument *FlowBuilder.
-
Create a <flowName>-flow.xml in the flow node folder to describe the flow.
<faces-config version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd"> <flow-definition id="flow2"> <flow-return id="taskFlowReturn1"> <from-outcome>#{flow2Bean.returnValue}</from-outcome> </flow-return> <inbound-parameter> <name>param1FromFlow1</name> <value>#{flowScope.param1Value}</value> </inbound-parameter> <inbound-parameter> <name>param2FromFlow1</name> <value>#{flowScope.param2Value}</value> </inbound-parameter> <flow-call id="callFlow1"> <flow-reference> <flow-id>flow1</flow-id> </flow-reference> <outbound-parameter> <name>param1FromFlow2</name> <value>param1 flow2 value</value> </outbound-parameter> <outbound-parameter> <name>param2FromFlow2</name> <value>param2 flow2 value</value> </outbound-parameter> </flow-call> </flow-definition> </faces-config>
This is a standard faces-config.xml.
Let's have a look at the basic concept in the flow definition.
- flow-return specifies an outcome that is returned to the calling flow
- inbound-parameter defines the parameters passed from another flow
- outbound-parameter defines the parameters should be passed to another flow
- flow-call call another flow in the current flow
- method-call execute a method and could perform navigation
- view the regular view document
-
switch provides a list of EL as switch cases, and evaluate them one by one, and turn the first node when the evaluation result is
true
, else go to the next switch case.
For example, the value of the h:inputText component can be put into the flowScope.
<h:form prependId="false"> <h1>Second Page in Flow 1</h1> <p>Flow bean name: #{flow1Bean.name}</p> <p>value: <h:inputText id="input" value="#{flowScope.value}" /></p> <p><h:commandButton id="start" value="back" action="flow1" /></p> <p><h:commandButton id="flow1b" value="next" action="flow1b" /></p> <p><h:commandButton id="index" value="home" action="taskFlowReturn1" /></p> </h:form>And the next view, the input value can be fetched from EL #{flowScope.value}.
<p>value: #{flowScope.value}</p>Besides use flowScope to store data in the flow, you can define a @FlowScoped bean for this purpose. @FlowScoped is a CDI compatible scope.
@Named @FlowScoped("flow1") public class Flow1Bean implements Serializable { public String getName() { return this.getClass().getSimpleName(); } public String getReturnValue() { return "/return1"; } }The @FlowScoped can accept a value attribute, it should be the flow name which it will be used in.
Either flowScope or @FlowScoped bean, they should be destroyed when leaves the flow.
I found the none flow command actions did not work when entered a flow, such as
<h:commandButton value="/nonFlow"/>
in the above example, it does not work. If it is designated as this, it is terrible in the real projects.Registration flow: an example
In the real world application, a registration progress can be split into two parts, registration and activation by the link in email.Two flows can be defined, registration and the followed activation.
/registration /registration-flow.xml /registraion.xhtml /account.xhtml /confirm.xhtml /activation /activation-flow.xml /activation.xhtml /ok.xhtmlBesides the web resources, two @FlowScoped beans are declared to process the logic the associate flow.
The registration includes three steps.
registration.xhtml contains license info and a checkbox, user has to accept the license and moves forward.
<h:form prependId="false"> <h:messages globalOnly="true" showDetail="false" showSummary="true"/> <p>LICENSE CONTENT HERE</p> <p><h:selectBooleanCheckbox id="input" value="#{registrationBean.licenseAccepted}" /></p> <p><h:commandButton id="next" value="next" action="#{registrationBean.accept}" /></p> </h:form>account.xhtml includes the basic info of an user account.
<h:form prependId="false"> <h:messages globalOnly="true" showDetail="false" showSummary="true"/> <p>First Name: <h:inputText id="firstName" value="#{registrationBean.user.firstName}" /><h:message for="firstName"/></p> <p>Last Name: <h:inputText id="lastName" value="#{registrationBean.user.lastName}" /><h:message for="lastName"/></p> <p>Email: <h:inputText id="email" value="#{registrationBean.user.email}" /><h:message for="email"/></p> <p>Password: <h:inputSecret id="password" value="#{registrationBean.user.password}" /><h:message for="password"/></p> <p>Confirm Password: <h:inputSecret id="passwordConfirm" value="#{registrationBean.passwordConfirm}" /></p> <p><h:commandButton id="pre" value="back" action="registration" immediate="true"/></p> <p><h:commandButton id="next" value="next" action="#{registrationBean.confirm}" /></p> </h:form>confirm.xhtml is a basic summary of the registration.
<h:form prependId="false"> <h:messages globalOnly="true" showDetail="false" showSummary="true"/> <p>User info #{registrationBean.user}</p> <p><h:commandButton id="back" value="back" action="account"/></p> <p><h:commandButton id="callActivationFlow" value="Enter Activation Flow" action="callActivationFlow" /></p> </h:form>A RegistrationBean is provided to process the registration logic, such as check if the checkbox is checked in the first step, and check the email existence in the second step.
Named @FlowScoped("registration") public class RegistrationBean implements Serializable { @Inject transient Logger log; @Inject UserService userService; private boolean licenseAccepted = false; private User user = null; private String passwordConfirm; @PostConstruct public void init() { log.info("call init..."); user = new User(); } public boolean isLicenseAccepted() { return licenseAccepted; } public void setLicenseAccepted(boolean licenseAccepted) { this.licenseAccepted = licenseAccepted; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public String getPasswordConfirm() { return passwordConfirm; } public void setPasswordConfirm(String passwordConfirm) { this.passwordConfirm = passwordConfirm; } public String getName() { return this.getClass().getSimpleName(); } public String getReturnValue() { return "/return1"; } public String accept() { log.info("call accept..."); if (this.licenseAccepted) { return "account"; } else { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "You have to read and accept the license!", "You have to read and accept the license!")); return null; } } public String confirm() { log.info("call confirm..."); if (!user.getPassword().equals(passwordConfirm)) { FacesContext.getCurrentInstance().addMessage("password", new FacesMessage(FacesMessage.SEVERITY_ERROR, "Password is mismatched!", "Password is mismatched!")); return null; } if (userService.findByEmail(this.user.getEmail()) != null) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "User " + this.user.getEmail() + " is existed!", "User " + this.user.getEmail() + " is existed!")); return null; } userService.save(this.user); return "confirm"; } }A dummy UserService is also provided for storing the User data into and fetch the data from "database"(I used a Map for test purpose).
@ApplicationScoped @Named public class UserService implements Serializable{ @Inject transient Logger log; private Map<String, User> users=new HashMap<String, User>(); public void save(User user){ log.info("user list size@"+users.size()); log.info("saving user..."); users.put(user.getEmail(), user); } public User findByEmail(String email){ log.info("user list size@"+users.size()); log.info("find by email...@"+email); return users.get(email); } public List<User> getUserList(){ log.info("user list size@"+users.size()); return new ArrayList(this.users.values()); } }The registration-flow.xml defines a flow-call to call the activation flow. In the confirm.xhtml view there is a h:commandButton available for this purpose.
<flow-call id="callActivationFlow"> <flow-reference> <flow-id>activation</flow-id> </flow-reference> <outbound-parameter> <name>email</name> <value>#{registrationBean.user.email}</value> </outbound-parameter> </flow-call>The activation flow is simple and stupid.
It inclues a activaton page and successful page(the view ok).
The backend bean ActivationBean provides a basic dummy logic to check the activation token value and change the activated property to true of the user and store the activated date.
@Named @FlowScoped("activation") public class ActivationBean implements Serializable { @Inject Logger log; @Inject UserService userService; private String email; private String tokenValue; public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getTokenValue() { return tokenValue; } public void setTokenValue(String tokenValue) { this.tokenValue = tokenValue; } public String getName() { return this.getClass().getSimpleName(); } public String getReturnValue() { return "/return1"; } public String activate() { log.info("call activate...."); User user = userService.findByEmail(this.email); if (user == null) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "User " + this.getEmail() + " not found!", "User " + this.getEmail() + " not found!")); return null; } if ("123".equals(this.tokenValue)) { user.setActivated(true); user.setActivatedOn(new Date()); userService.save(user); return "ok"; } return null; } }
Summary
I have used Spring Web Flow and Seam 2 page flow in projects.Spring Web Flow support Spring MVC and JSF, the flow definition is written in XML format. JBoss Seam 2 provides JBoss JBPM 3 based page flow(XML format) and long run conversation bean centered flow.
Compare the new JSF 2.2 Faces Flow and Spring Web Flow, Faces Flow lacks lots of features provided in Spring Web Flow.
- Spring Web Flow provides extended persistence support in the whole flow, so you never worry about the LazyInitializationException thrown by Hibernate.
- Spring Web Flow supports events on enter and exit point in every node.
- Spring Web Flow supports Ajax, thus the page fragment can participate into the flow.
- Spring Web Flow supports subflows in the main flow, and allow you share the data state between flows directly, the inbound-parameter/outbound-parameter in the Faces Flow is very stupid and ugly.
- Spring Web Flow is designated to remove any backend beans, and use flow to hold the data, and use stateless Spring bean to sync data with database.
- @Begin and @End are provided to declare the boundaries of a conversation, no need extra flow definition.
- Multi propagate types are provided to enter a conversation, such as new, nest, join etc.
- Conversation scoped persistence is provided, so you never worry about the classic LazyInitializationException in the conversation scope.
- Support manual flush, and transaction can be committed or rollback int the last exit node.
- Support nest conversation which behaves as the subflow in Spring Web Flow.
- The elements of the flow definition are tedious and not clear as the Spring Web Flow.
- The command action, especially, h:commandButton and h:commandLink became difficult to understand, the action value could be an implicit outcome, a flow name, a method call id, a flow call id etc.
- The Java based flow definition(@FlowDefinition and FlowBuilder) is every ugly, just converts the xml tag. It is not fluent APIs at all.
- The @FlowScoped bean does not work as the Seam 2 conversation bean.
Sample codes
Check out the complete codes from my github.com, and play it yourself.https://github.com/hantsy/ee7-sandbox
评论