Navigation Rules avec PrimeFaces... Mobile

Tiens, allez, c’est Noël après tout : encore un tutoriel technique de derrière les fagots (Noël – cheminée – fagots… tout le monde suit je pense).

Les règles de navigation de JSF 2 sont ma foi très pratiques pour découpler l’implémentation physique des vues de la logique “métier”. Vos méthodes d’action peuvent renvoyer un mot-clef, l’outcome, interprété par le NavigationHandler à partir de déclarations externalisées dans le fichier faces-config.xml standard. Exemple, pour rappel :

<!-- dans main.xhtml -->
<h:commandbutton value="Continuer" action="#{businessBean.doSomething}" />
/* dans BusinessBean.java */
public void doSomething() {
  try {
    ...
    return "success"; // outcome
  } catch (Exception) {
    return "error"; // outcome
  }
}
<!-- dans faces-config.xml -->
<navigation-rule>
  <from-view-id>/views/add.xhtml</from-view-id>
  <navigation-case>
    <from-outcome>success</from-outcome>
    <to-view-id>/views/next.xhtml</to-view-id>
  </navigation-case>
  <navigation-case>
    <from-outcome>error</from-outcome>
    <to-view-id>/views/errors.xhtml</to-view-id>
  </navigation-case>
</navigation-rule>

La logique ci-dessus indique, vous le voyez, que lorsque le résultat de l’action est “success” vous serez redirigé vers la vue “next”, tandis que vous seriez redirigé vers la vue “errors” si le résultat était “error”. Ainsi votre action est découplée à la fois de la réalité “physique” de l’implémentation des vues, et de la logique de navigation. En effet, il sera possible de modifier l’emplacement des vues visées et d’opter pour des redirections différentes selon que l’action sera appelée depuis un emplacement ou un autre (si doSomething était appelé depuis next.xhtml, alors to-view-id pourrait pointer vers end.xhtml pour l’outcome “success”, par exemple). Bref.

Si vous utilisez PrimeFaces Mobile, vous allez définir des vues multiples, internes aux vues JSF classiques. Sous forme de pages. Cela peut prendre la forme suivante :

<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<f:view xmlns:p="http://primefaces.org/ui" xmlns:pm="http://primefaces.org/mobile" contentType="text/html">
<pm:page title="Page Mobile">
<pm:view id="accueil">
<pm:header title="Accueil" />
<pm:content>
<h:outputText value="Page d'accueil" />
<p:dataTable value="#{bean.todos}" var="todo" id="table">
<p:column headerText="Nom">#{todo.nom}</p:column>
<p:column headerText="Description">#{todo.description}</p:column>
</p:dataTable>
<p:commandLink value="Ajout" action="#ajout" />
<p:commandLink value="Erreurs" action="#erreurs" />
</pm:content>
</pm:view>
<pm:view id="ajout">
<pm:header title="Ajout" />
<pm:content>
<h:form>
<h:outputLabel value="Nom" for="name" />
<p:inputText value="#{todoForm.name}" id="name" required="true"
requiredMessage="Il faut un nom !" />
<h:message for="name" />
<br />
<h:outputLabel value="Description" for="description" />
<p:inputTextarea value="#{todoForm.description}" id="description" />
<h:message for="description" />
<br />
<p:commandButton value="Ajouter"
action="#{bean.addTodo}" update=":table" />
</h:form>
<p:commandButton value="Accueil" action="pm:accueil?reverse=true" />
</pm:content>
</pm:view>
<pm:view id="erreurs">
<pm:header title="Ajout" />
<pm:content>
RIEN NE VA PLUS, ICI !
<p:commandButton value="Accueil" action="pm:accueil?reverse=true" />
</pm:content>
</pm:view>
</pm:page>
</f:view>

Vous constatez que PrimeFaces Mobile emploie une syntaxe particulière d’outcome, avec un préfixe “#” (commandLink) ou “pm:” (commandButton). Pour que la boucle soit bouclée, les méthodes d’action doivent retourner des outcomes préfixés par “pm:”, et le tour sera joué. Tout ceci grâce à la classe MobileNavigationHandler introduite par l’API qui fait comme ceci :

public void handleNavigation(FacesContext context, String fromAction, String outcome) {
  if (outcome != null && outcome.startsWith("pm:")) {
    String command = MobileUtils.buildNavigation(outcome); // montage d'une expression jQuery
    RequestContext requestContext = RequestContext.getCurrentInstance();
    if (requestContext != null) {
      requestContext.execute(command.toString()); // exécution
    }
  } else {
    base.handleNavigation(context, fromAction, outcome);
  }
}

Malheureusement, et c’est là que je voulais en venir, les règles de navigation ne sont pas prises en compte par cette API. Vous voyez ci-dessus que seul l’outcome est considéré, et que lorsque le préfixe est détecté alors les règles de navigation sont purement et simplement ignorées. C’est ben plate, comme on dit à Montréal. Concrètement, il n’est pas possible d’écrire ceci nativement :

<navigation-rule>
  <from-view-id>/mobile/main.xhtml</from-view-id>
  <navigation-case>
    <from-outcome>success</from-outcome>
    <to-view-id>#accueil</to-view-id>
  </navigation-case>
</navigation-rule>

Personnellement, je trouve ça navrant. Parce que cela nous force potentiellement à surcharger ou (pire) à recoder des beans, juste pour renvoyer les bons outcomes. C’est franchement la loose quand on veut avoir une couche vue web ET une couche vue mobile, sans toucher à la couche des managed beans (ce qui est la moindre des choses à mon avis). Mais au lieu de râler et de me lamenter, j’ai cherché une solution de contournement. Et ben j’ai trouvé (c’est Noël après tout, non ?). Voilà, c’est cadeau :

/**
 * Gestionnaire de navigation pour autoriser les Navigation Rules de type "PrimeFaces Mobile"
 */
public class MobileNavigationRulesHandler extends ConfigurableNavigationHandler {
  ...
  /**
   * Interception des règles de navigation où la destination (to view id) commence
   * par "#" ou "pm:" ; traitement jQuery comme pour les outcomes PrimeFaces Mobile
  */
@Override
public void handleNavigation(FacesContext context, String fromAction, String outcome) {
  NavigationCase navCase = base.getNavigationCase(context, fromAction, outcome);
  if (navCase != null) {
    String toViewId = navCase.getToViewId(context);
    // attention, un "/" est ajouté automatiquement
    if (toViewId != null && (toViewId.startsWith("/#") || toViewId.startsWith("/pm:"))) {
      String command = MobileUtils.buildNavigation(toViewId.substring(1));
      RequestContext requestContext = RequestContext.getCurrentInstance();
      requestContext.execute(command.toString());
      return;
    }
  }
  base.handleNavigation(context, fromAction, outcome);
}
...
<navigation-rule>
  <from-view-id>/mobile/main.xhtml</from-view-id>
  <navigation-case>
    <from-outcome>success</from-outcome>
    <to-view-id>#accueil</to-view-id>
  </navigation-case>
</navigation-rule>
<navigation-rule>
  <from-view-id>/mobile/main.xhtml</from-view-id>
  <navigation-case>
    <from-outcome>error</from-outcome>
    <to-view-id>pm:erreurs</to-view-id>
  </navigation-case>
</navigation-rule>

Vous remarquerez que nous n’étendons pas la classe MobileNavigationHandler, ce serait contraire au pattern Délégation ici en oeuvre. Cependant, nous avons une dépendance à MobileUtils. Après tout, “it’s all about PrimeFaces Mobile” ; cette classe n’a pas d’intérêt sans lui, c’est donc justifié.
Il ne reste plus qu’à enregistrer notre gestionnaire de navigation pour l’application dans faces-config.xml, nos méthodes d’action pourront continuer à renvoyer leurs outcomes agnostiques, ce sont les règles de navigation qui continueront à décider sereinement de la marche à suivre. Ce sera plus tant mieux !

Et Bonne Année à tous !