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 -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.

  • 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.

 
    
 
  
 
  

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.

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.

   
    
 
  
 
  
    
 
  
 
  

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;
    }
}

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

你可能感兴趣的:(Netbeans,Glassfish,JavaEE7,JSF2.2)