跳至主要内容

JSF 2.2: Flow

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.xhtml
Besides the view files, another flow description file is required.
There are two approaches to declare a flow.
  1. 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.
  2. 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.
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.
  • 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.
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.
<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.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.
<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.
  1. Spring Web Flow provides extended persistence support in the whole flow, so you never worry about the LazyInitializationException thrown by Hibernate.
  2. Spring Web Flow supports events on enter and exit point in every node.
  3. Spring Web Flow supports Ajax, thus the page fragment can participate into the flow.
  4. 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.
  5. 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.
In JBoss Seam 2, the most attractive feature is it's long run conversation. Compare Faces Flow, it has some advantage.
  1. @Begin and @End are provided to declare the boundaries of a conversation, no need extra flow definition.
  2. Multi propagate types are provided to enter a conversation, such as new, nest, join etc.
  3. Conversation scoped persistence is provided, so you never worry about the classic LazyInitializationException in the conversation scope.
  4. Support manual flush, and transaction can be committed or rollback int the last exit node.
  5. Support nest conversation which behaves as the subflow in Spring Web Flow.
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.
  1. The elements of the flow definition are tedious and not clear as the Spring Web Flow.
  2. 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.
  3. The Java based flow definition(@FlowDefinition and FlowBuilder) is every ugly, just converts the xml tag. It is not fluent APIs at all.
  4. The @FlowScoped bean does not work as the Seam 2 conversation bean.
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.

Sample codes

Check out the complete codes from my github.com, and play it yourself.
https://github.com/hantsy/ee7-sandbox

评论

此博客中的热门博文

AngularJS CakePHP Sample codes

Introduction This sample is a Blog application which has the same features with the official CakePHP Blog tutorial, the difference is AngularJS was used as frontend solution, and CakePHP was only use for building backend RESR API. Technologies AngularJS   is a popular JS framework in these days, brought by Google. In this example application, AngularJS and Bootstrap are used to implement the frontend pages. CakePHP   is one of the most popular PHP frameworks in the world. CakePHP is used as the backend REST API producer. MySQL   is used as the database in this sample application. A PHP runtime environment is also required, I was using   WAMP   under Windows system. Post links I assume you have some experience of PHP and CakePHP before, and know well about Apache server. Else you could read the official PHP introduction( php.net ) and browse the official CakePHP Blog tutorial to have basic knowledge about CakePHP. In these posts, I tried to follow the steps describ

JPA 2.1: Attribute Converter

JPA 2.1: Attribute Converter If you are using Hibernate, and want a customized type is supported in your Entity class, you could have to write a custom Hibernate Type. JPA 2.1 brings a new feature named attribute converter, which can help you convert your custom class type to JPA supported type. Create an Entity Reuse the   Post   entity class as example. @Entity @Table(name="POSTS") public class Post implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name="ID") private Long id; @Column(name="TITLE") private String title; @Column(name="BODY") private String body; @Temporal(javax.persistence.TemporalType.DATE) @Column(name="CREATED") private Date created; @Column(name="TAGS") private List<String> tags=new ArrayList<>(); } Create an attribute convert

Auditing with Hibernate Envers

Auditing with Hibernate Envers The approaches provided in JPA lifecyle hook and Spring Data auditing only track the creation and last modification info of an Entity, but all the modification history are not tracked. Hibernate Envers fills the blank table. Since Hibernate 3.5, Envers is part of Hibernate core project. Configuration Configure Hibernate Envers in your project is very simple, just need to add   hibernate-envers   as project dependency. <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> </dependency> Done. No need extra Event listeners configuration as the early version. Basic Usage Hibernate Envers provides a simple   @Audited   annotation, you can place it on an Entity class or property of an Entity. @Audited private String description; If   @Audited   annotation is placed on a property, this property can be tracked. @Entity @Audited public class Signup implements Serializa