Usate JSF per le vostre applicazioni Enterprise e non? Pensate che il sistema di navigazione sia inadatto alle vostre esigenze? Riflettete un attimo alla grande modularità con cui è stato concepito JSF: possiamo cambiare RenderKit, ViewHandler, introdurre PhaseListeners e… ovviamente cambiare il NavigationHandler! In questo post vedremo come è possibile modificare il sistema di navigazione JSF decorandone gli outcome.
Flussi di navigazione
Immaginiamo di aver a che fare con un flusso di lavoro intrecciato come quello in figura:
dove da una condizione iniziale si possono percorrere i blocchi attraverso 3 flussi:
- Flusso A:
- A1 > A2 > A3
- Flusso B:
- B1 > A2 > A3 > B2 > B3 > A3
- Flusso C:
- C1 > B2 > C2
Gestirli sarebbe pane per i denti di un motore di workflow, ma magari è eccessivamente costoso mettere in piedi una struttura di questo tipo. Forse una soluzione come Spring WebFlow potrebbe valer la pena. Ma siamo proprio sicuri che il puro JSF non sia in grado di risolvere il problema? In generale si, anche quando le cose si complicano ed è una singola azione a generare direzioni di uscita diverse.
La navigazione JSF
Caliamo quindi il grafico precedente nel mondo JSF dove ogni blocco è una pagina con il proprio backing bean. Immaginiamo per comodità che gli stati inizio e fine coincidano con il menù della nostra applicazione e che le frecce uscenti definiscano i percorsi possibili: se per ogni percorso uscente da una pagina esiste una azione diversa non ci sono problemi, ma se tali percorsi sono scaturiti dalla stessa azione del backing bean, la situazione si complica. Approfondiamo questo caso.
I blocchi interessati da questa particolarità sono A3 e B2: come facciamo a decidere dove andare visto che le frecce di uscita sono dirette verso due pagine distinte e separate quando l’azione che genera la navigazione è la stessa? Possiamo creare un outcome per ogni percorso (riempiendo il codice della action di if ... else
) oppure dovremmo riuscire a capire dinamicamente in che flusso ci troviamo per caricare l’outcome giusto.
Definire un flusso
Quando la navigazione si complica come nel caso che stiamo analizzando, definire un outcome in un backing bean per ogni possibile percorso a parità di azione diventa intrusivo e difficile da manutenere. L’ideale sarebbe quindi avere sempre un outcome fisso e definito nella action del backing bean capace di portare la navigazione verso pagine differenti in base ad un flusso funzionale, un workflow che stiamo seguendo.
Facciamo un esempio: dalla pagina B2 con la stessa azione (per esempio con outcome di nome outcomeB2
) è possibile andare in C2 oppure in B3. Seguendo la logica del workflow, l’idea è quella di riuscire a navigare correttamente verso C2 quando ci troviamo nel flusso C e verso B3 quando siamo nel flusso B.
Navigare tra flussi di lavoro
Come implementare questo concetto? Creiamo la seguente classe:
public class FlowManager implements Serializable {
private String flowId;
public String getFlowId() {
return flowId;
}
public void setFlowId(String flowId) {
this.flowId = flowId;
}
public void reset() {
this.flowId = null;
}
}
che verrà registrata come backing bean di sessione chiamata flowBB:
flowBB
it.cosenonjaviste.jsf.navigation.utils.FlowManager
session
e che ha lo scopo di memorizzare il nome del flusso in cui ci troviamo. Chi popola flowId
? In questo esempio può essere il menù stesso a taggare il flusso di lavoro che sta cominciando.
A questo punto rimane da capire come collegare il flusso con l’outcome delle action. Molto semplice: creiamo un wrapper del NavigationHandler
JSF come segue:
public class NavigationFlowHandler extends NavigationHandler {
private final NavigationHandler delegate;
public NavigationFlowHandler(NavigationHandler delegate) {
super();
this.delegate = delegate;
}
@Override
public void handleNavigation(FacesContext context, String fromAction, String outcome) {
FlowManager flowManager = (FlowManager) getBackingBeanByName(context, "flowBB");
if ("backToMenu".equalsIgnoreCase(outcome)) {
flowManager.reset();
}
else if (flowManager.getFlowId() != null) {
outcome += "@" + flowManager.getFlowId();
}
delegate.handleNavigation(context, fromAction, outcome);
}
private FlowManager getBackingBeanByName(FacesContext context, String bbName) {
ELResolver resolver = context.getApplication().getELResolver();
return (FlowManager) resolver.getValue(context.getELContext(), null, bbName);
}
}
che andremo poi a registrare nel faces-config.xml:
...
it.cosenonjaviste.jsf.navigation.utils.NavigationFlowHandler
...
Cosa fa esattamente questa classe? Innanzi tutto recupera il flow manager dal contesto JSF e poi controlla che non sia presente l’outcome backToMenu
che chiude un flusso e lo resetta (avendo registrato nelle regole di navigazione del faces-config che questo sia il valore che fa sempre tornare al menù). Successivamente avviene la magia: se siamo in un flusso (ovvero flowId impostato), l’outcome viene decorato con il flusso corrente in modo da generare un nuovo outcome che dovrà essere registrato nelle regole di navigazione. Riprendendo l’esempio precedente, se la action di B2 genera l’outcome outcomeB2
, la decorazione genererà due outcome distinti in base al flusso:
outcomeB2@flowB
outcomeB2@flowC
Non resta quindi che censire due regole di navigazione nel faces-config in modo opportuno.
Conclusioni
La navigazione JSF, grazie alla modularità del framework, riesce a semplificare notevolmente il problema proposto nel post. La soluzione trovata non vuole essere esaustiva, ma cerca solo di mostrare le capacità del framework stesso.
Alcune considerazioni finali. La navigazione tra A2 e A3 è sempre la stessa indipendentemente dal flusso. Con la decorazione della navigazione attiva siamo però costretti a censire 2 regole di navigazione:
- una che va da A2 a A3 con outcome
outcomeA2@flowA
; - un’altra che va sempre da A2 a A3 ma con outcome
outcomeA2@flowB
.
In questi casi si può evitare la ridondanza censendo la regola di navigazione tra A2 e A3 non in base all’outcome, ma in base alla action: indipendentemente dal flusso la navigazione sarà risolta correttamente a parità di action.
Infine, E’ possibile accedere programmaticamente alla navigazione JSF in caso di necessità: purtroppo il modo in cui si accede varia da implementazione a implementazione. Nel caso di Mojarra, il framework espone le regole di navigazione come mappa di view id e lista di navigation cases.
ApplicationAssociate applicationAssociate = ApplicationAssociate.getInstance(FacesContext.getCurrentInstance().getExternalContext());
Map> navRules = applicationAssociate.getNavigationCaseListMappings();
Per quanto riguarda le altre implementazioni JSF, basta guardare il sorgente della classe NavigationHandlerImpl
per capire come fare.
Nell’esempio proposto in questo post, può essere necessario entrare in merito alle regole di navigazione per rafforzare la condizione di reset del flusso (if ("backToMenu".equalsIgnoreCase(outcome))
) che, nella versione presentata attualmente, può essere debole. Prendiamo la pagina A3 del grafico iniziale per esempio: se la action che porta al menù nel flusso A e a B2 nel flusso B ha come outcome backToMenu
, la navigazione non funziona perché il flusso viene sempre resettato e il flusso B viene perso. Possiamo allora restringere il vincolo sul reset aggiungendo che non deve esistere una regola di navigazione che dalla viewId corrente ha outcome id flusso + @backToMenu
. Per questo l’accesso alle regole di navigazione programmaticamente è necessario.