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 or Seam 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.
There are two ways to define pageflow in Seam:
Using JSF or Seam 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!
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>
Here is the same example page flow definition using Seam navigation rules:
<page view-id="/numberGuess.jsp"> <navigation> <rule if-outcome="guess"> <redirect view-id="/numberGuess.jsp"/> </rule> <rule if-outcome="win"> <redirect view-id="/win.jsp"/> </rule> <rule if-outcome="lose"> <redirect view-id="/lose.jsp"/> </rule> </navigation> </page>
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/Seam 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.
When JSF or Seam 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 by 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.
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.
<core:jbpm> <core:pageflow-definitions> <value>pageflow.jpdl.xml</value> </core:pageflow-definitions> </core:jbpm>
The first line installs jBPM, the second points to a jPDL-based pageflow definition.
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() { ... }
Alternatively we can start a pageflow using pages.xml:
<page> <begin-conversation pageflow="numberguess"/> </page>
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>
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.
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.
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>
It is possible to compose pageflows and have one pageflow pause pause while another pageflow executes. The <process-state> node pauses the outer pageflow, and begins execution of a named pageflow:
<process-state name="cheat"> <sub-process name="cheat"/> <transition to="displayGuess"/> </process-state>
The inner flow begins executing at a <start-state> node. When it reaches an <end-state> node, execution of the inner flow ends, and execution of the outer flow resumes with the transition defined by the <process-state> element.
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>
We need to install jBPM, and tell it where to find the business process definitions:
<core:jbpm> <core:process-definitions> <value>todo.jpdl.xml</value> </core:process-definitions> </core:jbpm>
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() ); ... }
To initiate a business process instance, we use the @CreateProcess annotation:
@CreateProcess(definition="todo") public void createTodo() { ... }
Alternatively we can initiate a business process using pages.xml:
<page> <create-process definition="todo" /> </page>
When a process reaches a task node, 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>
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 taskInstanceListForType component includes tasks of a particular type that are assigned to the current user:
<h:dataTable value="#{taskInstanceListForType['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>
To begin work on a task, we use either @StartTask or @BeginTask on the listener method:
@StartTask public String start() { ... }
Alternatively we can begin work on a task using pages.xml:
<page> <start-task /> </page>
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 can use pages.xml:
<page> <end-task transition="completed" /> </page>
You can also use EL to specify the transition in pages.xml.
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.