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.
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.xhtml
Besides 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 -flow.xml in the flow node folder to describe the flow.
#{flow2Bean.returnValue} param1FromFlow1 #{flowScope.param1Value} param2FromFlow1 #{flowScope.param2Value} flow1 param1FromFlow2 param1 flow2 value param2FromFlow2 param2 flow2 value
This is a standard faces-config.xml.
The above two forms of the flow definition are equivalent. The Java class version just translate the XML one.
Let's have a look at the basic concept in the flow definition.
true
, else go to the next switch case.There is an implicit object named flowScope(which indicates the current flow) which can be used as a container to share data through the whole flow.
For example, the value of the h:inputText component can be put into the flowScope.
Second Page in Flow 1
Flow bean name: #{flow1Bean.name}
value:
And the next view, the input value can be fetched from EL #{flowScope.value}.
value: #{flowScope.value}
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.
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.xhtml
Besides 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.
LICENSE CONTENT HERE
account.xhtml includes the basic info of an user account.
First Name:
Last Name:
Email:
Password:
Confirm Password:
confirm.xhtml is a basic summary of the registration.
User info #{registrationBean.user}
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 users=new HashMap (); 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 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.
activation email #{registrationBean.user.email}
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; } }
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.
In JBoss Seam 2, the most attractive feature is it's long run conversation. Compare Faces Flow, it has some advantage.
Personally, the new Faces Flow is neither good nor bad, but I am bitterly disappointed with it. JSF EG declares it is inspired by ADF Task Flow and Spring Web Flow, but it seems they just copied ADF Task Flow and made it standardized.
Personally, I would like stay on Spring Web Flow if use JSF 2.2 in Spring framework based projects. For Java EE 6/7 application, I hope Apache DeltaSpike project could import Conversation feature from Apache MyFaces CODI as soon as possible.
Check out the complete codes from my github.com, and play it yourself.
https://github.com/hantsy/ee7-sandbox