站内搜索: 请输入搜索关键词
当前页面: 在线文档首页 > JBoss Seam 1.1.0 正式版英文参考手册

Chapter 7. Pageflows and business processes - JBoss Seam 1.1.0 正式版英文参考手册

Chapter 7. Pageflows and business processes

JBoss jBPM is a business process management engine for any Java SE or EE environment. jBPM lets you represent a business process or user interaction as a graph of nodes representing wait states, decisions, tasks, web pages, etc. The graph is defined using a simple, very readable, XML dialect called jPDL, and may be edited and visualised graphically using an eclipse plugin. jPDL is an extensible language, and is suitable for a range of problems, from defining web application page flow, to traditional workflow management, all the way up to orchestration of services in a SOA environment.

Seam applications use jBPM for two different problems:

  • Defining the pageflow involved in complex user interactions. A jPDL process definition defines the page flow for a single conversation. A Seam conversation is considered to be a relatively short-running interaction with a single user.

  • Defining the overarching business process. The business process may span multiple conversations with multiple users. Its state is persistent in the jBPM database, so it is considered long-running. Coordination of the activities of multiple users is a much more complex problem than scripting an interaction with a single user, so jBPM offers sophisticated facilities for task management and dealing with multiple concurrent paths of execution.

Don't get these two things confused ! They operate at very different levels or granularity. Pageflow, conversation and task all refer to a single interaction with a single user. A business process spans many tasks. Futhermore, the two applications of jBPM are totally orthogonal. You can use them together or independently or not at all.

You don't have to know jDPL to use Seam. If you're perfectly happy defining pageflow using JSF's navigation rules, and if your application is more data-driven that process-driven, you probably don't need jBPM. But we're finding that thinking of user interaction in terms of a well-defined graphical representation is helping us build more robust applications.

7.1. Pageflow in Seam

There are two ways to define pageflow in Seam:

  • Using JSF navigation rules - the stateless navigation model

  • Using jPDL - the stateful navigation model

Very simple applications will only need the stateless navigation model. Very complex applications will use both models in different places. Each model has its strengths and weaknesses!

7.1.1. The two navigation models

The stateless model defines a mapping from a set of named, logical outcomes of an event directly to the resulting page of the view. The navigation rules are entirely oblivious to any state held by the application other than what page was the source of the event. This means that your action listener methods must sometimes make decisions about the page flow, since only they have access to the current state of the application.

Here is an example page flow definition using JSF navigation rules:

<navigation-rule>
    <from-view-id>/numberGuess.jsp</from-view-id>
        
    <navigation-case>
        <from-outcome>guess</from-outcome>
        <to-view-id>/numberGuess.jsp</to-view-id>
        <redirect/>
    </navigation-case>

    <navigation-case>
        <from-outcome>win</from-outcome>
        <to-view-id>/win.jsp</to-view-id>
        <redirect/>
    </navigation-case>
        
    <navigation-case>
        <from-outcome>lose</from-outcome>
        <to-view-id>/lose.jsp</to-view-id>
        <redirect/>
    </navigation-case>

</navigation-rule>

If you find navigation rules overly verbose, you can return view ids directly from your action listener methods:

public String guess() {
    if (guess==randomNumber) return "/win.jsp";
    if (++guessCount==maxGuesses) return "/lose.jsp";
    return null;
}

Note that this results in a redirect. You can even specify parameters to be used in the redirect:

public String search() {
    return "/searchResults.jsp?searchPattern=#{searchAction.searchPattern}";
}

The stateful model defines a set of transitions between a set of named, logical application states. In this model, it is possible to express the flow of any user interaction entirely in the jPDL pageflow definition, and write action listener methods that are completely unaware of the flow of the interaction.

Here is an example page flow definition using jPDL:

<pageflow-definition name="numberGuess">
    
   <start-page name="displayGuess" view-id="/numberGuess.jsp">
      <redirect/>
      <transition name="guess" to="evaluateGuess">
      	<action expression="#{numberGuess.guess}" />
      </transition>
   </start-page>
   
   <decision name="evaluateGuess" expression="#{numberGuess.correctGuess}">
      <transition name="true" to="win"/>
      <transition name="false" to="evaluateRemainingGuesses"/>
   </decision>
   
   <decision name="evaluateRemainingGuesses" expression="#{numberGuess.lastGuess}">
      <transition name="true" to="lose"/>
      <transition name="false" to="displayGuess"/>
   </decision>
   
   <page name="win" view-id="/win.jsp">
      <redirect/>
      <end-conversation />
   </page>
   
   <page name="lose" view-id="/lose.jsp">
      <redirect/>
      <end-conversation />
   </page>
   
</pageflow-definition>

There are two things we notice immediately here:

  • The JSF navigation rules are much simpler. (However, this obscures the fact that the underlying Java code is more complex.)

  • The jPDL makes the user interaction immediately understandable, without us needing to even look at the JSP or Java code.

In addition, the stateful model is more constrained. For each logical state (each step in the page flow), there are a constrained set of possible transitions to other states. The stateless model is an ad hoc model which is suitable to relatively unconstrained, freeform navigation where the user decides where he/she wants to go next, not the application.

The stateful/stateless navigation distinction is quite similar to the traditional view of modal/modeless interaction. Now, Seam applications are not usually modal in the simple sense of the word - indeed, avoiding application modal behavior is one of the main reasons for having conversations! However, Seam applications can be, and often are, modal at the level of a particular conversation. It is well-known that modal behavior is something to avoid as much as possible; it is very difficult to predict the order in which your users are going to want to do things! However, there is no doubt that the stateful model has its place.

The biggest contrast between the two models is the back-button behavior.

7.1.2. Seam and the back button

When JSF navigation rules are used, Seam lets the user freely navigate via the back, forward and refresh buttons. It is the responsibility of the application to ensure that conversational state remains internally consistent when this occurs. Experience with the combination of web application frameworks like Struts or WebWork - that do not support a conversational model - and stateless component models like EJB stateless session beans or the Spring framework has taught many developers that this is close to impossible to do! However, our experience is that in the context of Seam, where there is a well-defined conversational model, backed by stateful session beans, it is actually quite straightforward. Usually it is as simple as combining the use of no-conversation-view-id with null checks at the beginning of action listener methods. We consider support for freeform navigation to be almost always desirable.

In this case, the no-conversation-view-id declaration goes in pages.xml. It tells Seam to redirect to a different page if a request originates from a page rendered during a conversation, and that conversation no longer exists:

<page view-id="/checkout.xhtml" 
        no-conversation-view-id="/main.xhtml"/>

On the other hand, in the stateful model, backbuttoning is interpreted as an undefined transition back to a previous state. Since the stateful model enforces a defined set of transitions from the current state, back buttoning is be default disallowed in the stateful model! Seam transparently detects the use of the back button, and blocks any attempt to perform an action from a previous, "stale" page, and simply redirects the user to the "current" page (and displays a faces message). Whether you consider this a feature or a limitation of the stateful model depends upon your point of view: as an application developer, it is a feature; as a user, it might be frustrating! You can enable backbutton navigation from a particular page node by setting back="enabled".

<page name="checkout" 
        view-id="/checkout.xhtml" 
        back="enabled">
    <redirect/>
    <transition to="checkout"/>
    <transition name="complete" to="complete"/>
</page>

This allows backbuttoning from the checkout state to any previous state!

Of course, we still need to define what happens if a request originates from a page rendered during a pageflow, and the conversation with the pageflow no longer exists. In this case, the no-conversation-view-id declaration goes into the pageflow definition:

<page name="checkout" 
        view-id="/checkout.xhtml" 
        back="enabled" 
        no-conversation-view-id="/main.xhtml">
    <redirect/>
    <transition to="checkout"/>
    <transition name="complete" to="complete"/>
</page>

In practice, both navigation models have their place, and you'll quickly learn to recognize when to prefer one model over the other.

7.2. Using jPDL pageflows

7.2.1. Installing pageflows

We need to install the Seam jBPM-related components, and tell them where to find our pageflow definition. We can specify this Seam configuration in components.xml.

<component class="org.jboss.seam.core.Jbpm">
    <property name="pageflowDefinitions">pageflow.jpdl.xml</property>
</component>

The first line installs jBPM, the second points to a jPDL-based pageflow definition.

7.2.2. Starting pageflows

We "start" a jPDL-based pageflow by specifying the name of the process definition using a @Begin, @BeginTask or @StartTask annotation:

@Begin(pageflow="numberguess")
public void begin() { ... }

If we are beginning the pageflow during the RENDER_RESPONSE phase—during a @Factory or @Create method, for example—we consider ourselves to be already at the page being rendered, and use a <start-page> node as the first node in the pageflow, as in the example above.

But if the pageflow is begun as the result of an action listener invocation, the outcome of the action listener determines which is the first page to be rendered. In this case, we use a <start-state> as the first node in the pageflow, and declare a transition for each possible outcome:

<pageflow-definition name="viewEditDocument">

    <start-state name="start">
        <transition name="documentFound" to="displayDocument"/>
        <transition name="documentNotFound" to="notFound"/>
    </start-state>
    
    <page name="displayDocument" view-id="/document.jsp">
        <transition name="edit" to="editDocument"/>
        <transition name="done" to="main"/>
    </page>
    
    ...
    
    <page name="notFound" view-id="/404.jsp">
        <end-conversation/>
    </page>
    
</pageflow-definition>

7.2.3. Page nodes and transitions

Each <page> node represents a state where the system is waiting for user input:

<page name="displayGuess" view-id="/numberGuess.jsp">
    <redirect/>
    <transition name="guess" to="evaluateGuess">
        <action expression="#{numberGuess.guess}" />
    </transition>
</page>

The view-id is the JSF view id. The <redirect/> element has the same effect as <redirect/> in a JSF navigation rule: namely, a post-then-redirect behavior, to overcome problems with the browser's refresh button. (Note that Seam propagates conversation contexts over these browser redirects. So there is no need for a Ruby on Rails style "flash" construct in Seam!)

The transition name is the name of a JSF outcome triggered by clicking a command button or command link in numberGuess.jsp.

<h:commandButton type="submit" value="Guess" action="guess"/>

When the transition is triggered by clicking this button, jBPM will activate the transition action by calling the guess() method of the numberGuess component. Notice that the syntax used for specifying actions in the jPDL is just a familiar JSF EL expression, and that the transition action handler is just a method of a Seam component in the current Seam contexts. So we have exactly the same event model for jBPM events that we already have for JSF events! (The One Kind of Stuff principle.)

In the case of a null outcome (for example, a command button with no action defined), Seam will signal the transition with no name if one exists, or else simply redisplay the page if all transitions have names. So we could slightly simplify our example pageflow and this button:

<h:commandButton type="submit" value="Guess"/>

Would fire the following un-named transition:

<page name="displayGuess" view-id="/numberGuess.jsp">
    <redirect/>
    <transition to="evaluateGuess">
        <action expression="#{numberGuess.guess}" />
    </transition>
</page>

It is even possible to have the button call an action method, in which case the action outcome will determine the transition to be taken:

<h:commandButton type="submit" value="Guess" action="#{numberGuess.guess}"/>
<page name="displayGuess" view-id="/numberGuess.jsp">
    <transition name="correctGuess" to="win"/>
    <transition name="incorrectGuess" to="evaluateGuess"/>
</page>

However, this is considered an inferior style, since it moves responsibility for controlling the flow out of the pageflow definition and back into the other components. It is much better to centralize this concern in the pageflow itself.

7.2.4. Controlling the flow

Usually, we don't need the more powerful features of jPDL when defining pageflows. We do need the <decision> node, however:

<decision name="evaluateGuess" expression="#{numberGuess.correctGuess}">
    <transition name="true" to="win"/>
    <transition name="false" to="evaluateRemainingGuesses"/>
</decision>

A decision is made by evaluating a JSF EL expression in the Seam contexts.

7.2.5. Ending the flow

We end the conversation using <end-conversation> or @End. (In fact, for readability, use of both is encouraged.)

<page name="win" view-id="/win.jsp">
    <redirect/>
    <end-conversation/>
</page>

Optionally, we can end a task, specify a jBPM transition name. In this case, Seam will signal the end of the current task in the overarching business process.

<page name="win" view-id="/win.jsp">
    <redirect/>
    <end-task transition="success"/>
</page>

7.3. Business process management in Seam

A business process is a well-defined set of tasks that must be performed by users or software systems according to well-defined rules about who can perform a task, and when it should be performed. Seam's jBPM integration makes it easy to display lists of tasks to users and let them manage their tasks. Seam also lets the application store state associated with the business process in the BUSINESS_PROCESS context, and have that state made persistent via jBPM variables.

A simple business process definition looks much the same as a page flow definition (One Kind of Stuff), except that instead of <page> nodes, we have <task-node> nodes. In a long-running business process, the wait states are where the system is waiting for some user to log in and perform a task.

<process-definition name="todo">
   
   <start-state name="start">
      <transition to="todo"/>
   </start-state>
   
   <task-node name="todo">
      <task name="todo" description="#{todoList.description}">
         <assignment actor-id="#{actor.id}"/>
      </task>
      <transition to="done"/>
   </task-node>
   
   <end-state name="done"/>
   
</process-definition>

It is perfectly possible that we might have both jPDL business process definitions and jPDL pageflow definitions in the same project. If so, the relationship between the two is that a single <task> in a business process corresponds to a whole pageflow <pageflow-definition>

7.4. Using jPDL business process definitions

7.4.1. Installing process definitions

We need to install jBPM, and tell it where to find the business process definitions:

<component class="org.jboss.seam.core.Jbpm">
    <property name="processDefinitions">todo.jpdl.xml</property>
</component>

7.4.2. Initializing actor ids

We always need to know what user is currently logged in. jBPM "knows" users by their actor id and group actor ids. We specify the current actor ids using the built in Seam component named actor:

@In Actor actor;

public String login() {
    ...
    actor.setId( user.getUserName() );
    actor.getGroupActorIds().addAll( user.getGroupNames() );
    ...
}

7.4.3. Initiating a business process

To initiate a business process instance, we use the @CreateProcess annotation:

@CreateProcess(definition="todo")
public void createTodo() { ... }

7.4.4. Task assignment

When a process starts, task instances are created. These must be assigned to users or user groups. We can either hardcode our actor ids, or delegate to a Seam component:

<task name="todo" description="#{todoList.description}">
    <assignment actor-id="#{actor.id}"/>
</task>

In this case, we have simply assigned the task to the current user. We can also assign tasks to a pool:

<task name="todo" description="#{todoList.description}">
    <assignment pooled-actors="employees"/>
</task>

7.4.5. Task lists

Several built-in Seam components make it easy to display task lists. The pooledTaskInstanceList is a list of pooled tasks that users may assign to themselves:

<h:dataTable value="#{pooledTaskInstanceList}" var="task">
    <h:column>
        <f:facet name="header">Description</f:facet>
        <h:outputText value="#{task.description}"/>
    </h:column>
    <h:column>
        <s:link action="#{pooledTask.assignToCurrentActor}" value="Assign" taskInstance="#{task}"/>
    </h:column>            	
</h:dataTable>

Note that instead of <s:link> we could have used a plain JSF <h:commandLink>:

<h:commandLink action="#{pooledTask.assignToCurrentActor}"> 
    <f:param name="taskId" value="#{task.id}"/>
</h:commandLink>

The pooledTask component is a built-in component that simply assigns the task to the current user.

The taskInstanceListByType component includes tasks of a particular type that are assigned to the current user:

<h:dataTable value="#{taskInstanceListByType['todo']}" var="task">
    <h:column>
        <f:facet name="header">Description</f:facet>
        <h:outputText value="#{task.description}"/>
    </h:column>
    <h:column>
        <s:link action="#{todoList.start}" value="Start Work" taskInstance="#{task}"/>
    </h:column>            	
</h:dataTable>

7.4.6. Performing a task

To begin work on a task, we use either @StartTask or @BeginTask on the listener method:

@StartTask
public String start() { ... }

These annotations begin a special kind of conversation that has significance in terms of the overarching business process. Work done by this conversation has access to state held in the business process context.

If we end the conversation using @EndTask, Seam will signal the completion of the task:

@EndTask(transition="completed")
public String completed() { ... }

(Alternatively, we could have used <end-conversation> as shown above.)

At this point, jBPM takes over and continues executing the business process definition. (In more complex processes, several tasks might need to be completed before process execution can resume.)

Please refer to the jBPM documentation for a more thorough overview of the sophisticated features that jBPM provides for managing complex business processes.