SeamFramework.orgCommunity Documentation
JBoss jBPM è un motore di gestione dei processi di business per ambiente Java SE o EE. jBPM ti consente di rappresentare un processo di business o un'interazione utente come un grafo di nodi, raffiguranti stati d'attesa, decisioni, compiti (task), pagine web, ecc. Il grafo viene definito usando un dialetto XML semplice, molto leggibile, chiamato jPDL, che può essere editato e visualizzato graficamente usando un plugin di eclipse. jPDL è un linguaggio estendibile ed è adatto per un range di problemi, dalla definizione di un flusso di pagine dell'applicazione web alla gestione tradizionale del workflow, fino all'orchestrazione di servizi in un ambiente SOA.
Le applicazioni Seam utilizzano jBPM per due tipi di problemi:
Complesse interazioni da parte dell'utente comportano la definizione un pageflow (flusso di pagina). Una definizione di un processo con jPDL stabilisce il flusso delle pagine per una singola conversazione. Una conversazione in Seam è considerata un'interazione di breve durata con un singolo utente.
Definizione del processo di business sottostante. Il processo di business può comportare una serie di conversazioni con più utenti. Il suo stato viene persistito nel database jBPM, divenendo così di lunga durata. Il coordinamento delle attività di più utenti è un problema molto più complesso che descrivere l'interazione di un singolo utente, cosicché jBPM offre dei modi sofisticati per la gestione dei compiti (task) e per la gestione di più percorsi concorrenti di esecuzione.
Non confondere le due cose! Queste operano a livelli molto diversi e con diverso grado di granularità. Pageflow, conversazione e task si riferiscono tutti alla singola interazione con il singolo utente. Un processo di business comporta più task. Quindi le due applicazione di jBPM sono totalmente ortogonali. Possono essere usate assieme, in modo indipendente, o si può non usarle affatto.
Non serve conoscere jPDL per usare Seam. Se ci si trova bene nel definire un pageflow con JSF o con le regole di navigazione di Seam, e se l'applicazione è guida più dai dati che dal processo, probabilmente non serve usare jBPM. Ma noi pensiamo che strutturare l'interazione dell'utente in termini di rappresentazione grafical ben definita aiuti a costruire applicazioni più robuste.
Ci sono due modi per definire il pageflow in Seam:
Uso di JSF o delle regole di navigazione di Seam - il modello di navigazione stateless
Utilizzo di jPDL - il modello di navigazione stateful
Applicazioni molto semplici richiedono soltanto un modello di navigazione stateless. Invece applicazioni molto complesse impiegano entrambi i modelli in differenti punti. Ciascun modello ha i suoi punti di forza e le sue debolezze!
Il modello stateless definisce una mappatura tra un set di esiti di un evento e la pagina della vista. Le regole di navigazione sono interamente senza memoria rispetto allo stato mantenuto dall'applicazione oltre che alla pagina origine dell'evento. Questo significa che i metodi dell'action listener devono di tanto in tanto prendere decisioni sul pageflow, poiché solo loro hanno accesso allo stato corrente dell'applicazione.
Ecco ora un esempio di definizione di pageflow usando le regole di navigazione JSF:
<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
>
Ecco lo stesso esempio di definizione di pageflow usando le regole di navigazione di Seam:
<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
>
Se ritieni che le regole di navigazione siano troppo lunghe, si può restituire l'id della vista direttamente dai metodi dell'action listener:
public String guess() {
if (guess==randomNumber) return "/win.jsp";
if (++guessCount==maxGuesses) return "/lose.jsp";
return null;
}
Si noti che questo comporta un redirect. Si possono persino specificare i parametri da usare nel redirect:
public String search() {
return "/searchResults.jsp?searchPattern=#{searchAction.searchPattern}";
}
Il modello stateful definisce un set di transizioni tra gli stati dell'applicazione. In questo modello è possibile esprimere il flusso di qualsiasi interazione utente interamente nella definizione jPDL di pageflow, e scrivere i metodi action listener completamente slegati dal flusso dell'interazione.
Ecco ora un esempio di definizione di pageflow usando 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
>
Ci sono due cose da notare immediatamente:
Le regole di navigazione JSF/Seam sono molto più semplici. (Comunque questo nasconde il fatto che il codice Java sottostante è molto complesso.)
jPDL rende l'interazione utente immediatamente comprensibile senza dover guardare il codice JSP o Java.
In aggiunta il modello stateful è più vincolato. Per ogni stato logico (ogni passo del pageflow) c'è un set vincolato di possibili transizioni verso altri stati. Il modello stateless è un modello ad hoc adatto ad una navigazione libera e senza vincoli in cui l'utente decide dove andare, non l'applicazione.
La distinzione di navigazione stateful/stateless è abbastanza simile alla tradizionale vista di interazione modale/senza modello. Ora le applicazioni Seam non sono solitamente modali nel semplice senso della parola - infatti, evitare il comportamento modale dell'applicazione è uno delle principali ragioni per usare le conversazioni! Comunque le applicazioni Seam possono essere, e spesso lo sono, modali a livello di una particolare conversazione. E' noto che il comportamento modale è qualcosa da evitare il più possibile; è molto difficile predire l'ordine in cui gli utenti vogliono fare le cose! Comunque non c'è dubbio che il modello stateful ha un suo utilizzo.
Il maggior contrasto fra i due modelli è nel comportamento col pulsante indietro.
Quando le regole di navigazione di JSF o Seam vendono impiegate, Seam consente all'utente di navigare liberamente avanti ed indietro e di usare il pulsante aggiorna. E' responsabilità dell'applicazione assicurare che lo stato conversazionale rimanga internamente consistente quando questo avviene. L'esperienza con la combinazione di framework web come Struts o WebWork - che non supportano un modello conversazionale - e modelli a componenti stateless come session bean EJB stateless o il framework Spring ha insegnato a molti sviluppatori che questo è quasi praticamente impossibile da realizzare! Comunqur la nostra esperienza è che il contesto di Seam, dove c'è un modello conversazionale ben definito, agganciato a session bean stateful, è in verità abbastanza semplice. E' tanto semplice quanto combinare l'uso di no-conversation-view-id
con controlli nulli all'inizio di metodi action listener. Riteniamo che il supporto alla navigazione libera sia quasi sempre desiderabile.
In questo casola dichiarazione no-conversation-view-id
va in pages.xml
. Questa dice a Seam di reindirizzare ad una pagina differente se la richiesta proviene da una pagina generata durante una conversazione, e questa conversazione non esiste più:
<page view-id="/checkout.xhtml"
no-conversation-view-id="/main.xhtml"/>
Dall'altro lato, nel modello stateful, il pulsante indietro viene interpretato come una transizione indietro ad un precedente stato. Poiché il modello stateful costringe ad un set di transizioni dallo stato corrente, il pulsante indietro viene di default disabilitato nel modello stateful! Seam rileva in modo trasparente l'uso del pulsante indietro e blocca qualsiasi tentativo di eseguire un'azione da una pagina precedente "in stallo", e reindirizza l'utente alla pagina "corrente" (mostrando un messagio faces). Se si consideri questa una funzionalità oppure una limitazione del modello stateful dipende dal proprio punto di vista: come sviluppatore è una funzionalità; come utente può essere frustrante! Si può abilitare la navigazione da un particolare nodo di pagina con il pulsante indietro impostando back="enabled"
.
<page name="checkout"
view-id="/checkout.xhtml"
back="enabled">
<redirect/>
<transition to="checkout"/>
<transition name="complete" to="complete"/>
</page
>
Questo permette l'uso del pulsante indietro dallo stato checkout
a qualsiasi altro stato!
Occorre ancora definire cosa succede se una richiesta ha origine da una pagina generata durante un pageflow mentre la conversazione con il pageflow non esiste più. In questo caso la dichiarazione no-conversation-view-id
va dentro la definizione del pageflow:
<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 pratica entrambi i modelli di navigazione hanno la loro utilità ed imparerai presto a riconoscere quando impiegare uno o l'altro.
Vanno installati i componenti di Seam relativi a jBPM e vanno collocate le definizione dei pageflow (usando l'estensione standard .jpdl.xml
) dentro l'archivio Seam (un archivio che contiene un file seam.properties
):
<bpm:jbpm />
Si può anche comunicare esplicitamente a Seam dove trovare le definizioni dei pageflow. Questo viene specificato dentro components.xml
:
<bpm:jbpm>
<bpm:pageflow-definitions>
<value
>pageflow.jpdl.xml</value>
</bpm:pageflow-definitions>
</bpm:jbpm
>
Si "inizia" un pageflow basato su jPDLspecificando il nome della definizione del processo usando un'annotazione @Begin
, @BeginTask
oppure @StartTask
:
@Begin(pageflow="numberguess")
public void begin() { ... }
In alternativa si può iniziare un pageflow usando pages.xml:
<page>
<begin-conversation pageflow="numberguess"/>
</page
>
Se il pageflow viene iniziato durante la fase RENDER_RESPONSE
— durante un metodo @Factory
o @Create
, per esempio — si presume di essere già nella pagina da generare, e si usa un nodo <start-page>
come primo nodo nel pageflow, come nell'esempio sopra.
Ma se il pageflow viene iniziato come risultato di un'invocazione di un action listener, l'esito dell'action listener determina quale è la prima pagina da generare. In questo caso si usa un <start-state>
come primo nodo del pageflow, e si dichiara una transizione per ogni possibile esito:
<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
>
Ogni nodo <page>
rappresenta uno stato in cui il sistema aspetta input da parte dell'utente:
<page name="displayGuess" view-id="/numberGuess.jsp">
<redirect/>
<transition name="guess" to="evaluateGuess">
<action expression="#{numberGuess.guess}" />
</transition>
</page
>
view-id
è l'id della vista JSF. L'elemento <redirect/>
ha lo stesso effetto di <redirect/>
in una regola di navigazione JSF: cioè un comportamento post-then-redirect per superare i problemi del pulsante aggiorna del browser. (Si noti che Seam propaga i contesti di conversazioni assieme a questi redirect. Quindi in Seam non serve alcun construtto "flash" dello stile di Ruby on Rails!)
Il nome della transizione è il nome dell'esito JSF lanciato cliccando un command button od un command link in numberGuess.jsp
.
<h:commandButton type="submit" value="Guess" action="guess"/>
Quando cliccando il pulsante verrà invocata la transizione, jBPM attiverà l'azione della transizione chiamando il metodo guess()
del componente numberGuess
. Si noti che la sintassi usata per specificare le azioni in jPDL non è che un'espressione JSF EL già familiare, e che l'action handler della transizione è solo un metodo di un componente Seam negli attuali contesti. Così per gli eventi jBPM si ha esattamente lo stesso modello ad eventi visto per gli eventi JSF! (Il principio Unico tipo di "cosa".)
Nel caso di esito nullo (per esempio un pulsante di comando senza la definizione di action
), Seam segnalerà la transizione senza nome se ne esiste una, oppure rivisualizzerà la pagina se tutte le transizioni hanno un nome. Così è possibile semplificare leggermente l'esempio del pageflow e questo pulsante:
<h:commandButton type="submit" value="Guess"/>
Esegue la seguente transizione senza nome:
<page name="displayGuess" view-id="/numberGuess.jsp">
<redirect/>
<transition to="evaluateGuess">
<action expression="#{numberGuess.guess}" />
</transition>
</page
>
E' anche possibile che il pulsante chiami un action method, nel qual caso l'esito dell'azione determinerà la transizione da prendere:
<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
>
Comunque questo è considerato uno stile inferiore, poiché sposta la responsabilità del controllo del flusso fuori dalla definizione del pageflow e la mette negli altri componenti. E' molto meglio centralizzare questo concern nel pageflow stesso.
Di solito non occorrono le funzionalità più potenti di jPDL nella definizione dei pageflow. Serve comunque il nodo <decision>
:
<decision name="evaluateGuess" expression="#{numberGuess.correctGuess}">
<transition name="true" to="win"/>
<transition name="false" to="evaluateRemainingGuesses"/>
</decision
>
Una decisione viene presa valutando un'espressione EL JSF nei contesti di Seam.
Si termina una conversazione usando <end-conversation>
oppure @End
. (Infatti per chiarezza si consiglia l'uso di entrambi.)"
<page name="win" view-id="/win.jsp">
<redirect/>
<end-conversation/>
</page
>
Opzionalmente è possibile terminare un task specificando un nome alla transition
jBPM. In questo modo Seam segnalerà la fine del task corrente al processo di business sottostante.
<page name="win" view-id="/win.jsp">
<redirect/>
<end-task transition="success"/>
</page
>
E' possibile comporre pageflow ed avere una pausa in un pageflow mentre viene eseguito un altro pageflow. Il nodo <process-state>
effettua una pausa nel pageflow più esterno e comincia l'esecuzione di un pageflow con nome:
<process-state name="cheat">
<sub-process name="cheat"/>
<transition to="displayGuess"/>
</process-state
>
Il flusso più interno comincia l'esecuzione al nodo <start-state>
. Quando raggiunge un nodo <end-state>
, l'esecuzione del flusso più interno termina, e riprende quella del flusso più esterno con la transizione definita dall'elemento <process-state>
.
Un processo di business è un set di task ben-definiti che deve essere eseguito dagli utenti o dai sistemi software secondo regole ben-definite riguardo chi può eseguire un task, e quandodeve essere eseguito. L'integrazione jBPM di Seam facilita la visione della lista di task agli utenti e consente loro di gestire questi task. Seam consente anche all'applicazione di memorizzare lo stato associato al processo di business nel contesto BUSINESS_PROCESS
, ed rendere questo stato persistente tramite variabili jBPM.
Una semplice definizione di processo di business appare più o meno come una definizione di pageflow (Unico tipo di cosa), tranne che invece dei nodi <page>
, si hanno i nodi <task-node>
. In un processo di business long-running, gli stati di attesa si verificano quando il sistema aspetta che un qualche utente entri ed esegua un 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
>
E' perfettamente possibile avere entrambi le definizioni di processo di business jPDL e le definizioni di pageflow jPDL nello stesso progetto. Se questo è il caso, la relazione tra i due è che un singolo <task>
in un processo di business corrisponde ad un intero pageflow <pageflow-definition>
Occorre installare jBPM ed indicare dove si trovano le definizioni dei processi di business:
<bpm:jbpm>
<bpm:process-definitions>
<value
>todo.jpdl.xml</value>
</bpm:process-definitions>
</bpm:jbpm
>
Poichéi processi jBPM sono persistenti nel corso dei riavvii dell'applicazione, quando si usa Seam in ambiente di produzione non si vorrà sicuramente installare ogni volta le definizioni dei processi adogni riavvio. Quindi, in ambiente di produzione, occorre fare il deploy del processo in jBPM fuori da Seam. In altre parole, si installano le definizioni dei processi dal file components.xml
durante lo sviluppo dell'applicazione.
Bisogna sempre sapere quale utente è attualmente loggato. jBPM "riconosce" gli utenti dal loro actor id e dai group actor id. Specificheremo gli attuali actor id usando il componente interno di Seam chiamato actor
:
@In Actor actor;
public String login() {
...
actor.setId( user.getUserName() );
actor.getGroupActorIds().addAll( user.getGroupNames() );
...
}
Per iniziare un'istanza di processo di business, si usa l'annotazione @CreateProcess
:
@CreateProcess(definition="todo")
public void createTodo() { ... }
In alternativa è possibile iniziare un processo di business usando pages.xml:
<page>
<create-process definition="todo" />
</page
>
Quando un processo raggiunge un task node, vengono create istante di compiti. Queste devono essere assegnate a utenti o gruppi di utenti. Si può codificare gli id degli attori (actor id) o delegare ad un componente Seam:
<task name="todo" description="#{todoList.description}">
<assignment actor-id="#{actor.id}"/>
</task
>
In questo caso si è semplicemente assegnato il task all'utente corrente. Possiamo anche assegnare compiti ad un pool (gruppo):
<task name="todo" description="#{todoList.description}">
<assignment pooled-actors="employees"/>
</task
>
Parecchi componenti incorporati in Seam consentono facilmente di visualizzare liste di compiti. pooledTaskInstanceList
è una lista di compiti che gli utenti possono assegnare a se stessi:
<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
>
Notare che invece di <s:link>
avremmo potuto usare un semplice JSF<h:commandLink>
:
<h:commandLink action="#{pooledTask.assignToCurrentActor}"
>
<f:param name="taskId" value="#{task.id}"/>
</h:commandLink
>
Il componente pooledTask
è un componente incorporato che semplicemente assegna il task all'utente corrente.
Il componente taskInstanceListForType
include i task di un particolare tipo che sono stati assegnati all'utente corrente:
<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
>
Per iniziare a lavorare ad un compito, si può usare o @StartTask
o @BeginTask
sul metodo listener.
@StartTask
public String start() { ... }
In alternativa si può iniziare a lavorare su un task usando pages.xml:
<page>
<start-task />
</page
>
Queste annotazioni iniziano uno speciale tipo di conversazione che ha significato in termini di processo di business. Il lavoro fatto da questa conversazione ha accesso allo stato mantenuto nel contesto di business process.
Se si termina una conversazione usando @EndTask
, Seam segnalerà il completamento del task:
@EndTask(transition="completed")
public String completed() { ... }
In alternativa si usa pages.xml:
<page>
<end-task transition="completed" />
</page
>
Si può anche usare EL per specificare la transizione in pages.xml.
A questo punto jBPM assume il controllo e continua l'esecuzione della definizione del processo di business. (In processi più complessi, parecchi task potrebbero aver bisogno di essere completati prima che l'esecuzione del processo possa riprendere.)
Fare riferimento alla documentazione jBPM per una panoramica delle funzionalità che jBPM fornisce per la gestione di processi complessi di business.