SeamFramework.orgCommunity Documentation

Capitolo 8. Pageflow e processi di business

8.1. Pageflow in Seam
8.1.1. I due modelli di navigazione
8.1.2. Seam ed il pulsante indietro
8.2. Utilizzo dei pageflow jPDL
8.2.1. Installazione dei pageflow
8.2.2. Avvio dei pageflow
8.2.3. Nodi e transizioni di pagina
8.2.4. Controllo del flusso
8.2.5. Fine del flusso
8.2.6. Composizione dei pageflow
8.3. La gestione del processo di business in Seam
8.4. Uso di jPDL nella definizione del processo di business
8.4.1. Installazione delle definizioni di processo
8.4.2. Inizializzazione degli actor id
8.4.3. Iniziare un processo di business
8.4.4. Assegnazione task
8.4.5. Liste di task
8.4.6. Esecuzione di un task

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:

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:

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:

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.

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.

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>

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
>