JavaServer Faces - techniques avancées

Chapitres traités   

Cette étude fait suite à l'étude précédente qui nous a permis de prendre connaissance de JSF. Cette connaissance est un pré-requis indispensable aux sujets que nous allons aborder tout au long de cette nouvelle étude. Il est évident que les notions que nous venons d'apprendre sont insuffisantes pour développer une application de gestion complète. Effectivement, nous devons appréhender un certains nombre de critères qui vont nous permettre d'aboutir à une démarche de qualité. Nous devons aborder les points suivants :

  1. Nous devons pouvoir créer des modèles afin que l'ensemble des pages de votre site partagent toutes le même aspect, un en-tête, un pied de page, un menu, etc.
  2. Toujours dans la même idée, il est fréquent d'avoir des parties de page dont l'aspect est également très similaire. Il serait alors intéressant de proposer une bibliothèque de nouvelles balises qui factorisent cet aspect commun.
  3. Lors de la saisie de données par l'utilisateur, il est souvent indispensable de valider et ou de convertir les valeurs saisies avant de les injecter dans un bean géré. Des messages adaptés peuvent alors survenir si les saisies sont incorrectes.
  4. Dans certaines situations, il est souhaitable de prendre en compte une gestion fine des événements afin que l'application Web soit suffisamment réactive au comportement de l'utilisateur.
  5. Une application web doit fournir une interface riche et rapide. Cette réactivité peut être obtenue en ne modifiant que de petites parties de la page de façon asynchrone, sans que la page ne soit rechargée entièrement. Nous verrons comment résoudre cette problématique au travers d'Ajax.

Avons d'aborder tous ces sujets intéressants, je vous propose de bien revoir tout le cycle de traitement des requêtes JSF (sujet que nous avons déjà traité dans l'étude précédente).

 

Choix du chapitre Cycle de traitement des requêtes JSF

Toutes les requêtes JSF suivent le même cycle de traitement, constitué de six étapes distinctes. Les lignes pleines décrivent le cycle normal et les lignes en pointillé rouge le traitement des erreurs.

  1. Création de l'arborescence des composants : À la réception de la requête, la hiérarchie des composants (arbre de vue) de la page demandée est créée.
  2. Récupération des valeurs de la requête : Les valeurs de la requête sont récupérées et stockées dans les composants de l'arbre de vue. La servlet contrôleur FacesServlet, non représentée ici, parcourt l'arbre de vue et appelle la méthode decode() de chacun de ses composants. Ensuite, les événements et les validators sont générés et stockés dans le contexte JSF.
  3. Traitement des validations : La méthode validate() est appelée pour tous les validators stockés dans l'arbre de vue créé à l'étape précédente.
  4. Modification des valeurs du modèle objet : Les valeurs contenues dans les composants sont ensuite recopiées dans les objets métiers (JavaBeans) éventuellement associés.
  5. Appel de l'événement application : L'événement de type <f:form> ou <h:command...> correspondant à la demande d'une nouvelle page est traitée.
  6. Rendu de la réponse : La hiérarchie de composants s'occupe de créer toutes les balises standard nécessaires pour représenter la réponse au format désiré.

Phase 1 - Création de l'arborescence des composants côté serveur (arbre de vue)

Une page HTML est un fichier texte qui contient un assemblage de balises spécialisées pour le formattage de l'information. Par contre JSF utilise, côté serveur, un arbre d'objets pour représenter la vue équivalente de cette page Web. Il s'agit là d'une structure bien différente d'un fichier texte. Cet arbre d'objets est un mirroir (un représentant) de l'interface visuelle du client.

Cette phase va donc consister à reconstituer l'arbre de composants qui correspond à la vue HTML de l'utilisateur qui soumet la requête. Cette arbre se nomme : arbre de vue.

Lorsque le client soumet la requête pour la première fois, JSF doit créer la vue correspondante au travers de cet arbre d'objets. A chaque balise de type <h:...> correspond un objet Interface Utilisateur équivalent qui va donc être placé sur l'arbre à une branche qui correspond à l'imbrication des balises proposées sur la page Web. Chaque objet est donc stocké dans cet arbre à l'endroit convenable.

La racine de cet arbre de vue est systématiquement une instance de la classe UIViewRoot qui correspond en réalité à la balise <f:view>. Nous remarquons au passage que le placement de cette balise dans une page JSP devient prépondérante afin de permettre l'élaboration de cet arbre de vue.

Cet arbre d'objets, qui est une image de la vue côté client, se créer automatiquement. Je veux dire que c'est le serveur qui s'en occupe sans aucune intervention de notre part.

Indépendamment de cet arbre, ces objets sont bien entendus issus d'un ensemble de classes qui respecte la hiérarchie suivante :

Toutes les classes ne sont pas représentées. Nous pouvons, à l'intérieur des Javabeans, travailler directement avec les classes correspondantes aux balises présentes sur la page Web, celles qui sont représentés en bleu. Toutefois, il est possible de travailler plutôt avec une des classes parentes, représentée en jaune. En effet, ces classes disposent déjà de certaines propriétés correspondantes aux attributs proposés par les balises équivalentes. Ainsi, par exemple, à partir de la balise suivante :

<h:outputText value="Un texte"/>

Vous pouvez créer un objet de type UIOutput qui dispose de la propriété équivalente à cet attribut value. Ainsi, vous pouvez solliciter les méthodes getValue() et setValue() qui travaillent donc en tâche de fond sur l'attribut value (de type Object) de la classe UIOutput.

Que se passe-t-il lorsque le client propose la requête la première fois, ou lorsqu'il la propose de nouveau au serveur ?

  1. Si la vue est demandée pour la première fois, le serveur d'application crée une instance de UIViewRoot et lui associe un nom, souvent le même nom que la page JSP. JSF mémorise alors l'arbre de vue.
  2. Si la vue existe déjà pour JSF, alors l'arbre de composants correspondant devient la vue courante. Dans ce cas là, la méthode restoreState() de la classe ancêtre UIComponent est appelée récursivement de façon polymorphique sur chacun des composants constituant l'arbre de vue. Ainsi, elle rétablit l'état du composant à partir de l'état sauvegardé durant la dernière phase appelée Rendu de la réponse (voir plus loin).

Lorsque JSF a déterminé la vue courante, celle-ci est accessible via la classe FacesContext.
.

Phase 2 - Récupération des valeurs de la requête

Lorsque la phase précédente est terminée, nous disposons d'un arbre de composants dont la racine est UIViewRoot. La requête HTTP soumise par le navigateur est porteuse des actions de l'utilisateur sur la vue. Ceci implique qu'il faut synchroniser la vue côté JSF avec la vue côté client. En effet, si l'utilisateur modifie une valeur dans un champs de formulaire, il faut que le composant graphique correspondant côté serveur reflète ce changement d'état.

Le but de cette phase est donc de récpercuter les actions de l'utilisateur sur l'arbre de composants courant (arbre de vue courant). Il faut décoder les valeurs contenues dans l'objet HttpServletRequest (nous connaissons particulièrement bien cet objet depuis que nous manipulons les servlets) et récupérer les changement d'états de la vue sur les composants concernés.

A cet effet, la classe UIComponent qui est la classe ancêtre de tout composant JSF dispose de la méthode processDecodes(). Cette méthode est appelée récursivement sur chaque composant de l'arbre. Par polymorphisme, les composants ont la responsabilité de décoder les informations qui les concernent.

Ainsi, chaque information envoyée lors de la requête est associée à l'objet correspondant dans l'arbre de vue. Plus précisément, les attributs de cet objet sont remis à jour par les nouvelles valeurs envoyées. Ainsi, si nous avons par exemple côté client cette ligne là :

<h:inputText value="5"/>

vous retrouver alors cette valeur 5 sur l'objet HtmlInputText correspondant qui est mis à jour par la propriété équivalente value, et ceci par l'intermédiaire de la méthode setValue().

Phase 3 - Traitement des validations

Certaines balises JSF sont parfois liés à des JavaBeans au moyen des expressions EL JSF. Les JavaBeans représentent la logique métier. Il est alors impératif de valider et ou de convertir les valeurs qui proviennent du client avant de les injecter dans le JavaBean correspondant. En effet, par exemple, si l'utilisateur saisie la chaîne suivante "345red" en lieu et place d'un nombre entier (valeur attendue), alors cela provoquera une erreur.

Vous allez le découvrir dans les chapitres qui suivent, JSF offre les concepts de validation (Validator) et de conversion (Converter) pour effectuer les traitements sur les valeurs soumises par l'utilisateur avant leurs injections dans les JavaBeans.

Un composant peut donc éventuellement posséder des Validator et un Converter qui sont invoqués lors de l'appel de la méthode processValidators() de la classe ancêtre UIComponent. Cette méthode est appelée récursivement sur l'arbre de vue. JSF procède d'abord à la conversion de la valeur extraite de la requête HTTP (String) puis valide cette valeur en utilisant les Validator.

Si une erreur survient durant cette phase, à cause d'un problème de validation, alors JSF arrête le processus normal et saute directement vers la dernière phase qui correspond au rendu de la réponse.

Phase 4 - Modification (Mise à jour) des valeurs du modèle objet

Lorsque la requête arrive à cette phase de traitement, les objets sont dans un état qui correspond à la vue du client. Les informations de la requête HTTP qui sont destinées à mettre à jour les données métiers ont été validées. La méthode processUpdates() de UIComponent est appelée récursivement sur l'arbre des composants. Sa responsabilité consiste à mettre à jour, cette fois-ci, les Javabeans correspondant à la logique métier.

Egalement durant cette phase, si une erreur survient, alors JSF arrête le processus normal et saute directement à la phase du rendu de la réponse.
.

Vous allez le découvrir dans les chapitres qui suivent, JSF offre un modèle événementiel qui permet de détecter les changements du modèle. A ce niveau, l'appel de la méthode processUpdates() peut donc éventuellement poster des événements.

Phase 5 - Appel de l'événement application

Lorsque cette phase est atteinte, le modèle métier est mis à jour, des événements sont en attentes dans la queue des événements. Ces événements sont de deux natures. Soit ils sont issus, de façon classique de l'attribut action des balises <h:commandButton> ou <h:commandLink>. Soit ils sont plutôt issus de l'attribut actionListener qui met donc en oeuvre un système d'écouteur supplémentaire (ActionListener). -- peut-être à revoir --

La méthode processApplication() de UIComponent est appelée récursivement sur l'arbre de vue. Sa responsabilité consiste à diffuser (Broadcast) certains événements de la queue vers les écouteurs d'événements associés (listeners).

Phase 6 - Rendu de la réponse

C'est la dernière phase du traitement d'une requête JSF. Sa première responsabilité consiste à encoder l'arbre de vue courant dans un langage compréhensible par le client, ici le HTML. JSF peut encoder le HTML, le WML, le XML, etc. Ici donc, nous partons de l'arbre de vue et chaque objet s'occupe de créer la balise correspondante dans le format standard requis.

Pour ce faire, le UIComponent dispose d'un jeu de méthodes dont la signature commence par encodeXXX(). ces méthodes sont particulièrement adaptées à l'encodage dans des langages à balises. Ces méthodes sont au nombre de trois :

  1. encodeBegin(),
  2. encodeChildren(),
  3. encodeEnd().

Ces méthodes correspondent grossièrement aux étapes d'encodage d'une balise : balise ouvrante, balises filles, balise fermante (sujet traité ultérieurement).
.

Sa deuxième responsabilité consiste à sauvegarder l'état des objets de l'arbre de vue. Pour ce faire une deuxième méthode saveState() est également appelée récursivement. Cette méthode répond à la méthode restoreState() que nous avons découvert durant la première phase Création de l'arbre de vue. Ce comportement est donné par l'interface StateHolder implémentée par UIComponent.

 

Choix du chapitre Les modèles de page

Une application web typique contient plusieurs pages partageant toutes le même aspect, un en-tête, un pied de page, un menu, etc. Facelets permet de définir une disposition de page dans un fichier modèle qui pourra être utilisé par toutes les autres pages.

Ce fichier définit les zones (à l'aide du marqueur <ui:inset>) dont le contenu sera remplacé grâce aux marqueurs <ui:component>, <ui:composition>, <ui:fragment> ou <ui:decorate> des pages clientes.

Balises Description
<ui:include> Intègre un contenu supplémentaire issu d'un autre document.
<ui:composition> Définit une composition dans la page réelle utilisant un modèle de page. Le même modèle peut être utilisé par plusieurs compositions. Un modèle de page est une page JSF normale qui comporte toute l'ossature figée que nous retrouverons systématiquement dans l'ensemble des pages, et des parties variables (spécifiées par la balise <ui:insert>) dont le contenu sera spécifique à chacune des pages web et qui pourront être donc complétées au moyen de cette balise <ui:composition>.
<ui:decorate> Permet de décorer le contenu d'une page. Nous utilisons cette balise à la place de la précédente lorsque le site est relativement modeste.
<ui:define> Définit un contenu qui sera inséré à l'endroit spécifique de la page, désigné par la balise <ui:insert> du modèle.
<ui:insert>

Spécifie un point d'insertion dans le modèle dans lequel nous pourrons éventuellement par la suite insérer un contenu au moyen de la balise <ui:define> de la page réelle qui se sert de ce modèle.

<ui:param> Spécifie un paramètre qui est passé dans un fichier inclu ou à un modèle de page.
<ui:component> Ce marqueur est similaire à <ui:composition>, mis à part qu'il crée un nouveau composant qui sera rajouté à l'arbre des composants de la page en cours.
<ui:fragment> Ajoute un fragment à une page. Ce marqueur est similaire à <ui:decorate>, mis à part qu'il crée un composant qui est rajouté à l'arbre des composants de la page en cours.
<ui:debug> Cette balise permet à l'utilisateur d'afficher automatiquement un nouvel onglet dans le navigateur, à l'aide d'un raccourci clavier, qui nous montre à la fois la hiérarchie des composants constituant la page en cours avec également l'ensemble des variables actuellement accessibles dans cette application web.
<ui:remove> Lors de phase de test, vous pouvez auculter momentanément une partie de la page web (ce qui evite les mises en commentaires). Tout ce qui sera introduit à l'intérieur de ce marqueur ne sera pas interprété par JSF.
<ui:repeat> Itération à partir d'une liste, d'un tableau, d'un ensemble de valeurs, ou même d'un simple objet.

Mise en oeuvre d'un modèle au travers du projet de Jeu aléatoire

Afin d'illustrer les modèles de page avec l'ensemble des balises qui nous concerne, je vous propose de reprendre l'application web sur le jeu de tirage de nombre aléatoite que nous avons mis en oeuvre lors de l'étude précédente dans laquelle nous allons rajouter un modèle de page.

Le projet est constitué d'un ensemble d'éléments que nous allons découvrir ensemble :

Le bean géré Nombre

Le bean géré nombre reste identique à la version précédente. Je rappelle que ce bean est le modèle qui réalise tous les traitements nécessaires en coulisse.

bean.Nombre.java
package bean;

import javax.inject.*;
import javax.enterprise.context.*;
import java.io.Serializable;

@Named
@SessionScoped
public class Nombre implements Serializable {
   private int valeurMax = 20;
   private int tentativeMax = 4;
   private int valeur;
   private int alea;
   private int tentative;
   private boolean test;

   public int getTentativeMax() { return tentativeMax; }
   public void setTentativeMax(int tentativeMax) { this.tentativeMax = tentativeMax; }

   public int getValeurMax() { return valeurMax; }
   public void setValeurMax(int valeurMax) { this.valeurMax = valeurMax; }

   public int getValeur() { return valeur;  }
   public void setValeur(int valeur) { this.valeur = valeur; }
   public int getTentative() { return tentative; }

   public String recommencer() {
     alea = (int)(Math.random()*valeurMax)+1;
     tentative = 0;
     valeur = 0;
     test = false;
     return "alea.xhtml";
   }
   
   public void calcul() {
     if (tentative<tentativeMax) {         
       tentative++;
       test = alea == valeur;
     }      
   }

   public String getRésultat() {
     if (test)  return "Bravo, vous avez trouvé !"; 
     if (tentative==tentativeMax) return "Désolé, vous avez perdu. La valeur à rechercher est ";
     if (tentative==0) return "Tentez votre chance";     
     else return "Non, ce n'est pas le bon chiffre, refaites un essai.";
   }

   public String getProgression() {
     if (tentative==0 || test) return "";
     return  "Le nombre est plus "+(alea>valeur ? "grand" : "petit");
   }

   public int getAlea() { return alea; }
   public boolean isFin() { return test || tentative == tentativeMax; }
}
Feuille de style CSS

Nous conservons également la feuille de style qui, je le rappelle se situe directement dans le répertoire de ressources (nommé resources), sans prise en compte de nom de bibliothèque :

web/resources/principal.css
root { 
    display: block;
}

body {
    background-color: orange; 
    font-weight: bold; 
    color: maroon; 
}

.saisie {
    text-align:right; 
    background-color:yellow; 
    font-weight:bold; 
    color:green; 
    width: 30px; 
    display: block;
}

.normal {
    font-weight:bold; 
}

h2, h3 {
    background-color: yellow; 
    padding: 5px; 
    border: groove; 
}

.progression {
    color: blue; 
}

.resultat {
    border: groove; 
    padding-right: 3px; 
}
Constition du modèle

Cette application web est constituée de deux pages web (deux vues). Je vous propose donc de réaliser un modèle commun à ces deux pages que je vais également placé dans le répertoire des ressources (ce n'est pas une obligation) :

web/resources/modèle.xhtml
<?xml version="1.0" encoding="UTF-8"?>

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html" 
      xmlns:ui="http://java.sun.com/jsf/facelets">

    <h:head>
       <title>Recherche d'un nombre aléatoire</title>
    </h:head>

    <h:outputStylesheet name="principal.css" />
    
    <h:body>
       <h:form>
          <ui:include src="/resources/limites.xhtml" />
          <hr />
          <ui:insert name="bouton" />
       </h:form>
       <ui:insert name="contenu"><hr /></ui:insert>
       <ui:debug />
    </h:body>
</html>
  1. Comme vous pouvez le constater, un modèle de page est une page comme une autre. Nous plaçons l'ensemble des balises communes à toutes les pages du site. Toute sa structure et son ossature demeurent figées (inchangées).
  2. Par contre, vous devez définir la bibliothèque de marqueurs nécessaires à savoir ici la bibliothèque facelets : xmlns:ui="http://java.sun.com/jsf/facelets".
  3. Une fois que vous avez développez la partie commune à toute les pages, vous devez définir les parties variables qui seront autorisées à être introduite dans le modèle en spécifiant un nom particulier. Pour cela, vous le précisez au moyen des balises <ui:insert>. Ici, par exemple, nous découvrons deux insertions possibles dans notre modèle : bouton et contenu.
  4. Il s'agit d'insertions possibles, ce qui veut dire que vous n'êtes pas obligé de vous en servir. Vous pouvez proposer un contenu par défaut dans le cas où une insertion n'est pas pris en compte, comme nous le proposons avec contenu. Ici, c'est la balise <hr /> qui est proposée à la place de l'insertion.
  5. Au dela de la notion de modèle, vous pouvez rajouter une page web dans une autre au moyen de la balise <ui:include>. Dans notre exemple ici, et c'est uniquement un cas d'école non justifié, nous intégrons dans notre modèle, la page limites.xhtml qui s'occupe de la gestion des valeurs maximales.
    web/resources/limites.xhtml
    <?xml version="1.0" encoding="UTF-8"?>
    
    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:h="http://java.sun.com/jsf/html">
    
          <h:panelGrid columns="2">
              <h:outputText value="Recherche d'un nombre aléatoire compris entre 1 et  " styleClass="normal" />
              <h:inputText value="#{nombre.valeurMax}" styleClass="saisie"/>
              <h:outputText value="Nombre de tentative possibles " styleClass="normal"/>
              <h:inputText value="#{nombre.tentativeMax}" styleClass="saisie"/>
          </h:panelGrid>
    </html>
  6. Pour finir, vous remarquez la présence de la balise <ui:debug /> qui n'est pas utile dans le fonctionnement normal de votre application web, mais qui peut s'avérer intéressante, en phase de développement, lorsque vous souhaitez connaître l'arborescence des composants constituant la vue en cours ainsi que l'ensemble des variables utilisées dans la session. Ce mode débug est activé par l'utilisateur en utilisant un raccourci clavier (par défaut CTRL+SHIFT+d). Une nouvelle fenêtre du navigateur apparaît, et vous pouvez dès lors consulter la rubrique qui vous intéresse.

    Il est possible de changer le raccourci clavier au moment de la définition de cette balise au moyen de l'attribut hotkey. Par exemple, si vous désirez activer la fenêtre de debug avec le raccourci CTRL+SHIFT+i, voici ce que vous devez écrire : <ui:debug hotkey="i" />.

Notre modèle est finalement pourvu d'un certain nombre d'informations. Nous disposons de toute l'ossature de la page web par défaut avec en plus, systématiquement pour toutes les pages, l'affichage des valeurs maximales choisies choisi par l'utilisateur.

Les vues configurer.xhtml et alea.xhtml

Dans ce projet, je rappelle que nous disposons de deux vues qui vont exploiter chacune le modèle de page que nous venons de mettre en oeuvre :

  1. La première configurer.xhtml qui permet, comme son nom l'indique, de faire les réglages nécessaires sur les valeurs maximales savoir, la valeur maximale du nombre à recherche et le nombre de tentative maximum.

  2. La deuxième alea.xhtml qui s'occupe de jeu proprement dit :

configurer.xhtml
<?xml version="1.0" encoding="UTF-8"?>

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html" 
      xmlns:ui="http://java.sun.com/jsf/facelets">

    <ui:composition template="resources/modèle.xhtml">
        <ui:define name="bouton">
          <h:commandButton value="Commencer le jeu" action="#{nombre.recommencer}" />
        </ui:define>
    </ui:composition>
</html>
  1. Afin d'utiliser le modèle que nous venons de mettre en oeuvre, les pages web doivent le déclarer au moyen da la balise prévue à cet effet : <ui:composition template="resources/modèle.xhtml">.
  2. Puis, le principe consiste à lier les attributs définis par les marqueurs <ui:define> de la page web à ceux des marqueurs <ui:insert> du modèle. Ainsi, dans cette page de configuration, le marqueur <ui:define name="bouton"> est associé au marqueur correspondant dans le modèle <ui:insert name="bouton" />.
  3. Lorsque le modèle est chargé, chaque balise <ui:insert> est ainsi remplacée par le contenu proposée dans les balises <ui:define> correspondantes. Dans notre exemple, juste le bouton de validation de la configuration est rajouté à notre modèle. C'est d'ailleurs uniquement cette page qui permet de configurer le jeu. Ce bouton n'apparaîtra pas dans la page du jeu lui-même.
  4. Nous remarquons que cette page est très réduite par rapport à la version originale, puisque tout est pratiquement fait dans le modèle lui-même.

    Vu que dans cette page nous n'utilisons pas l'insertion <ui:insert name="contenu">, une balise <hr /> est bien rajoutée automatiquement.

alea.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

    <ui:composition template="resources/modèle.xhtml">
        <ui:define name="contenu">
           <h:form>
              <h:panelGrid columns="3">
                 <h:outputText value="Votre nombre : " />
                 <h:inputText value="#{nombre.valeur}" styleClass="saisie" />
                 <h:outputText value="#{nombre.progression}" styleClass="progression" />
                 <h:outputFormat value="Tentative{0, choice, 0#|2#s} : ">
                    <f:param value="#{nombre.tentative}" />
                 </h:outputFormat>
                 <h:inputText value="#{nombre.tentative}" styleClass="saisie" disabled="true" />
              </h:panelGrid>
              <hr />
              <h:commandButton value="Valider votre nombre" action="#{nombre.calcul}" disabled="#{nombre.fin}" />
              &nbsp;
              <h:commandButton value="Recommencer" action="#{nombre.recommencer}" />
              &nbsp;
              <h:commandButton value="Changer les valeurs limites" action="configurer.xhtml" />
              <hr />
              <h2>
                <h:outputText value="#{nombre.résultat}" />
                <h:outputText value="#{nombre.alea}" styleClass="saisie" rendered="#{nombre.fin}" />
              </h2>
           </h:form>
       </ui:define>
    </ui:composition>
</html>
  
  1. Cette fois-ci, à l'intérieur de notre modèle, c'est la zone de contenu que nous rajoutons.
  2. Par rapport à la version originale, nous voyons dans notre page web les valeurs maximales du jeu, sans toutefois pouvoir les changer directement, puisque nous n'avons plus de bouton de soumission pour valider le changement. Nous sommes donc obligé de revenir dans la page de configuration si vous désirez changer les valeurs limites.

Les modèles paramétrables

Dans la partie fixe de votre modèle, il est possible d'assouplir cette partie normalement figée et de prévoir ainsi un petit réglage annexe qui peut s'avérer souvent très utile. Nous rendons ainsi notre modèle paramétrable.

Pour cela, nous utilisons la balise <ui:param> qui nous permet d'être en relation avec un paramètre défini dans le modèle au travers d'une expression EL.

Par exemple dans le projet précédent, j'aimerais que les valeurs maximales présentes dans la page web du jeu soit dans ce cas là en lecture seule pour que l'opérateur ne soit pas autorisé à modifier ces valeurs.
web/resources/limites.xhtml
<?xml version="1.0" encoding="UTF-8"?>

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">

      <h:panelGrid columns="2">
          <h:outputText value="Recherche d'un nombre aléatoire compris entre 1 et  " styleClass="normal" />
          <h:inputText value="#{nombre.valeurMax}" styleClass="saisie" readonly="#{lectureSeule}"/>
          <h:outputText value="Nombre de tentative possibles " styleClass="normal"/>
          <h:inputText value="#{nombre.tentativeMax}" styleClass="saisie" readonly="#{lectureSeule}"/>
      </h:panelGrid>
</html>

Pour cela, je rajoute dans la page qui représente les valeurs limites, l'attribut readonly dans les balises <h:input> et au moyen d'une expression EL. Je précise le nom du paramètre que je désire prendre en compte dans mon modèle, ici lectureSeule.

alea.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

    <ui:composition template="resources/modèle.xhtml">
        <ui:param name="lectureSeule" value="true">  <--- définition de la valeur à ajouter au paramètre lectureSeule
        <ui:define name="contenu"><h:form>
              <h:panelGrid columns="3">
                 <h:outputText value="Votre nombre : " />
                 <h:inputText value="#{nombre.valeur}" styleClass="saisie" />
                 <h:outputText value="#{nombre.progression}" styleClass="progression" />
                 <h:outputFormat value="Tentative{0, choice, 0#|2#s} : ">
                    <f:param value="#{nombre.tentative}" />
                 </h:outputFormat>
                 <h:inputText value="#{nombre.tentative}" styleClass="saisie" disabled="true" />
              </h:panelGrid>
              <hr />
              <h:commandButton value="Valider votre nombre" action="#{nombre.calcul}" disabled="#{nombre.fin}" />
              &nbsp;
              <h:commandButton value="Recommencer" action="#{nombre.recommencer}" />
              &nbsp;
              <h:commandButton value="Changer les valeurs limites" action="configurer.xhtml" />
              <hr />
              <h2>
                <h:outputText value="#{nombre.résultat}" />
                <h:outputText value="#{nombre.alea}" styleClass="saisie" rendered="#{nombre.fin}" />
              </h2>
           </h:form>
       </ui:define>
    </ui:composition>
</html>

Dans la page web concernée, j'utilise maintenant la balise <ui:param name="lectureSeule" value="true"> afin de spécifier la valeur que je désire pour le paramètre en question. Et le tour est joué.

Phase de développement

Quelquefois, lors de l'élaboration d'un projet, pour découvrir l'endroit qui cause un disfonctionnement, vous pouvez placer des commentaires sur une partie de code à l'aide de l'écriture suivante :

<!-- votre code -->

Le problème, c'est que si vous avez des expressions EL dans votre code, JSF les évaluent quand même, malgré les commentaires. Si, dans ces expressions, vous faites appel à des méthodes du bean géré, ces dernières risquent de lancer une exception voyant qu'aucune valeur normale n'est proposée.

Pour résoudre cette difficultés, vous pouvez utiliser la balise <ui:remove> et entourer ainsi la partie de code que vous ne désirez pas prendre en compte momentanément pour la visualisation de la page.

alea.xhtml
...
    <ui:composition template="resources/modèle.xhtml">
        <ui:remove>
           <ui:param name="lectureSeule" value="true">  <--- définition de la valeur à ajouter au paramètre lectureSeule 
        </ui:remove>
        <ui:define name="contenu"><h:form>
...
       </ui:define>
    </ui:composition>
</html>

Par exemple, dans ce code, j'empêche la mise en place de la lecture seule sur les zones de saisie des valeurs limites afin de bien visualiser ce fonctionnement là.

 

Choix du chapitre La conversion et la validation des données

La question qui se pose souvent est : Que se passe-t-il si l'opérateur fait une mauvaise saisie ? La première démarche consisterait à prendre en compte ce problème au niveau du modèle métier, c'est-à-dire à l'intérieur du bean géré qui traite l'information. Dans ce cas de figure, le code du bean géré serait considérablement alourdi. Par ailleurs, nous retrouverions du traitement qui ne correspondrait plus à la logique métier. Cette démarche n'est pas souhaitable. Il est préférable de séparer les problèmes.

JSF fournit un mécanisme standard de conversion et de validation permettant de traiter les saisies de utilisateurs afin d'assurer l'intégrité des données. Lorsque vous invoquez des méthodes métiers, vous pouvez donc vous fier à des données valides : la conversion et la validation permettent aux développeur de ce concentrer sur la logique métier au lieu de passer du temps à vérifier que les données saisies ne sont pas null, qu'elles appartiennent bien à un intervalle précis, etc.

Il est vraiment très important que les valeurs saisies soient d'abord adaptées au traitement souhaité, correspondant ainsi au format prédéfini et que les valeurs proposées correspondent également à la fourchette des valeurs requises.

La conversion a lieu lorsque les données saisies par l'utilisateur doivent être transformées de String en un objet et vice versa (le protocole HTTP ne manipule que les chaînes de caractères). Elle garantit que les informations sont du bon type - en convertissant, par exemple, un String en java.util.Date, un String en Integer ou des dollars en euros. Comme pour la validation, elle garantit que les données contiennent ce qui est attendu (une date au format jj/mm/aaaa, un réel compris entre 3.14 et 3.15, etc.).

Retour sur le cycle de traitement des requêtes JSF

Je fais juste un petit rappel sur le cycle de traitement des requêtes JSF afin de voir où se situe les phases de conversions et de validations.

  1. Au cours de la phase Application des valeurs de la requête, la valeur du composant de l'interface est convertie dans l'objet cible puis validée au cours de la phase de traitement des Validations.
  2. Il est logique que la conversion et la validation interviennent avant que les données du composant ne soient liées au bean géré qui réalise les traitements métiers en coulisse (qui a lieu au cours de la phase Modification des valeurs du modèle).
  3. En cas d'erreur, des messages d'alertes seront ajoutés et le cycle de vie sera écourté afin de passer directement à l'affichage de la Réponse (les messages seront alors affichés sur l'interface utilisateur avec <h:messages>).
  4. Au cours de cette dernière phase, les propriétés du bean géré seront reconverties en chaînes de caractères afin d'être affichées.

JSF fournit un ensemble de convertisseurs et de validateurs standard et vous permet également de créer les vôtres très facilement.
.

Les conversions standards

Lorsqu'un formulaire est affiché par un navigateur, l'utilisateur remplit les champs et appuie sur un bouton de soumission, ayant pour effet de transporter les données vers le serveur dans une requête HTTP formée de chaînes. Avant de mettre à jour le modèle du bean géré, ces données textuelles doivent être converties dans les objets cibles (Float, Integer, BigDecimal, etc.). L'opération inverse aura lieu lorsque les données seront renvoyées au client dans la réponse pour être affichées par le navigateur.

Il faut se souvenir que les valeurs qui sont soumises aux travers des requêtes ne sont, en réalité, que des chaînes de caractères puisque le proptocole HTTP ne connaît que ce type de données. Par contre, le modèle métier a besoin de travailler avec des données d'autres types. Par ailleurs, la représentation des valeurs sur la page Web doit être généralement affinée afin que l'utilisateur voit bien de quoi il s'agit. Il faut que l'interface utilisateur soit la plus ergonomique possible en proposant une information adaptée et bien présentée. Pour toutes ces raisons, nous avons besoin de convertir les données en pensant bien qu'il s'agit en réalité bien souvent d'un formatage sous forme de texte côté utilisateur.

JSF fournit des convertisseurs pour les types classiques comme les dates et les nombres. Si une propriété du bean géré est de type primitif (Integer, int, Float, float, etc.), JSF convertira automatiquement la valeur du composant d'interface dans le type adéquat et inversement. Si elle est d'un autre type, vous devrez fournir votre propre convertisseur.

JSF convertira automatiquement les valeurs saisies en nombre lorsque la propriété du bean géré est d'un type numérique primitif et en date ou en heure lorsque la propriété est d'un type date. Si ces conversions automatiques ne conviennent pas, afin d'avoir un aspect visuel plus confortable par exemple, vous pouvez les contrôler explicitement via les marqueurs standard <f:convertNumber> et <f:convertDateTime>. Pour ce faire, vous devez imbriquer le convertisseur dans un marqueur d'entrée ou de sortie.

  1. <f:convertNumber> : Cette balise possède des attributs permettant de convertir la valeur d'entrée en nombre (comportement par défaut), en valeur monétaire ou en pourcentage. Vous pouvez préciser un symbole monétaire particulier ou un nombre de chiffres après la virgule, ainsi qu'un motif déterminant le format du nombre et la façon dont il sera analysé. En coulisse, cette balise utilise indirectement les compétences de la JRE standard en faisant référence à la classe java.text.NumberFormat.
    <h:inputText value="#{conv.franc}" readonly="true">
        <f:convertNumber type="currency" currencySymbol="F"/>
    </h:inputText>  
    
  2. <f:convertDateTime> : Cette balise convertit les dates (type Date) dans différents formats (date, heure ou les deux). Il possède plusieurs attributs pour contrôler cette conversion ainsi que les zones horaires. L'attribut pattern, par exemple, permet de spécifier précisément le format de la chaîne à convertir. En coulisse, cette balise utilise indirectement les compétences de la JRE standard en faisant référence à la classe java.text.DateFormat.
    <h:outputText value="#{test.aujourdhui}">
          <f:convertDateTime dateStyle="full"/>      
    </h:outputText>
    
    <h:inputText value="#{test.expiration}">
          <f:convertDateTime pattern="MMMM yyyy"/> 
    </h:inputText>
    
  3. <f:converter> : Permet de réaliser des formatages et des conversions personnalisés.

Conversion implicite

Il ne faut pas oublier que JSF est déjà très compétent puisqu'il propose des conversions implicites entre les types primitifs : int, double, long, etc. et les chaîne de caractères String prévues par le protocole HTTP. Automatiquement, le modèle métier retrouve les valeurs dans le type désiré sans qu'il soit nécessaire d'écrire quoique se soit sur la page JSF.

En effet, lorsque vous désirez représenter des nombres réels sans présentation particulière, comme les pourcentages et les valeurs monétaires, vous pouvez donc vous passer de mettre en place le mécanisme de conversion explicite.

Ceci dit, même si cela n'est pas écrit, JSF utilise quand même un mécanisme de conversion qui permet de mettre en relation des classes spécialisées dans le type de conversion à réaliser avec les classes enveloppes des types primitifs.

Classes enveloppes Convertisseur
java.math.BigDecimal javax.faces.convert.BigDecimalConverter
java.math.BigInteger javax.faces.convert.BigIntegerConverter
java.lang.Boolean javax.faces.convert.BooleanConverter
java.lang.Byte javax.faces.convert.ByteConverter
java.lang.Character javax.faces.convert.CharacterConverter
java.util.Date javax.faces.convert.DateTimeConverter
java.lang.Double javax.faces.convert.DoubleConverter
java.lang.enum javax.faces.convert.EnumConverter
java.lang.Float javax.faces.convert.FloatConverter
java.lang.Integer javax.faces.convert.IntegerConverter
java.lang.Long javax.faces.convert.LongConverter
java.lang.Number javax.faces.convert.NumberConverter
java.lang.Short javax.faces.convert.ShortConverter

Conversion des valeurs numériques

Le choix du formatage et de la conversion standard s'exprime toujours côté page Web. Nous allons donc tout de suite nous pencher sur l'ensemble des attributs qui composent la balise <f:convertNumber> qui prévoit le formatage et la conversion des valeurs numériques :

Attributs Type Description
type String number (par défaut) pour des nombres réels classiques, currency pour les valeurs monétaires, ou percent pour les valeurs exprimées en poucentage. La représentation graphique de ces valeurs numériques tient compte de la séparation des milliers et de la séparation entre la partie entière et la partie décimale en respectant la région utilisée. Pour la France, nous aurons donc un espace pour la sépartion des milliers, et une virgule pour séparer la partie entière de la partie décimale.
pattern String Permet de proposer un motif de présentation personnalisé qui utilise derrière les compétences de la classe concrète java.text.DecimalFormat. A titre d'exemple, voici le motif correspondant à l'expression d'une valeur monétaire en Franc avec uniquement deux chiffres décimaux : "#,##0.00 F".
maxFractionDigits int Impose une valeur limite sur le nombre de chiffres décimaux à représenter.
minFractionDigits int Impose un nombre de chiffres décimaux à représenter. Si le nombre ne possède pas de valeurs décimales, alors les chiffres 0 sont proposés.
maxIntegerDigits int Impose une valeur limite sur le nombre de chiffres sur la partie entière de la valeur réelle à représenter.
minIntegerDigits int Impose un nombre de chiffres sur la partie entière à représenter.
integerOnly boolean Lorsque cet attribut est validé, le système vérifie que seule la partie entière est soumise (false par défaut).
groupingUsed boolean Permet de prendre en compte ou pas la séparation des milliers (true par défaut).
locale java.util.Locale ou String Choix du paramètre régional à utiliser.
currencyCode String Spécification de la monnaie à prendre en compte comme "EUR" par exemple.
currencySymbol String Choix d'une autre monnaie non prévue par le standard et permet ainsi de personnaliser l'affichage.

Conversion des dates et des heures

De la même façon, nous allons consulter l'ensemble des attributs de la balise <f:convertDateTime> qui prévoit le formatage et la conversion de la date et ou de l'heure associées à un objet de type Date.

Attributs Type Description
type String date (par défaut) pour prendre en compte la date uniquement, time pour prendre en compte l'heure uniquement, ou both pour prendre en compte à la fois la date et l'heure.
dateStyle String default, short, medium, long ou full. Affiche la date avec plus ou moins de renseignements.
timeStyle String default, short, medium, long ou full. Affiche l'heure avec plus ou moins de renseignements.
pattern String Permet de proposer un motif de présentation personnalisé qui utilise derrière les compétences de la classe concrète java.text.SimpleDateFormat.
locale java.util.Locale ou String Choix du paramètre régional à utiliser.
timeZone java.util.TimeZone Tient compte du fuseau horaire.

Exemple de conversion standard

Nous allons reprendre le projet conversion monétaire entre les €uros et les francs que nous avons déjà abordé lors de l'étude précédente. Nous allons juste changer son aspect en prévoyant une autre feuille de style et nous allons rajouter l'affichage de la date et de l'heure en cours comme cela vous est présenté ci-dessous :

Je vous propose de revoir les fichiers importants pour la bonne compréhension des interactions :
resources/css/principal.css
root { 
    display: block;
}

body { 
    background-color: green; 
    color: yellow; 
    font-weight: bold; 
}

.cadre { 
    background: darkgreen; 
    border-radius: 5px; 
    padding: 7px; 
    box-shadow: -1px -1px 3px lightgreen, 2px 2px 5px black;  
}

.saisie { 
    background: yellow;
    color: darkgreen; 
    font-weight: bold; 
    text-align: right; 
    padding-right: 7px; 
    padding-left: 7px;
    margin-right: 15px;  
    margin-left: 15px;
    width: 170px;
}

.entete {
    padding-bottom: 10px;
}

.titre {
    color: green;
    text-shadow: -1px -1px 1px black, 1px 1px 1px lightgreen;     
    margin: 10px;
}
 
.jour, .erreur { 
    border-radius: 3px; 
    box-shadow: 1px 1px 3px black inset, 1px 1px 1px lightgreen; 
    padding-left: 15px;
    padding-right: 15px;
    text-align: center;
    margin: 5px;
}

.erreur {
    width: 460px; 
    display: block;
    margin-left: 27px;
    margin-top: 10px;
    margin-bottom: 5px;
    padding: 3px;
}
bean.Conversion.java
package bean;

import javax.faces.bean.*;

@ManagedBean(name="conv")
@ViewScoped
public class Conversion {
    private final double TAUX = 6.55957;
    private double euro;
    private double franc;

    public double getEuro() { return euro;  }
    public void setEuro(double euro) { this.euro = euro; }

    public double getFranc() { return franc;  }
    public void setFranc(double franc) { this.franc = euro * TAUX;  }

    public Date getDate() { return new Date(); }
    
    public void euroFranc() { franc = euro * TAUX;  }
}
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    <h:head />

    <h:outputStylesheet library="css" name="principal.css" />
    
    <h:body>
        <h:form> 
            <h:panelGrid styleClass="cadre" headerClass="entete" columns="3">
                <f:facet name="header">
                    <h:outputText value="Conversion entre les €uros et les francs" styleClass="titre" />
                    <h:outputText value="#{conv.date}" styleClass="jour">
                        <f:convertDateTime pattern="EEEE dd MMMM" />
                    </h:outputText>        
                    <h:outputText value="#{conv.date}" styleClass="jour">
                        <f:convertDateTime type="time" timeStyle="short" timeZone="Europe/Paris"/>
                    </h:outputText> 
                </f:facet>
                <h:inputText value="#{conv.euro}" styleClass="saisie">        
                    <f:convertNumber type="currency" currencySymbol="Euros"/>
                </h:inputText>
                <h:commandButton value="Conversion" action="#{conv.euroFranc}" />
                <h:inputText value="#{conv.franc}" readonly="true" styleClass="saisie">
                    <f:convertNumber type="currency" currencySymbol="Francs"/>
                </h:inputText>            
            </h:panelGrid>
        </h:form>  
    </h:body>
</html>
  1. Dans la partie en-tête du panneau, nous proposons d'afficher la date du jour personnalisée, c'est-à-dire sans spécifier l'année en cours par exemple, avec également l'heure en version courte.
  2. Nous aurions pu prévoir qu'une seule balise pour afficher ces deux informations, mais je désirais proposer un style sur chacune de ces informations pour que cela soit plus agréable au niveau de l'affichage.
  3. Dans le premier cas, j'utilise l'attribut pattern afin de personnaliser à ma convenance l'affichage du jour.
  4. Dans le deuxième cas, je précise que je désire uniquement m'intéresser à l'heure que je spécifie au travers de l'attribut type. Ensuite, avec timeStyle je précise que je désire voir uniquement que les heures et les minutes. Enfin, par défaut, c'est l'heure GMT qui est toujours affichée. Je dois donc utiliser l'attribut timeZone pour préciser que je désire prendre en compte l'heure française.
  5. Pour les conversions monétaires, je précise qu'il s'agit de ce type de conversion au travers de l'attribut type et je spécifie ensuite la représentation de la monnaie au moyen de l'attribut currencySymbol.

Messages d'erreur associés à la conversion standard

Que se passe-t-il lorsque nous proposons une mauvaise valeur dans la zone de saisie ? Qu'est-ce que nous entendons d'ailleurs par "mauvaise valeur" ? Imaginons que nous tapions une chaîne de caractères à la place de la valeur réelle attendue, et que nous cliquions sur l'un des boutons de traitement. Nous avons l'impression que rien ne se passe. En tout cas le traitement demandé n'est pas exécuté. En réalité, le système se protège de toute erreur de conversion. Si la conversion ne peut être réalisée convenablement, la logique métier (le bean géré) n'est pas sollicité.

Lorsqu'une erreur de conversion se produit, les actions suivantes sont demandées :

  1. Le composant qui provoque l'erreur poste un message d'erreur et déclare que son comportement est invalide.
  2. Le système réaffiche la page en cours afin que l'utilisateur propose cette fois-ci une nouvelle valeur plus adaptée.

Le problème, dans cette situation, c'est que l'utilisateur n'est pas tellement au courant de ce qui se passe. Il a l'impression que le système ne fonctionne pas. Il serait souhaitable qu'il soit averti de sa mauvaise saisie. Pour cela, nous devons rajouter une balise <h:message> qui éventuellement récupère le message qui a été posté par le composant provoquant l'erreur. Rappelez-vous que nous devons choisir un identificateur afin d'établir la relation entre la balise <h:inputText> correspondant à la zone de saisie (au travers de l'attribut id) avec la balise de message d'erreur <h:message> (par l'intermédiaire de l'attribut for).

Rappels sur les balises <h:message> et <h:messages>

Typiquement, pour les erreurs de saisie, les messages sont associés avec un composant particulier et précise qu'une erreur de conversion ou de validation est survenue. Bien que nous utilisons les messages souvent de la même façon, il existe en réalité quatre variétés de message : Information, Avertissement, Erreur et Fatal.

Tous les types de message comporte une description sommaire et une description détaillée. Par exemple, de façon sommaire nous pourrions dire que l'entrée est invalide, alors que dans le détail nous pourrions indiquer que le nombre saisi est plus grand que la valeur maximal prévue. Les applications JSF utilise deux balises pour représenter ces messages : <h:messages> et <h:message>.

  1. <h:messages> : Cette balise affiche tous les messages d'erreur survenant dans l'ensemble de la page.
  2. <h:message> : Cette balise affiche un seul message d'erreur associé à un composant en particulier. Ce composant est spécifié au travers de l'attribut for, lui-même devra proposer un identifiant.
Attributs Description
errorClass, errorStyle, fatalClass, fatalStyle, infoClass, infoStyle, warnClass, warnStyle Styles CSS associés aux quatre types de message d'erreur.
for Identifiant désignant le composant à prendre en compte pour afficher un éventuel message d'erreur le concernant. (<h:message> uniquement).
globalOnly Instruction imposant d'afficher uniquement les messages d'erreur globaux, les messages particuliers étant alors désactivés. (<h:messages> uniquement).
layout Permet de spécifier l'orientation de l'ensemble des messages d'erreur survenant : "table" ou "list". (<h:messages> uniquement).
showDetail Booléen qui détermine si le message d'erreur doit être affiché sous forme détaillé. Par défaut, les valeurs sont false pour <h:messages> et true pour <h:message>.
showSummary Booléen qui détermine si le message d'erreur doit être affiché sous forme simplifiée. Par défaut, les valeurs sont true pour <h:messages> et false pour <h:message>.
tooltip Booléen qui détermine si un message détaillé doit être rendu visible dans une petite bulle d'avertissement. Cet avertissement ne sera toutefois visible uniquement à la condition que les attributs showDetail et showSummary soient validés.

Les messages d'erreurs proposées sont automatiquement traduite dans la langue locale. Ils sont relativement adaptés à la situation. Il est également possible de proposer un message personnalisé sur chaque balise de saisie au moyen des attributs converterMessage, validatorMessage et requiredMessage.

Après avoir effectué tous ces petits rappels, revenons à notre application web afin de prendre en compte les erreurs de saisie éventuels. Nous allons faire un certain nombre d'expérience qui vont nous conduire à élaborer la meilleure stratégie pour afficher un message d'erreur le plus adapté possible à la situation de non fonctionnalité.
index.xhtml
<h:body>
      <h:form> 
            <h:panelGrid styleClass="cadre" headerClass="entete" columns="3">
                <f:facet name="header">
                    <h:outputText value="Conversion entre les €uros et les francs" styleClass="titre" />
                    <h:outputText value="#{conv.date}" styleClass="jour">
                        <f:convertDateTime pattern="EEEE dd MMMM" />
                    </h:outputText>        
                    <h:outputText value="#{conv.date}" styleClass="jour">
                        <f:convertDateTime type="time" timeStyle="short" timeZone="Europe/Paris"/>
                    </h:outputText> 
                </f:facet>
                <h:inputText value="#{conv.euro}" styleClass="saisie" id="euro">        
                    <f:convertNumber type="currency" currencySymbol="Euros"/>
                </h:inputText>
                <h:commandButton value="Conversion" action="#{conv.euroFranc}" />
                <h:inputText value="#{conv.franc}" readonly="true" styleClass="saisie">
                    <f:convertNumber type="currency" currencySymbol="Francs"/>
                </h:inputText>  
                <f:facet name="footer">
                    <h:message for="euro" styleClass="erreur"/>
                </f:facet>               
            </h:panelGrid>
      </h:form>  
</h:body>
  1. Nous plaçons donc la balise <h:message> dans la partie basse du panneau en prenant en compte les attributs respectifs id et for que nous venons d'évoquer. Le résultat obtenu vous est montré ci-dessous :

  2. Ainsi, dans le cas où nous saisissons la lettre 'a' au milieu des autres chiffres, nous obtenons un message par défaut qui tient compte du fait qu'il s'agit bien d'une erreur de conversion de type monétaire.

    Deux choses me perturbent dans cet affichage par défaut, d'une part l'intitulé j_idt7:euro, et d'autre part la proposition de l'exemple Exemple : {1} qui ne me semble pas adapté pour ce cas de figure.

  3. Le premier concerne le nom du composant qui s'appelle donc j_idt7:euro. Il est possible de spécifier un nom plus adapté sur ce composant d'entrée au moyen de l'attribut label.
    index.xhtml
    <h:inputText value="#{conv.euro}" styleClass="saisie"  label="Saisie des euros" id="euro">        
    
  4. Afin d'éviter l'illustration de l'exemple, nous allons demander à afficher le message sans prendre en compte tous les détails, et donc de l'afficher de façon sommaire :
    index.xhtml
    <h:message for="euro" styleClass="erreur" showSummary="true" showDetail="false"/>
  5. Très souvent, l'idéal est de personnaliser son message d'erreur. Il suffit d'utiliser l'attribut converterMessage directement dans la balise d'entrée qui est succeptible de provoquer une erreur de conversion lors de la saisie :
    index.xhtml
    <h:inputText value="#{conv.euro}" styleClass="saisie" id="euro"
                        converterMessage="ATTENTION : Il faut une valeur monétaire. Ex : 15,24 Euros">        
            <f:convertNumber type="currency" currencySymbol="Euros"/>
    </h:inputText>

Les validations standards

Contrairement aux convertisseurs, les validations concernent uniquement les composants qui permettent la saisie des valeurs. La validation permet de s'assurer que le modèle métier ne se trouve pas dans une situation incohérente. En effet, la validation standard JSF contrôle la donnée saisie et vérifie qu'elle correspond bien au canevas souhaité. Si ce n'est pas le cas, le modèle métier n'est pas sollicité, et nous passons directement à la phase du rendu de la réponse.

Le modèle métier est ainsi protégé de toutes mauvaises valeurs non prévues. Du coup, le code du bean géré se trouve allégé et ne dispose que des opérations nécessaires correspondantes uniquement à la logique métier sans se préoccuper des cas ambigüs. C'est la vue qui s'occupe de faire en sorte que les bonnes valeurs soient bien introduites. Ainsi, chacun s'occupe de son propre travail avec une nette séparation entre la récupération des données d'une part, et le traitement de ces dernières d'autre part.

JSF simplifie la validation des données en fournissant des validateurs standard et en permettant d'en créer de nouveaux, adaptés à vos besoin. Les validateurs agissent comme des contrôles de premier niveau en validant les valeurs des composants de l'interface utilisateur avant qu'elles ne soient traitées par le bean géré.

Dans JSF, les composants d'interface mettent en oeuvre des validations simples qui permettent de :

  1. Contrôler si une donnée a bien été saisie (champ de la zone de saisie non vide).
  2. Contrôler la longueur d'une chaîne de caractères.
  3. Contrôler les bornes d'une valeur numérique entière ou réelle (par exemple 0<donnée<9999).
  4. Contrôler la validité d'une donnée à partir d'une expression régulière.
Balises Classe de validation Attributs Valeur
<f:validateDoubleRange> DoubleRangeValidator minimum
maximum
Compare la valeur du composant aux valeurs minimales et maximales indiquées (de type double).
<f:validateLongRange> LongRangeValidator minimum
maximum
Compare la valeur du composant aux valeurs minimales et maximales indiquées (de type long).
<f:validateLength> LenghtValidator minimum
maximum
Teste le nombre de caractères de la valeur textuelle du composant.
<f:validateRequired> RequiredValidator   Vérife qu'une valeur est bien saisie dans le composant (donnée obligatoire).
<f:validateRegex> RegexValidator motif Compare la valeur du composant à une expression régulière.
<f:validateBean> BeanValidator Groupe de validations Vérifie qu'un groupe de validations est bien respecté au travers d'un composant spécifique dont les propriétés sont déjà préétablies.
Nous allons développez quelques exemples classiques avec la prise en compte des messages d'erreur liés à des problèmes de validation.
Exemples de validation
  1. Lorsque vous désirez contrôler une zone de saisie, vous devez donc placer une de ces balises de validation à l'intérieur (par imbrication) de la zone de saisie concernée. Il est même possible de placer plusieurs types de validation pour une même balise d'entrée. Par exemple, pour vérifier un code postal, il faut être sûr que le code saisie possède pile 5 caractères. Par ailleurs, les valeurs permises vont de 01000 à 98900 :
    <h:inputText value="#{commande.codePostal}">
       <f:validateLength minimum="5" maximum="5" />
       <f:validateLongRange minimum="1000" maximum="98900" />
    </h:inputText>
  2. Il est possible d'imposer une valeur dans une zone de saisie (quelque soit son type). La première démarche consiste à utiliser la balise <f:validateRequired> à cet effet :
    <h:inputText value="#{commande.codePostal}">
       <f:validateRequired  />
       <f:validateLength minimum="5" maximum="5" />
       <f:validateLongRange minimum="1000" maximum="98900" />
    </h:inputText>
  3. Alternativement, et c'est généralement le plus simple, nous pouvons nous passer de cette balise particulière en proposant de valider l'attribut required directement dans la balise <h:inputText> :
    <h:inputText value="#{commande.codePostal}" required="true">
       <f:validateLength minimum="5" maximum="5" />
       <f:validateLongRange minimum="1000" maximum="98900" />
    </h:inputText>
  4. Nous pouvons aussi compliquer l'écriture en prenant la balise générique <f:validator>. Comme c'est une balise assez généraliste, vous êtes alors obligé de spécifier quelle est la nature de la validation et les différents paramètres en prendre en compte :
    <h:inputText value="#{commande.codePostal}" required="true">
       <f:validator validatorId="javax.faces.validator.LengthValidator">
          <f:attribute name="minimum" value="5" />
          <f:attribute name="maximum" value="5" />
       </f:validator> 
       <f:validator validatorId="javax.faces.validator.LongRangeValidator">
          <f:attribute name="minimum" value="1000" />
          <f:attribute name="maximum" value="98900" />
       </f:validator> 
    </h:inputText>
  5. Pour terminer, voici un exemple très intéressant qui permet de valider la saisie d'une adresse mail. Nous utilisons pour cela, une expression régulière qui permet de contrôler la présence de l'arobase, la présence également du point comme séparateur. A ce sujet, il faut qu'il y ait au moins un point dans la deuxième partie de l'adresse mail. Enfin, il faut à tout prix empêcher les caractères accentués :
    <h:form>
         <h:outputText value="Adresse mail : " />
          <h:inputText value="#{test.mail}" id="erreur" validatorMessage=" Ce n'est pas une adresse mail">
                <f:validateRegex pattern="[\w\.]+@\w+(\.\w+)+" />
          </h:inputText>
          <h:message for="erreur" />
    </h:form>
    
Afficher des erreurs de validation

A l'image de la conversion, nous pouvons proposer des messages associés à des problèmes de validation. L'idéal est de proposer des messages personnalisés à l'aide des attributs spécifiques validatorMessage et requiredMessage, qui sont présent notamment dans les zones de saisie. En reprenant l'exemple de la conversion monétaire, voici ce que nous pouvons faire :

index.xhtml
<h:body>
     <h:form>            
          <h:inputText value="#{conv.euro}"  styleClass="saisie"  id="euro" required="true"
                               converterMessage="Il faut une valeur monétaire"
                               validatorMessage="Uniquement les nombres positifs"
                               requiredMessage="Précisez votre valeur monétaire">
                
                <f:convertNumber type="currency" currencySymbol="€"/>
                <f:validateDoubleRange minimum="0.0" />
           </h:inputText>
            
           <h:commandButton value="Conversion" action="#{conv.euroFranc}" />
           
           <h:inputText value="#{conv.franc}" readonly="true" styleClass="resultat">
                <f:convertNumber type="currency" currencySymbol="F"/>
           </h:inputText>  
            
           <p><h:message for="euro" styleClass="erreur"/></p>
      </h:form>  
</h:body>
Inhiber le contrôle des saisies prévu par le système de validation

Grâce à ce système de validations, notre application Web devient très performante puisque nous contrôlons parfaitement toute les saisies réalisées par l'opérateur. Il existe quand même une situation où ce système de validations s'avère génant. Imaginons que l'utilisateur, après avoir afficher une page web correspondant à une commande en ligne, désire sortir sans finalement passer sa commande. Pour cela, il clique sur un bouton "Annuler". Voici ce qu'il se produit alors :

Le problème, c'est que pour passer vers une autre page Web, nous sollicitons systématiquement ce système de validations. Or l'utilisateur n'a saisie aucune valeur puisqu'il désirait ressortir tout de suite. Du coup, nous sommes dans une impasse. Vous êtes obliger de saisir les valeurs pour pouvoir ressortir. Ce qui est totalement incohérent.

Pour sortir de cette impasse, il existe une solution très simple qui a été justement mis en place pour prévenir ce cas d'utilisation. Si vous désirez inhiber le système de validation, vous devez activer l'attribut immediate (avec la valeur true) des balises correspondant à la navigation, comme le sont les balises <h:commandButton>, <h:button>, <h:commandLink> ou <h:link>. Voici ce que nous pourrions écrire dans le code correspondant à la vue ci-dessus :

commande.xhtml

<h:form>
      <h:panelGrid columns="3" styleClass="général" rowClasses="un, deux" cellpadding="3">
               <h:outputText value="Article : " />
               <h:inputText value="#{commande.article}" id="article" required="true"
                  requiredMessage="Précisez votre article."/>
               <h:message for="article" errorClass="erreur"/>
               <h:outputText value="Prix : " />
               <h:inputText value="#{commande.prix}" id="prix" required="true"
                  validatorMessage="Le prix doit être compris entre 10 et 10000.">
                  <f:convertNumber minFractionDigits="2" maxFractionDigits="2"/>
                  <f:validateDoubleRange minimum="10" maximum="10000" />
               </h:inputText>
               <h:message for="prix" errorClass="erreur" />
               <h:outputText value="Quantité : " />
               <h:inputText value="#{commande.quantité}" size="1" id="quantité"
                  validatorMessage="La quantité n'est pas bonne !" >
                  <f:validateLongRange minimum="1" />
               </h:inputText>
               <h:message for="quantité" errorClass="erreur" />
               <h:outputText value="Adresse : " />
               <h:inputText value="#{commande.adresse}" id="adresse" required="true" 
requiredMessage="Donnez votre adresse."/> <h:message for="adresse" errorClass="erreur" /> <h:outputText value="Code postal : " /> <h:inputText value="#{commande.codePostal}" size="3" maxlength="5" id="codePostal" required="true" validatorMessage="Votre code postal n'est pas bon !"> <f:validateLength minimum="5" maximum="5" /> <f:validateLongRange minimum="1000" maximum="98900" /> </h:inputText> <h:message for="codePostal" errorClass="erreur" /> <h:outputText value="Ville : " /> <h:inputText value="#{commande.ville}" id="ville" required="true"
requiredMessage="Précisez le nom de la ville."/> <h:message for="ville" errorClass="erreur" /> <h:outputText value="Adresse Mail : " /> <h:inputText value="#{commande.adresseMail}" id="mail" required="true" requiredMessage="Donnez votre adresse mail."/> <h:message for="mail" errorClass="erreur"/> <h:outputText value="Carte de crédit : " /> <h:inputText value="#{commande.carte}" id="carte" required="true" requiredMessage="Le code de la carte n'est pas valide !" validatorMessage="Le code de la carte n'est pas valide !"> <f:validateLength minimum="13" /> </h:inputText> <h:message for="carte" errorClass="erreur" /> <h:outputText value="Expiration (ex: 02/2007) : " /> <h:inputText value="#{commande.expiration}" size="7" id="expiration" required="true" requiredMessage="Précisez la date de validation."> <f:convertDateTime pattern="MM/yyyy" /> </h:inputText> <h:message for="expiration" errorClass="erreur" /> </h:panelGrid> <hr /> <h:commandButton action="Commander" value="Commander" /> <h:commandButton action="Annuler" value="Annuler" immediate="true" /> </h:form>

Les validations standard par bean géré

Depuis la version JSF 2.0, il existe une autre façon d'utiliser les validations standard. Plutôt que de les évoquer au niveau de la vue, il est possible maintenant de les spécifier directement dans le bean géré sur chacun des attributs où vous désirez placer une contrainte particulière. Cela se fait au travers d'annotation spécifiques dont voici la liste :

Annotation Attributs Description
@Null, @NotNull Aucun Vérifie que l'objet existe ou pas.
@Min, @Max Valeurs limites (long) Vérifie que la valeur proposée n'atteint pas les limites spécifiées. S'applique pour les entiers (int, long, short, byte, BigInteger et BigDecimal). Les réels ne sont pas pris en compte (double, float).
@DecimalMin, @DecimalMax Valeurs limites (String) Même chose, mais peut s'appliquer aussi pour les chaînes de caractères.
@Digits integer, fraction Vérife qu'une valeur ne dépasse pas le nombre de chiffres spécifier par la partie entière (avant la virgule) et la partie décimale (après la virgule). S'applique pour les entiers et les chaînes de caractères (int, long, short, byte, BigInteger, BigDecimal et String).
@AssertTrue, @AssertFalse Aucun Vérifie que la valeur saisie est bien un booléen avec soit la valeur true, soit la valeur false.
@Past, @Future Aucun Vérifie que la date proposée est antérieure ou postérieure à la date actuelle.
@Size min, max Vérifie que la taille de la chaîne, du tableau, d'une collection ou d'une carte respecte bien les limites imposées.
@Pattern regexp, flags Vérifie que la valeur proposée respecte bien l'expression régulière ou les options de compilation.

Toutes ces validations possèdent systématiquement deux attributs supplémentaires, message et groups. A l'aide de l'attribut message, vous pouvez préciser l'intitulé de l'alerte qui sera affiché ensuite par la balise <h:message>. Nous verrons l'intérêt de l'attribut groups un peu plus loin dans ce chapitre.

La validation proposée directement par le bean géré possède un énorme avantage. Supposez que vous ayez une application web qui intègre plusieurs pages pour un même bean géré. Vous n'êtes pas obligé de rajouter les règles de validation sur chacune des pages, ce qui est spécifié par le bean géré se répercute sur chacune de ces pages. Par ailleurs, si un même attribut possède plusieurs validations, vous pouvez proposer, grâce à cette technique, un message d'erreur circonstancié.

Afin de visualiser ce nouveau concept, je vous propose d'utiliser un exemple relativement classique qui consiste à enregistrer les coordonnées d'une personne, son nom, sa date de naissance, son adresse mail et son code postal :

Chacune de ses coordonnées sont vérifiées et dans la vue que je vous montre, sur la partie droite, se trouve les messages d'erreur circonstanciés.

bean.Personne.Java
package bean;

import java.util.Date;
import javax.faces.bean.*;
import javax.validation.constraints.*;

@ManagedBean
@SessionScoped
public class Personne {
    @Size(min=2, message="Votre identité !")
    private String nom;
    
    @NotNull
    @Past
    private Date naissance; 
    
    @Pattern(regexp="[\\w\\.]+@\\w+(\\.\\w+)+", message="Ce n'est pas une adresse mail")
    private String mail;

    @Min(value=1000, message= "Valeur minimale : 1000")
    @Max(value=98900, message= "Valeur maximale : 98900")
    private int codePostal;

    public String getNom() { return nom;    }
    public void setNom(String identité) {  this.nom = identité;   }
    
    public Date getNaissance() {  return naissance;   }
    public void setNaissance(Date naissance) {  this.naissance = naissance;  }
    
    public String getMail() { return mail;  }
    public void setMail(String mail) { this.mail = mail;  }
    
    public int getCodePostal() { return codePostal;  }
    public void setCodePostal(int codePostal) { this.codePostal = codePostal;  }
} 
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    
    <style type="text/css">
        body { background: green; color: yellow; font-weight: bold; text-shadow: 3px 3px 10px black; }
        .panneau { background: darkgreen; border-radius: 5px; box-shadow: 1px 1px 3px black; }
        .titre { font-size: 20px; }
    </style>

    <h:body>
        <h:form>
            <h:panelGrid columns="3" styleClass="panneau" headerClass="titre" cellspacing="5">
                <f:facet name="header">Vos coordonnées<hr /></f:facet>
                <h:outputText value="Nom : "/>
                <h:inputText value="#{personne.nom}" id="nom" />
                <h:message for="nom" />
                <h:outputText value="Date de naissance : " />                
                <h:inputText value="#{personne.naissance}" id="naissance" validatorMessage="Etes-vous vraiment né !?">
                    <f:convertDateTime pattern="dd/mm/yyyy" />
                </h:inputText>
                <h:message for="naissance" />
                <h:outputText value="Adresse mail : " />                
                <h:inputText value="#{personne.mail}" id="mail" />
                <h:message for="mail" />
               <h:outputText value="Code postal : " />
                <h:inputText value="#{personne.codePostal}" id="codePostal" />
                <h:message for="codePostal" />
                <f:facet name="footer"><hr /><h:commandButton value="Soumettre" /></f:facet>
            </h:panelGrid>           
        </h:form>
    </h:body>
</html>

Globalement, je trouve que cette approche est plus lisible et plus directe.

Les conversions personnalisées

Revenons à notre souci de convertir les valeurs afin qu'elles soient mises en forme, en étant adaptées aux propriétés prévues par le modèle. Les convertisseurs standard proposent déjà des conversions qui, dans 90% des cas, nous permettront de résoudre la plupart des problèmes rencontrés. Toutefois, il peut arriver dans certaines situations, que ces conversions standard ne résolvent pas notre problème particulier. Il faut alors proposer une conversion personnalisée adaptée à la situation présente.

J'aimerais, par exemple, pouvoir réaliser du traitement logique sur des valeurs binaires. Les calculs à entreprendre sont le OU logique, le ET logique et le OU Exclusif, sur deux valeurs binaires de 16 bits. Nous souhaitons, par ailleurs, représenter et saisir les nombres binaires par quartets (paquets de 4 bits). Par défaut, les valeurs binaires ne sont pas interprétées directement par le système, nous devons donc prévoir une conversion personnalisée.

  1. Java est capable de travailler sur des valeurs entières qui manipulent les opérateurs logiques correspondant (|, &, ^). Notre modèle à finalement besoin de récupérer tout simplement des valeurs entières (en base 10) issues des deux zones de saisies. Par ailleurs, le résultat délivré est également de nature entière, donc toujours en base 10.
  2. Par contre l'affichage et la saisie doit plutôt être représentée sous forme binaire avec en plus la séparation par quartets. Nous remarquons là qu'il faut bien passer par une étape intermédiaire qui permet donc de convertir d'une part, une valeur binaire (exprimée en réalité sous forme de chaîne de caractères) vers la valeur entière équivalente en base 10, et d'autre part faire l'inverse, c'est-à-dire de convertir la valeur entière (calculée) pour aboutir à la représentation binaire équivalente (toujours exprimée sous forme de chaîne de caractères).

Notre modèle MVC possède donc une seule page Web index.xhtml avec son modèle associé bean.Calcul, qui s'occupe de la logique métier. Par contre, vous découvrez entre les deux, une classe intermédiaire conversion.Binaire (passage obligatoire) qui s'occupe de la conversion entre une valeur binaire écrite sous forme de chaîne de caractères et une valeur décimale entière.

Souvenez-vous, qu'avant de soumettre la valeur au propriétés du modèle, le cycle de traitement des requêtes JSF nous impose de passer d'abord par une conversion éventuelle. Dans le même type de raisonnement, l'affichage de la valeur, proposée par le modèle passe également par une phase de conversion avant d'avoir le rendu effectif. Pour résumer, nous devons systématiquement passer par la conversion pour échanger des valeurs entre la page XHTML et le bean géré correspondant.

Mise en oeuvre d'un convertisseur

D'une façon générale, un convertisseur est une classe qui assure une conversion entre une chaîne de caractères (type String) et un objet (type Object). Effectivement, avec le protocole HTTP, les requêtes qui sont envoyées, sont toujours des chaînes de caractères qu'il faut ensuite transformer dans le type requis. Pour assurer la conversion vers n'importe quel type, l'idéal est donc de prendre plutôt un type générique comme la classe Object.

Lorsque vous désirez créer votre propre convertisseur, vous devez mettre en oeuvre une classe qui implémente l'interface Converter qui possède deux méthodes que vous devez donc impérativement redéfinir.

javax.faces.convert.Converter;
interface Converter {   
public Object getAsObject(FacesContext context, UIComponent component, String value) throws ConvertException;
public String getAsString(FacesContext context, UIComponent component, Object value) throws ConvertException;
}
  1. La première méthode getAsObject() convertit la valeur soumise par le client, qui est donc bien une chaîne de caractères que nous récupérons au travers du paramètre value, vers le type désiré qui doit hériter de la classe Object, ce qui est vrai quelque soit l'objet choisi. Cet objet est ensuite injecté vers le bean qui représente le modèle métier et soumet la valeur à la (ou les) propriété correspondante. Cette méthode lance une exception ConvertException si la conversion échoue.
  2. La deuxième méthode getAsString() effectue l'opération inverse, c'est-à-dire récupère la valeur issue de la propriété correspondante du JavaBean à l'aide du paramètre value, pour aboutir à une chaîne de caractères qui va permettre d'assurer le rendu sur la page xhtml, en passant donc par le protocole HTTP.

Pour utiliser un convertisseur dans une application web, il doit être enregistré. Cela consiste simplement à placer l'annotation spécifique @FacesConverter directement sur la classe qui implémente la conversion avec le nom d'objet que vous désirez.

Bien entendu, comme d'habitude, nous allons tout de suite exploiter cette approche particulière au travers de l'exemple concret sur les conversion binaires en rajoutant quelques informations complémentaires :

bean.Calcul.Java
package bean;

import javax.faces.bean.*;

@ManagedBean
@ViewScoped
public class Calcul {
   private int premier;
   private int deuxième;
   private int résultat;

   public int getPremier() { return premier;  }
   public void setPremier(int premier) { this.premier = premier; }
   
   public int getDeuxième() { return deuxième;  }
   public void setDeuxième(int deuxième) { this.deuxième = deuxième; }
   
   public int getRésultat() { return résultat; }
   
   public void ouLogique() { résultat = premier | deuxième; }
   public void etLogique() {  résultat = premier & deuxième;  }   
   public void ouExclusif() {  résultat = premier ^ deuxième;  }
}

Ce bean, finalement, est très simple, pour ne pas dire rudimentaire. Nous disposons de trois propriétés : premier, deuxième et résultat qui sont des entiers en base 10. Le bean possède trois méthodes supplémentaires : ouLogique(), etLogique() et ouExclusif() qui s'occupent d'effectuer le calcul demandé et seront donc sollicitées par les boutons correspondant prévues par la page xhtml.

convertisseur.Binaire.Java
package convertisseur;

import java.text.*;
import javax.faces.component.*;
import javax.faces.context.FacesContext;
import javax.faces.convert.*;

@FacesConverter("binaire")
public class Binaire implements Converter {  
   @Override
   public Object getAsObject(FacesContext context, UIComponent component, String value)  {
      StringBuilder chaîne = new StringBuilder(value);
      for (int i=0 ; i<chaîne.length(); )
         if (Character.isDigit(chaîne.charAt(i))) i++;
         else chaîne.deleteCharAt(i);
      return Integer.parseInt(chaîne.toString(), 2);
   }

   @Override
   public String getAsString(FacesContext context, UIComponent component, Object value) {
      long décimal = Long.parseLong(Integer.toBinaryString((Integer)value));
      DecimalFormat binaire = new DecimalFormat("0000,0000,0000,0000");
      return binaire.format(décimal);
   }
}

Revenons à notre sujet principal. La classe Binaire permet donc, au travers de la méthode getAsObject(), de récupérer une valeur binaire saisie par l'opérateur avec éventuellement des espaces entre les quartets. Cette valeur binaire est ensuite tranformée dans la valeur décimale équivalente. La méthode getAsString() prend la valeur décimale et fabrique une chaîne de caractères qui représente la valeur binaire correspondante et la formate pour qu'elle affiche tous les chiffres binaires (les 16 bits) par groupe de 4, avec comme séparateur, le séparateur espace.

Grâce à l'annotation @FacesConverter, nous déclarons un nouveau convertisseur qui se nomme binaire qui pourra par la suite être directement utilisé par la ou les pages web. Il existe une deuxième notation possible pour déclarer ce convertisseur :

@FacesConverter(value="binaire")
public class Binaire implements Converter {  
   @Override
   public Object getAsObject(FacesContext context, UIComponent component, String value)  { ...  }

   @Override
   public String getAsString(FacesContext context, UIComponent component, Object value) { ... }
}
resources/principal.css
root { 
    display: block;
}

body { 
    background: green; 
    color: yellow; 
    font-weight: bold; 
    text-shadow: 0px 0px 10px yellow; 
    text-align: center;
}

.panneau { 
    background: darkgreen; 
    border-radius: 5px; 
    box-shadow: 2px 2px 5px black, -2px -2px 5px lightgreen; 
}

.titre { font-size: 20px; }

input[type="text"] {
    text-align: center;
    border-radius: 5px;
    box-shadow: 2px 2px 5px black inset;
}
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    
    <h:head />

    <h:outputStylesheet name="principal.css" />
    
    <h:body>
        <h:form>
            <h:panelGrid columns="1" styleClass="panneau" headerClass="titre">
                <f:facet name="header">Calcul binaire<hr /></f:facet>
                <h:inputText value="#{calcul.premier}" converter="binaire" />             
                <h:inputText value="#{calcul.deuxième}" converter="binaire" />
                <h:panelGroup>
                    <h:commandButton value="OR" action="#{calcul.ouLogique}" />
                    <h:commandButton value="AND" action="#{calcul.etLogique}" />
                    <h:commandButton value="XOR" action="#{calcul.ouExclusif}" />
                </h:panelGroup>       
                <h:inputText value="#{calcul.résultat}" converter="binaire"  readonly="true"/>
            </h:panelGrid>           
        </h:form>
    </h:body>
</html>  

Pour utiliser ce convertisseur binaire, la solution la plus simple consiste à utiliser l'attribut converter de la balise <h:inputText>. Vous spécifiez alors le nom du convertisseur défini pa l'annotation @FacesConverter, ici binaire.

Une autre alternative, consiste à utiliser la balise <f:converter> à l'intérieur de la balise <h:inputText> et de spécifier le nom du convertisseur au travers de l'attribut converterId.

<h:panelGrid columns="1" styleClass="panneau" headerClass="titre">
     <f:facet name="header">Calcul binaire<hr /></f:facet>
     <h:inputText value="#{calcul.premier}">
         <f:converter converterId="binaire">
     </h:inputText>             
     <h:inputText value="#{calcul.deuxième}">
         <f:converter converterId="binaire">
     </h:inputText>          
     <h:panelGroup>
         <h:commandButton value="OR" action="#{calcul.ouLogique}" />
         <h:commandButton value="AND" action="#{calcul.etLogique}" />
         <h:commandButton value="XOR" action="#{calcul.ouExclusif}" />
     </h:panelGroup>       
     <h:inputText value="#{calcul.résultat}" readonly="true">
         <f:converter converterId="binaire">
     </h:inputText>   
</h:panelGrid>
Gestion des erreurs

Attention, ce que nous venons de proposer n'est pas complet. Si jamais un utilisateur réalise une mauvaise saisie, vous allez voir apparaître une page d'erreur qui montre l'exception qui est levée. Ce type de message est désastreux, puisque les utilisateurs ne savent pas du tout de quoi il s'agit. Ils ont juste l'impression que le système est "planté". Il faut à tout prix gérer la mauvaise saisie de l'opérateur en l'avertissant du problème rencontré.

Si la saisie est mauvaise, la conversion ne peut pas se faire correctement. C'est donc au niveau de la conversion qu'il est nécessaire d'effectuer un contrôle sur la saisie réalisée. Si cette saisie est incorrecte, il faut lever une exception correspondant au problème de conversion. Ce type d'exception existe, il s'agit de la classe ConverterException. Si une telle exception est effectivement envoyée, la page reste en l'état. Si vous désirez en plus avertir l'utilisateur du problème rencontré, vous pouvez alors passer, en paramètre de l'objet ConverterException, un objet de type FacesMessage qui spécifie le message d'erreur.

convertisseur.Binaire.Java
package convertisseur;

import java.text.*;
import javax.faces.component.*;
import javax.faces.context.FacesContext;
import javax.faces.convert.*;

@FacesConverter("binaire")
public class Binaire implements Converter {  
   @Override
   public Object getAsObject(FacesContext context, UIComponent component, String value)  {
      StringBuilder chaîne = new StringBuilder(value);
      for (int i=0 ; i<chaîne.length(); )
         if (Character.isDigit(chaîne.charAt(i))) i++;
         else chaîne.deleteCharAt(i);
      String chaîneBinaire = chaîne.toString();
      if (!chaîneBinaire.matches("[01]*")) {
            FacesMessage erreur = new FacesMessage("Valeur non valide", " Il faut saisir un nombre binaire");
            throw new ConverterException(erreur);
      }
      return Integer.parseInt(chaîneBinaire, 2);
   }

   @Override
   public String getAsString(FacesContext context, UIComponent component, Object value) {
      long décimal = Long.parseLong(Integer.toBinaryString((Integer)value));
      DecimalFormat binaire = new DecimalFormat("0000,0000,0000,0000");
      return binaire.format(décimal);
   }
}
  1. La plupart du temps, une erreur se produit lors d'une saisie, ce qui sous-entend que c'est uniquement à l'intérieur de la méthode getAsObject() qu'il faut éventuellement lever une exception et générer un message d'erreur adapté.
  2. Dans le sujet qui nous préoccupe, j'attends de récupérer la chaîne avec uniquement des valeurs numériques, sans les espaces et les éventuelles lettres supplémentaires. Ce n'est que plus tard que je l'évalue et la vérifie pour savoir si elle est composée uniquement des chiffres 0 ou 1. Si ce n'est pas le cas, c'est à ce moment là que je lance l'exception de mauvaise conversion. Dans cette façon de faire, les lettres sont considérées comme des 0 logique, je n'en tient pas compte.
  3. Vous remarquez que le constructeur de la classe FacesMessage prend deux paramètres : le premier indique un message d'erreur sommaire qui peut être utile pour la balise <h:messages> et le second indique le détail de l'erreur qui cette fois-ci est plutôt utilisée par la balise <h:message>.

Je rappelle que par défaut la balise <h:messages> diffuse les messages d'erreur sommaire, alors que par défaut la balise <h:message> affiche le détail de ces mêmes messages d'erreur. Il est toutefois possible de changer le comportement par défaut de ces balises au travers des attributs : showDetail et showSummary. Il est ainsi possible d'afficher pour un même message d'erreur à la fois le sommaire et le détail.

Il existe différents types d'erreur :

  1. Erreur de type informatif : représentée par la constante SEVERITY_INFO.
  2. Erreur de type alerte : représentée par la constante SEVERITY_WARN.
  3. Erreur classique : représentée par la constante SEVERITY_ERROR.
  4. Erreur fatale : représentée par la constante SEVERITY_FATAL.

Par défaut le type d'erreur proposé est de type informatif (SEVERITY_INFO). Il est toutefois possible de proposer un type d'erreur particulier au travers de la méthode setSeverity() de la classe FacesMessage. Vous avez une autre alternative qui consiste à prendre le constructeur de la classe FacesMessage avec trois paramètres, dont le premier est prévu pour le type d'erreur souhaité.

index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    
    <h:head />

    <h:outputStylesheet name="principal.css" />
    
    <h:body>
        <h:form>
            <h:panelGrid columns="2" styleClass="panneau" headerClass="titre" cellspacing="5">
                <f:facet name="header">Calcul binaire<hr /></f:facet>
                <h:inputText value="#{calcul.premier}" converter="binaire" id="premier" />    
                <h:message for="premier" />         
                <h:inputText value="#{calcul.deuxième}" converter="binaire" id="deuxieme" />
                <h:message for="deuxieme" />
                <h:panelGroup>
                    <h:commandButton value="OR" action="#{calcul.ouLogique}" />
                    <h:commandButton value="AND" action="#{calcul.etLogique}" />
                    <h:commandButton value="XOR" action="#{calcul.ouExclusif}" />
                </h:panelGroup>  
                &nbsp;     
                <h:inputText value="#{calcul.résultat}" converter="binaire"  readonly="true"/>
            </h:panelGrid>           
        </h:form>
    </h:body>
</html>  

Dans la vue, il suffit de rajouter les balises <h:message> et d'adapter le panneau en conséquence.
.

Les validations personnalisées

Comme pour les convertisseurs, il est possible de définir ces propres validations. Si vous le souhaitez, vous pouvez rajouter une validation personnalisée avec un convertisseur personnalisé. Rien ne l'empêche. Par contre, suivant le cycle de traitement des requêtes JSF, la validation  ne s'effectue  qu'après la conversion. Effectivement, la valeur doit d'abord être mise en forme avant de pouvoir être évaluée. Ce n'est qu'après cette nouvelle phase que la valeur est éventuellement injectée dans le bean métier suivant le résultat du test. 

A titre d'exemple, nous allons mettre en oeuvre une validation, qui n'existe pas en standard, qui va contrôler si la valeur saisie est un nombre premier. Nous prenons comme support l'application Web précédente.

Attention, contrairement aux convertisseurs personnalisés, la validation ne s'intéresse qu'à une valeur saisie par l'opérateur. Elle n'est utilisée que dans ce sens là. Elle ne s'intéresse pas du tout au rendu d'une réponse venant de la logique métier, puisque la valeur qui se trouve dans le bean géré correspondant est nécessairement correcte.

Mise en oeuvre une validation personnalisée

Comme les convertisseurs, un validateur est une classe qui doit implémenter une interface et redéfinir une méthode. Dans le cas des validateurs, cette interface est javax.faces.validator.Validator, qui n'expose que la méthode validate(). Voici d'ailleurs la déclaration complète de cette interface :

javax.faces.validator.Validator
interface Validator {   
public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException;
}

Cette méthode validate() comporte les mêmes paramètres que les méthodes issues de l'interface Converter. Toutefois, validate() ne renvoie pas de valeur. Effectivement, cette méthode teste uniquement si la valeur récupérée par le paramètre value correspond au critère de validation choisi. Si la valeur est correcte, rien ne se passe en particulier, si ce n'est que le cycle continu et propose donc cette valeur au modèle métier. Dans le cas contraire, une exception de type ValidatorException est levée avec un message d'erreur adaptée à la situation. Dans ce cas là, le cycle normal est interrompu, et le rendu de la page xhtml est alors proposé. La page JSF peut alors afficher le message d'erreur proposé.

Comme pour le convertisseur, un validateur doit être enregistré afin de pouvoir être utilisé dans la page web. Cela consiste simplement à placer cette fois-ci l'annotation spécifique @FacesValidator directement sur la classe qui implémente la validation avec le nom d'objet que vous désirez.

Nous allons maintenant développer notre validateur personnalisé au travers de notre application web :

validateur.NombrePremier.java
package validateur;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.*;

@FacesValidator("nombrePremier")
public class NombrePremier implements Validator {
    @Override
    public void validate(FacesContext context, UIComponent component, Object value)  throws ValidatorException {
        int nombre = (Integer)value;
        for (int i=2; i<=Math.sqrt(nombre); i++) {
           if (nombre%i == 0) {
              String sommaire = context.getMessages().hasNext() ? "" : "Aucun résultat : Erreur";
              FacesMessage erreur = new FacesMessage(sommaire, "Le nombre n'est pas premier");            
              throw new ValidatorException(erreur);
           } 
        }
    }    
}  

La classe NombrePremier implémente le validateur personnalisé en redéfinissant la méthode validate(). Grâce à l'annotation @FacesValidator, nous déclarons un nouveau validateur qui se nomme nombrePremier qui pourra par la suite être directement utilisé par la ou les pages web.

  1. Pour la gestion des erreurs, nous prévoyons un message détaillé qui previent de la nature du problème au travers de la classe FacesMessage.
  2. De plus, vous remarquez la présence d'un message sommaire suivant le nombre de messages d'erreur en activité. Si plus d'un message d'erreur est proposé, je propose une chaîne vide. Pour réaliser tout cela, je récupère l'ensemble des messages stockés dans le contexte de la page en cours au travers de la méthode getMessages() de la classe FacesContext.
bean.Calcul.Java
package bean;

import javax.faces.application.FacesMessage;
import javax.faces.bean.*;
import javax.faces.context.FacesContext;

@ManagedBean
@ViewScoped
public class Calcul {
   private int premier;
   private int deuxième;
   private int résultat;

   public int getPremier() { return premier;  }
   public void setPremier(int premier) { this.premier = premier; }
   
   public int getDeuxième() { return deuxième;  }
   public void setDeuxième(int deuxième) { this.deuxième = deuxième; }
   
   public int getRésultat() { return résultat; }
   
   public void ouLogique() { 
      résultat = premier | deuxième; 
      FacesContext context = FacesContext.getCurrentInstance();
      context.addMessage("messages", new FacesMessage("Résultat avec le OU logique"));
   }
   
   public void etLogique() {  
      résultat = premier & deuxième;  
      FacesContext context = FacesContext.getCurrentInstance();
      context.addMessage("messages", new FacesMessage("Résultat avec le ET logique"));       
   }   
   
   public void ouExclusif() {  
      résultat = premier ^ deuxième;  
      FacesContext context = FacesContext.getCurrentInstance();
      context.addMessage("messages", new FacesMessage("Résultat avec le OU Exclusif"));       
   }
}
  1. Je modifie également le bean géré afin que nous puissions mettre en place une autre série de messages qui sont de nature plutôt informative. Pour cela, nous utilisons le constructeur de FacesMessage() avec un seul paramètre, et ce paramètre est une chaîne de caractères correspondant à une information sommaire. A tout moment, nous pouvons donc proposer des messages, sans passer par une exception quelconque, tout simplement au travers de la méthode addMessage() de la classe FacesContext.
  2. La méthode addMessage() utilise deux arguments, le premier pour spécifier le composant (<h:messages> par exemple) à qui s'adresse le message. Ce composant (graphique) devra alors posséder un identifiant au moyen de l'attribut id. Il est tout à fait possible de ne pas spécifier de composant (null) si votre page ne dispose que d'un seul composant qui gère l'ensemble des messages.
  3. Le deuxième argument est le gestionnaire de message de type FacesMessage.
  4. Enfin, pour obtenir le contexte de la page en cours, nous devons faire appel à la méthode getCurrentInstance() de la classe FacesContext.
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    
    <h:head />

    <h:outputStylesheet name="principal.css" />
    
    <h:body>
        <h:form>
            <h:panelGrid columns="2" styleClass="panneau" headerClass="titre" cellspacing="5">
                <f:facet name="header">Calcul binaire<hr /></f:facet>
                <h:inputText value="#{calcul.premier}" converter="binaire" validator="nombrePremier" id="premier" />
                <h:message for="premier" />              
                <h:inputText value="#{calcul.deuxième}" converter="binaire" validator="nombrePremier" id="deuxieme" />
                <h:message for="deuxieme" />
                <h:panelGroup>
                    <h:commandButton value="OR" action="#{calcul.ouLogique}" />
                    <h:commandButton value="AND" action="#{calcul.etLogique}" />
                    <h:commandButton value="XOR" action="#{calcul.ouExclusif}" />
                </h:panelGroup>       
                &nbsp;
                <h:inputText value="#{calcul.résultat}" converter="binaire"  readonly="true"/>
                <h:messages layout="table" id="messages" />
            </h:panelGrid>           
        </h:form>
    </h:body>
</html>  

Pour utiliser ce validateur, la solution la plus simple consiste à utiliser l'attribut validator de la balise <h:inputText>. Vous spécifiez alors le nom du validateur défini pa l'annotation @FacesValidator, ici nombrePremier.

Une autre alternative, consiste à utiliser la balise <f:validator> à l'intérieur de la balise <h:inputText> et de spécifier le nom du validateur au travers de l'attribut validatorId.

<h:panelGrid columns="1" styleClass="panneau" headerClass="titre">
     <f:facet name="header">Calcul binaire<hr /></f:facet>
     <h:inputText value="#{calcul.premier}" id="premier">
         <f:converter converterId="binaire">
         <f:validator validatorId="nombrePremier">
     </h:inputText>           
     <h:message for="premier" />
     <h:inputText value="#{calcul.deuxième}" id="deuxieme">
         <f:converter converterId="binaire">
         <f:validator validatorId="nombrePremier">
     </h:inputText>       
     <h:message for="deuxieme" />
     <h:panelGroup>
         <h:commandButton value="OR" action="#{calcul.ouLogique}" />
         <h:commandButton value="AND" action="#{calcul.etLogique}" />
         <h:commandButton value="XOR" action="#{calcul.ouExclusif}" />
     </h:panelGroup>       
     <h:inputText value="#{calcul.résultat}" readonly="true">
         <f:converter converterId="binaire">
     </h:inputText>   
     <h:messages layout="table" id="messages" />
</h:panelGrid>
Gestion des erreurs

Attention, ce que nous venons de proposer n'est pas complet. Si jamais un utilisateur réalise une mauvaise saisie, vous allez voir apparaître une page d'erreur qui montre l'exception qui est levée. Ce type de message est désastreux, puisque les utilisateurs ne savent pas du tout de quoi il s'agit. Ils ont juste l'impression que le système est "planté". Il faut à tout prix gérer la mauvaise saisie de l'opérateur en l'avertissant du problème rencontré.

Si la saisie est mauvaise, la conversion ne peut pas se faire correctement. C'est donc au niveau de la conversion qu'il est nécessaire d'effectuer un contrôle sur la saisie réalisée. Si cette saisie est incorrecte, il faut lever une exception correspondant au problème de conversion. Ce type d'exception existe, il s'agit de la classe ConverterException. Si une telle exception est effectivement envoyée, la page reste en l'état. Si vous désirez en plus avertir l'utilisateur du problème rencontré, vous pouvez alors passer, en paramètre de l'objet ConverterException, un objet de type FacesMessage qui spécifie le message d'erreur.

Binaire.Java
package convertisseur;

import java.text.*;
import javax.faces.component.*;
import javax.faces.context.FacesContext;
import javax.faces.convert.*;

@FacesConverter("binaire")
public class Binaire implements Converter {  
   @Override
   public Object getAsObject(FacesContext context, UIComponent component, String value)  {
      StringBuilder chaîne = new StringBuilder(value);
      for (int i=0 ; i<chaîne.length(); )
         if (Character.isDigit(chaîne.charAt(i))) i++;
         else chaîne.deleteCharAt(i);
      String chaîneBinaire = chaîne.toString();
      if (!chaîneBinaire.matches("[01]*")) {
           String sommaire = context.getMessages().hasNext() ? "" : "Aucun résultat : Erreur";
           FacesMessage erreur = new FacesMessage(sommaire, "Il faut saisir un nombre binaire");
           throw new ConverterException(erreur);
      }
      return Integer.parseInt(chaîneBinaire, 2);
   }

   @Override
   public String getAsString(FacesContext context, UIComponent component, Object value) {
      long décimal = Long.parseLong(Integer.toBinaryString((Integer)value));
      DecimalFormat binaire = new DecimalFormat("0000,0000,0000,0000");
      return binaire.format(décimal);
   }
}

J'en profite pour changer également le code du convertisseur pour qu'il gère les messages de la même façon que la validateur. Voici d'ailleurs un exemple d'erreurs, à la fois de convertion et de validation :

Validation personnalisée au travers d'une méthode du bean géré

Nous venons de voir comment comment implémenter un validateur au travers d'une classe spécifique, ce qui, la plupart du temps est préférable puisque nous séparons bien chacune des fonctionnalités.

Il est toutefois envisageable d'intégrer cette validation directement dans le bean géré au travers d'une méthode spécifique, dont le nom est à votre libre initiative, à la condition toutefois que cette méthode possède exactement la même signature que la méthode validate() de l'interface Validator. Voici, par exemple, la méthode nombrePremier() qui réalise la même validation que précédemment :

bean.Calcul.Java
package bean;

import javax.faces.application.FacesMessage;
import javax.faces.bean.*;
import javax.faces.context.FacesContext;

@ManagedBean
@ViewScoped
public class Calcul {
   private int premier;
   private int deuxième;
   private int résultat;

   public int getPremier() { return premier;  }
   public void setPremier(int premier) { this.premier = premier; }
   
   public int getDeuxième() { return deuxième;  }
   public void setDeuxième(int deuxième) { this.deuxième = deuxième; }
   
   public int getRésultat() { return résultat; }
   
   public void ouLogique() { 
      résultat = premier | deuxième; 
      FacesContext context = FacesContext.getCurrentInstance();
      context.addMessage("messages", new FacesMessage("Résultat avec le OU logique"));
   }
   
   public void etLogique() {  
      résultat = premier & deuxième;  
      FacesContext context = FacesContext.getCurrentInstance();
      context.addMessage("messages", new FacesMessage("Résultat avec le ET logique"));       
   }   
   
   public void ouExclusif() {  
      résultat = premier ^ deuxième;  
      FacesContext context = FacesContext.getCurrentInstance();
      context.addMessage("messages", new FacesMessage("Résultat avec le OU Exclusif"));       
   }

   public void nombrePremier(FacesContext context, UIComponent component, Object value)  throws ValidatorException {
      int nombre = (Integer)value;
      for (int i=2; i<=Math.sqrt(nombre); i++) {
         if (nombre%i == 0) {
            String sommaire = context.getMessages().hasNext() ? "" : "Aucun résultat : Erreur";
            FacesMessage erreur = new FacesMessage(sommaire, "Le nombre n'est pas premier");            
            throw new ValidatorException(erreur);
         } 
      }
   }
}

Bien entendu, dans la vue, vous devez appeler votre validateur de façon différente, au moyen d'une expression EL, en faisant tout simplement référence à cette méthode spécifique.

index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    
    <h:head />

    <h:outputStylesheet name="principal.css" />
    
    <h:body>
        <h:form>
            <h:panelGrid columns="2" styleClass="panneau" headerClass="titre" cellspacing="5">
                <f:facet name="header">Calcul binaire<hr /></f:facet>
                <h:inputText value="#{calcul.premier}" converter="binaire" validator="#{calcul.nombrePremier}" id="premier" />
                <h:message for="premier" />              
                <h:inputText value="#{calcul.deuxième}" converter="binaire" validator="#{calcul.nombrePremier}" id="deuxieme" />
                <h:message for="deuxieme" />
                <h:panelGroup>
                    <h:commandButton value="OR" action="#{calcul.ouLogique}" />
                    <h:commandButton value="AND" action="#{calcul.etLogique}" />
                    <h:commandButton value="XOR" action="#{calcul.ouExclusif}" />
                </h:panelGroup>       
                &nbsp;
                <h:inputText value="#{calcul.résultat}" converter="binaire"  readonly="true"/>
                <h:messages layout="table" id="messages" />
            </h:panelGrid>           
        </h:form>
    </h:body>
</html>

 

Choix du chapitre Les flux binaires, les servlet et le contexte de l'application web

Avec JSF, nous avons une structure de type MVC où les pages xhtml s'occupent uniquement de l'affichage alors que les beans gérés s'occupent, de leurs côtés, de tout le traitement. Par ailleurs, le contrôle de l'ensemble des requêtes est assuré par une seule servlet FacesServlet qui travaille ici plutôt en tâche de fond. Du moins, avec JSF, nous nous en préoccupons pas.

Dans certaines situations, ce canevas peut être légèrement modifié afin d'intégrer des servlets supplémentaires qui prennent momentanément le contrôle d'une partie de la page Web, notamment pour gérer les flux binaires, comme des images créées de toute pièce que nous devons déployer dans un flux connexe de la page web principale.

Afin de démontrer l'utilité de ces servlets, nous allons mettre en oeuvre une application Web qui permet de délivrer l'histogramme d'une des photos présente sur le serveur. Tous les traitements nécessaires à cette application sont exécutés côté serveur Web.

L'avantage de ce système, c'est que le poste client n'a pas besoin de disposer d'un quelconque programme installé où même d'une machine virtuelle. Le client a juste besoin de disposer d'un navigateur. Ainsi chaque client peut consulter l'ensemble des photos stockés sur le serveur qui sert alors de serveur de photos.

Modèle MVC

Avec le protocole HTTP, nous manipulons essentiellement du texte. L'affichage des images dans une page Web est toutefois particulier. En effet, n'oubliez pas que, bien que cela ne se voit pas, les images sont des fichiers séparés de la page Web elle-même. Ainsi, pour afficher une image dans une page Web, nous utilisons la balise <img> du HTML et vous devez spécifier ensuite le nom du fichier image (le nom lui-même est un texte) au travers de l'attribut src. Enfin, le navigateur, à l'aide de ce nom, récupère l'image qui est finalement un flux binaire, pour l'afficher à l'endroit où se situe la balise. Dans le cas de JSF, nous avons le même comportement. Toutefois, le nom de la balise est <h:graphicImage> et l'attribut est value.

Attention, cet attribut value de cette balise <h:graphicImage> attend uniquement une entité de type texte et surtout pas un flux binaire. Le problème pour l'application que nous devons réaliser, c'est que les images à afficher ne sont pas des fichiers. En effet, dans un premier temps, nous devons retailler l'image originale afin de proposer une vignette d'une largeur de 300 pixels et permettre ainsi un téléchargement réduit en temps. Par ailleurs, l'histogramme correspondant à cette vignette, est également une image entièrement construite de toute pièce par l'application Web. La difficulté ici, c'est de mettre en relation la page Web avec ces deux images binaires construites par l'application Web.

La solution, au moment de l'utilisation de la balise <h:graphicImage>, est d'utiliser le protocole HTTP en choisissant le bon type MIME. Pour que le protocole soit activé, vous devez donc solliciter soit une nouvelle page xhtml qui va réaliser le traitement souhaité, soit une servlet. Lorsque vous devez présenter un élément qui n'est pas une page web, l'idéal est de prendre une servlet qui reste spécialisée dans le protocole HTTP, qui sait donc reconnaître les différentes requêtes proposée, mais qui est également capable de produire des documents de n'importe quel nature, comme les flux binaires représentant les images.

Il faut noter que les servlets sont bien adaptées à ce genre de situation puisqu'elles disposent de toute l'ossature nécessaire pour gérer convenablement les flux prévus par le protocole HTTP, notamment avec les objets request et response directement intégrés aux méthodes d'appel doGet() et doPost() (voir plus loin).

Récupérer le contexte de la page courante - FacesContext

Le bean géré doit quelquefois être en rapport direct avec la vue actuelle, comme c'est le cas ici, par exemple, lorsque nous proposons de télécharger le fichier image de la photo actuellement affichée.

JSF définit la classe abstraite javax.faces.context.FacesContext pour représenter les informations contextuelles associées au traitement d'une requête et à la production de la réponse correspondante. Cette classe permet d'interagir avec l'interface utilisateur et le reste de l'environnement JSF.

Pour y accéder, vous devez soit utiliser l'objet implicite facesContext directement dans vos pages xhtml, soit obtenir une référence dans vos bean gérés à l'aide de la méthode statique getCurrentInstance() qui renvoie alors l'instance du thread courant (contexte actuel) et vous pourrez alors invoquer une des méthodes prévues dans le tableau ci-dessous :

Voici la démarche à suivre pour récupérer l'objet représentant la page courante :
FacesContext contexte = FacesContext.getCurrentInstance();

Ici, l'objet contexte représente donc la page Web qui est en communication avec notre bean géré. Nous pouvons, dès lors l'utiliser pour effectuer un certain nombre d'opérations intéressantes par l'intermédiaire de méthodes adaptées à la situation. Voici juste quelques unes de ses méthodes.

Méthode Description
addMessage() Ajoute un message d'erreur et permet ainsi d'envoyer un message d'alerte ou d'erreur directement sur le composant client qui a provoqué l'erreur.
getApplication() Retourne l'objet représentant l'ensemble de l'application Web
getAttributes() Renvoie un objet Map représentant l'ensemble des attributs associés à l'instance FacesContext.
getCurrentInstance() Renvoie l'instance FacesContext pour la requête traitée par la thread courant.
getExternalContext() Retourne un objet relatif à tous les éléments externes à la page courante. Cet objet est d'une grande utilité. Il permet de récupérer toutes les informations issues de la requête, donc d'être en relation avec l'objet request que nous avons déjà utilisé avec les servlets. Nous pouvons également connaître l'objet response, l'objet session, etc.
getMaximumSeverity() Renvoie le niveau d'importance maximal pour tout FacesMessage mis en file d'attente.
getMessages() Renvoie une collection de FacesMessage.
getViewRoot() Renvoie le composant racine associé à la requête.
release() Libère les ressources associées à cette instance de FacesContext.
renderResponse() Signale à l'implémentation JSF que le contrôle devra être transmis à la phase de rendu de la réponse dès la fin de l'étape de traitement courante de la requête, en ignorant les étapes qui n'ont pas encore été exécutées.
responseComplete() Signale à l'implémentation JSF que la réponse HTTP de cette requête a déjà été produite et que le cycle de vie du traitement de la requête doit se terminer dès la fin de l'étape en cours.

Contexte de l'application Web - ExternalContext

La classe javax.faces.context.ExternalContext permet d'être en relation avec tous les éléments qui sont en relation avec notre page Web. Ainsi, il est possible de connaître tout ce qui concerne la requête, la réponse, la session en cours, etc.

Voici comment obtenir ce type d'info :
FacesContext contexte = FacesContext.getCurrentInstance();
ExternalContext externe = contexte.getExternalContext();
Méthode Description
addApplicationMap() Retourne les objets (les attributs) stockés dans l'application Web.
getInitParameter() Retourne le paramètre initialisé dans le contexte de l'application Web spécifié par le descripteur de déploiement.
getInitParameterMap() Délivre l'ensemble des paramètres d'initialisation de l'application Web.
getRemoteUser() Procure le nom de loggin de l'utilisateur fabriqué dans la requête courante.
getRequestCookieMap() Récupère l'ensemble des cookies de la requête.
getRequest() Récupère l'objet request représentant la requête envoyée.
getRequestContextPath() Retourne la portion d'URL qui correspond à l'emplacement de l'application Web.
getRequestHeaderMap() Retourne l'ensemble de l'en-tête de la requête.
getRequestHeaderValuesMap() Retourne l'ensemble de l'en-tête de la requête. Les valeurs sont cette fois-ci sous forme de tableaux de chaînes pour certains éléments d'en-tête.
getRequestMap() Retourne les attributs de l'application courante.
getRequestParameterMap() Retourne les paramètres de la requête.
getRequestParameterNames() Retourne les paramètres de la requête en passant par un itérateur.
getRequestParameterValuesMap() Retourne les paramètres de la requête en prenant en compte plutôt les tableaux de chaînes.
getRequestPathInfo() Retourne uniquement la portion de l'URL qui suit le nom de l'application Web.
getRessource() Permet de récupérer l'URL d'une ressource.
getRessourceAsStream() Récupère une ressource sous forme de flux d'entrée.
getResponse() Récupère l'objet response représentant le flux sortant en connexion avec le navigateur.
getSession() Mise en oeuvre ou consultation d'une session.
getSessionMap() Récupère les attributs créés dans la session.
log(String message) Envoie un message dans le journal de l'application Web.
Après toutes ces considérations techniques, nous allons mettre en oeuvre l'application web qui consiste à visualiser sous forme de vignette une des photos archivées sur le serveur dont la liste est diffusée à l'aide d'une boîte de liste. A chacune de ces photos est proposée l'histogramme correspondant. Il est ensuite possible de télécharger la photo en taille réelle depuis le serveur d'applications.

Pour comprendre le traitement relatif aux images, comme retailler une image ou le calcul d'un histogramme, revoyez les cours sur le traitement d'images.

bean.Traitement.java
package bean;

import java.awt.image.*;
import java.io.*;
import javax.annotation.PostConstruct;
import javax.faces.bean.*;
import javax.faces.context.*;
import javax.servlet.http.*;

@ManagedBean
@SessionScoped
public class Traitement {
   private String nomFichier;
   private BufferedImage vignette;
   private int largeurVignette = 300;
   private final String répertoire = "F:/Photos/";
   private String[] fichiers;

   @PostConstruct
   void init() {
       File photos = new File(répertoire);
       fichiers = photos.list();
       if (fichiers.length!=0) nomFichier = fichiers[0];
   }
   
   public String[] getFichiers() {  return fichiers; }
   
   public int getLargeurVignette() { return largeurVignette; }

   public long getPoids() {  return new File(répertoire+nomFichier).length();  }
   
   public BufferedImage getVignette() { return vignette;  }
   public void setVignette(BufferedImage vignette) {  this.vignette = vignette;  }

   public String getNomFichier() { return nomFichier;  }   
   public void setNomFichier(String nomFichier) { this.nomFichier = nomFichier;  }   
   
   public String getPath() { return répertoire+nomFichier; }
      
   public void télécharger() throws IOException {
      ExternalContext externe = FacesContext.getCurrentInstance().getExternalContext();
      HttpServletResponse réponse = (HttpServletResponse) externe.getResponse();
      File fichier = new File(répertoire+nomFichier);
      réponse.setContentType("application/octet-stream");
      réponse.setContentLength((int)fichier.length());
      réponse.setHeader("Content-disposition", "inline; filename=\""+nomFichier+"\"");      
      FileInputStream lectureOctets = new FileInputStream(fichier);
      byte[] octets = new byte[(int) fichier.length()];
      lectureOctets.read(octets);
      OutputStream out = new BufferedOutputStream(réponse.getOutputStream());
      out.write(octets);
      out.close();
   }
}  
  1. Ce bean géré sert d'intermédiaire entre les différents composants et factorise tous les éléments à prendre en compte.
  2. Il dispose pour cela d'un certain nombre de propriétés utiles à la fois pour la vue, bien entendu, mais également pour permettre des échanges avec les servlets qui vont créer les deux images personnalisées : la vignette et l'histogramme.
  3. Notamment, la propriété vignette n'est utile que pour les servlets mais pas pour la vue directement. En effet, c'est la servlet LireImage qui génère cette vignette, mais elle demeure importante pour la génération de l'histogramme qui est réalisé par la servlet Histogramme.
  4. La méthode télécharger() permet de télécharger la photo originale sélectionnée, présente dans le disque dur du serveur. Là aussi, nous devons envoyer un flux binaire, non interprétable ("application/octet-stream"), qui a pour conséquence de faire apparaître la boîte de dialogue du navigateur qui permet de récupérer le fichier.
  5. Nous aurions pu déployer ce flux binaire au travers d'une servlet mais, dans ce cas précis, l'alternative intéressante est de le faire tout simplement au travers d'une méthode du bean géré. Venons en maintenant au contenu de cette méthode.
  6. Il est nécessaire de savoir où envoyer ce flux binaire. Nous devons pour cela récupérer le contexte externe. Avec ce contexte, vous pouvez ensuite dialoguer avec le navigateur du client, mais attention, il faut bien entendu respecter le protocole HTTP. La classe HttpServletResponse, la même classe utilisée par les servlets, est tout à fait spécialisée pour renvoyer une réponse formatée dans ce protocole. Nous pouvons ainsi, d'une part régler la partie en-tête de la réponse afin de bien préciser la nature et les caractéristiques du document, et d'autre part envoyer la globalité du fichier image, en format binaire, dans le même flux de réponse.
servlet.LireImage.java
package servlet;

import java.awt.geom.AffineTransform;
import java.awt.image.*;
import java.io.*;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet("/LireImage")
public class LireImage extends HttpServlet {
   private BufferedImage image;
   private BufferedImage vignette;
   private bean.Traitement traitement;

   private void créerVignette() {
      double ratio = (double)traitement.getLargeurVignette()/image.getWidth();
      vignette = new BufferedImage((int)(image.getWidth()*ratio), (int)(image.getHeight()*ratio), image.getType());
      AffineTransform retailler = AffineTransform.getScaleInstance(ratio, ratio);
      int interpolation = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
      AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation);
      retaillerImage.filter(image, vignette);      
   }   

   @Override
   protected void doGet(HttpServletRequest request, HttpServletResponse response)  throws ServletException, IOException {
      response.setContentType("image/png");
      OutputStream out = response.getOutputStream();
      traitement = (bean.Traitement) request.getSession().getAttribute("traitement");
      image = ImageIO.read(new URL("file:///"+traitement.getPath()));
      créerVignette();
      traitement.setVignette(vignette);
      ImageIO.write(vignette, "PNG", out);
      out.close();
   }
}
  1. Cette première servlet LireImage, comme nous l'avons déjà évoqué permet de générer une vignette, photo en taille réduite de l'image originale. Je rappelle que les servlets sont des classes comme les autres mais sont également spécialisées pour interpréter et générer le protocole HTTP.
  2. Comme toutes les autres classes, avec les servlets, nous pouvons avoir autant d'attributs et de méthodes que nous voulons. Il existe toutefois une méthode importante qui se nomme doGet() et qui est automatiquement appelée lorsqu'une requête de type GET est envoyée au serveur. Dans les servlets, il existe également la méthode doPost() pour être en corrélation avec les requêtes de type POST.
  3. Ces méthodes permettent de dialoguer facilement avec le contexte extérieur puisqu'elles disposent de paramètres adaptés à la situation qui sont de type HttpServletRequest et HttpServletResponse.
  4. Avec l'objet response notamment, comme précédemment, nous pouvons renseigner l'en-tête de la réponse HTTP et, après la génération de la vignette, envoyer l'ensemble de l'image dans le flux de sortie au format binaire prévu.
  5. Remarquez toutefois, qu'avant d'envoyer la vignette au navigateur client, nous l'enregistrons au préalable dans le bean géré traitement. Cette vignette pourra ainsi être exploitée par l'autre servlet qui fabriquera l'histogramme en conséquence.
  6. A ce sujet, il est possible d'être en relation avec un bean géré, à la condition toutefois que ce dernier est une durée de vie de type session. Ainsi, l'objet request nous permet de se connecter à cet objet particulier, qui se nomme dans le cadre d'une application web un attribut, au moyen de la méthode getAttribute(). Il est nécessaire, par contre, d'être d'abord dans la session correspondante, ceci au moyen de la méthode getSession().
  7. Vous remarquez également la présence de l'annotation @WebServlet, à l'intérieur de laquelle se trouve le nom de l'URL qu'il faudra impérativement préciser au niveau de la vue, à l'aide de la balise <h:graphicImage url="LireImage" />, pour accéder à votre servlet et récupérer ainsi l'image correspondant à la vignette.
  8. Avec cette simple annotation, vous n'avez plus à vous préoccuper de compléter le descripteur de déploiement web.xml. Nous avons ainsi une production des applications web beaucoup plus rapide et efficace.
servlet.Histogramme.java
package servlet;

import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.*;
import java.io.*;
import javax.imageio.ImageIO;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet("/Histogramme")
public class Histogramme extends HttpServlet {
   private BufferedImage vignette;
   private BufferedImage histogramme;
   private bean.Traitement traitement;
   private int[] rouge;
   private int[] vert;
   private int[] bleu;
   private int largeur = 256;
   private int hauteur;
   private Graphics2D dessin;

   private void calculerHistogramme() {
      hauteur = vignette.getHeight();
      rouge = new int[256];
      vert = new int[256];
      bleu = new int[256];      
      récupérerRVB();
      tracerHistogrammes();      
   }   
   
   private void récupérerRVB() {
     Raster trame = vignette.getRaster();
     ColorModel modèle = vignette.getColorModel();
     for (int y=0; y<vignette.getHeight(); y++)
          for (int x=0; x<vignette.getWidth(); x++) {
            Object données = trame.getDataElements(x, y, null);
            rouge[modèle.getRed(données)]++;
            vert[modèle.getGreen(données)]++;
            bleu[modèle.getBlue(données)]++;
          }           
   }
   
   private void tracerHistogrammes() {
      histogramme = new BufferedImage(largeur, hauteur, BufferedImage.TYPE_INT_ARGB);
      dessin = histogramme.createGraphics();
      Rectangle2D rectangle = new Rectangle2D.Double(0, 0, largeur-1, hauteur-1);
      dessin.setPaint(Color.black);
      dessin.fill(rectangle);      
      changerAxes();
      dessin.setPaint(new Color(1F, 0F, 0F, 0.7F));
      tracerHistogramme(rouge);
      dessin.setPaint(new Color(0F, 1F, 0F, 0.7F));
      tracerHistogramme(vert);
      dessin.setPaint(new Color(0F, 0F, 1F, 0.7F));
      tracerHistogramme(bleu);
   }

   private void changerAxes() {
      dessin.translate(0, hauteur);
      double surfaceImage = vignette.getWidth()*vignette.getHeight();
      double surfaceHistogramme = histogramme.getWidth()*histogramme.getHeight();
      dessin.scale(1, -surfaceHistogramme/surfaceImage/3);
   }   
      
   private void tracerHistogramme(int[] couleur) {
      for (int i=0; i<255; i++) 
         dessin.drawLine(i, 0, i, couleur[i]);              
   }  

   @Override
   protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      response.setContentType("image/png");
      OutputStream out = response.getOutputStream();
      traitement = (bean.Traitement) request.getSession().getAttribute("traitement");
      vignette = traitement.getVignette();
      calculerHistogramme();
      ImageIO.write(histogramme, "PNG", out);
      out.close();
   }
}

Nous retrouvons la même architecture que la servlet précédente. Toutefois, le traitement étant beaucoup plus conséquent, la servlet dispose donc de pas mal de méthodes privées.

Nous avons bien séparé les différents traitements de fond à réaliser : permettre l'interaction entre les différents composants et le téléchargement de la photo complète au moyen du bean géré traitement, la génération de la vignette correspondante au moyen de la servlet LireImage, et enfin la génération de l'histogramme au moyen de la servlet Histogramme.

ATTENTION : Lorsque nous utilisons plusieurs servlets, ces dernières sont exécutées normalement dans des threads séparées, ce qui est la plus part du temps un énorme avantage puisque nous faisons du traitement parallèle. Dans notre cas, cela peut être problématique puisque la servlet de l'histogramme doit s'exécuter après la génération de la vignette (l'histogramme est déduit de cette dernière).

Dans un cas comme celui-ci, il est possible de réaliser l'ensemble des traitements uniquement à l'intérieur du bean géré, les servlets ne seront utilisées que pour envoyer les flux binaires séparés utiles pour la page web dans son ensemble.

Nous ne pouvons pas nous passer des servlets, parce que si nous envisagions d'utiliser une méthode du bean géré, à l'image de la méthode télécharcher(), cela aurait comme conséquence d'ouvrir de nouvelles page web pour ces deux images générées, et non de les intégrer à l'intérieur d'une même page web commune (un fichier image est bien indépendant de la page web qui le visualise).

/resources/css/principal.css
root { 
    display: block;
}

body { 
    background: green; 
    color: yellow; 
    font-weight: bold; 
    text-shadow: 0px 0px 10px yellow; 
    text-align: center;
}

.panneau { 
    background: darkgreen; 
    border-radius: 5px; 
    box-shadow: 2px 2px 5px black, -2px -2px 5px lightgreen; 
}

.titre { font-size: 20px; }

img {
    border-radius: 10px;
    box-shadow: 2px 2px 5px black;
}
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    
    <h:head />
    
    <h:outputStylesheet library="css" name="principal.css" />
   
    <h:body>
        <h:form>
            <h:panelGrid columns="2" cellpadding="3" styleClass="panneau">
                <f:facet name="header">
                    <h:outputText value="Fichier image : " /> 
                    <h:selectOneMenu value="#{traitement.nomFichier}" onchange="submit()">
                       <f:selectItems value="#{traitement.fichiers}" />
                    </h:selectOneMenu>                   
                </f:facet>
                <h:graphicImage url="LireImage" />
                <h:graphicImage url="Histogramme" />
                <h:outputText value="#{traitement.poids}">
                    <f:convertNumber pattern="#,##0 octets" />
                </h:outputText>
                <h:commandButton value="Télécharger fichier" action="#{traitement.télécharger}" />
            </h:panelGrid>           
        </h:form>
    </h:body>
</html>

Cette fois-ci, dans la balise <h:graphicImage>, nous utilisons plutôt l'attribut url en lieu et place de l'attribut value. C'est au travers de cet attribut url que nous faisons appel à la servlet correspondante. Vu que nous utilisons cet attribut, nous comprenons bien que le la requête soumise au serveur est de type GET.

Petite conclusion sur cette première approche

Nous avons bien séparé les différents traitements de fond à réaliser : permettre l'interaction entre les différents composants et le téléchargement de la photo complète au moyen du bean géré traitement, la génération de la vignette correspondante au moyen de la servlet LireImage, et enfin la génération de l'histogramme au moyen de la servlet Histogramme.

Dans un cas comme celui-ci, il est possible de réaliser l'ensemble des traitements uniquement à l'intérieur du bean géré, les servlets ne seront utilisées que pour envoyer les flux binaires séparés utiles pour la page web dans son ensemble.

bean.Traitement.java
package bean;

import java.awt.image.*;
import java.io.*;
import javax.annotation.PostConstruct;
import javax.faces.bean.*;
import javax.faces.context.*;
import javax.servlet.http.*;

@ManagedBean
@SessionScoped
public class Traitement {
   private BufferedImage image;
   private BufferedImage vignette;
   private BufferedImage histogramme;   
   private int largeurVignette = 300;
   private int[] rouge;
   private int[] vert;
   private int[] bleu;
   private int largeur = 256;
   private int hauteur;
   private Graphics2D dessin;
   private String nomFichier;
   private final String répertoire = "F:/Photos/";
   private String[] fichiers;

   @PostConstruct
   void init() throws IOException {
       File photos = new File(répertoire);
       fichiers = photos.list();
       if (fichiers.length!=0) setNomFichier(fichiers[0]);
   }
   
   public BufferedImage getVignette() { return vignette;  }
   public BufferedImage getHistogramme() { return histogramme;  }
   
   public String getNomFichier() { return nomFichier;  }   
   public String getPath() { return répertoire+nomFichier; }
   
   public long getPoids() {  return new File(répertoire+nomFichier).length();  }
   
   public void setNomFichier(String nomFichier) throws IOException {
      this.nomFichier = nomFichier;
      image = ImageIO.read(new URL("file:///"+répertoire+nomFichier));
      créerVignette();
      calculerHistogramme();
   }
   
   public void télécharger() throws IOException {
      ExternalContext externe = FacesContext.getCurrentInstance().getExternalContext();
      HttpServletResponse réponse = (HttpServletResponse) externe.getResponse();
      File fichier = new File(répertoire+nomFichier);
      réponse.setContentType("application/octet-stream");
      réponse.setContentLength((int)fichier.length());
      réponse.setHeader("Content-disposition", "inline; filename=\""+nomFichier+"\"");      
      FileInputStream lectureOctets = new FileInputStream(fichier);
      byte[] octets = new byte[(int) fichier.length()];
      lectureOctets.read(octets);
      OutputStream out = new BufferedOutputStream(réponse.getOutputStream());
      out.write(octets);
      out.close();
   }   

   private void créerVignette() {
      double ratio = (double)traitement.getLargeurVignette()/image.getWidth();
      vignette = new BufferedImage((int)(image.getWidth()*ratio), (int)(image.getHeight()*ratio), image.getType());
      AffineTransform retailler = AffineTransform.getScaleInstance(ratio, ratio);
      int interpolation = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
      AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation);
      retaillerImage.filter(image, vignette);      
   }   

   private void calculerHistogramme() {
      hauteur = vignette.getHeight();
      rouge = new int[256];
      vert = new int[256];
      bleu = new int[256];      
      récupérerRVB();
      tracerHistogrammes();      
   }   
   
   private void récupérerRVB() {
     Raster trame = vignette.getRaster();
     ColorModel modèle = vignette.getColorModel();
     for (int y=0; y<vignette.getHeight(); y++)
          for (int x=0; x<vignette.getWidth(); x++) {
            Object données = trame.getDataElements(x, y, null);
            rouge[modèle.getRed(données)]++;
            vert[modèle.getGreen(données)]++;
            bleu[modèle.getBlue(données)]++;
          }           
   }
   
   private void tracerHistogrammes() {
      histogramme = new BufferedImage(largeur, hauteur, BufferedImage.TYPE_INT_ARGB);
      dessin = histogramme.createGraphics();
      Rectangle2D rectangle = new Rectangle2D.Double(0, 0, largeur-1, hauteur-1);
      dessin.setPaint(Color.black);
      dessin.fill(rectangle);      
      changerAxes();
      dessin.setPaint(new Color(1F, 0F, 0F, 0.7F));
      tracerHistogramme(rouge);
      dessin.setPaint(new Color(0F, 1F, 0F, 0.7F));
      tracerHistogramme(vert);
      dessin.setPaint(new Color(0F, 0F, 1F, 0.7F));
      tracerHistogramme(bleu);
   }

   private void changerAxes() {
      dessin.translate(0, hauteur);
      double surfaceImage = vignette.getWidth()*vignette.getHeight();
      double surfaceHistogramme = histogramme.getWidth()*histogramme.getHeight();
      dessin.scale(1, -surfaceHistogramme/surfaceImage/3);
   }   
      
   private void tracerHistogramme(int[] couleur) {
      for (int i=0; i<255; i++) 
         dessin.drawLine(i, 0, i, couleur[i]);              
   }  

}  
servlet.LireImage.java
package servlet;

import java.io.*;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet("/LireImage")
public class LireImage extends HttpServlet {

   @Override
   protected void doGet(HttpServletRequest request, HttpServletResponse response)  throws ServletException, IOException {
      response.setContentType("image/png");
      OutputStream out = response.getOutputStream();
      bean.Traitement traitement = (bean.Traitement) request.getSession().getAttribute("traitement");
      ImageIO.write(traitement.getVignette(), "PNG", out);
      out.close();
   }
}
servlet.Histogramme.java
package servlet;

import java.io.*;
import javax.imageio.ImageIO;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet("/Histogramme")
public class Histogramme extends HttpServlet {

   @Override
   protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      response.setContentType("image/png");
      OutputStream out = response.getOutputStream();
      bean.Traitement traitement = (bean.Traitement) request.getSession().getAttribute("traitement");
      ImageIO.write(traitement.getHistogramme(), "PNG", out);
      out.close();
   }
}
Proposer qu'une seule servlet

Ces deux servlets sont très similaires. L'idée serait d'en proposer qu'une seule qui se préoccupe uniquement d'envoyer un flux binaire représentant une image au format PNG, quelque soit cette image. C'est la page web qui décidera de quelle image il s'agit, soit la vignette, soit l'histogramme.

Vu que nous utilisons une requête de type GET dans le protocole HTTP, il est alors facile de proposer un paramètre supplémentaire, que nous appelons image à l'intérieur duquel nous précisons le type d'image que nous souhaitons visualiser. Voici ainsi la modification que nous apportons sur la vue.

index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    
    <h:head />
    
    <h:outputStylesheet library="css" name="principal.css" />
   
    <h:body>
        <h:form>
            <h:panelGrid columns="2" cellpadding="3" styleClass="panneau">
                <f:facet name="header">
                    <h:outputText value="Fichier image : " /> 
                    <h:selectOneMenu value="#{traitement.nomFichier}" onchange="submit()">
                       <f:selectItems value="#{traitement.fichiers}" />
                    </h:selectOneMenu>                   
                </f:facet>
                <h:graphicImage url="LireImage?image=vignette" />
                <h:graphicImage url="LireImage?image=histogramme" />
                <h:outputText value="#{traitement.poids}">
                    <f:convertNumber pattern="#,##0 octets" />
                </h:outputText>
                <h:commandButton value="Télécharger fichier" action="#{traitement.télécharger}" />
            </h:panelGrid>           
        </h:form>
    </h:body>
</html>

Nous n'utilisons donc que la première servlet LireImage à l'intérieur de laquelle nous récupérons la valeur du paramètre soumis par la vue et nous prenons l'image désirée à l'aide du bean géré. Voici la modification de cette servlet en conséquence :

servlet.LireImage.java
package servlet;

import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet("/LireImage")
public class LireImage extends HttpServlet {

   @Override
   protected void doGet(HttpServletRequest request, HttpServletResponse response)  throws ServletException, IOException {
      response.setContentType("image/png");
      OutputStream out = response.getOutputStream();
      bean.Traitement traitement = (bean.Traitement) request.getSession().getAttribute("traitement");
      boolean isVignette = request.getParameter("image").equals("vignette");
      BufferedImage image = isVignette ? traitement.getVignette() : traitement.getHistogramme();
      ImageIO.write(image, "PNG", out);
      out.close();
   }
}

 

Choix du chapitre Contrôler les événements issus du client avec JSF

Dans ce chapitre, nous allons découvrir un des gros avantages de JSF qui permet la gestion des événements côté client. Lorsque l'utilisateur fait un choix, valide une information ou clique sur une partie d'une image par exemple, ces types d'événement peuvent alors être pris en compte en temps réel et ainsi soumettre ces choix instantanément au serveur afin qu'il réalise l'opération souhaitée et délivre finalement le résultat en reconstituant la page Web en cours.

Avec cette gestion d'événements, nous ne sommes plus obligé de proposer un bouton de soumission par type de sélection, comme les menus déroulant, les boutons radios, les cases à cocher, etc. Dès qu'une sélection est faite, elle est soumise instantanément au serveur, afin d'avoir un affichage adapté à la nouvelle situation et ainsi de proposer de nouveaux choix à l'utilisateur relatif au premiers déjà réalisés. L'évolution de la page est ainsi contextuelle et suit, à la volée, la saisie de l'opérateur.

Il est à noter que la gestion d'événements concerne systématiquement la même page Web. Il faut bien comprendre que la page évolue pour faire en sorte que la saisie des valeurs soit le plus rapide possible en proposant des choix adaptés suivant l'évolution de la saisie déjà réalisés par l'opérateur. Une fois que le formulaire est rempli, nous pouvons passer à une autre étape, souvent avec l'affichage d'une nouvelle page Web, en cliquant cette fois-ci sur l'unique bouton de soumission.

Les différents types d'événement

Le modèle d'événements proposé par JSF est très proche de celui utilisé par Java SE avec ses applications SWING. Les développeurs Web ne sont généralement pas familiarisés avec ce modèle de développemnt utilisant MVC en natif, considérablement différent du modèle utilisé par le protocole HTTP.

Il existe en effet deux concepts dans ce mécanisme : les événements eux mêmes (events) et les écouteurs (listeners). Un événement est généré, par exemple, lorsque la valeur d'une zone de saisie est modifiée. L'écouteur ou listener est, de son côté, responsable de l'implémentation du code dans l'application lorsque l'événement est déclenché.

Par exemple, lorsque l'utilisateur saisie un texte ou change sa valeur, nous souhaitons mettre à jour automatiquement un message assosié. Pour cela le framework déclanche un événement lorsque la valeur change dans le champ. Le développeur doit donc associer un écouteur (listener) pour cet événement (event). Autre exemple, lorsque nous utilisons un bouton et que nous cliquons dessus, celui-ci va envoyer un événement pour indiquer son changement d'état et son listener associé va répondre à l'événement.

En fait, avec JSF, lorsque nous cliquons sur le bouton, rien ne se passe réellement sur le client, les changements sont évalués par le cycle de vie des requêtes. JSF propose ainsi quatre types d'événements avec la possibilité de les écouter par l'intermédiaire de listeners :

  1. Changement de valeur : ValueChangeListener : Ce type d'événement est associé à des changements de valeur des composants. Peut être utilisé par les zones de saisie, les boutons radio, les cases à cocher, les menus déroulants, les listes.
  2. Validation par clic de souris ou touche "Entrée" : ActionListener : Des événements d'applications déclenchés par l'utilisateur (lien, bouton et autres).
  3. Différentes phases du cycle de traitement des requêtes JSF : PhaseListener : déclenchés avant et après chacune des six phases du cycle de vie.
  4. Evénements système : SystemEventListener : En relation avec l'application web dans son ensemble. Permet de prendre en compte certaines phases particulières, comme l'instant de démarrage de l'application web, le changement de vue, le prise en compte de la session, etc.

Cycle des événements par rapport au cycle de traitement des requêtes

Avant de traiter ces différents types d'événement, il est bon de revenir sur le cycle de vie afin de connaître à quel moment sont pris en compte les deux principaux type d'événements dans le cycle de traitement des requêtes.

  1. Les événements de type ValueChangeListener sont pris en compte après la phase 3 du cycle de traitement des requêtes, c'est-à-dire juste après la conversion et la validation des valeurs d'entrée, mais aussi juste avant l'injection de ces valeurs dans les propriétés respectives du bean géré. ATTENTION, lorsque vous désirez traiter l'action associé à l'événement, vous ne pouvez pas encore prendre la valeur des attributs du bean géré puisque ces derniers ne sont pas mis à jour. Lorsque vous désirez prendre la nouvelle valeur, saisie ou choisi par l'opérateur, vous êtes donc obligé de prendre systématiquement la méthode getNewValue() de la classe ValueChangeEvent.
  2. Les événements de type ActionListener sont pris en compte juste avant le traitement de l'action demandé qui permet d'évoluer dans la navigation des pages. Ainsi, le traitement demandé par ce type d'événement permet de mettre à jour un certain nombre d'information avant d'évoluer vers une autre page.

Evénement lié au changement de valeur

Nous allons désormais rentrer dans le vif du sujet en étudiant en profondeur ce qui se passe réellement lorsqu'un événement de type "Changement de valeur" se produit. Typiquement, ce type d'événement est extrêmement utilisé lorqu'un composant dépend d'un autre dans la même page web.

Par exemple, j'aimerai avoir automatiquement la liste des prénoms associés à un nom que je sélectionne au préalable depuis une autre liste, afin de renseigner le plus rapidement possible le formulaire d'une page. Dans le même ordre d'idée, il pourrait être sympatique de connaître l'ensemble de l'identité de la personne choisi en sélectionnant cette fois-ci le prénom parmi la liste proposée automatiquement.

Avec JSF, du côté de la vue, un changement de valeur est toujours associé à une méthode JavaScript de type onchange ou onclick. JSF propose alors pour cela l'attribut valueChangeListener à l'intérieur duquel vous spécifier la méthode du bean qui prend en compte ce type d'événement.

Remarques importantes : Ce qui fait le caractère d'instantanéité est l'appel de la méthode JavaScript (très souvent submit()). Nous avons d'ailleurs déjà utiliser cette méthode lors du chapitre précédent. Ainsi, à chaque fois que nous choisissions une nouvelle photo dans la liste déroulante, cette dernière s'affichait instantanément à la place de la précédente. Je vous rappelle d'ailleurs la partie du code correspondant à la vue :
<h:selectOneMenu value="#{traitement.nomFichier}" onchange="submit()">
     <f:selectItems value="#{traitement.fichiers}" />
</h:selectOneMenu>                   

A ce moment là, nous n'avons jamais évoqué cette notion d'événement (nous nous servions tout simplement de l'attribut value) et nous pourrions à juste titre nous demander de l'opportunité d'une telle gestion (en prenant en compte dans ce cas là l'attribut valueChangeListener).

  1. Dans le cas de l'application web de traitement d'image, je rappelle que l'objectif (sa finalité) était de visualiser une photo en modèle réduit avec son histogramme suivant le choix effectué dans la liste déroulante. Mise à part le caractère d'instantanéité, nous sommes devant une situation classique de soumission d'une requête avec une réponse en conséquence (la finalité est d'afficher la vignette avec son histogramme).
  2. Dans le cas d'une gestion d'événements, dès qu'une sélection est faite, elle est soumise instantanément au serveur comme dans le cas précédent, dans le soucis cette fois-ci de permettre un affichage adapté à la nouvelle situation et ainsi de proposer de nouveaux choix à l'utilisateur relatif au premiers déjà réalisés. L'évolution de la page est ainsi contextuelle et suit, à la volée, la saisie de l'opérateur.

Il est à noter que la gestion d'événements concerne systématiquement la même page Web. Il faut bien comprendre que la page évolue pour faire en sorte que la saisie des valeurs soit le plus rapide possible en proposant des choix adaptés suivant l'évolution de la saisie déjà réalisés par l'opérateur. Une fois que le formulaire est rempli, nous pouvons, en finalité, passer à une autre étape, souvent avec l'affichage d'une nouvelle page Web, en cliquant cette fois-ci sur l'unique bouton de soumission.

Quand appliquer une gestion événementielle ? Lorsque vous disposez de plusieurs zones de sélection dont les choix sont figés, il s'agit d'une soumission classique. Lorsque par contre, des sélections évoluent en fonctions des autres, il est nécessaire de prévoir une gestion événementielle qui permet de prendre en compte les changements proposés sur la même page avant d'aboutir à la requête définitive.

Techniques de mise en oeuvre d'un événement de type changement de valeur

Lorsque vous désirez prendre en compte cet événement, vous devez alors spécifier la méthode du bean qui va le traiter à l'aide de l'attribut valueChangeListener de la balise concernée. Par ailleurs, vous devez demander à soumettre immédiatement votre requête en sollicitant la fonction JavaScript submit(). Pour une zone de saisie, voici ce qu'il faut donc écrire d'un point de vue général :

<h:inputText value="#{bean.propriété}" valueChangeListener="#{bean.méthodeEvénement}" onchange="submit()" />

Ensuite, dans votre JavaBean, vous devez donc implémenter cette méthode particulière qui doit alors posséder un attribut de type ValueChangeEvent :

public class UnJavaBeanGéré {
   private int propriété;
   
   public int getPropriété() {
      return propriété;
   }

   public void setPropriété(int propriété) {
      this.propriété = propriété;
   }

   public void méthodeEvénement(ValueChangeEvent evt) {
      propriété = (Integer)evt.getNewValue();
			...
   }
}

Il est très facile de gérer les événements. Ici, l'objet evt de type ValueChangeEvent possède un certain nombre de méthodes. La plus importante est certainement la méthode getNewValue() qui permet de récupérer instantanément la nouvelle valeur saisie dans la zone d'entrée.

Nous pourrions nous poser la question de savoir pourquoi récupérer cette valeur saisie au travers de cette méthode getNewValue(), alors que normalement la propriété correspondante doit s'occuper elle-même de la récupérer. Nous l'avons déjà évoqué lors du cycle de traitement des requêtes, la propriété ne l'a pas encore récupérée puisque cette phase de récupération se situe après.

A ce sujet, dans bien des cas, nous n'avons même pas besoin de créer une propriété spécifique puisque nous avons la possibilité de récupérer la valeur souhaité au travers de cette méthode getNewValue(). Ceci est vrai pour tout ce qui est sélections, par contre, pour les zones de saisie, vous êtes obligé de conserver la propriété. Ainsi, en reprenant le canevas général ci-dessus nous pouvons nous passer de l'attribut value et garder uniquement l'attribut valueChangeListener :

<h:selectOneMenu valueChangeListener="#{bean.méthodeEvénement}" onchange="submit()">

public class UnJavaBeanGéré {
   private int attribut;

   public void méthodeEvénement(ValueChangeEvent evt) {
      attribut = (Integer)evt.getNewValue();
			...
   }
}
Méthode Retour Description de la classe javax.faces.event.ValueChangeEvent
addComponent() UIComponent Retourne le composant qui a sollicité l'événement.
getNewValue() Object Retourne la valeur (ou le choix) proposée par le composant représentant l'entrée qui a sollicité l'événement, après toutefois être passé par la phase de conversion et de validation.
getOldValue() Object Retourne l'ancienne valeur (dans le cas où vous n'avez pas prévu d'attribut pour cette propriété).
Mise en oeuvre de l'application web sur la gestion du personnel

Afin de valider cette partie, nous allons mettre en oeuvre, sous forme très réduite, une application web qui permet de faire la gestion du personnel dont je rappelle l'aperçu :

entité.Personne.java
package entité;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@NamedQueries({
    @NamedQuery(name="personneExiste", query="SELECT p FROM Personne p WHERE p.nom = :nom AND p.prenom = :prenom"),
    @NamedQuery(name="noms", query="SELECT DISTINCT p.nom FROM Personne p ORDER BY p.nom"),
    @NamedQuery(name="prénoms", query="SELECT p.prenom FROM Personne p WHERE p.nom = :nom ORDER BY p.prenom")
})
public class Personne implements Serializable {
    @Id  @GeneratedValue(strategy= GenerationType.IDENTITY)
    private long id;
    private String nom; 
    private String prenom;
    private int age;
    
    public long getId() {  return id; }
    public String getNom() { return nom;  }
    public String getPrenom() { return prenom;   }
    public int getAge() { return age; }   
    
    public void setNom(String nom) {  this.nom = nom.toUpperCase();  }

    public void setPrenom(String prénom) {
       if (prénom.isEmpty()) return;
       StringBuilder chaine = new StringBuilder(prénom.toLowerCase());
       chaine.setCharAt(0, Character.toUpperCase(chaine.charAt(0)));
       this.prenom = chaine.toString();
    }

    public void setAge(int âge) {  this.age = âge;  } 
}
  1. Dans cette application web, nous rendons les identités des personnels persistants au travers d'une entité Personne qui propose, bien entendu, les propriétés relatant de l'ensemble de l'identité.
  2. Nous en profitons pour enregistrer dans la base de données le nom du personnel en majuscule, le prénom en minuscule avec la première lettre en majuscule.
  3. Enfin, nous prévoyons un certain nombre de requête nommées pour savoir respectivement ; si une personne n'est pas déjà enregistrée, d'avoir la liste de tous les noms du personnel et d'avoir la liste de tous les prénoms associés à un nom en particulier.
bean.Gestion.java
package bean;

import entité.Personne;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.inject.Named;
import javax.enterprise.context.SessionScoped;
import javax.ejb.Stateful;
import javax.faces.event.ValueChangeEvent;
import javax.persistence.*;

@Named
@SessionScoped
@Stateful
public class Gestion {
    @PersistenceContext
    private EntityManager bd;
    private Personne personne;
    private enum Enregistrement {Nouveau, Enregistré, ExisteDéjà};
    private Enregistrement etat;
    private String nom;
    private String prénom;
    private List<String> noms;
    private List<String> prénoms;
    
    public Personne getPersonne() { return personne;  }
    public void setPersonne(Personne personne) { this.personne = personne;  }
    
    public String getEtat() { return etat.toString(); }

    public String getNom() { return nom;  }    
    public void setNom(String nom) {  this.nom = nom;  }
  
    public List<String> getNoms() {  return noms;  }
    public List<String> getPrénoms() {  return prénoms;   } 

    @PostConstruct
    private void init() {
        rechercheNoms();
        if (noms!=null && !noms.isEmpty()) {
            nom = noms.get(0);
            miseAJour();
        }
        else {
            personne = new Personne();
            etat = Enregistrement.Nouveau;
        }        
    }
       
    private void miseAJour() {
        recherchePrénoms();
        prénom = prénoms.get(0);
        personne = recherchePersonne(nom, prénom);
        etat = Enregistrement.Enregistré;              
    }  
    
    public void nouveau() {
        personne = new Personne();
        etat = Enregistrement.Nouveau;
    }
    
    public void enregistrer() {    
        try {
           recherchePersonne(personne.getNom(), personne.getPrenom());
           etat = Enregistrement.ExisteDéjà;
        }
        catch (Exception ex) {   
            bd.persist(personne);  
            etat = Enregistrement.Enregistré;
            rechercheNoms();
            recherchePrénoms();
        }
    }
    
    public void supprimer() {
        bd.remove(bd.find(Personne.class, personne.getId())); 
        init();
    }  
    
    public void changeNom(ValueChangeEvent evt) {
        nom = (String) evt.getNewValue();
        miseAJour();
    }
    
    public void changePrénom(ValueChangeEvent evt) {
        prénom = (String) evt.getNewValue();
        personne = recherchePersonne(nom, prénom);
        etat = Enregistrement.Enregistré;        
    }    
  
    private void rechercheNoms() {
        Query requête = bd.createNamedQuery("noms");
        noms = requête.getResultList();
    }

    private void recherchePrénoms() {
        Query requête = bd.createNamedQuery("prénoms");
        requête.setParameter("nom", nom);
        prénoms = requête.getResultList();
    }    
    
    private Personne recherchePersonne(String nom, String prénom) {
        Query requête = bd.createNamedQuery("personneExiste");
        requête.setParameter("nom", nom);
        requête.setParameter("prenom", prénom);
        return (Personne) requête.getSingleResult();
    }
}
  1. Il s'agit ici du bean géré Gestion qui est donc la partie modèle de l'application Web. Il est particulier puisqu'il s'agit d'un bean injecté (CDI) qui est également un bean session de type Stateful. Ce bean a finalement deux casquettes, d'une part le côté bean session qui permet de contrôler complètement la persistance, et de l'autre le bean géré qui réalise le traitement en coulisse des requêtes soumises par la vue.
  2. Ce bean possède un certain nombre de propriétés : personne qui représente soit un nouveau personnel ou un déjà sauvegardé dans la base de données, etat qui renseigne sur l'état d'enregistrement d'un personnel, nom qui est en relation avec un nom sélectionné (la présence de cette propriété est uniquement utile lors de la suppression d'un personnel dans la base de données), noms qui représente la liste de tous les noms enregistrés dans la base de données, et prénoms qui représente l'ensemble des prénoms associés au nom sélectionné.

    Remarquez l'absence de la propriété prénom (seul l'attribut existe) qui n'est pas particulièrement utile puisque c'est l'événement associé à la sélection du prénom qui enregistre le choix effectué.

  3. Au démarrage de l'application web, la méthode init() est automatiquement exécutée afin, de connaître tous les noms enregistrés, de connaître tous les prénoms associés au premier de la liste et de récupérer enfin le personnel correspondant également au premier prénom de la liste, qui va servir de référence pour la suite. Ainsi, la page web commence par une personne par défaut déjà enregistré, sauf la première fois bien entendu.
  4. Nous retrouvons ensuite les méthodes en relation directe avec les boutons de soumissions et qui sont en relation avec l'unité de persistance au travers du gestionnaire d'entités : nouveau(), enregistrer() et supprimer().
  5. A la fin de ce bean, nous trouvons toutes les méthodes qui s'occupent de rechercher toutes les informations issues de la base de données au travers des requêtes nommées proposées par l'entité Personne : rechercheNoms(), recherchePrénoms() et recherchePersonne().
  6. Enfin, il nous reste les méthodes qui prennent en compte les événements de type changement de valeur : changeNom() et changePrénom(). Pour qu'elle soient valide dans ce contexte, elles doivent donc posséder un paramètre de type ValueChangeEvent. Ensuite, nous devons récupérer la valeur soumise par la sélection au travers de la méthode getNewValue() et renseigner l'attribut correspondant et enfin traiter la recherche souhaitée.
resources/css/principal.css
root { 
    display: block;
}

body { 
    background: green; 
    color: yellow; 
    font-weight: bold; 
    text-shadow: 0px 0px 10px yellow; 
}

.panneau { 
    background: darkgreen; 
    border-radius: 5px; 
    box-shadow: 2px 2px 5px black, -2px -2px 5px lightgreen; 
    padding: 5px;
}

.entete { 
    font-size: 20px; 
    padding-bottom: 7px; 
}

.pied {
   padding-top: 7px; 
}

input[type="text"]  { 
    background: yellow;
    color: darkgreen; 
    padding-right: 7px; 
    padding-left: 7px;
    border-radius: 3px; 
}
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    <h:head />
    <h:outputStylesheet library="css" name="principal.css" />

    <h:body>
        <h:form>
            <h:panelGrid columns="2" styleClass="panneau" headerClass="entete" footerClass="pied">
                <f:facet name="header">
                    <h:outputText value="Gestion du personnel" />
                </f:facet>
                <h:outputText value="Prénom : " />
                <h:inputText value="#{gestion.personne.prenom}"/>
                <h:outputText value="Nom : " />
                <h:inputText value="#{gestion.personne.nom}" />
                <h:outputText value="Âge : " />
                <h:inputText value="#{gestion.personne.age}" />
                <h:outputText value="#{gestion.etat}" style="color: aquamarine" />
                <f:facet name="footer">
                    <h:commandButton value="Nouveau" action="#{gestion.nouveau}"/>
                    <h:commandButton value="Enregistrer" action="#{gestion.enregistrer}" />
                    <h:commandButton value="Supprimer" action="#{gestion.supprimer}" />
                </f:facet>
            </h:panelGrid>
        </h:form>    
        <br />
        <h:panelGrid columns="2" styleClass="panneau"  headerClass="entete">
            <f:facet name="header">
                <h:outputText value="Recherche" />
            </f:facet>
            <h:form>
                <h:selectOneMenu value="#{gestion.nom}" valueChangeListener="#{gestion.changeNom}" onchange="submit()">
                    <f:selectItems value="#{gestion.noms}" />
                </h:selectOneMenu>
            </h:form>
            <h:form>
                <h:selectOneMenu valueChangeListener="#{gestion.changePrénom}" onchange="submit()">
                    <f:selectItems value="#{gestion.prénoms}" />
                </h:selectOneMenu>
            </h:form>
        </h:panelGrid>                
    </h:body>
</html>
  1. Globalement nous retrouvons une ossature classique d'une vue en relation avec le bean géré qui travaille en coulisse.
  2. Une petite particularité sur le premier panneau, c'est que nous sommes en contact avec l'entité Personne que nous pouvons ainsi renseigner directement ou lire l'ensemble de ses caractéristiques si la personne existe déjà.
  3. La partie importante qui nous préoccupe se situe sur le deuxième panneau. Sur les deux listes déroulantes, nous prévoyons donc une gestion événementielle de type changement de valeur. L'attribut valueChangeListener nous indique la méthode qui est sollicitée lors d'un nouveau choix et l'attribut onchange permet de soumettre instantanément le changement effectué.
  4. Comme prévu, lorsque nous utilisons une gestion événementielle, vous n'êtes pas obligé de proposer une propriété, à l'aide de l'attribut value, à la zone de sélection dans son ensemble, ce que nous faisons avec les prénoms. Pour le nom, j'ai quand même rajouté la propriété correspondante parce qu'il existe un comportement aléatoire lors de la suppression d'un personnel. Il s'agit d'être sûr que le nom affiché soit en correspondance avec le prénom.
  5. Un dernier petit commentaire qui a son importance. Vous remarquez la présence de plusieurs balises <h:form> qui entoure chaque élément qui propose des modifications éventuelles. Il est très important de cibler la requête à soumettre sinon nous obtenons des effets de bord indésirables.

Evénement lié à une validation

Pour cet événement, vous devez spécifier la méthode du bean qui va traiter votre événement au travers de l'attribut actionListener. Par contre, il n'est plus nécessaire de solliciter la fonction JavaScript submit() puisque les balises concernées <h:commandButton> et <h:commandLink> sont déjà prévues pour émettre une soumission à une requête. Pour un bouton, voici ce qu'il faut donc écrire d'un point de vue général :

<h:commandButton action="#{bean.changerVue}" actionListener="#{bean.méthodeEvénement}" />

Ensuite, dans votre bean géré, vous devez donc implémenter cette méthode particulière qui doit alors posséder un attribut de type ActionEvent :

public class UnBeanGéré {
   public String changerVue() {
		...
      return "Passer à la page Web suivante";
   }

   public void méthodeEvénement(ActionEvent evt) {
			...
   }
}
Méthode Retour Description de la classe javax.faces.event.ActionEvent
getComponent() UIComponent Retourne le composant qui a sollicité l'événement.

La classe ActionEvent possède très peu de méthodes. Il faut dire que dans la plupart du temps il s'agit juste de savoir si une validation a été effectué. Toutefois, il peut être nécessaire de connaitre, par exemple, les coordonnées de la souris au moment du clic. Voici la procédure à suivre dans ce cas là :

  1. Il faut d'abord connaître le composant dans l'arbre de vue (côté serveur) qui représente la balise qui a sollicité l'action, ceci au moyen de la seule méthode utile de ActionEvent, je veux dire getComponent().
  2. Retrouver ensuite, la balise correspondante du client au moyen de la méthode getClientId().
  3. Il faut, pour terminer, récupérer d'une part l'ensemble des requêtes issue de la page Web en cours, et filtrer ensuite uniquement celles qui correspondent à la valeur de x et de y qui sont envoyées par la balise cliente correspondante. Ceci se fait au travers d'un Map.
public void méthodeEvénement(ActionEvent evt) {
      FacesContext page = FacesContext.getCurrentInstance();
      String client = evt.getComponent().getClientId(page);
      Map requête = page.getExternalContext().getRequestParameterMap();
      int x = Integer.parseInt((String)requête.get(client+".x"));
      int y = Integer.parseInt((String)requête.get(client+".y")); 
      ...   
}

Nous pourrions légitimement nous poser la question de l'utilité de l'attribut actionListener alors qu'il existe déjà l'attribut action. En réalité chacun joue son propre rôle. actionListener permet de valider un certain nombre de choses avant de passer éventuellement à une autre page. Elle devient impérative si vous désirez gérer les coordonnées de la souris. L'attribut action, de son côté, est prévu pour la navigation entre les pages, c'est ce qui permet d'évoluer dans le site.

L'événement de type action est déclenché durant la phase d'invocation de la logique applicative. Cependant, il est parfois nécessaire d'exécuter une méthode avant la phase de validation. Par exemple, un bouton de type Annuler permet d'initialiser le formulaire en vidant les champs et doit appeler une méthodes avant la validation des saisies.

Nous allons toute de suite prendre un exemple pour valider ce concept. Nous reprenons l'applications web sur le traitement d'image avec un certain nombre de modifications :
  1. Je supprime l'histogramme, mais je rajoute la possibilité de retoucher la photo originale en réglant éventuellement la luminosité et le contraste (sans perte d'informations).
  2. Nous pouvons contrôler une partie de l'image à l'aide d'une loupe qui visualise un endroit spécifique suivant le clic effectué sur la vignette originale.
  3. Il est possible, cette fois-ci, de régler la largeur des vignettes.
  4. Le téléchargement prend en compte maintenant la retouche effectuée.
  5. Tous ces clics et changements de valeurs sont répercutés instantanément, et le serveur résoud la requête spécifique demandée.
bean.Traitement.java
package bean;

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import java.net.URL;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.faces.bean.*;
import javax.faces.context.*;
import javax.faces.event.*;
import javax.imageio.ImageIO;
import javax.servlet.http.*;

@ManagedBean
@SessionScoped
public class Traitement {
   private String nomFichier;
   private BufferedImage image;
   private BufferedImage imageRetouchée;
   private BufferedImage vignette;
   private BufferedImage retouche;
   private int largeur = 300;
   private int hauteur;   
   private int centreZoomX, centreZoomY;
   private int intensité;
   private int contraste;   
   private Graphics2D dessin;
   private final String répertoire = "D:/Photos/";
   private String[] fichiers;
   
   @PostConstruct
   void init() throws IOException {
       File photos = new File(répertoire);
       fichiers = photos.list();
       if (fichiers.length!=0) setNomFichier(fichiers[0]);
   }
   
   public String[] getFichiers() {  return fichiers; }
    
   public BufferedImage getVignette() { return vignette;  }
   public BufferedImage getRetouche() { return retouche;  }
   
   public BufferedImage getZoom() {
      return imageRetouchée.getSubimage(centreZoomX-largeur/2, centreZoomY-hauteur/2, largeur, hauteur);
   }
      
   public String getNomFichier() { return nomFichier;  }   
   public String getPath() { return répertoire+nomFichier; }
   
   public long getPoids() {  return new File(répertoire+nomFichier).length();  }
   
   public void setNomFichier(String nomFichier) throws IOException {
      this.nomFichier = nomFichier;
      image = ImageIO.read(new URL("file:///"+répertoire+nomFichier));
      imageRetouchée = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
      créerVignette();
      centreZoomX = image.getWidth()/2;
      centreZoomY = image.getHeight()/2;
      intensité = contraste = 0;
      retouche();      
   }

   public int getLargeur() { return largeur;  }
   public void setLargeur(int largeur) { this.largeur = largeur;  }

   public int getContraste() {  return contraste;  }
   public void setContraste(int contraste) {  this.contraste = contraste;  }

   public int getIntensité() { return intensité;  }
   public void setIntensité(int intensité) { this.intensité = intensité;  }
   
   public void télécharger() throws IOException {
      ExternalContext externe = FacesContext.getCurrentInstance().getExternalContext();
      HttpServletResponse réponse = (HttpServletResponse) externe.getResponse();
      réponse.setContentType("application/octet-stream");
      réponse.setHeader("Content-disposition", "inline; filename=\""+nomFichier+"\""); 
      OutputStream out = réponse.getOutputStream();
      ImageIO.write(imageRetouchée, "JPEG", out);
      out.close();     
   }

   private void créerVignette() {
      double ratio = (double)largeur/image.getWidth();
      vignette = new BufferedImage((int)(image.getWidth()*ratio), (int)(image.getHeight()*ratio), image.getType());
      hauteur = vignette.getHeight();       
      AffineTransform retailler = AffineTransform.getScaleInstance(ratio, ratio);
      int interpolation = AffineTransformOp.TYPE_BICUBIC;
      AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation);
      retaillerImage.filter(image, vignette);      
   }   

   private void retouche() {
      int[] courbeInitiale = new int[256];
      byte[] courbe = new byte[256];
      for (int i=0; i<256; i++) {
         courbeInitiale[i] = (int) (i+intensité*Math.sin(i*Math.PI/255)-contraste*Math.sin(i*2*Math.PI/255));
         if (courbeInitiale[i]<0) courbe[i] = (byte)0;
         else if (courbeInitiale[i]>255) courbe[i] = (byte)255;
         else courbe[i] = (byte)courbeInitiale[i];
      }
      retouche = new BufferedImage(vignette.getWidth(), vignette.getHeight(), vignette.getType());
      ByteLookupTable table = new ByteLookupTable(0, courbe);
      LookupOp opération = new LookupOp(table, null);
      opération.filter(vignette, retouche);
      opération.filter(image, imageRetouchée);
   }   

   public void changeLargeur(ValueChangeEvent evt) {
      largeur = (Integer)evt.getNewValue();
      créerVignette();
      retouche();
   }
   
   public void changerCentreZoom(ActionEvent evt) {
      FacesContext ctx = FacesContext.getCurrentInstance();
      String clientId = evt.getComponent().getClientId(ctx);
      Map requête = ctx.getExternalContext().getRequestParameterMap();
      int x = Integer.parseInt((String)requête.get(clientId+".x"));
      int y = Integer.parseInt((String)requête.get(clientId+".y")); 
      centreZoomX = image.getWidth()*x/largeur;
      centreZoomY = image.getHeight()*y/hauteur;      
   }

   public void changeIntensité(ValueChangeEvent evt) {
      intensité = (Integer) evt.getNewValue();
      retouche();
   }
   
   public void changeContraste(ValueChangeEvent evt) {
      contraste = (Integer) evt.getNewValue();
      retouche();
   }
}
resources/css/principal.css
root { 
    display: block;
}

body { 
    background: green; 
    color: yellow; 
    font-weight: bold; 
    text-shadow: 0px 0px 10px yellow; 
}

.panneau { 
    background: darkgreen; 
    border-radius: 5px; 
    box-shadow: 2px 2px 5px black, -2px -2px 5px lightgreen;
    padding: 5px;
}

.entete { 
    font-size: 18px;
    padding-bottom: 5px;
}

img, input[type="image"]  {
    border-radius: 10px;
    box-shadow: 2px 2px 5px black;
}

form {
    float: right;
}
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    
    <h:head />
    
    <h:outputStylesheet library="css" name="principal.css" />
   
    <h:body>
        <h:panelGrid columns="2" cellpadding="3" styleClass="panneau" headerClass="entete">
            <f:facet name="header">
                <h:form>
                    <h:outputText value="Fichier image : " />                 
                    <h:selectOneMenu value="#{traitement.nomFichier}" onchange="submit()">
                       <f:selectItems value="#{traitement.fichiers}" />
                    </h:selectOneMenu>
                </h:form>                   
            </f:facet>
            <h:form>
                <h:commandButton image="LireImage?image=vignette" actionListener="#{traitement.changerCentreZoom}"/>
            </h:form>
            <h:graphicImage url="LireImage?image=retouche" />
            <h:graphicImage url="LireImage?image=zoom" />
            <h:panelGrid columns="3" styleClass="panneau" style="float: right">
                 <h:outputText value="Largeur : " />
                 <h:form>   
                    <h:inputText value="#{traitement.largeur}" valueChangeListener="#{traitement.changeLargeur}" onchange="submit()" size="3" />
                 </h:form>
                 &nbsp;
                 <h:outputText value="Luminosité : " />
                 <h:form>                    
                    <h:inputText size="3" value="#{traitement.intensité}" 
                                        valueChangeListener="#{traitement.changeIntensité}" onchange="submit()"
                                        validatorMessage="(-50 à 50)" id="intensité">
                        <f:validateLongRange minimum="-50" maximum="50" />
                    </h:inputText>
                </h:form>
                <h:message for="intensité" />
                <h:outputText value="Contraste : " />
                <h:form>
                    <h:inputText size="3" value="#{traitement.contraste}" 
                                        valueChangeListener="#{traitement.changeContraste}" onchange="submit()"
                                        validatorMessage="(-50 à 50)" id="contraste">
                         <f:validateLongRange minimum="-50" maximum="50" />
                     </h:inputText>
                 </h:form>
                 <h:message for="contraste" />
                 <h:outputText value="#{traitement.poids}">
                     <f:convertNumber pattern="#,##0 octets" />
                 </h:outputText>
                 <h:form>                    
                    <h:commandButton value="Télécharger" action="#{traitement.télécharger}" /> 
                 </h:form>
             </h:panelGrid>                 
        </h:panelGrid>             
    </h:body>
</html>
servlet.LireImage.java
package servlet;

import java.awt.image.BufferedImage;
import java.io.*;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet("/LireImage")
public class LireImage extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)  
    throws ServletException, IOException {
       response.setContentType("image/png");
       OutputStream out = response.getOutputStream();
       bean.Traitement traitement = (bean.Traitement)request.getSession().getAttribute("traitement");
       String typeImage = request.getParameter("image");
       BufferedImage image = null;
       if (typeImage.equals("vignette")) image =  traitement.getVignette();
       if (typeImage.equals("retouche")) image =  traitement.getRetouche();
       if (typeImage.equals("zoom")) image =  traitement.getZoom();
       ImageIO.write(image, "PNG", out);
       out.close();
    }
}

Gestion immédiate des événements

Revenons sur le cycle de traitement des événements : Vous remarquez que la gestion des événements, quelque soit le type, est toujours réalisée après la phase de conversion et de validation.

Court-circuiter le phase de validation

Il est possible, pour une entrée particulière, de ne pas passer par tout le cycle de traitement des requêtes avec la prise en compte des différentes phases. Il est effectivement possible de traiter uniquement la gestion des événements, sans passer donc par la phase 3 de conversion et de validation, et de passer ensuite, dès que le traitement de l'événement est réalisé, directement en phase 6 correspondant au rendu de la page Web.

Cette fois-ci, c'est tout de suite à l'issue de la phase 2, qui s'occupe de récupérer la valeur des requêtes, que la gestion des événements est pris en compte.

Notez toutefois, que si vous désirez prendre en compte un changement de valeur, il est nécessaire que cette nouvelle valeur subisse éventuellement une conversion et une validation afin qu'elle soit correcte et puisse donc être interprétée de façon convenable.

Lorsque vous désirez qu'un des éléments gère immédiatement l'événement associé, sans que tous les autres composants subissent les différents traitements prévus pour l'ensemble de la page Web, il suffit de spécifier la valeur true sur l'attribut immediat de la balise concernée.

Dans le projet que nous venons de construire, si je désire avoir une gestion immédiate du clic sur la vignette pour proposer une loupe adaptée, voici la modification à apporter sur la vue.

index.xhtml
<h:form>
     <h:commandButton image="LireImage?image=vignette" actionListener="#{traitement.changerCentreZoom}" immediat="true"/>
</h:form>

Ensuite, dans la méthode qui réalise le traitement de l'événement, après avoir géré le problème demandé, il est préférable d'atteindre directement la phase de rendu de la réponse, et ainsi de court-circuiter tous les autres phases intermédiaires, au moyen de la méthode renderResponse() de la classe FacesContext. Toujours pour notre exemple, voici donc le code correspondant dans le bean traitement.

bean.Traitement.java
public void changerCentreZoom(ActionEvent evt) {
    FacesContext ctx = FacesContext.getCurrentInstance();
    String clientId = evt.getComponent().getClientId(ctx);
    Map requête = ctx.getExternalContext().getRequestParameterMap();
    int x = Integer.parseInt((String)requête.get(clientId+".x"));
    int y = Integer.parseInt((String)requête.get(clientId+".y")); 
    centreZoomX = image.getWidth()*x/largeur;
    centreZoomY = image.getHeight()*y/hauteur;   
    ctx.renderResponse();    // ligne à rajouter
}

Evénements système

JSF 2.0 introduit une nouvelle gestion d'événements qui est capable de prendre en compte les notifications globales d'une application web, ce que nous nommons événements système. Nous pouvons ainsi prendre en compte certaines notifications dans des phases bien particulières qui sont recensées dans le tableau ci-dessous :

Classe Evenements Source de l'événement Description
PostConstructApplicationEvent
PreDestroyApplicationEvent
Application Après le démarrage de l'application web.
Avant qu'elle se termine définitivement.
PostAddToViewEvent
PreRemoveFromViewEvent
UIComponent Après l'ajout du composant dans l'arbre de vue.
Avant qu'il soit enlever de l'arbre de vue.
PostRestoreStateEvent UIComponent Après la restauration du composant.
PreValidateEvent
PostValidateEvent
UIComponent Avant et après la validation d'un composant.
PreRenderViewEvent UIViewRoot Avant que le rendu de l'élément racine de la page ne soit effectué.
PreRenderComponentEvent UIComponent Avant que le rendu du composant ne soit effectué.
PostConstructViewMapEvent
PreDestroyViewMapEvent
UIViewRoot Après que l'élément racine de la page ne soit créé
Avant sa destruction.
PostConstructCustomScopeEvent
PreDestroyCustomScopeEvent
ScopeContext Après que l'élément personnalisé de la page ne soit créé
Avant sa destruction.
ExceptionQueuedEvent ExceptionQueueEventContext Après le lancement d'une exception.
Voici la technique à suivre pour prendre en compte ce type d'événement :
<h:inputText value="#{bean.propriété}">
     <f:event name="postValidate" listener="#{bean.méthodeEvénement}" />
</h:inputText>                   

Ensuite, dans votre bean géré, vous devez donc implémenter une méthode spécifique qui doit alors posséder un attribut de type ComponentSystemEvent :

public class UnJavaBeanGéré {
   private int propriété;
   
   public int getPropriété() {
      return propriété;
   }

   public void setPropriété(int propriété) {
      this.propriété = propriété;
   }

   public void méthodeEvénement(ComponentSystemEvent evt) {
			...
   }
}

Il est possible de passer par une classe spécifique qui implémente l'action associée à l'événement souhaité. Cette classe doit alors être annotée avec @ListenerFor.


@ListenerFor(systemEventClass=PreRenderViewEvent.class)
public class ClasseEvénement extends Rendered {
 ...
}
Validation pour une combinaison de plusieurs entrées

Il existe des situations où vous avez besoin de recenser un ensemble d'informations venant de zones de saisie différentes et de contrôler la cohérence des unes envers les autres. C'est le cas notamment de la saisie d'une date. Certes, nous pouvons prévoir une seule zone de saisie où l'opérateur doit entrer la date en entier. Il est toutefois plus facile de prévoir une zone de saisie par élément, comme le jour du mois, le mois et l'année. Ainsi, la date est répartie. Par contre, il faut tout de même vérifier que la saisie est correcte en contrôlant, par exemple, que l'opérateur ne propose pas le 31 avril.

bean.JourSemaine.java
package bean;

import java.text.SimpleDateFormat;
import java.util.*;
import javax.annotation.PostConstruct;
import javax.faces.application.FacesMessage;
import javax.faces.bean.*;
import javax.faces.component.*;
import javax.faces.context.FacesContext;
import javax.faces.event.ComponentSystemEvent;

@ManagedBean(name="semaine")
@ViewScoped
public class JourSemaine {
    private int jour;
    private enum Mois {Janvier, Février, Mars, Avril, Mai, Juin, Juillet, Août, Septembre, Octobre, Novembre, Décembre};
    private Mois mois;
    private int année;
    private String erreur;

    public int getAnnée() { return année; }
    public void setAnnée(int année) { this.année = année; }

    public int getJour() { return jour; }
    public void setJour(int jour) { this.jour = jour; }

    public Mois getMois() { return mois; }
    public void setMois(Mois mois) { this.mois = mois; }
    
    public Mois[] getListeMois() { return Mois.values(); }
    
    @PostConstruct
    private void init() {
       Calendar date = Calendar.getInstance();
       jour = date.get(Calendar.DAY_OF_MONTH);
       mois = Mois.values()[date.get(Calendar.MONTH)];
       année = date.get(Calendar.YEAR);
    }
       
    public void recherche() {
       Calendar date = new GregorianCalendar(année, mois.ordinal(), jour);
       SimpleDateFormat jourSemaine = new SimpleDateFormat("EEEE");
       FacesContext ctx = FacesContext.getCurrentInstance();
       ctx.addMessage(null, new FacesMessage(jourSemaine.format(date.getTime()).toUpperCase()));
    }
       
    public void validationGlobale(ComponentSystemEvent evt) {
       UIComponent source = evt.getComponent();
       UIInput saisieJour = (UIInput) source.findComponent("jour");
       UIInput saisieMois = (UIInput) source.findComponent("mois");
       UIInput saisieAnnée = (UIInput) source.findComponent("annee");
       int jour = (Integer) saisieJour.getLocalValue();
       Mois mois = (Mois) saisieMois.getLocalValue();
       int année = (Integer) saisieAnnée.getLocalValue();
       if (!dateValide(jour, mois.ordinal(), année)) {
           FacesContext ctx = FacesContext.getCurrentInstance();
           ctx.addMessage(null, new FacesMessage(erreur));       
           ctx.renderResponse();
      }
   }
   
   private boolean dateValide(int jour, int mois, int année) {  
      if (jour<1)  {
         erreur = "Le jour est trop petit (1)";
         return false;
      }     
      erreur = "Le jour est trop grand ";
      if (mois==1) {
         boolean bissextile = année%4==0 && (année%400==0 || année%100!=0); 
         erreur += bissextile ? "(29)" : "(28)";
         return bissextile ? jour<=29 : jour<=28;
      }
      boolean trente = mois==3 || mois==5 || mois==8 || mois==10;
      erreur += trente ? "(30)" : "(31)";
      return  trente ? jour<=30 : jour<=31;
   }
}
  1. L'énumération Mois, comme vous le remarquez, permet d'avoir un affichage agréable pour les mois de l'année.
  2. La méthode init() propose la date du jour comme date par défaut lors du lancement de la page web.
  3. La méthode recherche() permet de spécifier le jour de la semaine de la date valide soumise par l'utilisateur.
  4. La méthode validationGlobale() en corrélation avec la méthode dateValide() permet de vérifier la validité de la date dans son ensemble. Elle est automatiquement appelée à l'issue de la validation des données, avant la prise en compte des valeurs par le modèle.
resources/css/principal.css
root { 
    display: block;
}

body { 
    background: green; 
    color: yellow; 
    font-weight: bold; 
    text-shadow: 0px 0px 10px yellow; 
}

.panneau { 
    background: darkgreen; 
    border-radius: 5px; 
    box-shadow: 2px 2px 5px black, -2px -2px 5px lightgreen;
    padding: 5px;
}

.entete { 
    font-size: 18px;
    padding-bottom: 7px;
    color: aquamarine;
}

.pied {
    padding-top: 7px;
}

input[type="text"]  { 
    background: yellow;
    padding-right: 7px; 
    padding-left: 7px;
    border-radius: 3px; 
    width: 78px;
}

select {
    background: yellow;
}

.resultat {
    float: right;
    color: aquamarine;
}
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    <h:head />
    <h:outputStylesheet name="principal.css" library="css" />
    <h:body>
        <h:form>
            <h:panelGrid columns="2" styleClass="panneau" headerClass="entete" footerClass="pied">
                <f:event type="postValidate" listener="#{semaine.validationGlobale}" />
                <f:facet name="header">
                    <h:outputText value="Recherche du jour de la semaine" />
                </f:facet>
                <h:outputText value="Jour : " />
                <h:inputText value="#{semaine.jour}" id="jour" />
                <h:outputText value="Mois : " />
                <h:selectOneMenu value="#{semaine.mois}" id="mois" >
                    <f:selectItems value="#{semaine.listeMois}" /> 
                </h:selectOneMenu>
                <h:outputText value="Année : " />
                <h:inputText value="#{semaine.année}" id="annee" />
                <f:facet name="footer">
                    <h:commandButton value="Calcul" action="#{semaine.recherche}"/>
                    <h:messages layout="table" styleClass="resultat" />
                </f:facet>            
            </h:panelGrid>
        </h:form>
    </h:body>
</html>

L'élément important ici est la balise <f:event> qui permet de choisir le type d'événement que vous désirez prendre en compte. Le choix se fait justement au travers de l'attribut type, ici donc l'événement postValidate. Il suffit ensuite de spécifier la méthode qui implémente les fonctionnalités attendues en relation avec cet événement spécifique au travers de l'attribut listener. Remarquez que le placement de cette balise se situe juste au niveau du panneau puisque nous désirons globaliser plusieurs valeurs de composants répartis dans ce dernier.

Afficher une page de façon conditionnelle (décision avant le rendu d'une page)

Quelquefois, vous désirez prendre des décisions particulières avant d'afficher définitivement une page et de rentrer donc dans la phase du rendu, par exemple pour télécharger des données, réaliser des changement sur un composant de la page, ou tout simplement de rendre une navigation conditionnelle vers une autre page.

Par exemple, sur le projet précédent, nous pourrions aboutir à notre page de recherche du jour de la semaine à la condition de s'être déjà authentifier. Par contre, si l'authentification a déjà eu lieu, il ne doit pas être nécessaire de s'authentifier de nouveau et d'accéder ainsi directement à la page de recherche.

Nous allons modifier notre site en conséquence. La page de recherche ne s'appelle plus index.xhtml mais semaine.xhtml. La page index.xhtml servira maintenant à l'authentification. Nous allons également rajouter un bean géré Login qui s'occupera du traitement spécifique de l'authentification.

bean.Login.java
package bean;

import javax.faces.application.ConfigurableNavigationHandler;
import javax.faces.bean.*;
import javax.faces.context.FacesContext;
import javax.faces.event.*;

@ManagedBean
@SessionScoped
public class Login {
    private String utilisateur = "";
    private String motDePasse = "";
    private boolean valide;

    public String getMotDePasse() { return motDePasse;  }
    public void setMotDePasse(String motDePasse) { this.motDePasse = motDePasse;  }

    public String getUtilisateur() { return utilisateur;  }
    public void setUtilisateur(String utilisateur) { this.utilisateur = utilisateur; }

    public String connexion() {
        valide = utilisateur.equalsIgnoreCase("manu") && motDePasse.equals("manu");
        return valide ? "semaine" : "index";
    }
    
    public void test(ComponentSystemEvent evt) {
        if (valide) {
            FacesContext ctx = FacesContext.getCurrentInstance();
            ConfigurableNavigationHandler nav =(ConfigurableNavigationHandler) ctx.getApplication().getNavigationHandler();
            nav.performNavigation("semaine");
        }
    }
}  
  1. Nous retrouvons les propriétés propres à l'authentification d'un utilisateur.
  2. La méthode connexion() permet d'évaluer si l'utilisateur est autorisé à aller sur le site (Ici, le nom et le mot de passe sont codés en dur, il s'agit juste d'un cas d'école). Suivant la réussite à cette évalution nous revenons sur la page d'accueil index.xhtml ou au contraire nous pouvons continuer vers la recherche du jour de la semaine semaine.xhtml.
  3. La méthode test() s'exécute automatiquement avant le rendu de la page d'accueil. Si l'authentification est valide, c'est que nous sommes déjà venu sur le site et qu'il n'est pas nécessaire de s'authentifier de nouveau, nous nous redirigeons donc vers la page semaine.xhtml.
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<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>
        <f:event type="preRenderView" listener="#{login.test}" />
        <h:head />
        <h:outputStylesheet name="principal.css" library="css" />
        <h:body>
            <h:form>
                <h:panelGrid columns="2" styleClass="panneau" headerClass="entete" footerClass="pied">
                    <f:facet name="header">
                        <h:outputText value="Authentification" />
                    </f:facet>
                    <h:outputText value="Utilisateur : " />
                    <h:inputText value="#{login.utilisateur}" />
                    <h:outputText value="Mot de passe : " />
                    <h:inputSecret value="#{login.motDePasse}" />
                    <f:facet name="footer">
                        <h:commandButton value="Connexion" action="#{login.connexion}"/>
                    </f:facet>            
                </h:panelGrid>
            </h:form>
        </h:body>
    </f:view>
</html>

Encore une fois, l'élément important est la balise <f:event> qui me permet de choisir cette fois-ci l'événement preRenderView, c'est-à-dire juste le moment avant le rendu de cette page d'accueil. C'est la méthode test() qui est alors lancée automatiquement pour savoir si la page doit effectivement s'afficher ou pas.

Dans ce cas particulier, vous remarquez la présence de la balise <f:view> qui d'habitude n'a aucun intérêt alors qu'ici elle justifie largement sa présence. Elle est effet indispensable puisque le test se fait sur le rendu de la page et cette balise représente l'objet racine UIViewRoot sur lequel repose tous les autres objets graphiques de la page.

Vous devez toujours placer la balise <f:event> juste à l'intérieur de la balise qui sert de référence, ici donc <f:view>. Dans l'exemple précédent, j'avais placer la balise <f:event> juste à l'intérieur <h:panelGrid>.

 

Choix du chapitre Les composants composites

Tous les composants que nous avons présentés lors de l'étude précédente font partis de JSF et sont disponibles dans toutes les implémentations qui respectent la spécification. En outre, comme elle repose sur des composants réutilisables, JSF fournit le moyen de créer et d'intégrer aisément dans les applications ses propres composants ou de composants provenant de tierces parties (comme nous le verrons à la fin de cette étude).

Nous avons déjà mentionné le fait que tous les composants héritaient, directement ou indirectement, de la classe javax.faces.component.UIComponent. Avant JSF 2.0, pour créer son propre composant, il fallait étendre la classe la plus proche du nouveau composant (UICommand, UIGraphics, UIOutput, etc.), la déclarer dans le fichier faces-config.xml et fournir un descripteur de marqueur et une représentation. Ces étapes étaient complexe.

Le but des composants composites est de permettre aux développeurs de créer de vrais composants graphiques réutilisables sans avoir besoin d'écrire du code Java ou de mettre en place une configuration XML.

Cette nouvelle approche consiste à créer une page XHTML contenant les composants, puis de l'utiliser comme composant élémentaire dans d'autres pages. Cette page XHTML est alors vue comme un véritable composant supportant des validateurs, des convertisseurs et des écouteurs. Les composants composites peuvent contenir n'importe quel marqueur valide et utiliser les modèles. Ils sont traités comme des ressources et doivent donc impérativement se trouver dans les nouveaux répertoires standard des ressources.

Balises Description
<composite:interface> Déclare le contrat d'un composant, avec éventuellement : des attributs, des actions, des valeurs par défaut, possibilité de le rendre éditable ou pas, ou avec des facettes.
<composite:implementation> Définit l'implémentation d'un composant qui contient un certain nombre d'autres balises JSF usuelles.
<composite:attribute> Déclare un attribut pouvant être fourni à une instance du composant. Un marqueur <composite:interface> peut en contenir plusieurs.
<composite:facet> Déclare que ce composant supporte une facette avec un nom spécifique.
<composite:insertFacet> Insère une facette définie par la page qui fait appel à ce composant. La facette insérée sera alors représentée dans le composant.
<composite:renderFacet> Visualise la facette spécifiée par la page qui utilise le composant comme un composant fils.
<composite:insertChildren> Tous les composants fils (imbriqués) seront tout simplement insérés dans la représentation de ce composant à l'endroit de cette balise.
<composite:valueHolder> Expose une implémentation de ValueHolder.
<composite:editableValueHolder> Expose une implémentation de EditableValueHolder.
<composite:actionSource> Expose le composant pour une action ou un événement à l'image des boutons et des liens.
<composite:extension> La page qui fait appel à ce composant peut placer cette balise dans une interface. La balise <composite:extension> peut contenir des données XML.

Ecriture d'un composant composite

L'écriture d'un composant avec JSF 2.0 est relativement proche de celle que nous utilisons pour Java :

  1. Nous écrivons d'abord une interface, <composite:interface> qui sert de point d'entrée pour le composant - Il s'agit en quelque sorte d'une déclaration où nous décrivons éventuellement les noms des attributs qu'il utilise avec un certain nombre d'éléments annexes éventuels.
  2. Puis nous passons ensuite à l'implémentation, <composite:implementation> qui est le corps du composant, sa constitution, écrit en XHTML avec les balises JSF que nous connaissons, avec d'éventuels modèles de page.
  3. L'interface et l'implémentation se trouvent sur la même page.

Par exemple, nous pourrions construire un composant d'authentification (sujet relativement fréquent) qui implémente un formulaire avec un nom de login, un mot de passe et un bouton de validation. Nous devons donc implémenter ce composant d'authentification à l'aide des composants JSF <h:form>, <h:inputText>, <h:inputSecret>, <h:commandButton>, etc.

Afin d'être réutilisable, les composants composites ont besoin quelquefois de plus qu'une simple implémentation, et en même temps elles doivent être le plus configurable possible. A titre d'exemple, en reprenant notre composant d'authentification, pour qu'il soit facile à utiliser, avec un maximum de souplesse, il serait intéressant de personnalisé les intitulés associés au nom du login et du mot de passe. Parallèlement, nous pouvons rattâcher un validateur sur l'ensemble des deux champs, rattâcher également un événement de type action sur le bouton de soumission. Vous devez préciser tous ces critères lors de la déclaration de l'interface du composant.

Mise en oeuvre

Nous allons tout de suite voir comment créer notre premier composant composite. Pour le premier nous n'allons pas proposer d'attribut. Il s'appelle <comp:debug>. Comme son nom l'indique, il va servir au mode débugage afin de visualiser automatiquement le listing complet de la partie en-tête d'une requête HTTP suivi de ses paramètres, comme cela vous est présenté ci-dessous :

Nous allons maintenant découvrir toute la procédure à suivre pour générer ce nouveau composant <comp:debug> :
Implémentation du composant
  1. Il existe un menu spécialisé dans l'environnement Netbeans pour générer un nouveau composant :

  2. Une boîte de dialogue apparaît à l'intérieur de laquelle vous spécifez le nom du composant. Attention, précisez bien sa localisation qui doit donc se trouver impérativement dans le répertoire resources. Il est d'usage de prévoir un sous-répertoire qui englobe tous les nouveaux composants composites, ici par exemple composants :

/resources/composants/debug.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:c="http://java.sun.com/jsp/jstl/core"
      xmlns:f="http://java.sun.com/jsf/core">

    <!-- INTERFACE -->
    <cc:interface />

    <!-- IMPLEMENTATION -->
    <cc:implementation>
        <h:panelGrid columns="3" styleClass="panneau" >
            <f:facet name="header">
                <h:outputText value="En-têtes et paramètres de la requête HTTP" />
            </f:facet>
            <c:forEach items="#{headerValues}" var="élément">
                <h:outputText value="#{élément.key}" style="color: white" />
                <c:forEach var="valeur" items="#{élément.value}">
                    <h:outputText value="#{valeur}" escape="false" style="color: black" />
                    <br />
                </c:forEach>
            </c:forEach>
            <c:forEach items="#{paramValues}" var="élément">
                <h:outputText value="#{élément.key}" style="color: white" />
                <c:forEach var="valeur" items="#{élément.value}">
                    <h:outputText value="#{valeur}" escape="false" style="color: black" />
                    <br />
                </c:forEach>
            </c:forEach>
        </h:panelGrid>
    </cc:implementation>
</html>
  1. La première remarque que nous pouvons faire sur la création de ce nouveau composant, c'est qu'il s'agit d'une page XHTML classique avec des composants JSF standards.
  2. Ensuite, pour tous les composants composites, vous devez spécifier obligatoirement, même si la balise ne possède pas d'attributs, une partie interface et une partie implémentation.
  3. La partie interface se déclare donc avec <cc:interface> à l'intérieur de laquelle vous spécifiez le nom des attributs que vous souhaitez utiliser. Ici, et c'est très rare, nous n'avons pas besoin, la balise est donc vide.
  4. La partie implémentation est délimitée par la balise <cc:implementation> à l'intérieur de laquelle vous placez toutes les balises internes standards JSF qui vont résoudre le traitement spécifique souhaité.
Utilisation du composant

Maintenant que notre composant est construit, nous pouvons l'utiliser comme n'importe quelle balise standard JSF. Rappelez-vous que nous avons décidé de l'appeler <comp:debug> :

index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:comp="http://java.sun.com/jsf/composite/composants">  <!--nouvelle déclaration -->
    <f:view>
        <f:event type="preRenderView" listener="#{login.test}" />
        <h:head />
        <h:outputStylesheet name="principal.css" library="css" />
        <h:body>
            <h:form>
                <h:panelGrid columns="2" styleClass="panneau" headerClass="entete" footerClass="pied">
                    <f:facet name="header">
                        <h:outputText value="Authentification" />
                    </f:facet>
                    <h:outputText value="Utilisateur : " />
                    <h:inputText value="#{login.utilisateur}" />
                    <h:outputText value="Mot de passe : " />
                    <h:inputSecret value="#{login.motDePasse}" />
                    <f:facet name="footer">
                        <h:commandButton value="Connexion" action="#{login.connexion}"/>
                    </f:facet>            
                </h:panelGrid>
            </h:form>
            <comp:debug /> <!-- nouveau composant -->
        </h:body>
    </f:view>
</html>
  1. C'est vraiment d'une simplicité déconcertante. Il s'agit de placer cette nouvelle petite balise <comp:debug /> à l'endroit que vous désirez pour prendre en compte tout le traitement de fond réalisé par ce nouveau composant composite.
  2. Comme convenu, cette balise est vide puisque nous n'avons pas défini d'attributs particuliers ni de contenu interne possible.
  3. Pour qu'elle puisse être pris en compte, vous devez déclarer l'espace de nom choisi <comp: > à l'aide de l'URL suivante : http://java.sun.com/jsf/composite/composants. Remarquez bien que cette URL se termine par composants qui correspond à notre sous-répertoire défini dans le répertoire resources.
  4. Le nom du composant que nous utilisons doit donc posséder une implémentation qui porte ce nom là. Ainsi lorsque nous écrivons <comp:debug />, vu la déclaration de l'espace de nom que nous venons d'évoquer, dans le répertoire composants il doit exister un fichier XHTML qui porte le nom que nous spécifions dans la balise, donc ici debug.xhtml.

Les attributs

Les composants composites sont très utiles puisqu'ils sont par nature réutilisables, et ils sont généralement réutilisables parce qu'ils peuvent être configurés suivant les circonstances. Pour cela, ces composants doivent être paramètrables. Ceci se réalise tout simplement en prévoyant des attributs sur ces balises personnalisées.

Nous pouvons, par exemple, contruire un nouveau composant icone qui permet de placer dans votre page web une petite image qui va permettre, lors d'un clic sur celle-ci, de lancer une action particulière et de se rediriger éventuellement vers une nouvelle page.

index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:comp="http://java.sun.com/jsf/composite/composants">

    <h:body style="background: green; color: yellowgreen">
        <h2>Gestion des liens au travers d'icônes</h2>
        <hr />
        <h:form>
            <comp:icone image="ouvrir.png" action="#{gestion.ouvrir}"/>
            <comp:icone image="sauvegarder.png" action="#{gestion.enregistrer}" bulle="Sauvegarder" />
        </h:form>
    </h:body>
</html>

Ce composant personnalisé possède trois attributs : le nom de l'image qui représente l'icône, la méthode qui va se lancer lorsque nous cliquons sur cette icône et éventuellement une petite information sous forme de bulle d'aide qui s'affiche lorsque le curseur de la souris passe dessus.

/resources/composants/icone.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html">

    <!-- INTERFACE -->
    <cc:interface>
        <cc:attribute name="image" />
        <cc:attribute name="action" method-signature="java.lang.String traitementDuLien()" />
        <cc:attribute name="bulle" />
    </cc:interface>

    <!-- IMPLEMENTATION -->
    <cc:implementation>
        <h:commandLink action="#{cc.attrs.action}" title="#{cc.attrs.bulle}" >
                <h:graphicImage library="images" name="#{cc.attrs.image}" style="margin: 5px"/>
            </h:commandLink>
    </cc:implementation>
</html>
Partie Interface
  1. La partie interface de la page de création de notre composant personnalisé n'est pas vide comme lors du projet précédent. Elle possède effectivement la déclaration des attributs nécessaires à sa bonne utilisation.
  2. Pour cela, vous devez utiliser la balise <cc:attribut /> prévue à cet effet en spécifiant les noms que vous désirez utiliser, ceci au moyen du paramètre name. Sans précision supplémentaire, comme nous le verrons dans la suite, ces attributs sont optionnels.
  3. Vous remarquez que le deuxième attribut, que nous avons décidé de nommer action, est particulier puisqu'il va être en relation avec l'appel d'une méthode du bean géré. Du coup, nous prévoyons un paramètre supplémentaire, method-signature qui permet de spécifier le prototype de la méthode qui va être sollicité.
  4. Par défaut, JSF considère que chaque valeur d'attribut est de type java.lang.Object. Si vous désirez qu'un attribut du composant représente une sous-classe de Object, vous devez préciser de quel type est cet attribut. Il existe pour cela un paramètre supplémentaire, nommé type, dans cette balise <cc:attribute /> qui permet d'indiquer la nature de l'attribut. Ainsi, par exemple, si vous souhaitez prendre en compte un attribut qui gère une date, voici ce qu'il faudrait écrire :
    <!-- INTERFACE -->
    <cc:interface>
        <cc:attribute name="date" type="java.util.Date" />
    </cc:interface>
    
  5. Dans le même ordre d'idée, pour en revenir au paramètre method-signature de la balise <cc:attribute />, JSF résoud la valeur de l'attribut bien comme un appel d'une méthode au lieu d'être considéré comme un simple objet, donc de type Object.
Partie Implémentation
  1. Maintenant, dans la partie implémentation, au delà de la structure même du composant avec l'utilisation des balises standard JSF qui vous intéressent, vous devez prendre en compte ces différents attributs.
  2. Ceci se fait finalement très simplement puisque un objet est automatiquement créé qui représente votre composant personnalisé. Il se nomme cc. Ce dernier possède en interne la propriété attrs qui est également un objet qui cette fois-ci représente l'ensemble des attributs que vous venez juste de déclarer dans la partie interface.
  3. Ainsi, par exemple pour prendre en compte le nom de l'image que l'utilisateur souhaite utiliser pour son icône, vous devez juste écrire à l'endroit voulu de l'implémentation #{cc.attrs.image}.
Les attributs obligatoires et les valeurs par défaut

Ce composant <comp:icone /> que nous venons de construire est déjà très intéressant, mais il est possible d'aller plus loin dans notre démarche en le rendant plus sophistiqué. Nous pouvons par exemple :

  1. Faire en sorte que l'attribut image soit rendu obligatoire.
  2. Proposer éventuellement un style CSS adapté, sinon en proposer un par défaut.
  3. Permettre ou pas de passer par la phase de validation lorsque le clic est effectué.
/resources/composants/icone.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html">

    <!-- INTERFACE -->
    <cc:interface>
        <cc:attribute name="image" required="true"/>
        <cc:attribute name="phaseValidation" default="false" />
        <cc:attribute name="styleClass" default="icone" />
        <cc:attribute name="action" method-signature="java.lang.String traitementDuLien()" />
        <cc:attribute name="bulle" />
    </cc:interface>

    <!-- IMPLEMENTATION -->
    <cc:implementation>
        <h:commandLink action="#{cc.attrs.action}" title="#{cc.attrs.bulle}" immediate="#{not cc.attrs.phaseValidation}">
            <h:graphicImage library="images" name="#{cc.attrs.image}" styleClass="#{cc.attrs.styleClass}"/>
        </h:commandLink>
    </cc:implementation>
</html>
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:comp="http://java.sun.com/jsf/composite/composants">   
    <h:head />
    <h:outputStylesheet name="principal.css" />
    <h:body>
        <h2>Gestion des liens au travers d'icônes</h2>
        <hr />
        <h:form>
            <comp:icone image="ouvrir.png" action="#{gestion.ouvrir}"/>
            <comp:icone image="sauvegarder.png" action="#{gestion.enregistrer}" bulle="Sauvegarder" />
        </h:form>
    </h:body>
</html>
/resources/principal.css
root { 
    display: block;
}

body {
    background: green; 
    color: yellowgreen;
}

.icone {
    border-radius: 22px;
    margin: 5px;
    padding: 6px;
    box-shadow: 1px 1px 3px black, -1px -1px 2px white;
}
Traitement en coulisse

Jusqu'à présent, nous avons construit des composants où seuls les balises de la partie implémentation réalise le traitement souhaité. Il s'agit, dans ce cadre là d'une simple factorisation d'un ensemble de balises pour en avoir une seule qui correspond à une fonctionnalité donnée.

Dans certains cas, nous pouvons aussi avoir besoin de traitement de fond et donc d'avoir, dans la génération de ces nouveaux composants, une interaction avec un bean géré pour réaliser le traitement souhaité.

Nous allons voir comment faire en reprenant le projet sur le calcul du jour de la semaine et proposer un nouveau composant <comp:login />, qui sera d'ailleurs bien utile pour beaucoup d'applications web.

index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:comp="http://java.sun.com/jsf/composite/composants">

    <h:head />
    <h:outputStylesheet name="principal.css" library="css" />
    
    <h:body>
        <comp:login utilisateur="#{utilisateur}" soumettre="#{utilisateur.connexion}" />
    </h:body>
</html>
  1. Voici donc une utilisation possible de ce composant personnalisé <comp:login />, ce qui permet d'avoir une page d'accueil très réduite. Ce composant possède deux attributs :
  2. Le premier attribut, utilisateur, correspond à un bean géré qui représente l'utilisateur autorisé à intervenir dans cette application web.
  3. Le deuxième attribut va servir à faire appel à la méthode qui gère cette connexion. Dans notre cas, cette méthode se situe dans le bean géré utilisateur, mais ce n'est pas une obligation.
/resources/composants/login.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

    <!-- INTERFACE -->
    <cc:interface>
        <cc:attribute name="utilisateur" required="true" />
        <cc:attribute name="soumettre" method-signature="java.lang.String action()" />
    </cc:interface>

    <!-- IMPLEMENTATION -->
    <cc:implementation>
        <h:form>
            <h:panelGrid columns="2" styleClass="panneau" headerClass="entete" footerClass="pied">
                <f:facet name="header">
                    <h:outputText value="Authentification" />
                </f:facet>
                <h:outputText value="Utilisateur : " />
                <h:inputText value="#{cc.attrs.utilisateur.nom}" />
                <h:outputText value="Mot de passe : " />
                <h:inputSecret value="#{cc.attrs.utilisateur.motDePasse}" />
                <f:facet name="footer">
                    <h:commandButton value="Connexion" action="#{cc.attrs.soumettre}"/>
                </f:facet>            
            </h:panelGrid>
        </h:form>
    </cc:implementation>
</html>
  1. Dans la constitution de notre composant, nous retrouvons bien la déclaration des deux attributs que nous venons d'évoquer. Celui qui est particulier, c'est utilisateur, sachant qu'il s'agit d'un objet représentant le bean géré. Ce dernier possède des propriétés que nous pouvons maintenant utiliser, comme nom et motDePasse.
  2. Attention, il est nécessaire que le bean géré possède exactement le même nom des propriétés utilisées par ce composant personnalisé.
bean.Utilisateur.java
package bean;

import javax.faces.bean.*;

@ManagedBean
@SessionScoped
public class Utilisateur {
    private String nom = "";
    private String motDePasse = "";

    public String getMotDePasse() { return motDePasse;  }
    public void setMotDePasse(String motDePasse) { this.motDePasse = motDePasse;  }

    public String getNom() { return nom;  }
    public void setNom(String nom) { this.nom = nom; }

    public String connexion() {
        boolean valide = nom.equalsIgnoreCase("manu") && motDePasse.equals("manu");
        return valide ? "semaine" : "index";
    }
}
Exposer ses composants

Afin d'offrir le maximum de souplesse, il est possible de faire en sorte que le composant soit configurable par l'utilisateur, afin de permettre, par exemple, de rattacher un validateur sur les entrées internes du composants. Il est nécessaire, dans ce cas là, d'exposer ces zones de saisie afin de les mettre en relation avec les validateurs souhaités par l'utilisateur.

Afin de permettre cette interaction, il suffit d'utiliser la balise <cc:editableValueHolder /> à l'intérieur de laquelle vous spécifiez, d'une part l'identificateur qui sera exploité par l'utilisateur, au travers de l'attribut name, et d'autre part la ou les cibles associées figurant à l'intérieur du composant, à l'aide de l'attribut targets.

/resources/composants/login.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

    <!-- INTERFACE -->
    <cc:interface>
        <cc:editableValueHolder name="saisieNom" targets="form:nom" />
        <cc:editableValueHolder name="saisieMotDePasse" targets="form:motDePasse" />
        <cc:editableValueHolder name="lesDeux" targets="form:nom form:motDePasse" />        
        <cc:attribute name="utilisateur" required="true" />
        <cc:attribute name="soumettre" method-signature="java.lang.String action()" />
    </cc:interface>

    <!-- IMPLEMENTATION -->
    <cc:implementation>
        <h:form id="form">
            <h:panelGrid columns="3" styleClass="panneau" headerClass="entete" footerClass="pied">
                <f:facet name="header">
                    <h:outputText value="Authentification" />
                </f:facet>
                <h:outputText value="Utilisateur : " />
                <h:inputText value="#{cc.attrs.utilisateur.nom}" id="nom" label=" " />
                <h:message for="nom" />
                <h:outputText value="Mot de passe : " />
                <h:inputSecret value="#{cc.attrs.utilisateur.motDePasse}" id="motDePasse" label=" " />
                <h:message for="motDePasse" />
                <f:facet name="footer">
                    <h:commandButton value="Connexion" action="#{cc.attrs.soumettre}"/>
                </f:facet>            
            </h:panelGrid>
        </h:form>
    </cc:implementation>
</html>
  1. Toujours dans le même projet, je propose trois interactions possibles, sur chacune des zones de saisie, mais aussi pour les deux en même temps, respectivement : saisieNom, saisieMotDePasse et lesDeux.
  2. L'attribut targets permet de spécifier les identifiants utilisés en interne, comme form:nom. Ces identifiants doivent donc être spécifiés à l'aide de l'attribut id sur les balises à prendre en compte, en portant bien entendu le nom prévu.
  3. Toutefois, la syntaxe proposée est particulière. Ainsi form:nom veut dire qu'il existe une balise identifiée par nom qui se situe à l'intérieur d'une autre balise identifiée par form.
    <h:form id="form">
          <h:inputText value="#{cc.attrs.utilisateur.nom}" id="nom" label=" " />
    </h:form>
    
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:comp="http://java.sun.com/jsf/composite/composants"
      xmlns:f="http://java.sun.com/jsf/core">

    <h:head />
    <h:outputStylesheet name="principal.css" library="css" />
    
    <h:body>
        <comp:login utilisateur="#{utilisateur}" soumettre="#{utilisateur.connexion}">
            <f:validateLength maximum="10" for="lesDeux" />
            <f:validateLength minimum="3" for="saisieNom" />
            <f:validateLength minimum="4" for="saisieMotDePasse" />
        </comp:login>
    </h:body>
</html>
  1. Il est donc maintenant possible que l'utilisateur de ce composant personnalisé propose ses propres validateurs correspondant à l'application web du moment.
  2. Ici, par exemple, nous souhaitons prévoir le même nombre maximum de caractères pour les deux zones de saisie et des valeurs minimales particulières sur chaque zone.
  3. Afin que l'interaction avec l'extérieur se passe correctement, il suffit de spécifier le nom de l'identifiant déterminé dans la partie <cc:interface /> du composant à l'aide de l'attribut for.

Pour exposer ses composants, nous venons de découvrir la balise <cc:editableValueHolder> qui est spécialisée dans les zones de saisie. Il existe deux autres balises qui permettent d'exposer d'autres parties du composant, <cc:valueHolder> et <cc:actionSource>.

A titre d'exemple, nous pouvons exposer le bouton de soumission de la validation de la connexion.
.

/resources/composants/login.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

    <!-- INTERFACE -->
    <cc:interface>
        <cc:actionSource name="bouton" targets="form:actionListener" />
        <cc:editableValueHolder name="saisieNom" targets="form:nom" />
        <cc:editableValueHolder name="saisieMotDePasse" targets="form:motDePasse" />
        <cc:editableValueHolder name="lesDeux" targets="form:nom form:motDePasse" />        
        <cc:attribute name="utilisateur" required="true" />
        <cc:attribute name="soumettre" method-signature="java.lang.String action()" />
    </cc:interface>

    <!-- IMPLEMENTATION -->
    <cc:implementation>
        <h:form id="form">
            <h:panelGrid columns="3" styleClass="panneau" headerClass="entete" footerClass="pied">
                <f:facet name="header">
                    <h:outputText value="Authentification" />
                </f:facet>
                <h:outputText value="Utilisateur : " />
                <h:inputText value="#{cc.attrs.utilisateur.nom}" id="nom" label=" " />
                <h:message for="nom" />
                <h:outputText value="Mot de passe : " />
                <h:inputSecret value="#{cc.attrs.utilisateur.motDePasse}" id="motDePasse" label=" " />
                <h:message for="motDePasse" />
                <f:facet name="footer">
                    <h:commandButton value="Connexion" action="#{cc.attrs.soumettre}" id="actionListener" />
                </f:facet>            
            </h:panelGrid>
        </h:form>
    </cc:implementation>
</html>
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:comp="http://java.sun.com/jsf/composite/composants"
      xmlns:f="http://java.sun.com/jsf/core">

    <h:head />
    <h:outputStylesheet name="principal.css" library="css" />
    
    <h:body>
        <comp:login utilisateur="#{utilisateur}" soumettre="#{utilisateur.connexion}">
            <f:actionListener for="bouton" type="LoginActionListener" />
            <f:validateLength maximum="10" for="lesDeux" />
            <f:validateLength minimum="3" for="saisieNom" />
            <f:validateLength minimum="4" for="saisieMotDePasse" />
        </comp:login>
    </h:body>
</html>
Les facettes

Toujours, dans la notion de composant personnalisé configurable, il est également possibles de rajouter des facettes à l'image des panneaux et des tableaux sur lesquelles nous avons déjà travaillé.

Nous pourrions par exemple proposer une invite de l'authentification paramétrable (éventuellement sans invite) sur la partie haute du panneau. Il suffit pour cela, comme nous l'avons déjà pratiqué, de proposer la balise <f:facet>.

index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:comp="http://java.sun.com/jsf/composite/composants"
      xmlns:f="http://java.sun.com/jsf/core">

    <h:head />
    <h:outputStylesheet name="principal.css" library="css" />
    
    <h:body>
        <comp:login utilisateur="#{utilisateur}" soumettre="#{utilisateur.connexion}">            
            <f:facet name="header">Authentification</f:facet>
            <f:validateLength maximum="10" for="lesDeux" />
            <f:validateLength minimum="3" for="saisieNom" />
            <f:validateLength minimum="4" for="saisieMotDePasse" />
        </comp:login>
    </h:body>
</html>
/resources/composants/login.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

    <!-- INTERFACE -->
    <cc:interface>
        <cc:facet name="header" />
        <cc:editableValueHolder name="saisieNom" targets="form:nom" />
        <cc:editableValueHolder name="saisieMotDePasse" targets="form:motDePasse" />
        <cc:editableValueHolder name="lesDeux" targets="form:nom form:motDePasse" />        
        <cc:attribute name="utilisateur" required="true" />
        <cc:attribute name="soumettre" method-signature="java.lang.String action()" />
    </cc:interface>

    <!-- IMPLEMENTATION -->
    <cc:implementation>
        <h:form id="form">
            <h:panelGrid columns="3" styleClass="panneau" headerClass="entete" footerClass="pied">
                <cc:insertFacet name="header" />
                <h:outputText value="Utilisateur : " />
                <h:inputText value="#{cc.attrs.utilisateur.nom}" id="nom" label=" " />
                <h:message for="nom" />
                <h:outputText value="Mot de passe : " />
                <h:inputSecret value="#{cc.attrs.utilisateur.motDePasse}" id="motDePasse" label=" " />
                <h:message for="motDePasse" />
                <f:facet name="footer">
                    <h:commandButton value="Connexion" action="#{cc.attrs.soumettre}" id="actionListener" />
                </f:facet>            
            </h:panelGrid>
        </h:form>
    </cc:implementation>
</html>
  1. Comme toujours, vous devez déclarer l'utilisation d'une facette dans la partie interface. Cela se fait au travers de la balise spécialisée <cc:facet /> à l'intérieur de laquelle vous spécifiez le type souhaité avec l'attribut name : header, footer ou error.
  2. Ensuite, dans la partie implémentation, vous précisez l'endroit où vous désirez introduire votre facette. Il existe deux cas de figure :
    1. Soit, nous désirons introduire une facette sur un composant qui déjà est capable les prendre en compte, comme <h:panelGrid> ou <h:dataTable>, à ce moment là vous devez utiliser la balise <cc:insertFacet> à l'endroit même de l'introduction.
    2. Soit, on considère que la facette est un simple composant enfant, comme tous les autres, auquel cas nous utilisons plutôt la balise <cc:renderFacet>.
Balises enfants

Les composants composites sont représentés par un ensemble de balises. Quelquefois, il serait judicieux de pouvoir proposer éventuellement un contenu supplémentaire à ce composant que nous placerions dans le corps de sa balise.

Par défaut, si vous placez quelque chose à l'intérieur du corps de la balise, JSF n'en tient pas compte. Vous pouvez changer ce comportement en utilisant la balise <cc:insertChildren /> que vous placez à l'endroit voulu dans la partie implémentation de votre définition de composant.

A titre d'exemple, nous pourrions éventuellement placer un lien vers une nouvelle page d'enregistrement à l'intérieur du composant <comp:login> :

index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:comp="http://java.sun.com/jsf/composite/composants"
      xmlns:f="http://java.sun.com/jsf/core">

    <h:head />
    <h:outputStylesheet name="principal.css" library="css" />
    
    <h:body>
        <comp:login utilisateur="#{utilisateur}" soumettre="#{utilisateur.connexion}">            
            <f:facet name="header">Authentification</f:facet>
            <f:validateLength maximum="10" for="lesDeux" />
            <f:validateLength minimum="3" for="saisieNom" />
            <f:validateLength minimum="4" for="saisieMotDePasse" />
            <h:link style="color: white">Nouvel utilisateur...</h:link>
        </comp:login>
    </h:body>
</html>
/resources/composants/login.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

    <!-- INTERFACE -->
    <cc:interface>
        <cc:facet name="header" />
        <cc:editableValueHolder name="saisieNom" targets="form:nom" />
        <cc:editableValueHolder name="saisieMotDePasse" targets="form:motDePasse" />
        <cc:editableValueHolder name="lesDeux" targets="form:nom form:motDePasse" />        
        <cc:attribute name="utilisateur" required="true" />
        <cc:attribute name="soumettre" method-signature="java.lang.String action()" />
    </cc:interface>

    <!-- IMPLEMENTATION -->
    <cc:implementation>
        <h:form id="form">
            <h:panelGrid columns="3" styleClass="panneau" headerClass="entete" footerClass="pied">
                <cc:insertFacet name="header" />
                <h:outputText value="Utilisateur : " />
                <h:inputText value="#{cc.attrs.utilisateur.nom}" id="nom" label=" " />
                <h:message for="nom" />
                <h:outputText value="Mot de passe : " />
                <h:inputSecret value="#{cc.attrs.utilisateur.motDePasse}" id="motDePasse" label=" " />
                <h:message for="motDePasse" />
                <cc:insertChildren />
                <f:facet name="footer">
                    <h:commandButton value="Connexion" action="#{cc.attrs.soumettre}" id="actionListener" />
                </f:facet>            
            </h:panelGrid>
        </h:form>
    </cc:implementation>
</html>
Conclusion

A titre de conclusion, je vous propose de fabriquer un nouveau composant composite qui permet de visualiser une vignette d'une photo originale en taille réduite dont la largeur peut être configurable. Pour cela, nous allons nous servir de la servlet que nous avons déjà mis en oeuvre. Je vous donne la liste du code sans commentaire particulier.

servlet.Vignette.java
package servlet;

import java.awt.geom.AffineTransform;
import java.awt.image.*;
import java.io.*;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet("/Vignette")
public class Vignette extends HttpServlet {
   private BufferedImage image;
   private BufferedImage vignette;
   private int largeurVignette;

   private void créerVignette() {
      double ratio = (double)largeurVignette / image.getWidth();
      vignette = new BufferedImage((int)(image.getWidth()*ratio), (int)(image.getHeight()*ratio), image.getType());
      AffineTransform retailler = AffineTransform.getScaleInstance(ratio, ratio);
      int interpolation = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
      AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation);
      retaillerImage.filter(image, vignette);      
   }   

   @Override
   protected void doGet(HttpServletRequest request, HttpServletResponse response)  throws ServletException, IOException {
      response.setContentType("image/png");
      OutputStream out = response.getOutputStream();
      largeurVignette = Integer.parseInt(request.getParameter("largeur"));
      String fichierImage = request.getParameter("image");
      image = ImageIO.read(new URL("file:///"+fichierImage));
      créerVignette();
      ImageIO.write(vignette, "PNG", out);
      out.close();
   }
}
/resources/css/vignette.css
.vignette {
    border-radius: 10px;
    box-shadow: 2px 2px 5px black;
    margin: 5px;
}
/resources/composants/vignette.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html">

    <!-- INTERFACE -->
    <cc:interface>
        <cc:attribute name="image" required="true" />
        <cc:attribute name="largeur" default="300" />
    </cc:interface>

    <!-- IMPLEMENTATION -->
    <cc:implementation>
        <h:outputStylesheet library="css" name="vignette.css" />
        <h:graphicImage url="Vignette?image=#{cc.attrs.image}&amp;largeur=#{cc.attrs.largeur}" styleClass="vignette" />
    </cc:implementation>
</html>

Les trois fichiers précédents constituent le composant composite dans son ensemble. Afin de bien montrer son utilisation, je vous propose la page d'accueil suivante avec son bean géré correspondant.

bean.GaleriePhoto.java
package bean;

import java.io.File;
import javax.annotation.PostConstruct;
import javax.faces.bean.*;

@ManagedBean(name="galerie")
@ViewScoped
public class GaleriePhoto implements java.io.Serializable {
   private final String répertoire = "D:/Photos/";
   private String[] fichiers;
   private int largeur = 250;

   @PostConstruct
   void init() {
       fichiers = new File(répertoire).list();
   }
   
   public String[] getFichiers() {  return fichiers; }

   public int getLargeur() { return largeur;  }
   public void setLargeur(int largeurVignette) { largeur = largeurVignette;  }

   public String getRépertoire() { return répertoire; }
}
/resources/css/principal.css
root { 
    display: block;
}

body { 
    background: darkgreen; 
    color: yellow; 
    font-weight: bold; 
    text-shadow: 0px 0px 10px yellow; 
    text-align: center;
}


input[type="text"] {
    text-align: center;
    border-radius: 5px;
    box-shadow: 2px 2px 5px black inset;
}
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:comp="http://java.sun.com/jsf/composite/composants"
      xmlns:c="http://java.sun.com/jsp/jstl/core">
    
    <h:head />    
    <h:outputStylesheet library="css" name="principal.css" />
   
    <h:body>
        <h:form>
            <h2>Galerie de photos</h2>
            Largeur : <h:inputText value="#{galerie.largeur}" size="3"/>
            <hr />
            <c:forEach items="#{galerie.fichiers}" var="fichierImage">
                <comp:vignette image="#{galerie.répertoire}/#{fichierImage}" largeur="#{galerie.largeur}" />
            </c:forEach>
        </h:form>
    </h:body>
</html>

 

Choix du chapitre Ajax (Asynchonous JavaScript and XML)

Une application web doit fournir une interface riche et rapide. Cette réactivité peut être obtenue en ne modifiant que de petites parties de la page de façon asynchrone, sans que la page ne soit rechargée entièrement, et c'est exactement pour cela qu'Ajax a été conçu.

Par exemple, sur notre application web de conversion que nous avons traité lors de l'étude précédente, nous pourrions souhaiter que seule la zone des francs soit changée lors de la soumission de la requête de conversion à partir des €uros :

Voici le code correspondant :

Je rappelle que le bean géré est la classe Java annotée par @ManagedBean qui possède deux propriétés, euro et franc, qui sont synchronisées avec les valeurs des composants de la page. La méthode euroFranc() de ce bean managé réalise le traitement de la conversion.

Ajax et JSF

Conceptuellement, Ajax est simple. Les requêtes Ajax diffèrent des requêtes standard HTTP sur seulement deux points essentiels :

  1. Le système exécute un processus partiel du formulaire durant l'appel d'une requête Ajax.
  2. Le système met à jour le rendu d'une partie de la page en cours (rendu partiel) juste après le retour de l'évocation d'une requête Ajax.

Mise en oeuvre

Lorsque vous désirez soumettre un tout petit traitement en relation avec un ou plusieurs composants de votre page web, sans prendre en compte la globalité du formulaire, vous devez tenir compte des critères suivants :

  1. Associer un composant et un événement particulier avec une requête Ajax.
  2. Identifier les composants qui jouent un rôle dans le traitement demandé au serveur, durant la requête Ajax.
  3. Identifier les composants dont le rendu sera remis à jour à l'issue de la requête Ajax.
Vous associez un appel Ajax avec un événement, comme par exemple touche relâchée (keyup) ou perte de focus (blur), déclenché par un composant spécifique. Vous spécifiez ainsi le composant qui va servir à l'exécution de la requête souhaitée et le composant dont vous désirez rafraîchir le résultat et avoir un rendu en conséquence.

Nous pourrions, à titre d'exemple très simple pour valider ce principe, réaliser une toute petite application web qui permet de faire écho à toute ce que l'utilisateur saisie. A chaque fois que l'utilisateur tape une nouvelle lettre, nous la voyons intantanément réécrite sur la partie droite du panneau.

  1. Dans cette application, l'événement se déclenche à chaque relêchement d'une touche du clavier.
  2. La requête Ajax exécute le traitement spécifié par le composant désigné comme l'appelant. Ici, la valeur @this dans l'attribut execute de la balise <f:ajax> se réfère à la balise englobante, c'est-à-dire dans notre cas à la zone de saisie <h:inputText>.
  3. A l'issue de la requête Ajax, le composant dont l'identifiant est echo se remet à jour, et affiche exactement ce qui est écrit dans la zone de saisie puisque les deux composants font référence à la même propriété.

Il est également possible d'avoir plusieurs exécutions et plusieurs rendus possibles en même temps. Dans le cas d'école suivant, lorsque nous quittons la zone de saisie (blur), cette dernière est analysée avec la zone du mot de passe. A l'issue du traitement Ajax demandé, les deux sorties identifiées par erreur et erreurPassword sont remises à jour.

<h:inputText >
     <f:ajax event="blur" execute="@this motDePasse" render="erreur erreurPassword" />
</h:inputText>
                
<h:outputText  id="erreur" />
              
<h:inputSecret id="motDePasse" />
<h:outputText id="erreurPassword" />

Ceci dit, dans la plupart des cas, vous exécutez uniquement se qui est géré par la zone de saisie qui englobe la balise <f:ajax>. Comme ce cas de figure est très fréquent, JSF exécute @this par défaut. Il est ainsi possible d'omettre l'écriture execute="@this.

index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

    <h:head />
    <h:outputStylesheet name="principal.css" library="css" />
    <h:body>
        <h:form>
            <h:panelGrid columns="3" styleClass="panneau" cellpadding="5">
                <h:outputText value="Nom : " />
                <h:inputText value="#{utilisateur.nom}">
                    <f:ajax event="keyup" render="echo" />
                </h:inputText>
                <h:outputText value="#{utilisateur.nom}" id="echo" />
            </h:panelGrid>
        </h:form>
    </h:body>
</html>

Au dela de @this, vous pouvez également utiliser d'autres valeurs prédéfinies comme @form, @all ou @none dans l'attribut execute de la balise <f:ajax>. Dans le tableau ci-dessous, j'en profite pour recenser les attributs intégrés dans cette balise.

Attribut Description
disabled Désactive la balise lorsque vous spécifiez la valeur true, comme pour les autres balises qui possèdent cet attribut.
event Evénement qui lance la requête Ajax. Vous pouvez choisir tous les événements JavaScript que nous avons abordé lors de l'étude précédente, sans toutefois le prefixe on. Il est également possible de choisir les événements action et valueChange que nous connaissons déjà.
execute Liste de composants, séparé par un espace, que JSF prend en compte lors d'une soumission à une requête Ajax. Les mots clés valides sont @this @form @all @none. Si vous ne spécifiez pas cet attribut, JSF utilise la mot clé @this comme valeur par défaut.
immediate Lorsque cet attribut est validé avec la valeur true, la validation est occultée.
listener JSF invoque la méthode méthode(AjaxBehaviorEvent evt) à chaque requête Ajax durant la phase "Appel de l'application". C'est intéressant que cela soit durant cette phase, puisque dans ce cas là, les propriétés du bean géré sont déjà validées et introduites dans le bean avant l'exécution de cette méthode.
render Liste d'identifiants, séparés par un espace, associés aux différents composants qui vont être remis à jour à l'issue de la requête Ajax.

Vous avez le droit de prendre également les mots clés valides utilisés par l'attribut execute, savoir @this @form @all @none.

Si vous ne spécifiez pas cet attribut render, la valeur par défaut est @none, ce qui aura pour conséquence de ne proposer aucun rendu après la requête complète Ajax.
Validation des zones de saisie

Un des grands intérêt d'utiliser Ajax est de vérifier "à la volée" les valeurs saisies par l'utilisateur. Grâce à ce système, nous ne sommes plus obligé d'attendre la soumisssion de la requête entière, en cliquant sur le bouton prévu à cet effet, pour vérifier la validité des données introduites.

A titre d'exemple, je vous propose de reprendre la partie correspondant à l'identification d'un utilisateur. Le nom de l'identifiant doit avoir au moins 2 caractères, uniquement alphabétiques. Le contrôle s'effectue après chaque caractère saisie. Le mot de passe doit lui-même posséder au moins quatre caractères. Cette fois-ci, le contrôle s'effectue à la perte du focus. Pour terminer, lorsque le curseur de la souris passe au-dessus du bouton de soumission, les deux zones de saisie sont évaluées.

bean.Login.java
package bean;

import javax.faces.bean.*;
import javax.validation.constraints.*;

@ManagedBean
@SessionScoped
public class Login {
    @Pattern(regexp="[a-zA-Z]*", message="Lettres alphabétiques uniquement")
    @Size(min=2, message="Il faut au moins 2 caractères")
    private String utilisateur;
    @Size(min=4, message="Il faut au moins 4 caractères")
    private String motDePasse;

    public String getMotDePasse() { return motDePasse;  }
    public void setMotDePasse(String motDePasse) { this.motDePasse = motDePasse;  }

    public String getUtilisateur() { return utilisateur;  }
    public void setUtilisateur(String utilisateur) { this.utilisateur = utilisateur; }

    public String connexion() {
        boolean valide = utilisateur.equalsIgnoreCase("manu") && motDePasse.equals("manu");
        return valide ? "semaine" : "index";
    }
}
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

    <h:head />
    <h:outputStylesheet name="principal.css" library="css" />
    
    <h:body>
        <h:form>
            <h:panelGrid columns="3" styleClass="panneau" headerClass="entete" footerClass="pied">
                <f:facet name="header">
                    <h:outputText value="Authentification" />
                </f:facet>
                <h:outputText value="Utilisateur : " />
                <h:inputText value="#{login.utilisateur}" id="nom">
                    <f:ajax event="keyup" render="erreurNom" />
                </h:inputText>
                <h:message for="nom" id="erreurNom" />
                <h:outputText value="Mot de passe : " />
                <h:inputSecret value="#{login.motDePasse}" id="passe">
                    <f:ajax event="blur" render="erreurPasse" />
                </h:inputSecret>
                <h:message for="passe" id="erreurPasse" />
                <f:facet name="footer">
                    <h:commandButton value="Connexion" action="#{login.connexion}">
                        <f:ajax event="mouseover" execute="nom passe" render="erreurNom erreurPasse" />
                    </h:commandButton>
                </f:facet>            
            </h:panelGrid>
        </h:form>
    </h:body>
</html>
Groupes Ajax

Dans tout ce que nous venons de voir, nous plaçons systématiquement la balise <f:ajax> à l'intérieur d'autres composants qui servent alors de soumission à une requête Ajax associée. JSF permet également d'associer une requête Ajax cette fois-ci sur un groupe de composants.

En reprenant l'exemple précédent, la balise <f:ajax> peut englober les deux zones de saisies afin de les prendre en compte automatiquement. Il reste juste à définir le rendu que vous désirez prendre en compte.

Par défaut, ce sont les événements de type action (pour les boutons et les liens) et les événement de type valueChange (pour les composants de sortie, d'entrée et de sélection) qui sont automatiquement évalués.

index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

    <h:head />
    <h:outputStylesheet name="principal.css" library="css" />
    
    <h:body>  
        <h:form>
            <h:panelGrid columns="3" styleClass="panneau" headerClass="entete" footerClass="pied">
                <f:facet name="header">
                    <h:outputText value="Authentification" />
                </f:facet><h:outputText value="Utilisateur : " />
                    <h:inputText value="#{login.utilisateur}" id="nom" />
                    <h:message for="nom" id="erreurNom" />
                    <h:outputText value="Mot de passe : " />
                    <h:inputSecret value="#{login.motDePasse}" id="passe" />
                    <h:message for="passe" id="erreurPasse" />
                </f:ajax>
                <f:facet name="footer">
                    <h:commandButton value="Connexion" action="#{login.connexion}">
                        <f:ajax event="mouseover" execute="nom passe" render="erreurNom erreurPasse" />
                    </h:commandButton>
                </f:facet>     
            </h:panelGrid>
        </h:form>
    </h:body>
</html>  

Dans cet exemple, c'est lors d'un changement de valeur sur une des zones de saisie que la validation est vérifiée. Si vous souhaitez prendre un autre type d'événement, et donc ne pas prendre ceux proposer par défaut, il suffit, comme précédemment, d'utiliser l'attribut event et de spécifier celui qui vous intéresse. Par exemple, si vous désirez soumettre la validation après chaque changement de focus, voilà ce qu'il faudrait changer :

<f:ajax event="blur" render="erreurNom erreurPasse">.

Exemple de conclusion

Pour terminer ce chapitre, je vous propose de traiter un dernier exemple. Nous allons reprendre notre application sur le calcul du jour de la semaine suivant la date que nous fixons.

Cette fois-ci, toutefois, nous proposons une liste des jours du mois en correspondance du mois et de l'année en cours. Ainsi, nous n'aurons plus de problème de validation. Par ailleurs, dès qu'un changement est effectué, la liste des jours doit être recalculée et le résultat doit être affichée instantanément.

bean.JourSemaine.java
package bean;

import java.text.SimpleDateFormat;
import java.util.*;
import javax.annotation.PostConstruct;
import javax.faces.bean.*;
import javax.faces.event.AjaxBehaviorEvent;

@ManagedBean(name="semaine")
@ViewScoped
public class JourSemaine {
    private int jour;
    private int[] jours;
    private enum Mois {Janvier, Février, Mars, Avril, Mai, Juin, Juillet, Août, Septembre, Octobre, Novembre, Décembre};
    private Mois mois;
    private int année;
    private String résultat;

    public int getJour() { return jour; }
    public void setJour(int jour) { this.jour = jour; }

    public Mois getMois() { return mois; }
    
    public void setMois(Mois mois) { 
        this.mois = mois;
        listeJours();
    }       
    
    public int getAnnée() { return année; }
    
    public void setAnnée(int année) { 
        this.année = année; 
        listeJours();
    }
 
    public Mois[] getListeMois() { return Mois.values(); }
    public int[] getJours() { return jours; }

    public String getRésultat() { return résultat;  }    
    
    @PostConstruct
    private void init() {
       Calendar date = Calendar.getInstance();
       jour = date.get(Calendar.DAY_OF_MONTH);
		  année = date.get(Calendar.YEAR);
       setMois(Mois.values()[date.get(Calendar.MONTH)]);
       SimpleDateFormat jourSemaine = new SimpleDateFormat("EEEE");
       résultat = jourSemaine.format(date.getTime()).toUpperCase();       
    }
       
    public void recherche(AjaxBehaviorEvent evt) {
       Calendar date = new GregorianCalendar(année, mois.ordinal(), jour);
       SimpleDateFormat jourSemaine = new SimpleDateFormat("EEEE");
       résultat = jourSemaine.format(date.getTime()).toUpperCase();
    }

   private void listeJours() {
       int nombreJourMaxi;
       int intMois = mois.ordinal();
       if (intMois==1) {
          boolean bissextile = année%4==0 && (année%400==0 || année%100!=0); 
          nombreJourMaxi = bissextile ? 29 : 28;
       }       
       else {
          boolean trente = intMois==3 || intMois==5 || intMois==8 || intMois==10;
          nombreJourMaxi = trente ? 30 : 31;
       }
       jours = new int[nombreJourMaxi];
       for (int i=0; i<nombreJourMaxi; i++) jours[i] = i+1;
   }
}
  1. Par rapport au projet précédent, pas mal de petites modifications ont été apportées. Globalement le code est plus simple.
  2. Vous remarquez que nous faisons appel à la méthode listeJours() lorsque nous modifions la valeur du mois ou de l'année.
  3. La méthode recherche() sera lancée automatiquement après chaque requête Ajax, puisque elle en relation avec un événement de type AjaxBehaviorEvent.
semaine.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    <h:head />
    <h:outputStylesheet name="principal.css" library="css" />
    <h:body>
        <h:form>
            <h:panelGrid columns="2" styleClass="panneau" headerClass="entete" footerClass="pied">
                <f:facet name="header">
                    <h:outputText value="Recherche du jour de la semaine" />
                </f:facet>
                <f:ajax render="jour resultat" listener="#{semaine.recherche}">
                    <h:outputText value="Jour : " />
                    <h:selectOneMenu value="#{semaine.jour}" id="jour">
                        <f:selectItems value="#{semaine.jours}" />
                    </h:selectOneMenu>
                    <h:outputText value="Mois : " />
                    <h:selectOneMenu value="#{semaine.mois}">
                        <f:selectItems value="#{semaine.listeMois}" />                  
                    </h:selectOneMenu>
                    <h:outputText value="Année : " />
                    <h:inputText value="#{semaine.année}" />
                </f:ajax>
                <f:facet name="footer">
                    <h:outputText value="#{semaine.résultat}" styleClass="resultat" id="resultat" />
                </f:facet>            
            </h:panelGrid>
        </h:form>
    </h:body>
</html>

La balise <f:ajax> entoute toutes les zones de saisie. Dès qu'un changement est effectuée, la liste de la sélection du jour est remise à jour systématiquement et le calcul du jour de la semaine est automatiquement redemandé grâce à l'écouteur placée sur cette balise.

 

Choix du chapitre PrimeFaces

Avant de conclure sur cette étude, je désire juste vous évoquer qu'il existe une librairie très riche et très performante, avec des composants JSF de très haut niveau. Cette librairie s'appelle PrimeFaces, et elle peut être automatiquement introduite dans Netbeans.

Afin de connaître toutes les possibilités qui vous sont offertes dans cette librairie, je vous invite à rejoindre le site correspondant.
.