JavaServer Faces - techniques avancées

publicité
JavaServer Faces - techniques avancées
Chapitres traités
Cycle de traitement des requêtes JSF
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 maîtriser la structure et le mécanisme interne d'un système élaboré par JSF. Il est en effet important de connaître l'enchaînement des opérations qui
conduisent au résultat final lorsqu'une issue du requête est proposée.
2. 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 JavaBean.
Des messages adaptés peuvent alors survenir si les saisies sont incorrectes.
3. 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.
4. Dans le cas de développements importants, il également utile de créer des composants réutilisables pour augmenter la productivité et faciliter la maintenance.
(Cette partie n'est pas traitée).
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.
Classes JSF utiles pour le développement côté JavaBean
Lorsque nous developpions des JavaBeans avec les pages JSP sans tenir compte de la technologie JSF, il était très difficile d'être directement en interaction avec les
éléments constituant la page JSP et tout ce qui concernait l'application Web dans son ensemble. Nous étions souvent obligés de passer par des servlets. Une autre
solution était de faire en sorte que la page JSP passe tous les renseignements requis avec des propriétés supplémentaires dans le JavaBean concerné. ce qui alourdissait
considérablement le code.
Dans le cas de JSF, les JavaBeans peuvent récupérer directement tous les renseignements nécessaires pour mettre en oeuvre les traitements désirés. C'est un des
très gros avantage de cette technologie. Nous allons recenser ici, sans toutefois être pleinement exhaustif, les classes qui peuvent nous aider dans notre démarche.
Le but de ce chapitre est juste de présenter ces classes sans s'attarder sur les détails d'implémentation. Dans les chapitres suivant, nous développerons des sujets
qui interagiront avec ces classes et nous démontrerons l'intérêt de les utiliser.
Hiérarchie des composants Interface Utilisateur
Nous l'avons déjà découvert dans le chapitre précédent, JSF intègre une hiérarchie de composants qui représente l'interface utilisateur côté client. Ces composants sont
issus du paquetage javax.faces.component.
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.
Présentation de tableaux dynamiques
A titre d'exemple, nous pouvons consulter quelques méthodes intéressantes de la classe UIData qui représente la balise <h:dataTable> par l'intermédiaire de
HtmlTableData, et qui peuvent s'avérer bien utiles dans bien des cas. Effectivement, nous avons souvent besoin de cette balise lors de la mise en valeur d'un
ensemble d'éléments.
javax.faces.component.UIData
Méthodes
Retour
Explication
getRowCount()
int
Retourne le nombre de lignes total que comporte l'ensemble du tableau de données ou alors -1 si le tableau est inconnu.
getRows()
int
Retourne le nombre de lignes qui peut être affiché dans la page, ou 0 pour toutes les lignes restantes dans la table. Le nombre de
lignes que nous pouvons afficher est déterminé par la méthode setRows() ci-dessous.
getRowsIndex()
int
Retourne la valeur de l'index de la ligne courante.
getFirst()
int
Retourne la valeur de l'index de la première ligne qui peut être affichée.
getRowData()
Object
Retourne la valeur de la donnée de la ligne courante.
setRowIndex(int index)
void
Sélectionne la ligne que nous désirons consulter.
setFirst(int index)
void
Sélectionne la première ligne qui peut être affichée.
setRows(int lignes)
void
Sélectionne le nombre de lignes à afficher.
Récupérer le contexte de la page courante - FacesContext
C'est certainement la classe la plus importante. Elle est issue du paquetage javax.faces.context. Cette classe nous permet d'être en relation avec la page JSP courante qui
communique avec notre JavaBean. L'objet relatif de cette classe FacesContext représente donc la page JSF, et la plupart du temps, nous passerons par cet objet pour
consulter d'autres objets d'une autre nature qui auront un comportement spécifique sur la page.
Voici la démarche à suivre pour récupérer l'objet représentant la page JSP courante :
FacesContext contexte = FacesContext.getCurrentInstance();
Ici, l'objet contexte représente donc la page Web qui est en communication avec notre JavaBean. 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.
javax.faces.context.FacesContext
Méthodes
Retour
Explication
getCourentInstance()
FacesContext
Retourne le contexte courant, c'est-à-dire, la page JSP qui sert de conteneur.
addMessage(String client,
FacesMessage message)
void
Permet d'envoyer un message d'alerte ou d'erreur directement sur le composant client qui a provoqué l'erreur.
getApplication()
Application
Retourne l'objet représentant l'ensemble de l'application Web. Nous connaissons déjà cet objet que nous avons appris à
connaître lors de l'étude sur les pages JSP.
getExternalContext()
ExternalContext
Retourne un objet relatif à tous les éléments externes à la page courante. Cet objet est d'une grande utilitée. 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.
Il existe bien d'autres méthodes comme getViewRoot() ou getRenderResponse() où nous voyons bien à quoi elles correspondent. Mais, elles sont généralement d'un
intérêt plutôt limité.
Ajout de messages sur l'interface utilisateur du client - FacesMessage
Nous pouvons afficher des messages d'alerte sur nos pages JSP par l'intermédiaire de balises adaptées <h:messages> ou <h:message>. Ces alertes apparaissent dans le
cas, par exemple, d'une mauvaise saisie. Nous en avons déjà donné un petit aperçu dans l'étude précédente. L'intérêt de ces messages, c'est qu'ils n'apparaissent qu'en
cas de problème. Ces messages sont implémentés par la classe javax.faces.application.FacesMessage. Dans la suite de cette étude, nous consacrerons tout un chapitre
entier à la gestion de ces messages.
Voici pour l'instant comment nous pouvons créer un message :
FacesContext contexte = FacesContext.getCurrentInstance();
FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Attention", "Entier attendu");
contexte.addMessage("saisie", message);
javax.faces.application.FacesMessage
Méthodes
Retour
Explication
SEVERITY_ERROR
FacesMessage.Severity
Indique qu'une erreur est survenue.
SEVERITY_FATAL
FacesMessage.Severity
Indique qu'une erreur fatale est survenue.
SEVERITY_INFO
FacesMessage.Severity
Juste un message d'information.
SEVERITY_WARN
FacesMessage.Severity
Message qui réclame une attention particulière.
FacesMessage()
Construit un message sans valeurs initiales.
setSeverity(FacesMessage.Severitysévérité)
void
Précise le type de message.
setSummary(String erreurSommaire)
void
Donne le message mais de façon sommaire.
setDetail(String détailErreur)
void
Indique précisément le type d'erreur.
FacesMessage(String erreurSommaire)
Construit un message sommaire.
FacesMessage(FacesMessage.Severity
sévérité, String sommaire, String détail)
Construit un message complet avec le type d'erreur adapté. Certainement le constructeur le plus
utilisé.
Application Web - Application
La classe javax.faces.application.Application permet de gérer des éléments qui sont placés dans l'application Web. Depuis n'importe quelle page, il est alors possible de
créer de nouveaux éléments. Par la suite, une autre page peut ensuite consulter ces nouvelles informations. Nous pouvons par exemple créer de nouveaux types de
convertisseur, de nouveaux types de validateur, gérer l'internationalisation.
Voici comment obtenir ce type d'info :
FacesContext contexte = FacesContext.getCurrentInstance();
Application externe = contexte.getApplication();
javax.faces.application.Application
Méthodes
getRessourceBundle(FacesContext
String nom)
ctx,
Retour
Explication
RessourceBundle
Récupère la ressource qui gère l'internationalisation spécifiée en argument.
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();
javax.faces.context.ExternalContext
Méthodes
Retour
Explication
getApplicationMap()
Map
Retourne les objets (les attributs) stockés dans l'application Web.
getInitParameter(String paramètre)
String
Retourne le paramètre initialisé dans le contexte de l'application Web spécifié par le descripteur de déploiement.
getInitParameterMap()
Map
Délivre l'ensemble des paramètres d'initialisation de l'application Web.
getRemoteUser()
String
Procure le nom de loggin de l'utilisateur fabriqué dans la requête courante.
getRequestCookieMap()
Map
Récupère l'ensemble des cookies de la requête.
getRequest()
Object
Récupère l'objet request que nous connaissons bien.
getRequestContextPath()
String
Retourne la portion d'URL qui correspond à l'emplacement de l'application Web.
getRequestHeaderMap()
Map
Retourne l'ensemble de l'en-tête de la requête.
getRequestHeaderValuesMap()
Map
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()
Map
Retourne les attributs de l'application courante.
getRequestParameterMap()
Map
Retourne les paramètres de la requête.
getRequestParameterNames()
Iterator
Retourne les paramètres de la requête en passant par un itérateur.
getRequestParameterValuesMap()
Map
Retourne les paramètres de la requête en prenant en compte plutôt les tableaux de chaînes.
getRequestPathInfo()
String
Retourne uniquement la portion de l'URL qui suit le nom de l'application Web.
getRessource(String chemin)
URL
Permet de récupérer l'URL d'une ressource.
getRessourceAsStream(String
chemin)
InputStream
Récupère une ressource sous forme de flux d'entrée.
getResponse()
Object
Récupère l'objet response que nous connaissons bien.
getSession(boolean création)
Object
Mise en oeuvre ou consultation d'une session.
getSessionMap()
Map
Récupère les attributs créés dans la session.
log(String message)
void
Envoie un message dans le journal de l'application Web.
Attribut binding
Le binding permet de lier un composant JSF à une propriété d'un bean. Lorsque le binding d'un composant est fait, il est alors possible d'agir sur ce composant, et ainsi de
changer éventuellement son comportement par programmation directement à l'intérieur du bean. L'attribut binding est présent sur la plupart des balises JSF. Lorsque la
balise est exécutée, alors le composant JSF associé sera accessible à partir du bean spécifié.
Nous allons mettre en oeuvre cette spécifité au travers de l'application Web qui permet de retrouver un nombre aléatoire. Nous avons déjà construit cette
application Web lors de l'étude précédente. Bien entendu, nous allons rajouter un certain nombre de comportements spécifiques qui vont nous permettre de
valider nos propos.
Nous allons modifier notre page d'accueil de telle sorte que lorsque la valeur est trouvée, d'une part la zone de saisie se retrouve en lecture seule, et d'autre part, le
bouton "Valider votre choix" se retrouve lui aussi désactivé. Ainsi, il ne sera plus possible de proposer et de valider une autre valeur alors que le nombre est déjà
trouvé. La seule possiblité, si nous désirons rester sur cette application Web, est de demander à tout recommencer. Ce qui est d'ailleurs le processus normal
attendu.
Dans le cas où le nombre n'est pas trouvé, la page fini.jsp s'affiche alors. Là aussi, nous avons mis en oeuvre une petite nouveauté. Nous rajoutons cette fois-ci une
nouvelle colonne à gauche dans le tableau qui précise en réalité le numéro de la ligne courante du tableau.
Nous allons profiter de cette restructuration pour étoffer le comportement de notre application Web. Ainsi, cette fois-ci, il sera possible de régler le nombre de
tentative possible pour trouver le bon chiffre. Nous pourrons également choisir la fourchette des valeurs. C'est réglages doivent être spécifiés dans le fichier de
configuration <faces-config.xml>.
Modèle MVC
Notre modèle continu à prendre en compte la navigation entre les deux pages alea.jsp et fini.jsp. Par ailleurs, le traitement des informations est toujours réalisé par le bean
alea.Nombre qui comportera toutefois des attributs et des méthodes supplémentaires, vu que notre site s'est enrichi.
Constitution de l'application Web et descripteur de déploiement
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servletclass>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>
faces/alea.jsp
</welcome-file>
</welcome-file-list>
</web-app>
Notre descipteur de déploiement <web.xml> est totalement identique à celui que nous avions déjà défini. Effectivement, nous faisons appel à la même page d'accueil.
Par ailleurs le contrôleur est toujours le même lorsque nous développons un système JSF. Je rappelle que c'est à l'aide du fichier de configuration <facesconfig.xml> que nous décrivons la navigation entre les deux pages Web alea.jsp et fin.jsp ainsi que la déclaration du bean traitant de l'information.
Fichier de configuration <faces-config.xml>
Ce fichier de configuration dispose toujours de trois éléments. Effectivement, nous le savons déjà, nous devons gérer le bean nombre. Par ailleurs, nous disposons de deux
pages Web qui sont sources, à partir desquelles, nous pouvons évoluer vers une autre page. Par contre, notre bean nombre possède deux propriétés supplémentaires que
nous réglons à l'aide de ce fichier de configuration. Il s'agit de la propriété valeurMaxi qui permet de donner la limite supérieure du nombre à rechercher. Nous avons
également la propriété tentativeMaxi qui précise le nombre de coups possibles avant de trouver la solution.
faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
<navigation-rule>
<navigation-case>
<from-action>#{nombre.fini}</from-action>
<from-outcome>finir</from-outcome>
<to-view-id>/fin.jsp</to-view-id>
<redirect />
</navigation-case>
</navigation-rule>
<navigation-rule>
<navigation-case>
<from-action>#{nombre.recommencer}</from-action>
<from-outcome>recommencer</from-outcome>
<to-view-id>/alea.jsp</to-view-id>
<redirect />
</navigation-case>
</navigation-rule>
<managed-bean>
<managed-bean-name>nombre</managed-bean-name>
<managed-bean-class>alea.Nombre</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>valeurMaxi</property-name>
<value>15</value>
</managed-property>
<managed-property>
<property-name>tentativeMaxi</property-name>
<value>4</value>
</managed-property>
</managed-bean>
</faces-config>
Page d'accueil du site alea.jsp
Sur cette page d'accueil, nous devons faire en sorte, cette fois-ci, que la zone de saisie, ainsi que le bouton "Valider votre choix" sont éventuellement grisés lorsque
l'opérateur trouve la valeur désirée. C'est le bean nombre qui doit gérer cette situation, puisque c'est lui qui dispose de tous les renseignements nécessaires pour indiquer
à quel moment ces composants graphiques doivent être non utilisables.
alea.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<f:view>
<html>
<body style="background-color: yellow">
<h2><h:outputText value="Recherche d'un nombre entre 1 et #{nombre.valeurMaxi}" /></h2>
<hr />
<h:form>
<h4>
Introduisez votre nombre :
<h:inputText value="#{nombre.valeur}" size="2" binding="#{nombre.saisie}" />
<h:outputText value="#{nombre.progression}" style="color: red" />
</h4>
<h4>Tentatives : <h:outputText value="#{nombre.tentative}" style="color: red" /> fois</h4>
<p>
<h:commandButton action="#{nombre.fini}" value="Valider votre choix" binding="#{nombre.bloquer}"/>
<h:commandButton action="#{nombre.recommencer}" value="Tout recommencer" />
</p>
</h:form>
<hr />
<h3><h:outputText value="#{nombre.résultat}" style="color: blue"/></h3>
</body>
</html>
</f:view>
Il faut donc qu'il existe une liaison entre la balise concernée et une propriété qui va représentée le composant dans le bean nombre. Cela s'effectue par
l'intermédiaire de l'attribut binding. Vous réglez l'attribut binding en spécifiant la propriété qui va gérer l'intercommunication.
Il faut bien comprendre qu'en réalité une balise JSF est une représentation graphique d'un composant interne non visible. D'ailleurs, nous avons vu que, par exemple,
à la balise <h:inputText> correspond l'objet issu de la classe équivalente HtmlInputText. Ainsi, le binding permet tout simplement au bean de se mettre en relation
directement avec cet objet interne non visuel. Du coup, tout changement que vous effectuez sur cet objet se répercute bien évidemment sur la partie visuelle. La
propriété doit alors correspondre au type du composant pour que la liaison puisse se faire correctement. Ainsi, en reprenant l'exemple de la balise <h:inputText>,
nous devons proposer une propriété soit de type HtmlInputText, soit de type UIInput. Si vous désirez faire des manipulations sophistiquées, il est préférable de
prendre le premier type d'objet puisqu'il disposera de plus de propriétés. Si seule la valeur saisie vous intéresse, il est alors plus judicieux de prendre un objet de
type UIInput.
Nous avons deux composants à griser. Nous devons donc mettre en place l'attribut binding sur les balises correspondantes :
1. <h:inputText> (Ligne 12 ) : Ici, la propriété saisie du bean nombre est l'objet qui représente cette balise d'entrée.
2. <h:commandButton> (Ligne 17) : Ici, la propriété bloquer du bean nombre est l'objet qui représente le bouton "Valider votre choix".
Le bean nombre issu de la classe alea.Nombre
Nous passons tout de suite sur le bean nombre afin de découvrir comment est gérer le binding. Le traitement à réaliser est relativement sophistiqué puisque nous agissons
sur l'apparence des composants, donc sur leurs parties visuelles. Il parait donc judicieux de prendre des objets qui représentent parfaitement les balises à gérer.
alea.Nombre
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package alea;
import javax.faces.component.*;
import javax.faces.component.html.*;
public class Nombre {
private int valeurMaxi;
private int tentativeMaxi;
private int valeur;
private int nombreARechercher;
private int tentative;
private boolean test;
private Integer[] historique;
private HtmlInputText saisie;
private HtmlCommandButton bloquer;
private UIData table;
public HtmlInputText getSaisie() {
return saisie;
}
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110 }
public void setSaisie(HtmlInputText saisie) {
this.saisie = saisie;
}
public HtmlCommandButton getBloquer() {
return bloquer;
}
public void setBloquer(HtmlCommandButton bloquer) {
this.bloquer = bloquer;
}
public UIData getTable() {
return table;
}
public void setTable(UIData table) {
this.table = table;
}
public int getTentativeMaxi() {
return tentativeMaxi;
}
public void setTentativeMaxi(int tentativeMaxi) {
this.tentativeMaxi = tentativeMaxi;
historique = new Integer[tentativeMaxi];
}
public int getValeurMaxi() {
return valeurMaxi;
}
public void setValeurMaxi(int valeurMaxi) {
this.valeurMaxi = valeurMaxi;
nombreARechercher = (int)(Math.random()*valeurMaxi)+1;
}
public void setValeur(int valeur) {
this.valeur = valeur;
historique[tentative++] = valeur;
test = nombreARechercher == valeur;
if (test) {
saisie.setReadonly(true);
bloquer.setDisabled(true);
}
}
public int getValeur() {
return valeur;
}
public int getTentative() {
return tentative;
}
public String getRésultat() {
if (tentative==0) return "Tentez votre chance";
if (test) return "Bravo, vous avez trouvé !";
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 "+(nombreARechercher>valeur ? "grand" : "petit");
}
public String fini() {
return !test && tentative>=tentativeMaxi ? "finir" : "continuer";
}
public String recommencer() {
nombreARechercher = (int)(Math.random()*valeurMaxi)+1;
tentative = 0;
valeur = 0;
saisie.setReadonly(false);
bloquer.setDisabled(false);
return "recommencer";
}
public Integer[] getHistorique() {
return historique;
}
public int getNombreARechercher() {
return nombreARechercher;
}
1. Lignes 15, 16 et 19 à 33 : Je mets donc en oeuvre la propriété saisie qui est un objet de type HtmlInputText. De même, la propriété bloquer est de type
HtmlCommandButton. Il est à noter que nous devons proposer systématiquement des propriétés en mode lecture et écriture. Ainsi, les objets saisie et bloquer
représentent l'état actuel des balises correspondantes, bien entendu, celles seulement qui ont proposées le binding (la liaison).
2. Lignes 65 à 68 et 98 à 99 : Une fois que nous sommes en communication avec ces balises par l'intermédiaire de ces objets, nous pouvons modifier leurs
comportement respectifs en écrivant un code adapté. Il s'agit tout simplement de faire appel aux méthodes correspondant aux traitements souhaités. Ainsi, la
méthode setReadonly() de l'objet HtmlInputText permet de griser cette zone de saisie et donc d'empêcher momentanément l'écriture de nouvelles valeurs. De
même, nous faisons appel à la méthode setDisabled() de l'objet HtmlCommandButton pour que ce dernier bloque le comportement normal du bouton "Valider votre
choix".
3. Lignes 17 et 35 à 41 : Nous disposons ici d'une propriété table qui correspond au tableau de l'historique géré par la page fin.jsp. Remarquez au passage qu'aucun
traitement particulier n'est demandé. Cette fois-ci, cette propriété est de type UIData. Je n'ai pas voulu prendre la classe HtmlTableData. En effet, cette classe
UIData est suffisante, puisqu'elle comporte déjà les propriétés qui m'intéressent pour le traitement que je souhaite réaliser.
La page fin.jsp
Passons donc à cette page fin.jsp où nous allons découvrir comment nous récupérons la ligne courante d'un tableau dynamique issu de la balise <h:dataTable>.
fin.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
.cadre { background-color: #FFCC00; padding: 5px; border : groove; font-weight: bold; }
.rouge { color: red; }
</style>
<f:view>
<html>
<body style="background-color: yellow">
<h2 class="cadre">
C'est fini : il fallait trouver le nombre en
<h:outputText value="#{nombre.tentativeMaxi}" styleClass="rouge"/>
coups
</h2>
<h:dataTable value="#{nombre.historique}" var="historique" styleClass="cadre"
binding="#{nombre.table}" rules="all" cellpadding="3">
<f:facet name="header">
<h:outputText value="Historique des coups réalisés" />
</f:facet>
<h:column>
<h:outputText value="Coup n°#{nombre.table.rowIndex}" />
</h:column>
<h:column>
<h:outputText value=" #{historique}" />
</h:column>
</h:dataTable>
<h2 class="cadre">
Nombre à retrouver :
<h:outputText value="#{nombre.nombreARechercher}" styleClass="rouge"/>
<h:form>
<h:commandLink action="#{nombre.recommencer}" value="Recommencer" title="Refaire d'autres essais"/>
</h:form>
</h2>
</body>
</html>
</f:view>
1. Ligne 17 et 18 : mise en place du binding en mettant en relation cette balise <h:dataTable> avec la propriété table que nous venons d'évoqué dans la rubrique
précédente.
2. Ligne 23 : Cette propriété table est donc un objet de type UIData et représente le tableau dynamique de cette page. Cet objet dispose, bien entendu, d'un certain
nombre de propriétés que nous pouvons utiliser directement. Ici, j'utilise tout simplement la propriété rowIndex qui me délivre le numéro de la ligne courante sans
que j'ai besoin d'écrire une quelconque ligne de code dans le bean correspondant. Du coup, nous pouvons nous demander si la déclaration de l'objet de type
UIData est nécessaire dans le bean nombre, vu qu'aucune ligne de code n'est écrite dans ce dernier. Effectivement, il reste nécessaire de créer une propriété dans
le bean correspondant à cette table afin que par la suite nous puissions l'utiliser à des endroits différents dans la page Web.
Récupération des informations sur le contexte externe à la page courante
Ce chapitre va nous permettre de découvrir comment récupérer des informations sur l'ensemble du contexte de l'application Web. Nous en profiterons pour réaliser un
téléchargement. De nouveau, nous allons nous servir de la même application Web que nous avons mis en oeuvre dans le chapitre précédent. Nous allons juste rajouter sur
la page d'accueil un bouton "Informations générales" qui lorsque nous cliquons dessus permet de télécharger les informations requises.
Ce type d'information n'a pas un gros intérêt sur cette application Web. En réalité, j'ai pris cette application pour éviter d'en refaire une autre.
.
Pour ce chapitre, nous consulterons juste les fichiers qui ont subit des modifications. Nous allons donc revoir le fichier alea.jsp et le JavaBean alea.Nombre.
.
Page d'accueil du site alea.jsp
Sur cette page d'accueil, je place juste une ligne supplémentaire qui permet d'afficher ce nouveau bouton (Ligne 19). Comme action, je propose de lancer la méthode info()
du bean nombre.
alea.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<f:view>
<html>
<body style="background-color: yellow">
<h2><h:outputText value="Recherche d'un nombre entre 1 et #{nombre.valeurMaxi}" /></h2>
<hr />
<h:form>
<h4>
Introduisez votre nombre :
<h:inputText value="#{nombre.valeur}" size="2" binding="#{nombre.saisie}" />
<h:outputText value="#{nombre.progression}" style="color: red" />
</h4>
<h4>Tentatives : <h:outputText value="#{nombre.tentative}" style="color: red" /> fois</h4>
<p>
<h:commandButton action="#{nombre.fini}" value="Valider votre choix" binding="#{nombre.bloquer}"/>
<h:commandButton action="#{nombre.recommencer}" value="Tout recommencer" />
<h:commandButton action="#{nombre.info}" value="Informations générales" />
</p>
</h:form>
<hr />
<h3><h:outputText value="#{nombre.résultat}" style="color: blue"/></h3>
</body>
</html>
</f:view>
Le bean nombre issu de la classe alea.Nombre
Nous passons tout de suite sur le bean nombre afin de découvrir ce que fait cette méthode info(). D'ailleurs, j'ai enlevé tout le reste du code pour que nous soyons
concentré sur le sujet.
alea.Nombre
1
2
3
4
5
6
7
package alea;
import
import
import
import
import
java.io.IOException;
java.io.PrintWriter;
javax.faces.component.*;
javax.faces.component.html.*;
javax.faces.context.*;
8 import javax.servlet.http.*;
9
10 public class Nombre {
114
...
115
public void info() {
116
ExternalContext externe = FacesContext.getCurrentInstance().getExternalContext();
117
HttpServletResponse réponse = (HttpServletResponse) externe.getResponse();
118
HttpServletRequest requête = (HttpServletRequest) externe.getRequest();
119
réponse.setContentType("application/octet-stream");
120
try {
121
PrintWriter out = réponse.getWriter();
122
out.println("Type de contenu du document : "+requête.getContentType());
123
out.println("Adresse IP locale : "+requête.getLocalAddr());
124
out.println("Application Web : "+requête.getContextPath());
125
out.println("Nom local : "+requête.getLocalName());
126
out.println("Port local : "+requête.getLocalPort());
127
out.println("Page web sollicité : "+requête.getPathInfo());
128
out.println("Répertoire local : "+requête.getPathTranslated());
129
out.println("Protocole : "+requête.getProtocol());
130
out.println("Adresse client : "+requête.getRemoteAddr());
131
out.println("Nom ordinateur client : "+requête.getRemoteHost());
132
out.println("Port du client : "+requête.getRemotePort());
133
out.println("URI complète de l'application Web : "+requête.getRequestURI());
134
out.println("Identifiant de la session : "+requête.getRequestedSessionId());
135
out.println("Nom du serveur : "+requête.getServerName());
136
out.println("Port du serveur : "+requête.getServerPort());
137
out.println("URI du servlet contrôleur : "+requête.getServletPath());
138
out.println("Application Web : "+externe.getRequestContextPath());
139
out.println("Page web sollicité : "+externe.getRequestPathInfo());
140
out.println("URI du servlet contrôleur : "+externe.getRequestServletPath());
141
out.close();
142
}
143
catch (IOException ex) {
144
ex.printStackTrace();
145
}
146
}
147 }
1. Lignes 116 : Vu que je m'intéresse au contexte général de l'application Web, je passe par un objet de type ExternelContext qui est fourni par le contexte de la page
elle-même au travers, cette fois-ci, de la classe FacesContext..
2. Lignes 117 et 118 : A partir de là, il est possible de récupérer à la fois l'objet request correspondant à la requête proposée sur notre page Web courante, ainsi que
l'objet response qui représente l'information qui va être envoyée en résultat de la requête.
3. Lignes 119 : Pour qu'un téléchargement soit proposé au moyen d'une boîte de dialogue lancée automatiquement par les navigateurs, il faut régler le type MIME en
conséquence au moyen de la méthode setContentType() de l'objet response. Si vous prévoyez par exemple le type "text/html", tout ce que vous écrivez dans
response va être considéré comme la structure d'une page Web, et va donc être affiché par le navigateur à la place de la page courante. Si vous prévoyez plutôt le
type "text/plain", le navigateur va également affiché votre texte à la place de votre page courante sans toutefois l'interpréter. Il s'agit dans ce cas là d'un texte brut.
Si vous désirez conserver votre page Web courante et ainsi solliciter le téléchargement, il faut prendre alors un type MIME plus générique, non interprétable par
quoi que ce soit. Pour cela, vous indiquez que c'est une application spécifique (et non le navigateur) qui va gérer cette information. Du coup, nous envoyons notre
ensemble d'information tout simplement sous forme de flux d'octets qui sera interprété, non plus par le navigateur, mais par le logiciel prévu à cet effet, qui sera
choisi ultérieurement après téléchargement. Finalement, le type MIME adapté à tout téléchargement est "application/octet-stream".
4. Lignes 121 : Le navigateur va recevoir un flux d'octets. Pour lui, la seule solution est donc d'afficher une boîte de dialogue qui propose le téléchargement. Par
contre, rien empêche pour le programme client, d'avoir une information de plus haut niveau. Ce qui m'intéresse ici, c'est de stocker mes informations sous forme
de texte. Ainsi, il sera possible de l'ouvrir avec un simple bloc-note. Du coup, je propose de mettre en oeuvre un flux de plus haut niveau correspondant finalement
à un flux de texte au moyen de la classe PrintWriter.
5. Lignes 122 à 140 : Ensuite, il suffit de faire appel à une méthode particulière de l'objet request pour récupérer votre renseignement spécifique. L'objet externe
propose également quelques petites méthodes qui délivrent peut-être plus directement l'information souhaitée.
Nous avons ici une technique simple pour mettre en oeuvre un téléchargement. Malheureusement, il n'est pas prévu de choisir le nom du fichier à télécharger. Le
système choisi, comme nom de fichier, le nom de la page Web en cours, donc ici alea.jsp. Si vous désirez maîtriser le nom du fichier, vous êtes obligé de passer par
une servlet annexe et de régler le descripteur de déploiement en conséquence. Nous avons déjà mis en oeuvre ce dernier point lors d'une étude précédente.
Rôle de la conversion et de 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 JavaBean qui traite l'information. Dans ce cas de figure, le code du bean 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.
En réalité, JSF réagit tout autrement. L'idée ici, est que le JavaBean reçoivent déjà les bonnes valeurs adaptées au traitement souhaité pour qu'il sache uniquement
comment se comporter devant une situation normale sans qu'il soit nécessaire de rajouter du code supplémentaire. Du coup, 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 JavaBean.
Il est vraiment très important que les valeurs saisies soient d'abord adaptées au traitement souhaité, correspondent ainsi au format prédéfini et que les valeurs
proposées correspondent également à la fourchette des valeurs requises.
Dans ce chapitre nous allons réaliser juste une petite expérience sur la validation et la conversion de valeurs, ainsi qu'à l'affichage de messages d'erreurs adaptés à
la situation. Nous reprendrons chacun de ces points bien en détail dans les chapitres suivants.
Application Web - Alea
Afin de valider notre petite expérience, nous allons de nouveau se servir de l'application Web sur la recherche d'un nombre aléatoire.
Sources du bean alea.Nombre et de la page d'accueil
Sur ce bean, nous rajoutons juste une petite propriété jour qui calcule et nous renvoie la date du jour :
Nombre.java
public class Nombre {
...
public Date getJour() {
return new Date();
}
}
Par ailleurs, nous modifions très légèrement le code de la page d'accueil.
alea.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
.info { color: blue; }
.erreur { color: red; }
.date { color: blue; float: right; font-weight: bold; }
</style>
<f:view>
<html>
<body style="background-color: yellow">
<h2><h:outputText value="Recherche d'un nombre entre 1 et #{nombre.valeurMaxi}" /></h2>
<hr />
<div class="date"><h:outputText value="#{nombre.jour}" /></div>
<h:form>
<h4>
Introduisez votre nombre :
<h:inputText value="#{nombre.valeur}" size="2" binding="#{nombre.saisie}" id="valeur">
<f:validateLongRange minimum="1" maximum="#{nombre.valeurMaxi}" />
</h:inputText>
<h:message for="valeur" styleClass="erreur"/>
<h:outputText value="#{nombre.progression}" styleClass="info" />
</h4>
<h4>Tentatives : <h:outputText value="#{nombre.tentative}" styleClass="info" /> fois</h4>
<p>
<h:commandButton action="#{nombre.fini}" value="Valider votre choix" binding="#{nombre.bloquer}"/>
<h:commandButton action="#{nombre.recommencer}" value="Tout recommencer" />
</p>
</h:form>
<hr />
<h3><h:outputText value="#{nombre.résultat}" styleClass="info"/></h3>
</body>
</html>
</f:view>
1. Ligne 15 : Ligne supplémentaire qui permet l'affichage de la date et qui fait ainsi appel à la propriété jour du bean alea.Nombre que nous venons de décrire.
2. Ligne 20 : Ligne supplémentaire. Nous trouvons ici la balise <f:validateLongRange> qui spécifie que la valeur attendue est de type entier, et que les valeurs saisies
doivent être comprises entre 1 et la valeur maximale prévue. Cette balise est associée (intégrée) à la balise <h:inputText> décrite à la ligne 19. Ainsi, le contrôle de
la valeur saisie n'est réalisé que sur cette zone d'entrée.
3. Ligne 21 : Affichage d'un message d'erreur suivant la saisie proposée par l'opérateur. Remarquer que ce message est associé également à la zone de saisie décrite
en ligne 19. Cette relation est mise en place au moyen de l'attribut for. Dans cet attribut, vous spécifiez l'identificateur qui correspond à la balise que souhaitez
prendre en compte.
4. Ligne 19 : Rajout de l'attribut id qui met en oeuvre une identification de cette balise. Ainsi, d'autres balises peuvent y faire référence par la suite. C'est d'ailleurs le
cas avec la balise <h:message> qui, comme nous venons de le voir, utilise l'attribut for pour se rattacher à la balise qui comporte l'identificateur déjà spécifié.
Première expérience - Démarrage de l'application Web
La nouveauté sur cette page d'accueil est maintenant la présence de la date à droite de la page. Malheureusement , l'affichage proposé n'est pas des meilleurs.
Effectivement, nous voyons apparaître à la fois la date et l'heure, et surtout le résultat est proposé en anglais, ce qui nous fait dire que la localisation du pays n'est
pas pris en compte.
Il est indispensable que cette date soit formaté convenablement suivant l'affichage désiré par le concepteur. Il ne faut pas oublier que les pages Web ne contiennent
en réalité que du texte. Le seul type d'information qu'une page Web soit capable de comprendre est le type String. Il faut donc qu'il y ait un mécanisme de conversion
entre une chaîne de caractères et d'autres types de valeur. 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. Nous remarquons ici que JSF se débrouille à convertir la date en chaîne de caractères équivalente. En fait, il ne
s'agit pas réellement d'une conversion. En réalité, le système fait appel, dans le cas où la valeur est un objet, à la méthode toString() de cet objet, et c'est pour cela
que nous retrouvons cet affichage là. Ultérieurement, il sera donc nécessaire de revoir ce mécanisme de conversion et de l'adapter à notre désir.
Deuxième expérience - Saisie d'une valeur numérique en dehors de la fourchette des valeurs prévues
Si nous choisissons la valeur 0, une erreur apparaît sur la page d'accueil juste en dessous de la zone de saisie. Effectivement, nous avons placé la balise
<h:message> juste après la balise <h:inputText>.
Le point le plus important ici est que nous somme toujours sur le coup 0, c'est-à-dire que le coup n'est pas pris en compte. Cela veut dire que le bean alea.Nombre
n'a même pas été sollicité. JSF est très compétent, si la valeur n'est pas valide, la logique métier est laissée de côté. JSF met en oeuvre un mécanisme de validation
très puissant et performant.
Le message d'erreur possède une partie en français, et même si le contenu est compréhensible, il serait quand même plus judicieux de prévoir un affichage
personnalisé.
Troisième expérience - Saisie d'un caractère à la place d'un chiffre
Ici, nous faisons une grosse erreur de saisie en plaçant une lettre à la place du chiffre attendu. Nous obtenons un autre type d'erreur qui correspond à une erreur
de conversion.
En effet, le système, puisqu'une valeur numérique est attendue, tente de réaliser une conversion entre une chaîne de caractères et un nombre. Le problème, c'est que
nous n'avons jamais défini de comportement adapté à ce genre de situation. Ici aussi, JSF est très compétent. S'il le faut, il est capable de proposer une conversion
avant d'injecter la bonne valeur dans le JavaBean qui s'occupe de la logique métier et qui réalise le traitement demandé.
Quatrième expérience - Saisie d'une bonne valeur
Cette fois-ci, le coup est pris en compte, ce qui veut dire que le bean a bien été sollicité et qu'il réalise le travail pour lequel il a été conçu. Le nombre introduit a
respecté avec succès la phase de validation.
Retour sur le cycle de traitement des requêtes JSF
Pour terminer ce chapitre, 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.
Il est important de noter que les conversions sont aussi bien associées avec des composants d'entrée de type <h:inputText>, qu'avec des composants de sortie de
type <h:outputText>. Par contre, les validations ne s'effectuent uniquement qu'avec des composants d'entrée ne correspondant donc qu'à des saisies de valeurs.
Ainsi, dans un premier temps, les conversions et les validations s'effectuent dans la troisième phase qui se trouve avant l'injection des valeurs dans le bean métier.
Si des erreurs (de conversion ou de validation) sont produites durant cette phase, nous allons directement à la sixième phase qui correspond au rendu de la réponse.
Si les valeurs sont correctes et formatées avec les bons types, le bean métier effectue les traitements demandés et sollicite éventuellement des conversions pour les
résultats souhaités. Si également durant cette phase se trouve des erreurs de conversion, nous allons de nouveau directement à la phase correspondant au rendu de
la réponse.
Il est important de noter que lorsqu'une erreur se prodruit, la phase 4 est sautée, et ainsi le modèle métier n'est pas modifié. Cela empêche la mise à jour du modèle
avec des données non validées et donc erronées.
Les conversions standards
Nous allons maintenant rentrer dans le vif du sujet et voir comment réaliser les conversions. Toutefois, nous nous limiterons ici sur les conversions standards. 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.
Nous avons déjà largement travailler sur le formatage de l'information, notamment sur deux types de données ; les nombres, et les dates qui sont respectivement
représentés par les classes abstraites java.text.NumberFormat et java.text.DateFormat.
Retour sur l'étude du formatage des nombres et des dates.
La conversion standard en JSF consiste justement à formater l'information suivant ces deux types de données ; les nombres et les dates. Côté page Web, les
balises prévues pour la conversion sont :
1. <f:convertNumber> : formatage du nombre et conversion vers une valeur réelle avec une représentation possible sur des valeurs monétaires ou sur des
pourcentages. Derrière, cette balise utilise indirectement les compétences de la JRE standard en faisant référence à la classe java.text.NumberFormat évoquée plus
haut.
2. <f:convertDateTime> : formatage des dates et conversion vers un objet de type Date. Derrière, cette balise utilise indirectement les compétences de la JRE
standard en faisant référence à la classe java.text.DateFormat évoquée plus haut.
3. <f:converter> : Permet de réaliser des formatages et des conversions de façon personnalisée. Ce sujet sera traité dans un autre chapitre.
Je rappelle que ces balises de conversions doivent être associées (intégrées) au balises correspondant aux zones de saisies. Ainsi, le contrôle de la valeur saisie
n'est réalisé que sur cette zone d'entrée.
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 JSP.
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
Convertisseurs
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.lang.Double
javax.faces.convert.DoubleConverter
java.lang.Float
javax.faces.convert.FloatConverter
java.lang.Integer
javax.faces.convert.IntegerConverter
java.lang.Long
javax.faces.convert.LongConverter
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 :
<f:convertNumber>
Attributs
Type
Valeur
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 (revoir le cours sur ce sujet). 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 récupère uniquement la partie entière du nombre proposé.
groupingUsed
Boolean
Permet de prendre en compte ou pas la séparation des milliers. Vrai par défaut.
locale
java.util.Locale
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.
<f:convertDateTime>
Attributs
Type
Valeur
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
int
default, short, medium, long ou full. Affiche l'heure avec plus ou moins de renseignements.
pattern
int
Permet de proposer un motif de présentation personnalisé qui utilise derrière les compétences de la classe concrète
java.text.SimpleDateFormat (revoir le cours sur ce sujet).
locale
java.util.Locale
Choix du paramètre régional à utiliser.
timeZone
String
Tient compte du fuseau horaire.
Exemple pratique sur l'ensemble de ces conversions
Passons tout de suite sur un exemple concret qui va nous permettre de mettre en oeuvre des conversions et ainsi de maîtriser les attributs spécifiques de ces balises de
formatage. Pour cela, nous construisons une application Web qui permet d'effectuer des conversions monétaires entre des euros et des francs. Sur la page Web, vous
saisissez une valeur numérique et vous choisissez ensuite de type de conversion à réaliser. L'affichage du résultat s'effectue de toute façon pour les deux monnaies. Par
ailleurs, vous avez la possibilité de rajouter dans vos calculs un coefficient multiplicateur qui devra être systématiquement une valeur entière.
Sources de l'application Web
Dans cette application Web, nous utilisons deux éléments : d'une part la page d'accueil conversion.jsp et d'autre part le bean conversion correspondant à la classe
bean.Conversion qui s'occupe de la logique métier et qui réalise tous les traitements nécessaires au bon comportement de l'application Web.
faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
<managed-bean>
<managed-bean-name>conversion</managed-bean-name>
<managed-bean-class>bean.Conversion</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
</faces-config>
bean.Conversion.java
package bean;
import java.util.Date;
public class Conversion {
private final double TAUX = 6.55957;
private double euro;
private double franc;
private double valeur;
private double nombre = 1;
private Date aujourdhui = new Date();
public double getEuro() {
return euro;
}
public double getFranc() {
return franc;
}
public double getValeur() {
return valeur;
}
public double getNombre() {
return nombre;
}
public void setEuro(double euro) {
this.euro = euro;
}
public void setFranc(double franc) {
this.franc = franc;
}
public void setValeur(double valeur) {
this.valeur = valeur;
}
public void setNombre(double nombre) {
this.nombre = nombre;
}
public Date getAujourdhui() {
return aujourdhui;
}
public void euroFranc() {
euro = valeur;
franc = euro*TAUX;
}
public void francEuro() {
franc = valeur;
euro = franc/TAUX;
}
}
conversion.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
.couleur { color: blue; }
body { background-color: orange; color: yellow; }
</style>
<f:view>
<html>
<body>
<h3><h:outputText value="Conversion monétaire" /></h3>
<hr />
<h4>Aujourd'hui, nous sommes le :<br />
<h:outputText value="#{conversion.aujourdhui}" styleClass="couleur">
<f:convertDateTime dateStyle="full" />
</h:outputText><br />
et il est :<br />
<h:outputText value="#{conversion.aujourdhui}" styleClass="couleur">
<f:convertDateTime type="time" pattern="HH'h' mm'mn' ss's'" />
</h:outputText>
</h4>
<h:form>
<h4>
<h:inputText value="#{conversion.valeur}" size="19">
<f:convertNumber minFractionDigits="2" groupingUsed="false" />
</h:inputText> x
<h:inputText value="#{conversion.nombre}" size="2">
<f:convertNumber integerOnly="true" />
</h:inputText>
</h4>
<p>
<h:commandButton action="#{conversion.euroFranc}" value="Euro->Franc" />
<h:commandButton action="#{conversion.francEuro}" value="Franc->Euro" />
</p>
<hr />
<h3>
<h:outputText value="#{conversion.euro}">
<f:convertNumber type="currency" />
</h:outputText> x
<h:outputText value="#{conversion.nombre}">
<f:convertNumber integerOnly="true" />
</h:outputText> =
<h:outputText value="#{conversion.euro*conversion.nombre}">
<f:convertNumber type="currency"/>
</h:outputText>
</h3>
<h3>
<h:outputText value="#{conversion.franc}">
<f:convertNumber pattern="#,##0.00 F" />
</h:outputText> x
<h:outputText value="#{conversion.nombre}">
<f:convertNumber integerOnly="true" />
</h:outputText> =
<h:outputText value="#{conversion.franc*conversion.nombre}">
<f:convertNumber type="currency" currencySymbol="F"/>
</h:outputText>
</h3>
</h:form>
</body>
</html>
</f:view>
Les deux premiers fichiers n'appellent pas beaucoup de commentaires puisque nous connaisons déjà toutes les techniques de base. Par contre, je vais m'attarder
un peu plus sur la page JSP puisque c'est à l'intérieur de cette dernière que nous utilisons les balises de conversion standard.
1. Lignes 15 et 19 : La propriété aujourdhui, en mode lecture, délivre la date d'aujourd'hui à l'aide d'un objet de type Date.
2. Lignes 15 à 17 : A partir de la propriété aujourdhui, affichage de la date uniquement, en version complète toutefois. Le résultat est le suivant "mardi 27 février
2007". Pour réaliser cette opération, il suffit de prendre l'attribut dateStyle et de spécifier la valeur full.
3. Lignes 19 à 21 : Toujours à partir de la même propriété aujourdhui, affichage cette fois-ci de l'heure uniquement avec un format personnalisé. Pour choisir
l'affichage de l'heure uniquement, vous êtes obligé de prendre l'attribut type et de spécifier la valeur time. Pour fabriquer le motif correspondant au format de la
présentation, vous devez utiliser l'attribut pattern en spécifiant les bons caractères de formatage (Revoir les cours sur la classe SimpleDateFormat pour bien
maîtriser les symboles spécifiques à prendre en compte pour constituer votre motif).
4. Lignes 25 à 27 : Zone de saisie de la valeur à traiter. Cette valeur est tout simplement un nombre réel représenté par la propriété valeur. Toutefois, nous désirons
avoir un formatage particulier en respectant l'écriture prévue pour la France, c'est-à-dire en prenant le séparateur virgule entre la partie entière et la partie décimale.
Par contre, comme il s'agit d'une saisie, il est généralement préférable d'éviter de mettre en place la séparation des milliers qui, pour la France, est un espace.
Attention, parce que le système prend l'espace comme une séparation entre deux valeurs numériques et donc ne renvoie que la première partie du nombre. Pour
ce dernier critère, il est nécessaire de solliciter l'attribut groupingUsed et de le désactiver. Pour terminer, j'aimerais que le nombre réel soit affiché avec
systématiquement une partie décimal avec au moins deux chiffres, ce qui se réalise par l'intermédiaire de l'attribut minFractionDigits.
5. Lignes 28 à 30 : La propriété nombre est un réel. Toutefois, nous désirons prendre uniquement la partie entière de ce nombre afin que cela corresponde bien à un
coefficient multiplicateur. Pour cela, il faut proposer une conversion numérique et prendre l'attribut integerOnly qu'il suffit de valider.
6. Lignes 38 à 40 : La propriété euro est également un réel. Nous désirons avoir l'affichage classique de ce réel sous forme de valeur monétaire exprimée en euro. La
réalisation de ce formatage est très simple puisqu'il suffit de prendre l'attribut type est de le régler sur currency. Comme nous sommes localisés en France, c'est la
monnaie européenne qui est automatiquement prise en compte.
7. Lignes 49 à 51 : La propriété franc est également un réel. Nous désirons avoir l'affichage classique de ce réel sous forme de valeur monétaire exprimée cette foisci en franc. Malgré que nous soyons en France, le franc n'existe plus comme valeur monétaire. Vous êtes donc obligé de spécifier un formatage personnalisé, soit
en créant un motif de mise en forme, soit en précisant le caractère correspondant au symbole du franc. Ici, c'est la première technique qui est utilisé. Pour cela
prenez plutôt l'attribut pattern à l'intérieur duquel vous spécifiez votre motif.
8. Lignes 55 à 57 : Ici aussi, nous désirons avoir l'affichage sous forme de valeur monétaire exprimée en franc. Par contre, nous utilisons cette-fois-ci plutôt la
deuxième technique, qui consiste à dire qu'il s'agit bien d'une valeur monétaire en spécifiant l'attribut type à currency et en proposant le symbole monétaire F 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 JavaBean) 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.
Avertir l'utilisateur au moyen de la balise <h:message>
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). Voici d'ailleurs le code correspondant au traitement du
message d'erreur :
conversion.jsp
...
<h4>
<h:inputText value="#{conversion.valeur}" size="19" id="valeur">
<f:convertNumber minFractionDigits="2" groupingUsed="false" />
</h:inputText> x
<h:inputText value="#{conversion.nombre}" size="2">
<f:convertNumber integerOnly="true" />
</h:inputText>
<br /><h:message for="valeur" style="color: red" />
</h4>
...
Du coup, voici l'affichage qui est proposé lorsque l'utilisateur saisie une mauvaise valeur :
Changer le message d'erreur de conversion prévu par défaut
C'est pas mal, mais le message est loin d'être explicite, surtout qu'il est en anglais. Il serait préférable de redéfinir le comportement par défaut afin que le message proposé
soit suffisamment évocateur pour orienter l'opérateur vers une meilleure saisie. Voici un message possible :
Une solution pour arriver à ce résultat est de mettre en oeuvre un fichier de propriétés, comme nous l'avons déjà fait dans l'étude précédente. Je rappelle que ce
fichier de propriétés centralise l'ensemble des textes qui constitue votre page Web, comme le titre de la page, le libellé des boutons, etc.. Du coup, grâce à ce
fichier de propriétés, vous pouvez changer ultérieurement très facilement un intitulé qui ne vous convient plus. Il suffit de le faire directement dans ce fichier sans
qu'il soit nécessaire de recompiler l'application Web et de la redéployer.
Dans ce fichier de configuration, vous avez donc la possibilité de redéfinir les messages proposés par défaut. Ici, nous devons redéfinir un message d'erreur de
conversion associé à une zone d'entrée. Le nom complet de ce message par défaut est javax.faces.component.UIInput.CONVERSION.
Voici le fichier de propriétés messages.properties pour cette application Web :
Rappelez-vous que pour que ce fichier de propriétés soit prise en compte dans la page Web conversion.jsp, il est nécessaire de le spécifier au moyen de la balise
<f:loadBundle>. Voici, d'ailleurs la partie du source concernant ce changement :
conversion.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
.couleur { color: blue; }
body { background-color: orange; color: yellow; }
</style>
<f:view>
<f:loadBundle basename="ressources.messages" var="msg" />
<html>
<body>
<h3><h:outputText value="#{msg.titre}" /></h3>
<hr />
Ce n'est pas fini. Dans le cas où vous changer le comportement par défaut, il faut que l'application Web en soit au courant. Il faut alors le préciser dans le fichier
de configuration <faces-config.xml>. Dans ce fichier de configuration, vous spécifiez juste où se situe le fichier de propriétés.
faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
<application>
<message-bundle>ressources.messages</message-bundle>
</application>
<managed-bean>
<managed-bean-name>conversion</managed-bean-name>
<managed-bean-class>bean.Conversion</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
</faces-config>
Ainsi, grâce à cette évocation, l'application Web est au courant qu'elle doit à tout prix consulter ce fichier avant de prendre une décision quelconque. Lors de la
lecture de ce fichier, l'application Web remarque que javax.faces.component.UIInput.CONVERSION a été modifié et mémorise cette information pour la suite des
événements au cas où.
Toutes les erreurs de conversions recensées dans une même balise <h:messages>
Nous sommes déjà dans une meilleure situation. Toutefois, si l'utilisateur tape une chaîne de caractères à la place du coefficient de type entier prévue dans la deuxième
zone de saisie, nous nous retrouvons dans la même situation du départ. En effet, pout l'instant, nous n'avons pas placé de balise <h:message> associée à cette zone de
saisie. Bien entendu, vous pouvez le faire et placer en conséquence un identificateur sur le composant qui gère la deuxième zone de saisie. Voici du coup, ce que nous
pouvons obtenir :
Toutefois, si vous décidez de prendre exactement le même message d'erreur pour un ensemble de zone de saisie, il peut être souhaitable de centraliser toutes les
erreurs qui surviennent sur une seule balise. Cette balise existe, il s'agit de <h:messages>. Dans cette démarche, nous n'avons plus besoin de dire d'où vient l'erreur,
et donc de spécifier un identificateur à chacun des composants qui gère les zones d'entrée. En effet, cette balise <h:messages> prend automatiquement toutes les
erreurs de conversion venant de toutes les zones de saisie.
Voici le code source de la page conversion.jsp modifié en conséquence :
conversion.jsp
...
<h4>
<h:inputText value="#{conversion.valeur}" size="19">
<f:convertNumber minFractionDigits="2" groupingUsed="false" />
</h:inputText> x
<h:inputText value="#{conversion.nombre}" size="2">
<f:convertNumber integerOnly="true" />
</h:inputText>
<br /><h:messages style="color: red" />
</h4>
...
Plusieurs erreurs de saisie sur la même page Web
Que se passe-t-il si nous faisons plusieurs erreurs de saisie sur la même page Web lorsque nous utilisons la balise <h:messages> pour recenser l'ensemble des erreurs ?
Vous remarquez que les messages d'erreurs s'écrivent les uns à la suite des autres. Il est généralement préférable qu'il soient plutôt écrits les uns en dessous des
autres. Pour cela, prenez l'attribut layout de la balise <h:messages> et proposez alors la valeur table.
L'attribut layout peut prendre deux valeurs : soit table, soit list. Cette dernière valeur est la valeur par défaut et correspond donc à un affichage des erreurs les uns à
la suite des autres.
Affichage d'un message d'erreur de conversion adapté à chaque zone de saisie
L'idéal est quand même de proposer un affichage personnalisé suivant la zone de saisie qui provoque l'erreur de conversion :
Depuis la version 1.2 de JSF, les balises d'entrées possèdent un attribut supplémentaire qui permet de spécifier la valeur par défaut du message d'erreur de
conversion. Il s'agit de l'attribut converterMessage. Du coup, il n'est plus nécessaire de mettre en oeuvre le fichier de propriétés. Par contre, vous devez
impérativement spécifier un message personnel à chacune des balises d'entrée. Toutefois, il est également possible de mixer les deux types de solution.
Pour obtenir, le résultat ci-dessus, voici la partie du code source correspondant :
conversion.jsp
...
<h4>
<h:inputText value="#{conversion.valeur}" size="19" id="valeur"
converterMessage=" Il faut saisir une valeur numérique avec éventuellement des décimales">
<f:convertNumber minFractionDigits="2" groupingUsed="false" />
</h:inputText> <h:message for="valeur" style="color: red" />
<br />x<br />
<h:inputText value="#{conversion.nombre}" size="2" id="coef"
converterMessage=" Il faut saisir une valeur numérique sans décimales">
<f:convertNumber integerOnly="true" />
</h:inputText> <h:message for="coef" style="color: red" />
</h4>
...
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 JavaBean 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 page JSP 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.
Contrôles effectués par les validations Standards
La validation standards JSF peut éventuellement réaliser les opérations suivantes :
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).
Les validateurs standards
La validation des données saisies par l'opérateur, dans le cas où la zone de saisie est non vide, s'effectue au travers de plusieurs balises. La première <f:validateLength>
vérifie la longueur de la chaîne saisie. Les deux autres, <f:validateLongRange> et <f:valideteDoubleRange> vérifient que les nombres saisis respectent la fourchette de
valeurs prévues.
Balises
Classes de validation
Attributs
Valeur
<f:validateDoubleRange>
DoubleRangeValidator
minimum,
maximum
Valide une valeur réelle saisie par l'opérateur, avec en option une limite possible sur les valeurs
permises.
<f:validateLongRange>
LongRangeValidator
minimum,
maximum
Valide une valeur entière saisie par l'opérateur, avec en option une limite possible sur les valeurs
permises.
<f:validateLength>
LenghtValidator
minimum,
maximum
Valide une chaîne de caractères en contrôlant le nombre de caractères saisis.
Tous ces validateurs standards possèdent un attribut minimum et un attribut maximum. Vous avez la possibilité de choisir l'un ou l'autre de ces attributs ou même
les deux.
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>
Champ de la zone de saisie non vide
Il est possible d'imposer une valeur dans une zone de saisie (quelque soit son type). Dans ce cas, il faut intégrer l'attribut required dans la balise d'entrée avec la valeur
true :
<h:inputText value="#{commande.expiration}" required="true">
<f:convertDateTime pattern="MM/yyyy" />
</h:inputText>
Il est possible de combiner l'attribut required avec un autre validateur standard :
<h:inputText value="#{commande.codePostal}" required="true">
<f:validateLength minimum="5" maximum="5" />
<f:validateLongRange minimum="1000" maximum="98900" />
</h:inputText>
Attention : Si vous ne prenez pas l'attribut required, et si le champ de la zone de saisie reste vierge, aucune validation n'est alors interprété. Effectivement, le fait de
laisser le champ vide correspond à une demande de ne pas changer la valeur par défaut prévue par cette zone de saisie.
Exemple pratique sur l'ensemble de ces validations
Passons sur un exemple concret qui met en oeuvre l'ensemble des validations standards et ainsi adapte le contrôle sur chaque zone de saisie afin que le système devienne
performant. Pour cela, nous construisons une application Web qui permet de passer une commande en ligne. Cette application n'est pas très réaliste, mais elle montre
comment réaliser des contrôles spécifiques sur chacun des champs.
Sources de l'application Web
Nous avons ici une partie de l'application Web. Nous avons besoin de deux pages JSP, une pour la saisie de la commande, Commande.jsp, l'autre pour valider la
commande saisie, Confirmation.jsp. Par ailleurs, le bean commande, correspondant à la classe bean.Commande, assure le traitement (le stockage des données) et la
relation entre ces deux pages.
bean.Commande.java
package bean;
import java.util.Date;
public class Commande {
private String article;
private double prix;
private int quantité = 1;
private String adresseMail;
private String carte;
private Date expiration;
private String adresse;
private int codePostal;
private String ville;
public String getAdresse() {
return adresse;
}
public String getAdresseMail() {
return adresseMail;
}
public String getArticle() {
return article;
}
public String getCarte() {
return carte;
}
public int getCodePostal() {
return codePostal;
}
public Date getExpiration() {
return expiration;
}
public double getPrix() {
return prix;
}
public int getQuantité() {
return quantité;
}
public String getVille() {
return ville;
}
public void setAdresse(String adresse) {
this.adresse = adresse;
}
public void setAdresseMail(String adresseMail) {
this.adresseMail = adresseMail;
}
public void setArticle(String article) {
this.article = article;
}
public void setCarte(String carte) {
this.carte = carte;
}
public void setCodePostal(int codePostal) {
this.codePostal = codePostal;
}
public void setExpiration(Date expiration) {
this.expiration = expiration;
}
public void setPrix(double prix) {
this.prix = prix;
}
public void setQuantité(int quantité) {
this.quantité = quantité;
}
public void setVille(String ville) {
this.ville = ville;
}
}
Commande.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
.un {background-color: #00CC00; }
.deux {background-color: #009900; }
.général {font-weight: bold; }
h3 { background-color: #006633; padding: 5px; border : groove; }
body {font-family: verdana, Arial; color: yellow; background-color: green; }
.erreur {color: white; }
</style>
<f:view>
<html>
<body>
<h3>Passer votre commande</h3><hr />
<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"/>
<h:message for="article" errorClass="erreur"/>
<h:outputText value="Prix : " />
<h:inputText value="#{commande.prix}" id="prix" required="true">
<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" maxlength="2" id="quantité">
<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" />
<h:message for="adresse" errorClass="erreur" />
<h:outputText value="Code postal : " />
<h:inputText value="#{commande.codePostal}" size="3" maxlength="5" id="codePostal" required="true">
<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" />
<h:message for="ville" errorClass="erreur" />
<h:outputText value="Adresse Mail : " />
<h:inputText value="#{commande.adresseMail}" id="mail" required="true"/>
<h:message for="mail" errorClass="erreur"/>
<h:outputText value="Carte de crédit : " />
49
<h:inputText value="#{commande.carte}" id="carte" required="true">
50
<f:validateLength minimum="13" />
51
</h:inputText>
52
<h:message for="carte" errorClass="erreur" />
53
<h:outputText value="Expiration (ex: 02/2007) : " />
54
<h:inputText value="#{commande.expiration}" size="7" id="expiration" required="true">
55
<f:convertDateTime pattern="MM/yyyy" />
56
</h:inputText>
57
<h:message for="expiration" errorClass="erreur" />
58
</h:panelGrid>
59
<hr />
60
<h:commandButton action="Commander" value="Commander" />
61
<h:commandButton action="Annuler" value="Annuler"/>
62
</h:form>
63
</body>
64
</html>
65 </f:view>
Je m'intéresse uniquement à ce dernier source puisque c'est sur cette page Web que sont placés les différents éléments de validation :
1. Lignes 21 : Nous contrôlons que cette zone de saisie possède bien une valeur et qu'elle soit donc non vide. Il faut donc saisir impérativement l'article.
2. Lignes 22 à 26 : Il faut également que le prix soit saisi. Nous en profitons pour réaliser un formatage du nombre réel pour qu'il apparaisse systématiquement avec 2
chiffres après la virgule. Par ailleurs, il faut que la valeur introduite soit comprise entre 10 et 10000. Remarquez au passage que nous pouvons, bien entendu,
combiner les convertisseurs avec les validateurs.
3. Lignes 29 à 31 : Nous contrôlons ici que la quantité soit au moins égale à 1. Par ailleurs, nous limitons le nombre de caractères saisis au moyen de l'attribut
maxlenght.
4. Lignes 37 à 40: Ici aussi nous contrôlons le nombre de caractères saisis. La valeur doit être exactement composées de 5 caractères et doit en plus être comprise
entre 1000 et 98900.
5. Lignes 49 à 51 : Vérification que le nombre de caractères composant le numéro de la carte banquaire soit au moins de 13 caractères (prend en compte tous les
types de carte).
6. Lignes 54 à 56 : Mise en forme personnalisée de la zone de saisie prévue pour la date d'expiration de la carte.
A titre informatif, voici ci-dessous le code source de la page de confirmation :
Confirmation.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
.un {background-color: #00CC00; }
.deux {background-color: #009900; }
.général {font-weight: bold; }
h3 { background-color: #006633; padding: 5px; border : groove; }
body {font-family: verdana, Arial; color: yellow; background-color: green; }
</style>
<f:view>
<html>
<body>
<h3>Validation de votre commande</h3><hr />
<h:panelGrid columns="2" styleClass="général" rowClasses="un, deux" cellpadding="3">
<h:outputText value="Article : " />
<h:outputText value="#{commande.article}"/>
<h:outputText value="Prix : " />
<h:outputText value="#{commande.prix}">
<f:convertNumber type="currency" />
</h:outputText>
<h:outputText value="Quantité : " />
<h:outputText value="#{commande.quantité}"/>
<h:outputText value="Total :" />
<h:outputText value="#{commande.prix*commande.quantité}">
<f:convertNumber type="currency" />
</h:outputText>
<h:outputText value="Adresse : " />
<h:outputText value="#{commande.adresse}"/>
<h:outputText value="Code postal : " />
<h:outputText value="#{commande.codePostal}"/>
<h:outputText value="Ville : " />
<h:outputText value="#{commande.ville}"/>
<h:outputText value="Adresse Mail : " />
<h:outputText value="#{commande.adresseMail}"/>
<h:outputText value="Carte de crédit : " />
<h:outputText value="#{commande.carte}"/>
<h:outputText value="Expiration (ex: 02/2007) : " />
<h:outputText value="#{commande.expiration}">
<f:convertDateTime pattern="MMMM yyyy" />
</h:outputText>
</h:panelGrid>
<hr />
<h:commandButton value="Confirmer votre commande" />
</body>
</html>
</f:view>
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 la page Commande.jsp correspondant à la
commande en ligne, désire sortir sans finalement passer la commande. Pour cela, il clique sur le 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>
ou <h:commandLink>.
Voici donc ce qu'il faut faire pour que notre bouton "Annuler" remplisse correctement sa fonction :
Commande.jsp
1 <%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
2 <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
59
<hr />
60
<h:commandButton action="Commander" value="Commander" />
61
<h:commandButton action="Annuler" value="Annuler" immediate="true" />
62
</h:form>
63
</body>
64
</html>
65 </f:view>
Messages d'erreur associés à la validation standard
Comme pour la conversion, il est bien entendu possible que l'utilisateur fasse une mauvaise saisie, et nous nous retrouvons dans le même type de problème. Le système
se protège de toute erreur de saisie. Si la validation ne peut être réalisée convenablement, la logique métier (le JavaBean) n'est pas sollicité.
Lorsqu'une erreur de validation 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.
Avertir l'utilisateur au moyen de la balise <h:message>
Il est souhaitable que l'utilisateur soit averti de sa mauvaise saisie. Comme pour les convertisseurs, 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). Dans le
code précédent, vous avez remarqué que nous avons justement placé une balise <h:message> à chacune des zones d'entrée.
Les messages d'erreur par défaut des validations standards
L'utilisateur peut provoquer plusieurs types d'erreur suivant les balises et les l'attributs que nous avons choisis dans notre page Web. Du coup, nous avons plusieurs
messages d'erreur par défaut. Les voici :
Classes de validation
Message d'erreur par défaut
Délivré par le composant
javax.faces.component.UIInput.REQUIRED
Erreur de validation: Valeur requise.
UIInput qui prend en compte l'attribut required et lorsqu'aucune valeur
n'est saisie.
javax.faces.validator.NOT_IN_RANGE
Erreur
de
validation:
(localize): 10 - 10 000.
DoubleRangeValidator et LongRangeValidator lorsque la valeur saisie se
trouve en dehors des limites spécifiées respectivement par les attributs
minimum et maximun.
javax.faces.validator.DoubleRangeValidator.MAXIMUM
javax.faces.validator.LongRangeValidator.MAXIMUM
Erreur de validation: La valeur
spécifiée est supérieure ´ la valeur
maximale permise '60'.
PENDING
DoubleRangeValidator et LongRangeValidator lorsque la valeur saisie est
plus grande que la limite spécifiée par l'attribut maximun.
javax.faces.validator.DoubleRangeValidator.MINIMUM
javax.faces.validator.LongRangeValidator.MINIMUM
Erreur de validation: La valeur
spécifiée est inférieure ´ la valeur
minimale permise '5'.
DoubleRangeValidator et LongRangeValidator lorsque la valeur saisie est
plus petite que la limite spécifiée par l'attribut minimun.
javax.faces.validator.DoubleRangeValidator.TYPE
javax.faces.validator.LongRangeValidator.TYPE
Erreur de validation: La valeur
spécifiée nest pas dans le bon type.
DoubleRangeValidator et LongRangeValidator lorsque la valeur saisie ne
peut être convertie vers un double ou vers un int.
javax.faces.validator.LenghtValidation.MAXIMUM
Erreur de validation: La valeur
spécifiée est supérieure ´ la valeur
maximale permise '25'.
LenghtValidator lorsque la longueur de la chaîne saisie est plus grande
que la limite spécifiée par l'attribut maximun.
javax.faces.validator.LenghtValidation.MINIMUM
Erreur de validation: La valeur
spécifiée est inférieure ´ la valeur
minimale permise '13'.
LenghtValidator lorsque la longueur de la chaîne saisie est plus petite
que la limite spécifiée par l'attribut minimun.
A partir de là, comme nous l'avons découvert lors de la conversion des données, vous pouvez redéfinir vos messages par défaut au travers du fichier de propriétés.
Affichage d'un message d'erreur de conversion adapté à chaque zone de saisie
Encore une fois, les messages d'erreur par défaut ne sont pas toujours bien explicites. L'idéal est quand même de proposer un affichage personnalisé suivant la zone de
saisie qui provoque l'erreur de validation :
Depuis la version 1.2 de JSF, les balises d'entrées possèdent des attributs supplémentaire qui permettent de spécifier la valeur par défaut du message d'erreur de
validation. D'une part, il s'agit de l'attribut requiredMessage qui permet de proposer un message d'erreur personnalisé lorsque l'utilisateur oubli de saisir une valeur
et que l'attribut required de la même balise est mis en place. Nous avons également l'attribut validatorMessage qui propose un message d'erreur personnalisé
lorsque l'utilisateur donne une valeur non prévue par le canevas souhaité par les balises <f:validateLongRange>, <f:validateDoubleRange> et <f:validateLength>.
Pour obtenir, le résultat ci-dessus, voici la partie du code source correspondant :
Commande.jsp
<f:view>
<html>
<body>
<h3>Passer votre commande</h3><hr />
<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>
</body>
</html>
</f:view>
Les conversions personnalisées
Revenons à notre souci de convertir les valeurs afin qu'elles soient mises en forme, et donc directement 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. Il faut alors proposer une conversion personnalisée adaptée à la situation présente.
Nous allons traiter un exemple qui a besoin de mettre en oeuvre une conversion personnalisé. Nous souhaitons effectivement réaliser une application Web qui
permet de faire du calcul 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). Voici ci-dessous un exemple d'utilisation :
Modèle MVC
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 en base 10.
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 binaire.jsp avec le modèle associé bean.Conversions, 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, ce qui est parfaitement représenté ici. 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, ce qui encore une fois, est bien visualisé ici. Pour résumer, nous devons systématiquement passer par la
conversion pour échanger des valeurs entre la page JSP et le JavaBean correspondant.
Lorsque plusieurs conversions personnalisées sont prévues pour un même JavaBean, nous devons systématiquement mettre en place une classe par type de
conversion, indépendamment de la classe représentant le JavaBean. Dans le cas où une seule conversion est prévue, comme c'est le cas ici, nous aurions pu nous
passer de cette classe intermédiaire et placer le système de conversion directement à l'intérieur du JavaBean. Nous verrons une version de cette alternative,
toutefois ici, j'ai voulu privilégier le côté pédagogique de la situation.
Logique métier au travers du JavaBean Conversions
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 JSP.
bean.Conversions
package bean;
public class Conversions {
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;
}
}
Mise en oeuvre du convertisseur Binaire
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.
package javax.faces.convert;
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.
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 JSP, en passant donc par le protocole HTTP.
convertisseur.Binaire
package convertisseur;
import
import
import
import
java.text.DecimalFormat;
javax.faces.component.UIComponent;
javax.faces.context.FacesContext;
javax.faces.convert.Converter;
public class Binaire implements Converter {
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.isWhitespace(chaîne.charAt(i))) chaîne.deleteCharAt(i);
else i++;
return Integer.parseInt(chaîne.toString(), 2);
}
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. 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.
Revoir les cours sur le traitement de chaînes de caractères et sur le formatage des nombres pour de plus amples informations.
Déclaration du convertisseur au travers du fichier de configuration faces-config.xml
Il faut qu'un objet représentant la conversion soit créé pour permettre son utilisation dans la page JSP. Comme le JavaBean, et je dirais comme tous les objets à créer, cela
se fait systématiquement dans le fichier de configuration faces-config.xml. Vous disposez d'une balise prévue à cet effet qui s'appelle <converter>. Vous précisez ensuite le
nom de votre objet au travers de la balise <converter-id> avec le nom de la classe correspondante au travers de la balise <converter-class>.
faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
<managed-bean>
<managed-bean-name>conversion</managed-bean-name>
<managed-bean-class>bean.Conversions</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
<converter>
<converter-id>binaire</converter-id>
<converter-class>convertisseur.Binaire</converter-class>
</converter>
</faces-config>
Page d'accueil binaire.jsp
Lorsque vous désirez proposer une conversion sur une zone de saisie ou sur une balise d'affichage, vous prenez une balise <f:converter> et vous spécifiez l'objet qui
réalise la conversion au moyen de l'attribut converterId.
binaire.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
body { background-color: green; color: yellow; }
</style>
<f:view>
<html>
<body>
<h2>Fonctions logiques binaires</h2>
<hr />
<h:form>
<h:panelGrid columns="1">
<h:inputText value="#{conversion.premier}" size="19">
<f:converter converterId="binaire" />
</h:inputText>
<h:inputText value="#{conversion.deuxième}" size="19">
<f:converter converterId="binaire" />
</h:inputText>
<h:panelGroup>
<h:commandButton action="#{conversion.ouLogique}" value="OR" />
<h:commandButton action="#{conversion.etLogique}" value="AND" />
<h:commandButton action="#{conversion.ouExclusif}" value="XOR" />
</h:panelGroup>
<h:inputText value="#{conversion.résultat}" readonly="true" size="19">
<f:converter converterId="binaire" />
</h:inputText>
</h:panelGrid>
</h:form>
</body>
</html>
</f:view>
Plutôt que d'utiliser la balise spécifique <f:converter>, vous avez aussi la possibilité de prendre directement l'attribut converter qui existe sur la plupart des balises
de saisies ou de présentations. Le code source devient beaucoup plus concis et s'en trouve donc allégé.
binaire.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
body { background-color: green; color: yellow; }
</style>
<f:view>
<html>
<body>
<h2>Fonctions logiques binaires</h2>
<hr />
<h:form>
<h:panelGrid columns="1">
<h:inputText value="#{conversion.premier}" converter="binaire" size="19"/>
<h:inputText value="#{conversion.deuxième}" converter="binaire" size="19" />
<h:panelGroup>
<h:commandButton action="#{conversion.ouLogique}" value="OR" />
<h:commandButton action="#{conversion.etLogique}" value="AND" />
<h:commandButton action="#{conversion.ouExclusif}" value="XOR" />
</h:panelGroup>
<h:inputText value="#{conversion.résultat}" converter="binaire" readonly="true" size="19"/>
</h:panelGrid>
</h:form>
</body>
</html>
</f:view>
Cas où un seul convertisseur personnalisé est à implémenter
Bien que cela soit pas toujours souhaitable, si vous n'avez qu'un seul convertisseur à mettre en oeuvre, vous pouvez placer votre système de conversion directement dans
le JavaBean représentant la logique métier. Effectivement, un convertisseur est considéré comme tel s'il implémente l'interface Converter. Rien n'empêche à la classe du
JavaBean de le faire et ainsi de redéfinir les méthodes getAsObject() et getAsString(). Par contre, on comprend bien que nous ne pouvons pas appliquer cette technique si
nous devons mettre en oeuvre plusieurs convertisseurs sur le même JavaBean.
bean.Conversions
package bean;
import
import
import
import
java.text.DecimalFormat;
javax.faces.component.UIComponent;
javax.faces.context.FacesContext;
javax.faces.convert.Converter;
public class Conversions implements Converter {
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;
}
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.isWhitespace(chaîne.charAt(i))) chaîne.deleteCharAt(i);
else i++;
return Integer.parseInt(chaîne.toString(), 2);
}
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);
}
}
Attention, dans ce cas là, vous devez revoir, à la fois votre fichier de configuration faces-config.xml, mais également votre page d'accueil pour qu'elle sollicite le bon
objet représentant la conversion.
faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
<managed-bean>
<managed-bean-name>conversion</managed-bean-name>
<managed-bean-class>bean.Conversions</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
<converter>
<converter-id>conversion</converter-id>
<converter-class>bean.Conversions</converter-class>
</converter>
</faces-config>
binaire.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
body { background-color: green; color: yellow; }
</style>
<f:view>
<html>
<body>
<h2>Fonctions logiques binaires</h2>
<hr />
<h:form>
<h:panelGrid columns="1">
<h:inputText value="#{conversion.premier}" converter="conversion" size="19" />
<h:inputText value="#{conversion.deuxième}" converter="conversion" size="19" />
<h:panelGroup>
<h:commandButton action="#{conversion.ouLogique}" value="OR" />
<h:commandButton action="#{conversion.etLogique}" value="AND" />
<h:commandButton action="#{conversion.ouExclusif}" value="XOR" />
</h:panelGroup>
<h:inputText value="#{conversion.résultat}" converter="conversion" readonly="true" size="19" />
</h:panelGrid>
</h:form>
</body>
</html>
</f:view>
Créer une méthode qui retourne un convertisseur
Dans la même logique, nous pouvons éventuellement aller encore plus loin, et proposer une méthode qui retourne un Converter. Dans ce cas là, il n'est plus nécessaire de
définir l'objet de conversion dans le fichier de configuration faces-config.xml. Par contre, il faut tout de même que l'objet soit créé afin de permettre son utilisation dans la
page Web. C'est ce que doit réaliser la méthode qui retourne le Converter.
bean.Conversions
package bean;
import
import
import
import
java.text.DecimalFormat;
javax.faces.component.UIComponent;
javax.faces.context.FacesContext;
javax.faces.convert.Converter;
public class Conversions {
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;
}
public Converter getBinaire() {
return new Converter() {
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.isWhitespace(chaîne.charAt(i))) chaîne.deleteCharAt(i);
else i++;
return Integer.parseInt(chaîne.toString(), 2);
}
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);
}
};
}
}
Cet objet est effectivement créé au travers d'une classe anonyme de type Converter qui doit, comme nous l'avons déjà souligné, redéfinir les méthodes de l'interface.
La syntaxe est du coup un peu plus compliqué, mais cela a le mérite de proposer plusieurs conversions dans le même composant JavaBean qui s'occupe de la
logique métier. Je vous donne toutes les solutions possibles, à vous de voir qu'elle est la meilleure.
faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
<managed-bean>
<managed-bean-name>conversion</managed-bean-name>
<managed-bean-class>bean.Conversions</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
</faces-config>
binaire.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
body { background-color: green; color: yellow; }
</style>
<f:view>
<html>
<body>
<h2>Fonctions logiques binaires</h2>
<hr />
<h:form>
<h:panelGrid columns="1">
<h:inputText value="#{conversion.premier}" converter="#{conversion.binaire}" size="19" />
<h:inputText value="#{conversion.deuxième}" converter="#{conversion.binaire}" size="19" />
<h:panelGroup>
<h:commandButton action="#{conversion.ouLogique}" value="OR" />
<h:commandButton action="#{conversion.etLogique}" value="AND" />
<h:commandButton action="#{conversion.ouExclusif}" value="XOR" />
</h:panelGroup>
<h:inputText value="#{conversion.résultat}" converter="#{conversion.binaire}" readonly="true" size="19" />
</h:panelGrid>
</h:form>
</body>
</html>
</f:view>
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
quelle est 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é. En voici un exemple :
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.
bean.Conversions
package bean;
import
import
import
import
java.text.DecimalFormat;
javax.faces.component.UIComponent;
javax.faces.context.FacesContext;
javax.faces.convert.Converter;
public class Conversions {
...
public Converter getBinaire() {
return new Converter() {
public Object getAsObject(FacesContext context, UIComponent component, String value) {
String contrôle = "([0-1]{4} )*[0-1]{4}|[0-1]+";
if (!value.matches(contrôle)) {
FacesMessage erreur = new FacesMessage("Valeur non valide", " Il faut saisir un nombre binaire");
throw new ConverterException(erreur);
}
StringBuilder chaîne = new StringBuilder(value);
for (int i=0 ; i<chaîne.length(); )
if (Character.isWhitespace(chaîne.charAt(i))) chaîne.deleteCharAt(i);
else i++;
return Integer.parseInt(chaîne.toString(), 2);
}
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);
}
};
}
}
Voici comment récupérer ensuite ces messages d'erreur sur votre page d'accueil :
binaire.jsp
<f:view>
<html>
<body>
<h2>Fonctions logiques binaires</h2>
<hr />
<h:form>
<h:messages/>
<h:panelGrid columns="1">
<h:panelGroup>
<h:inputText value="#{conversion.premier}" converter="#{conversion.binaire}" size="19" id="premier" />
<h:message for="premier" />
</h:panelGroup>
<h:panelGroup>
<h:inputText value="#{conversion.deuxième}" converter="#{conversion.binaire}" size="19" id="deuxième" />
<h:message for="deuxième" />
</h:panelGroup>
<h:panelGroup>
<h:commandButton action="#{conversion.ouLogique}" value="OR" />
<h:commandButton action="#{conversion.etLogique}" value="AND" />
<h:commandButton action="#{conversion.ouExclusif}" value="XOR" />
</h:panelGroup>
<h:inputText value="#{conversion.résultat}" converter="#{conversion.binaire}" readonly="true" size="19" />
</h:panelGrid>
</h:form>
</body>
</html>
</f:view>
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>.
Effectivement, 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é.
Le chapitre suivant va nous permettre de prendre en compte différents types de sévérité de messages d'erreur. Nous en profiterons également pour faire une gestion
beaucoup plus fine de la balise <h:messages> puisqu'elle servira de proposer aussi bien des informations normales que des messages d'erreur.
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.
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 JavaBean correspondant est
nécessairement correcte.
Nous allons donc mettre en oeuvre une validation, qui n'existe pas en standard, qui va contrôler si la valeur saisie est un nombre premier. Nous allons prendre,
comme support, l'application Web précédente. Voici un exemple d'utilisation, où nous avons une saisie non convertie, et une autre parfaitement convertie mais non
valide, c'est-à-dire que le nombre proposé n'est pas premier :
Nous profiterons de ce chapitre pour faire une gestion plus poussée sur les messages. Voici d'ailleur l'affichage lorsque les valeurs sont parfaitement converties et
valides :
.
Comment mettre en oeuvre une validation personnalisée
Nous retrouvons le même principe qu'avec le convertisseur personnalisé. Nous devons effectivement mettre en oeuvre une classe qui implémente une interface, et cette
fois-ci, il s'agit de l'interface javax.faces.validator.Validator. Nous pouvons créer une nouvelle classe pour cela, ou bien utiliser directement la classe du JavaBean. C'est
cette dernière solution que nous allons plutôt choisir. Cette interface Validator possède qu'une seule méthode (contrôle de la saisie uniquement) que la classe doit donc
redéfinir. Il s'agit de la méthode validate(). Voici d'ailleurs la déclaration complète de cette interface :
package javax.faces.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 JSP est alors
proposé. La page JSP peut alors afficher le message d'erreur proposé.
Reconstitution du bean métier bean.Conversions
Nous nous servons de cette classe pour implémenter, à la fois, le convertisseur personnalisé ainsi que le validateur personnalisé.
bean.Conversions
package bean;
import
import
import
import
import
import
java.text.DecimalFormat;
javax.faces.application.FacesMessage;
javax.faces.component.UIComponent;
javax.faces.context.FacesContext;
javax.faces.convert.*;
javax.faces.validator.*;
public class Conversions implements Converter, Validator {
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(null, new FacesMessage(" Résultat avec le OU logique"));
}
public void etLogique() {
résultat = premier&deuxième;
FacesContext context = FacesContext.getCurrentInstance();
context.addMessage(null, new FacesMessage(" Résultat avec le ET logique"));
}
public void ouExclusif() {
résultat = premier^deuxième;
FacesContext context = FacesContext.getCurrentInstance();
context.addMessage(null, new FacesMessage(" Résultat avec le OU Exclusif"));
}
public Object getAsObject(FacesContext context, UIComponent component, String value) {
String contrôle = "([0-1]{4} )*[0-1]{4}|[0-1]+";
if (!value.matches(contrôle)) {
String sommaire = context.getMessages().hasNext() ? "(2)" : " Aucun résultat : Erreur";
FacesMessage erreur = new FacesMessage(FacesMessage.SEVERITY_ERROR, sommaire, " Il faut saisir un nombre binaire");
throw new ConverterException(erreur);
}
StringBuilder chaîne = new StringBuilder(value);
for (int i=0 ; i<chaîne.length(); )
if (Character.isWhitespace(chaîne.charAt(i))) chaîne.deleteCharAt(i);
else i++;
return Integer.parseInt(chaîne.toString(), 2);
}
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);
}
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() ? "(2)" : " Aucun résultat : Erreur";
FacesMessage erreur = new FacesMessage(FacesMessage.SEVERITY_ERROR, sommaire, " Le nombre n'est pas premier");
throw new ValidatorException(erreur);
}
}
}
}
Je dirais qu'il n'y a pas trop de commentaire à faire si ce n'est la gestion des messages :
1. Cette fois-ci, lorsque nous proposons des messages d'erreur, nous indiqons le type de sévérité, en précisant bien la constante FacesMessage.SEVERITY_ERROR
en premier paramètre du constructeur FacesMessage().
2. De plus, nous prososons un message sommaire suivant le nombre de messages d'erreur en activité. Pour 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.
3. Nous mettons en place également 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.
4. Enfin, pour obtenir le contexte de la page en cours, faites appel à la méthode getCurrentInstance() de la classe FacesContext.
Enregistrer les validations personnalisées dans le fichier faces-config.xml
Comme pour la conversion personnalisée, vous devez enregistrer dans le fichier de configuration faces-config.xml quel est le validateur qui contrôle si les nombres saisis
sont des nombres premiers. Pour cela, spécifiez votre validateur au travers de la balise <validator>. Le nom de l'objet est alors indiqué par la balise <validator-id> et la
classe correspondante par la balise <validator-class>.
faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
<managed-bean>
<managed-bean-name>conversion</managed-bean-name>
<managed-bean-class>bean.Conversions</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
<converter>
<converter-id>conversion</converter-id>
<converter-class>bean.Conversions</converter-class>
</converter>
<validator>
<validator-id>conversion</validator-id>
<validator-class>bean.Conversions</validator-class>
</validator>
</faces-config>
Appel au validateur et aux différents types de message dans la page d'accueil binaire.jsp
Comme pour les balises <f:convertor>, vous pouvez placer vos <f:validator> à l'intérieur de la balise qui représente la zone de saisie. En effet, ces balises <f:validator>
permettent de ce connecter à l'objet qui réalise la validation de votre valeur saisie. L'objet est alors référencé au moyen de l'attribut validatorId.
binaire.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
body { background-color: orange; color: yellow; }
.info { color: green; }
.erreur { color: red; }
</style>
<f:view>
<html>
<body>
<h2>Fonctions logiques binaires</h2>
<hr />
<h:form>
<h:panelGrid columns="1">
<h:panelGroup>
<h:inputText value="#{conversion.premier}" size="19" id="premier">
<f:converter converterId="conversion" />
<f:validator validatorId="conversion" />
</h:inputText>
<h:message for="premier" errorClass="erreur"/>
</h:panelGroup>
<h:panelGroup>
<h:inputText value="#{conversion.deuxième}"size="19" id="deuxième">
<f:converter converterId="conversion" />
<f:validator validatorId="conversion" />
</h:inputText>
<h:message for="deuxième" errorClass="erreur" />
</h:panelGroup>
<h:panelGroup>
<h:commandButton action="#{conversion.ouLogique}" value="OR" />
<h:commandButton action="#{conversion.etLogique}" value="AND" />
<h:commandButton action="#{conversion.ouExclusif}" value="XOR" />
</h:panelGroup>
<h:panelGroup>
<h:inputText value="#{conversion.résultat}"readonly="true" size="19">
<f:converter converterId="conversion" />
</h:inputText>
<h:messages infoClass="info" errorClass="erreur"/>
</h:panelGroup>
</h:panelGrid>
</h:form>
</body>
</html>
</f:view>
Cette fois-ci, la balise <h:messages> affiche aussi bien une information sur le type d'opération qui a été réalisé, mais également, au cas où, affiche le sommaire des
erreurs rencontrées. Une couleur est choisie fonction du type de message à afficher au travers des attributs infoClass et errorClass. Ici, bien que j'aurais pu le faire,
je n'ai pas changé le comportement par défaut des balises <h:message> et <h:messages>. En effet, la première se contente d'afficher le détail du message d'erreur,
alors que la deuxième ne s'occupe que du sommaire de l'erreur.
Attribut validator des balises de saisies et choix de la méthode de validation du bean
Au lieu de prendre systématiquement la méthode validate() et d'implémenter l'interface Validator, JSF offre de choisir sa propre méthode. Le tout, c'est de conserver les
mêmes attributs et la même signature de l'exception à gérer. L'avantage de cette formule, c'est que nous pouvons ainsi définir plusieurs validations personnalisées pour le
même composant. Par ailleurs, il n'est plus nécessaire d'enregistrer votre validation personnelle dans le fichier de configuration faces-config.xml. Pour que cela puisse
fonctionner, il faut alors prendre l'attribut validator d'une des balises d'entrées, plutôt que d'utiliser la balise spécifique <f:validator>.
Voici en conséquence ce qui est modifié sur l'ensemble de vos sources :
faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
<managed-bean>
<managed-bean-name>conversion</managed-bean-name>
<managed-bean-class>bean.Conversions</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
<converter>
<converter-id>conversion</converter-id>
<converter-class>bean.Conversions</converter-class>
</converter>
</faces-config>
bean.Conversions
package bean;
import
import
import
import
import
import
java.text.DecimalFormat;
javax.faces.application.FacesMessage;
javax.faces.component.UIComponent;
javax.faces.context.FacesContext;
javax.faces.convert.*;
javax.faces.validator.*;
public class Conversions implements Converter {
private int premier;
private int deuxième;
private int résultat;
...
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() ? "(2)" : " Aucun résultat : Erreur";
FacesMessage erreur = new FacesMessage(FacesMessage.SEVERITY_ERROR, sommaire, " Le nombre n'est pas premier");
throw new ValidatorException(erreur);
}
}
}
}
binaire.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
body { background-color: orange; color: yellow; }
.info { color: green; }
.erreur { color: red; }
</style>
<f:view>
<html>
<body>
<h2>Fonctions logiques binaires</h2>
<hr />
<h:form>
<h:panelGrid columns="1">
<h:panelGroup>
<h:inputText value="#{conversion.premier}" size="19" id="premier" converter="conversion" validator="#
{conversion.nombrePremier}" />
<h:message for="premier" errorClass="erreur"/>
</h:panelGroup>
<h:panelGroup>
<h:inputText value="#{conversion.deuxième}"size="19" id="deuxième" converter="conversion" validator="#
{conversion.nombrePremier}" />
<h:message for="deuxième" errorClass="erreur" />
</h:panelGroup>
<h:panelGroup>
<h:commandButton action="#{conversion.ouLogique}" value="OR" />
<h:commandButton action="#{conversion.etLogique}" value="AND" />
<h:commandButton action="#{conversion.ouExclusif}" value="XOR" />
</h:panelGroup>
<h:panelGroup>
<h:inputText value="#{conversion.résultat}"readonly="true" size="19" converter="conversion" />
<h:messages infoClass="info" errorClass="erreur"/>
</h:panelGroup>
</h:panelGrid>
</h:form>
</body>
</html>
</f:view>
Validation pour une combinaison d'entrées
Nous restons toujours dans le domaine de la validation personnalisée. Effectivement, nous pouvons rencontrer 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.
Nous allons voir comment résoudre cette problématique au travers d'une nouvelle application Web qui permet de déterminer le jour de la semaine d'une date
quelconque saisie par l'opérateur. Voici d'ailleurs les résultats possibles :
Comment contrôler plusieurs zones de saisie avant injection des valeurs vers le bean métier ?
Souvenez-vous que lors de la phase de validation, les valeurs ne sont pas encore injectées dans le bean métier. Ainsi, nous ne pouvons pas encore contrôler les valeurs
saisies en passant par les attributs du bean. Nous devons nous adresser directement aux valeurs qui sont encore dans les zones de saisie. Nous avons déjà effectuer ce
genre d'opération. Il suffit de travailler avec l'attribut binding de chacune des zones de saisie, et créer ensuite des propriétés relatives de type UIInput sur le bean
correspondant.
faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
<managed-bean>
<managed-bean-name>date</managed-bean-name>
<managed-bean-class>bean.JourSemaine</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
</faces-config>
Sur cette application Web, nous avons juste deux éléments : d'une part, la page d'accueil JourSemaine.jsp, et d'autre part le bean date correspondant au JavaBean
bean.JourSemaine.
JourSemaine.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
body { background-color : yellow; color : teal; }
.résultat { color: navy; }
.erreur { color: red; }
</style>
<f:view>
<html>
<body>
<h3>Recherche du jour de la semaine</h3>
<hr />
<h:form>
<h4>
Jour <h:inputText value="#{date.jour}" size="1" maxlength="2" binding="#{date.saisieJour}" />
Mois
<h:selectOneMenu value="#{date.mois}" binding="#{date.saisieMois}">
<f:selectItems value="#{date.listeMois}" />
</h:selectOneMenu>
Année <h:inputText value="#{date.année}" size="3" maxlength="4" binding="#{date.saisieAnnée}" />
</h4>
<h:inputHidden value="contrôle" validator="#{date.contrôle}"/>
<h:commandButton action="#{date.changer}" value="Soumettre" />
</h:form>
<hr />
<h3>Jour de la semaine :
<h:messages infoClass="résultat" errorClass="erreur" showDetail="true" />
</h3>
</body>
</html>
</f:view>
1. La particularité sur cette page d'accueil c'est, d'une part de proposer des attributs binding sur les balises qui sont à consulter, et d'autre part de proposer une
balise <h:inputHidden> qui va servir uniquement à lancer la validation personnalisée. Attention, vous devez systématiquement prendre l'attribut value et lui
proposer une valeur quelconque, même si cette valeur ne sert à rien. Nous aurions pu prendre une des zones de saisie pour faire la même chose, mais je trouve
que c'est plus logique d'avoir un élément séparé qui contrôle les autres. A vous de voir qu'elle est la meilleure solution.
2. Encore une fois, je prend une balise <h:messages> pour présenter le résultat normal, ou pour indiquer l'erreur rencontrée lors d'une mauvaise saisie. Cette fois-ci,
cette balise présente à la fois le sommaire et le détail de l'erreur grâce à la validation de l'attribut showDetail.
bean.JourSemaine
package bean;
import
import
import
import
import
import
import
java.text.SimpleDateFormat;
java.util.*;
javax.faces.application.FacesMessage;
javax.faces.component.*;
javax.faces.context.FacesContext;
javax.faces.model.SelectItem;
javax.faces.validator.*;
public class JourSemaine {
private Calendar date = Calendar.getInstance();
private String détailErreur;
private int jour = date.get(date.DAY_OF_MONTH);
private int mois = date.get(date.MONTH);
private int année = date.get(date.YEAR);
private UIInput saisieJour;
private UIInput saisieMois;
private UIInput saisieAnnée;
private SelectItem[] listeMois = {
new SelectItem(0, "Janvier"),
new
new
new
new
new
new
new
new
new
new
new
SelectItem(1, "Février"),
SelectItem(2, "Mars"),
SelectItem(3, "Avril"),
SelectItem(4, "Mai"),
SelectItem(5, "Juin"),
SelectItem(6, "Juillet"),
SelectItem(7, "Août"),
SelectItem(8, "Septembre"),
SelectItem(9, "Octobre"),
SelectItem(10, "Novembre"),
SelectItem(11, "Décembre")
};
public JourSemaine() {
changer();
}
public int getJour() {
return jour;
}
public int getMois() {
return mois;
}
public int getAnnée() {
return année;
}
public SelectItem[] getListeMois() {
return listeMois;
}
public void setJour(int jour) {
this.jour = jour;
}
public void setMois(int mois) {
this.mois = mois;
}
public void setAnnée(int année) {
this.année = année;
}
public UIInput getSaisieJour() {
return saisieJour;
}
public UIInput getSaisieMois() {
return saisieMois;
}
public UIInput getSaisieAnnée() {
return saisieAnnée;
}
public void setSaisieAnnée(UIInput saisieAnnée) {
this.saisieAnnée = saisieAnnée;
}
public void setSaisieMois(UIInput saisieMois) {
this.saisieMois = saisieMois;
}
public void setSaisieJour(UIInput saisieJour) {
this.saisieJour = saisieJour;
}
public void changer() {
date = new GregorianCalendar(année, mois, jour);
SimpleDateFormat jourSemaine = new SimpleDateFormat("EEEE");
FacesContext page = FacesContext.getCurrentInstance();
page.addMessage(null, new FacesMessage(jourSemaine.format(date.getTime()).toUpperCase(), ""));
}
public void contrôle(FacesContext context, UIComponent component, Object value) throws ValidatorException {
int j = (Integer)saisieJour.getValue();
int m = (Integer)saisieMois.getValue();
int a = (Integer)saisieAnnée.getValue();
if (!dateValide(j, m, a)) {
FacesMessage erreur = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Date incorrecte -", détailErreur);
throw new ValidatorException(erreur);
}
}
private boolean dateValide(int j, int m, int a) {
if (j<1) {
détailErreur = "Le jour doit être une valeur positive";
return false;
}
détailErreur = "Le jour est trop grand ";
if (m==1) {
boolean bissextile = a%4==0 && (a%400==0 || a%100!=0);
détailErreur += bissextile ? "(29)" : "(28)";
return bissextile ? j<=29 : j<=28;
}
boolean trente = m==3 || m==5 || m==8 || m==10;
détailErreur += trente ? "(30)" : "(31)";
return trente ? j<=30 : j<=31;
}
}
1. L'attribut date permet de proposer une date par défaut qui est la date au moment où l'utilisateur se connecte. Ainsi, les propriétés relatives à la date sont
automatiquement initialisées sur la date du jour.
2. Remarquez la présence d'une liste de sélection listeMois qui permet de proposer le texte correspondant au mois.
3. Les propriétés saisieJour, saisieMois et saisieAnnée sont les représentantes des balises utilisées dans les zones de saisie.
4. La méthode changer() permet de calculer le jour de la semaine suivant la date proposée par l'utilisateur. Cette méthode est sollicité uniquement lorsque la date a
été validée.
5. Pour une fois, j'utilise le constructeur par défaut. Puisque les propriétés relatives à la date sont toutes correctement initialisées, je propose de donner déjà le
résultat sur la page d'accueil (la toute première fois) en lançant la méthode changer().
6. La méthode contrôle() effectue la validation de la date. Dans un premier temps, elle récupère les valeurs présentes dans la zone de saisie et les met en forme afin
de pouvoir être interprétées. Une série de test est ensuite proposé pour contrôler la validité de la date. Si la date est incorrecte, un message est alors envoyé avec
un texte adapté au type de l'erreur rencontré.
Retour sur les servlets pour la gestion des images
Avec JSF, nous avons une structure de type MVC où les pages JSP s'occupent uniquement de l'affichage alors que les JavaBeans 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 des informations binaires, comme des images créées de toute pièce.
Pour 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 sont également 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 400 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 JSP qui va réaliser le traitement souhaité, soit une servlet. J'ai souvent dit que la page JSP sert de
présentation et elle parfaitement adaptée lorsque le résultat du traitement est une page Web. Dans tous les autres cas, il faut utiliser une servlet.
La fabrication des deux images sera réalisé par le JavaBean bean.Traitement qui s'occupe de la page Web. Par contre la récupération de ces deux images sous forme
de flux binaire sera réalisé par deux servlets. Ce sont ces deux servlets qui mettent en relation la page Web avec les deux images binaires créées par le JavaBean.
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().
Constitution de l'application Web et descripteur de déploiement
Cette fois-ci, le descripteur de déploiement permet de mettre en oeuvre plusieurs servlet. Bien entendu, nous devons d'abord
décrire le contrôleur, mais également les deux servlets qui permettent de prendre une image et de la tranformer en flux binaire. La
première servlet va permettre de restituer la vignette et peut être atteinte au travers de l'URL LireImage. La deuxième restitue
l'histogramme de la vignette et peut être atteinte au travers de l'URL Histogramme.
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/
j2ee/web-app_2_4.xsd">
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>LireImage</servlet-name>
<servlet-class>servlet.LireImage</servlet-class>
</servlet>
<servlet>
<servlet-name>Histogramme</servlet-name>
<servlet-class>servlet.Histogramme</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>LireImage</servlet-name>
<url-pattern>/LireImage</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Histogramme</servlet-name>
<url-pattern>/Histogramme</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>
faces/images.jsp
</welcome-file>
</welcome-file-list>
</web-app>
Fichier de configuration <faces-config.xml>
Ce fichier de configuration permet juste de créer l'objet traitement correspondant au JavaBean bean.Traitement dans la session de l'application Web. Les servlets ne sont
pas du tout gérées par ce fichier là, mais par le descripteur de déploiement que nous venons de consulter.
faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
<managed-bean>
<managed-bean-name>traitement</managed-bean-name>
<managed-bean-class>bean.Traitement</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
</faces-config>
Page d'accueil images.jsp
La page d'accueil est très réduite et elle est finalement composé de deux parties.
1. La première partie est composée d'une zone de saisie qui permet à l'utilisateur d'indiquer où se situe le fichier image à consulter. Cette partie là, il faut bien le
reconnaître, n'est pas très sophistiquée ni très élégante. Il aurait été préférable de donner le listing des images présentes dans le répertoire de stockage et de
naviguer ensuite entre les différents liens. Je n'ai pas voulu compliquer l'interface. Le sujet est ailleurs.
2. La deuxième partie s'affiche en option. Dans le cas où une image a bien été trouvée, cette dernière s'affiche alors sous forme de vignette dans un panneau, avec à
sa droite l'histogramme correspondant, et en dessous ses dimensions réelles.
images.jpg
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
.général {background-color: black; color: yellow; }
body {font-family: verdana, Arial; background-color: yellow; color: blue; }
</style>
<f:view>
<html>
<body>
<h:form>
<h4>
Fichier image : <h:inputText value="#{traitement.nomFichier}" size="50" />
</h4>
<h:panelGrid columns="2" cellpadding="3" styleClass="général" rendered="#{traitement.présente}">
<h:graphicImage url="LireImage" />
<h:graphicImage url="Histogramme" />
<h:outputText value="Taille originale : #{traitement.largeur} x #{traitement.hauteur}" />
</h:panelGrid>
</h:form>
</body>
</html>
</f:view>
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.
Le JavaBean correspondant au modèle bean.Traitement
Ce bean s'occupe de la relation avec la page d'accueil précédente, mais réalise également tous les traitements d'image. D'une part, la redimension de l'image sous forme de
vignette, et d'autre part tout le calcul de l'histogramme. Ces deux traitements sont sollicités lorsque l'opérateur a choisi son fichier image à l'aide de la propriété nomFichier.
bean.Traitement.java
package bean;
import
import
import
import
import
import
import
java.awt.*;
java.awt.geom.*;
java.awt.image.*;
java.io.*;
java.net.URL;
javax.faces.context.*;
javax.imageio.ImageIO;
public class Traitement {
private String nomFichier;
private BufferedImage image;
private BufferedImage vignette;
private BufferedImage histogramme;
private int largeurVignette = 400;
private int[] rouge;
private int[] vert;
private int[] bleu;
private int largeur = 256;
private int hauteur;
private Graphics2D dessin;
public BufferedImage getVignette() {
return vignette;
}
public BufferedImage getHistogramme() {
return histogramme;
}
public String getNomFichier() {
return nomFichier;
}
public void setNomFichier(String nomFichier) {
this.nomFichier = nomFichier;
try {
image = ImageIO.read(new URL("file:///"+nomFichier));
retailler();
calculerHistogramme();
}
catch (IOException ex) {}
}
public int getLargeur() { return image==null ? 0 : image.getWidth(); }
public int getHauteur() { return image==null ? 0 : image.getHeight(); }
public boolean isPrésente() { return image!=null; }
private void retailler() {
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_BICUBIC;
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();
int maximum = 0;
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]);
}
}
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.
.
Les servlets qui récupèrent les images et les transforment en flux binaire
La première chose à faire est d'indiquer le type d'information à renvoyer au client. Cela se fait au travers de la méthode setContentType() de l'objet response. Il faut ensuite
se connecter avec l'objet traitement créé dans la session au moyen de la méthode getAttribute() de l'objet session qui est lui même possible d'atteindre au moyen de la
méthode getSession() de l'objet request. Le flux binaire d'image est effectué au travers de la méthode statique write() de la classe ImageIO. Vous spécifier à ce moment là le
type de codage de l'image, respectivement ici JPEG et GIF.
servlet.LireImage.java
package servlet;
import java.io.*;
import java.net.*;
import javax.imageio.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class LireImage extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("image/jpeg");
OutputStream out = response.getOutputStream();
bean.Traitement traitement = (bean.Traitement)request.getSession().getAttribute("traitement");
ImageIO.write(traitement.getVignette(), "JPEG", out);
out.close();
}
}
servlet.Histogramme.java
package servlet;
import java.io.*;
import java.net.*;
import javax.imageio.ImageIO;
import javax.servlet.*;
import javax.servlet.http.*;
public class Histogramme extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("image/gif");
OutputStream out = response.getOutputStream();
bean.Traitement traitement = (bean.Traitement)request.getSession().getAttribute("traitement");
ImageIO.write(traitement.getHistogramme(), "GIF", out);
out.close();
}
}
Contrôler les événements issus du client avec JSF
Pour terminer cette étude, 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 n'avons plus besoin de 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.
Pour bien maîtriser cette gestion d'événements, je vous propose de reprendre l'application Web du chapitre précédent mais en la sophistiquant un peu plus.
Effectivement, cette fois-ci, une fois que le fichier a été choisi, nous pouvons :
1. Régler la largeur des vignettes,
2. Choisir les courbes de l'histogramme à afficher, soit une des couleurs fondamentales, soit les trois couleurs en même temps,
3. Afficher une partie de l'image et faire un zoom par rapport à la zone souhaitée,
4. Exécuter un petit traitement d'image en proposant une retouche pour la luminosité et pour le contraste. Cette partie traitement est optionnelle et non visible par
défaut.
Les événements pris en compte sur cette application Web sont les suivant :
1. Lorsque vous choisissez la largeur de la vignette dans le menu déroulant correspondant, les images sont instantanément retaillées pour qu'elles soient toutes de
la même largeur.
2. Lorsque, vous cliquez sur un des radios boutons correspondant aux courbes de l'histogramme, seule la courbe choisie est instantanément affichée.
3. Lorsque vous cliquez sur un point de la vignette image, en haut à gauche, l'image du zoom est instantanément calculée en conséquence, de telle sorte que le
centre de la zone de zoom correspond au point cliqué.
4. Lorsque vous cliquez sur la case à cocher Traitement, l'image correspondante à la retouche, ainsi que les réglages Luminosité et Contraste, sont alors affichés
instantanément.
5. Lorsque vous soumettez une nouvelle valeur (en validant avec la touche Entrée) sur une des zones de saisie Luminosité ou Contraste, l'image du mode retouche
prend instantanément en compte les modifications souhaitées.
Pour cette application Web, je n'ai pas placé de bouton de soumission. Seule, la gestion d'événements m'intéresse. Toutefois, et normalement, le but de ces
différents traitements intermédiaires sont là pour atteindre un résultat définitif. Ainsi, nous aurions pu, par exemple, rajouter un bouton qui permet de télécharger
l'image sélectionnée issu du serveur en prenant en compte les retouches de l'utilisateur et ainsi avoir une image adaptée au besoin.
Les différents types d'événement
Quels sont les types d'événement que JSF intègre. Ce n'est pas une application fenêtrée ou même une applet qui permettent de gérer beaucoup de type d'événement. JSF
permet de gérer uniquement (c'est la plupart du temps suffisant) trois types d'événement :
1. Changement de valeur : ValueChangeListener : Ce type d'événement 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 : Ce type d'événement n'est pris en compte que par les boutons ou les liens uniquement.
3. Différentes phases du cycle de taritement des requêtes JSF : PhaseListener : (non traité ici)
Evénement lié au 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 UnJavaBean {
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();
...
}
}
C'est fini. Voyez qu'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. On pourrait se 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 ellemême de la récupérer. En réalité, la propriété ne l'a pas encore récupérée, nous le verrons dans la suite, en revenant sur le cycle de traitemement des requêtes JSF.
javax.faces.event.ValueChangeEvent
Méthodes
Retour
Explication
getComponent()
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é).
Evénement lié à une validation
Pour cet événement, vous devez alors spécifier la méthode du bean qui va traiter votre événement à l'aide, cette fois-ci, de l'attribut actionListener de la balise concernée.
Par contre, il n'est plus nécessaire de solliciter la fonction JavaScript submit(). Pour un bouton, voici ce qu'il faut donc écrire d'un point de vue général :
<h:commandButton action="#{bean.activer}" actionListener="#{bean.méthodeEvénement}" />
Ensuite, dans votre JavaBean, vous devez donc implémenter cette méthode particulière qui doit alors posséder un attribut de type ActionEvent :
public class UnJavaBean {
private int propriété;
public int getPropriété() {
}
public String activer() {
...
return "Passer à la page Web suivante";
}
...
public void méthodeEvénement(ActionEvent evt) {
...
}
}
javax.faces.event.ActionEvent
Méthodes
Retour
Explication
getComponent()
UIComponent
Retourne le composant qui a sollicité l'événement.
Vous remarquez que la classe ActionEvent possède très peu de méthodes. Il faut dire que dans la plupart des cas, ce qui vous intéresse, c'est juste de savoir
qu'une validation a été effectué. Toutefois, il peut être nécessaire de connaitre, par exemple, les coordonnées de la souris au moment où le clic a été effectué. 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"));
...
}
On pourrait légitimement se 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.
Cycle des événements par rapport au cycle de traitement des requêtes
Il est bon de connaître à quel moment sont pris en compte les é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 JavaBean. Voilà pourquoi, lorsque vous désirez
traiter l'action associé à l'événement, vous ne pouvez pas encore prendre la valeur des attributs du JavaBean 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.
Modèle MVC
Nous retrouvons la même ossature que précédemment. Nous avons juste besoin de deux servlets supplémentaires pour transmettre le flux binaire correspondant
respectivement, à l'image du zoom et à l'image de la retouche.
Tous les différents traitements supplémentaires sont, encore une fois, exécutés par le JavaBean bean.Traitement.
.
Constitution de l'application Web et descripteur de déploiement
Comme précédemment, le descripteur de déploiement permet de mettre en oeuvre l'ensemble des servlets de l'application Web
qui sont finalement au nombre de cinq.
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/
j2ee/web-app_2_4.xsd">
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>LireImage</servlet-name>
<servlet-class>servlet.LireImage</servlet-class>
</servlet>
<servlet>
<servlet-name>Histogramme</servlet-name>
<servlet-class>servlet.Histogramme</servlet-class>
</servlet>
<servlet>
<servlet-name>Zoom</servlet-name>
<servlet-class>servlet.Zoom</servlet-class>
</servlet>
<servlet>
<servlet-name>Retouche</servlet-name>
<servlet-class>servlet.Retouche</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>LireImage</servlet-name>
<url-pattern>/LireImage</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Histogramme</servlet-name>
<url-pattern>/Histogramme</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Zoom</servlet-name>
<url-pattern>/Zoom</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Retouche</servlet-name>
<url-pattern>/Retouche</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>
faces/images.jsp
</welcome-file>
</welcome-file-list>
</web-app>
Fichier de configuration <faces-config.xml>
Le fichier de configuration n'a pas changé, puisque nous avons toujours besoin du même bean, même si celui-ci propose des compétences supplémentaires.
faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
<managed-bean>
<managed-bean-name>traitement</managed-bean-name>
<managed-bean-class>bean.Traitement</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
</faces-config>
Page d'accueil images.jsp
La page d'accueil est cette fois-ci un peu plus étoffée puisque nous devons visualiser des images et des zones d'entrée supplémentaires. Par ailleurs, cette page doit mettre
en oeuvre le mécanisme de gestion des événements. Toutefois, malgré une certaine sophistication, l'ensemble du code à écrire et somme toute modeste.
images.jpg
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
.général {background-color: black; color: yellow; }
body {font-family: Verdana, Arial; background-color: yellow; color: blue; }
</style>
<f:view>
<html>
<body>
<h:form>
<h4>
Fichier image : <h:inputText value="#{traitement.nomFichier}" size="50" />
</h4>
<h:panelGrid columns="2" cellpadding="3" styleClass="général" rendered="#{traitement.présente}">
<h:commandButton image="LireImage" actionListener="#{traitement.changerCentreZoom}"/>
<h:graphicImage url="Histogramme" />
<h:panelGroup>
<h:outputText value="#{traitement.largeur} x #{traitement.hauteur} -- " />
<h:outputText value="Vignette : " />
<h:selectOneMenu value="#{traitement.largeurVignette}" valueChangeListener="#{traitement.changeLargeur}"
onchange="submit()">
<f:selectItems value="#{traitement.largeurs}" />
</h:selectOneMenu>
</h:panelGroup>
<h:selectOneRadio value="#{traitement.typeCourbe}" valueChangeListener="#{traitement.changeHistogramme}"
onchange="submit()">
<f:selectItem itemLabel="R" itemValue="rouge"/>
<f:selectItem itemLabel="V" itemValue="vert"/>
<f:selectItem itemLabel="B" itemValue="bleu"/>
<f:selectItem itemLabel="RVB" itemValue="toutes"/>
</h:selectOneRadio>
<h:graphicImage url="Retouche" rendered="#{traitement.traitement}"/>
<h:graphicImage url="Zoom" />
<h:panelGroup rendered="#{traitement.traitement}">
<h:outputText value="Luminosité " />
<h:inputText size="3" value="#{traitement.intensité}" valueChangeListener="#{traitement.changeIntensité}"
onchange="submit()">
<f:validateLongRange minimum="-50" maximum="50" />
</h:inputText>
<h:outputText value=" -- Contraste " />
<h:inputText size="3" value="#{traitement.contraste}" valueChangeListener="#{traitement.changeContraste}"
onchange="submit()">
<f:validateLongRange minimum="-50" maximum="50" />
</h:inputText>
</h:panelGroup>
<h:panelGroup>
<h:selectBooleanCheckbox value="#{traitement.traitement}" valueChangeListener="#
{traitement.changeTraitement}" onchange="submit()"/>
<h:outputText value="Traitement" />
</h:panelGroup>
</h:panelGrid>
</h:form>
</body>
</html>
</f:view>
Pour le traitement d'image, j'impose une certaine limitation des valeurs, d'une part pour la gestion de la luminosité, d'autre part pour la gestion du contraste.
Le JavaBean correspondant au modèle bean.Traitement
Ce bean s'occupe maintenant de la gestion des événements avec le codage des méthodes correspondantes. Nous devons également modifier légèrement la fabrication de
l'histogramme, et surtout, nous devons rajouter deux traitement supplémentaires que sont la gestion du zoom et la retouche de l'image.
bean.Traitement.java
package bean;
import
import
import
import
import
import
import
import
import
import
java.awt.*;
java.awt.geom.*;
java.awt.image.*;
java.io.*;
java.net.URL;
java.util.Map;
javax.faces.context.*;
javax.faces.event.*;
javax.faces.model.SelectItem;
javax.imageio.ImageIO;
public class Traitement {
private String nomFichier;
private BufferedImage image;
private BufferedImage vignette;
private BufferedImage histogramme;
private BufferedImage retouche;
private int centreZoomX, centreZoomY;
private int largeurVignette = 400;
private int[] rouges;
private int[] verts;
private int[] bleus;
private int largeur = 256;
private int hauteur;
private Graphics2D dessin;
private String typeCourbe = "toutes";
private int intensité;
private int contraste;
private
private
private
private
boolean
boolean
boolean
boolean
traitement;
rouge = true;
vert = true;
bleu = true;
public boolean isTraitement() {
return traitement;
}
public void setTraitement(boolean traitement) {
this.traitement = traitement;
}
public String getTypeCourbe() {
return typeCourbe;
}
public void setTypeCourbe(String typeCourbe) {
this.typeCourbe = typeCourbe;
}
public void setLargeurs(SelectItem[] largeurs) {
this.largeurs = largeurs;
}
public SelectItem[] getLargeurs() {
return largeurs;
}
private SelectItem[] largeurs = {
new SelectItem(200, "200"),
new SelectItem(250, "250"),
new SelectItem(300, "300"),
new SelectItem(350, "350"),
new SelectItem(400, "400"),
new SelectItem(450, "450"),
new SelectItem(500, "500")
};
public int getLargeurVignette() {
return largeurVignette;
}
public void setLargeurVignette(int largeurVignette) {
this.largeurVignette = largeurVignette;
}
public BufferedImage getVignette() {
return vignette;
}
public BufferedImage getHistogramme() {
return histogramme;
}
public BufferedImage getZoom() {
return image.getSubimage(centreZoomX-largeurVignette/2, centreZoomY-hauteur/2, largeurVignette, hauteur);
}
public BufferedImage getRetouche() {
return retouche;
}
public String getNomFichier() {
return nomFichier;
}
public void setNomFichier(String nomFichier) {
if (!nomFichier.equals(this.nomFichier)) {
this.nomFichier = nomFichier;
try {
image = ImageIO.read(new URL("file:///"+nomFichier));
retailler();
calculerHistogramme();
centreZoomX = image.getWidth()/2;
centreZoomY = image.getHeight()/2;
retouche();
}
catch (IOException ex) {}
}
}
public int getLargeur() { return image==null ? 0 : image.getWidth(); }
public int getHauteur() { return image==null ? 0 : image.getHeight(); }
public int getContraste() {
return contraste;
}
public void setContraste(int contraste) {
this.contraste = contraste;
}
public void setIntensité(int intensité) {
this.intensité = intensité;
}
public int getIntensité() {
return intensité;
}
public boolean isPrésente() { return image!=null; }
private void retailler() {
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_BICUBIC;
AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation);
retaillerImage.filter(image, vignette);
}
private void calculerHistogramme() {
hauteur = vignette.getHeight();
rouges = new int[256];
verts = new int[256];
bleus = new int[256];
récupérerRVB();
tracerHistogrammes();
}
private void récupérerRVB() {
Raster trame = vignette.getRaster();
ColorModel modèle = vignette.getColorModel();
int maximum = 0;
for (int y=0; y<vignette.getHeight(); y++)
for (int x=0; x<vignette.getWidth(); x++) {
Object données = trame.getDataElements(x, y, null);
rouges[modèle.getRed(données)]++;
verts[modèle.getGreen(données)]++;
bleus[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.yellow);
dessin.fill(rectangle);
changerAxes();
if (rouge) {
dessin.setPaint(new Color(1F, 0F, 0F, 0.7F));
tracerHistogramme(rouges);
}
if (vert) {
dessin.setPaint(new Color(0F, 1F, 0F, 0.7F));
tracerHistogramme(verts);
}
if (bleu) {
dessin.setPaint(new Color(0F, 0F, 1F, 0.7F));
tracerHistogramme(bleus);
}
}
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]);
}
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);
}
public void changeHistogramme(ValueChangeEvent evt) {
typeCourbe = (String)evt.getNewValue();
rouge = typeCourbe.equals("rouge") || typeCourbe.equals("toutes");
vert = typeCourbe.equals("vert") || typeCourbe.equals("toutes");
bleu = typeCourbe.equals("bleu") || typeCourbe.equals("toutes");
tracerHistogrammes();
}
public void changeLargeur(ValueChangeEvent evt) {
largeurVignette = (Integer)evt.getNewValue();
retailler();
calculerHistogramme();
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/largeurVignette;
centreZoomY = image.getHeight()*y/hauteur;
}
public void changeTraitement(ValueChangeEvent evt) {
traitement = (Boolean) evt.getNewValue();
}
public void changeIntensité(ValueChangeEvent evt) {
intensité = (Integer) evt.getNewValue();
retouche();
}
public void changeContraste(ValueChangeEvent evt) {
contraste = (Integer) evt.getNewValue();
retouche();
}
}
Pour comprendre le traitement relatif aux images, revoyez les cours sur le traitement d'images.
.
Les servlets qui récupèrent les images et les transforment en flux binaire
En prenant le même principe que précédemment, voici l'ensemble des servlets qui permettent de prendre l'image correspondante pour en faire un flot binaire utile pour la
page Web en cours :
servlet.LireImage.java
package servlet;
import java.io.*;
import java.net.*;
import javax.imageio.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class LireImage extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("image/jpeg");
OutputStream out = response.getOutputStream();
bean.Traitement traitement = (bean.Traitement)request.getSession().getAttribute("traitement");
ImageIO.write(traitement.getVignette(), "JPEG", out);
out.close();
}
}
servlet.Histogramme.java
package servlet;
import java.io.*;
import java.net.*;
import javax.imageio.ImageIO;
import javax.servlet.*;
import javax.servlet.http.*;
public class Histogramme extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("image/gif");
OutputStream out = response.getOutputStream();
bean.Traitement traitement = (bean.Traitement)request.getSession().getAttribute("traitement");
ImageIO.write(traitement.getHistogramme(), "GIF", out);
out.close();
}
}
servlet.Zoom.java
package servlet;
import java.io.*;
import java.net.*;
import javax.imageio.ImageIO;
import javax.servlet.*;
import javax.servlet.http.*;
public class Zoom extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("image/jpeg");
OutputStream out = response.getOutputStream();
bean.Traitement traitement = (bean.Traitement)request.getSession().getAttribute("traitement");
ImageIO.write(traitement.getZoom(), "JPEG", out);
out.close();
}
}
servlet.Retouche.java
package servlet;
import java.io.*;
import java.net.*;
import javax.imageio.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class Retouche extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("image/jpeg");
OutputStream out = response.getOutputStream();
bean.Traitement traitement = (bean.Traitement)request.getSession().getAttribute("traitement");
ImageIO.write(traitement.getRetouche(), "JPEG", 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. Que se passe-t-il si nous
proposons une mauvaise saisie sur une zone d'entrée alors que nous effectuons des réglages sur une autre zone avec la gestion d'événement appropriée ? Pour
connaître la réponse, reprenons l'application Web précédente.
Par exemple, lorsque nous proposons une valeur de luminosité à 80, alors que la valeur est limitée à 50, et qu'ensuite nous cliquons sur la vignette afin d'avoir le
zoom associé à la zone cliquée, nous remarquons que le changement ne s'effectue pas. Ce comportement paraît normal, puisque si une validation ne se fait pas
normalement, le système passe directement en phase 6, c'est-à-dire, en phase du rendu de la réponse, sans passer donc par la phase de la gestion des
événements.
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.
Mise en oeuvre de la gestion immédiate des événements
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. Remarquez d'ailleurs que nous avons déjà utilisé cet attribut
lors de l'étude d'un chapitre précédent.
Dans le cas qui nous intéresse, voici donc la modification à apporter sur la page Web :
images.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<style type="text/css">
.général {background-color: black; color: yellow; }
body {font-family: Verdana, Arial; background-color: yellow; color: blue; }
</style>
<f:view>
<html>
<body>
<h:form>
<h4>
Fichier image : <h:inputText value="#{traitement.nomFichier}" size="50" />
</h4>
<h:panelGrid columns="2" cellpadding="3" styleClass="général" rendered="#{traitement.présente}">
<h:commandButton image="LireImage" actionListener="#{traitement.changerCentreZoom}" immediate="true"/>
...
</h:panelGrid>
</h:form>
</body>
</html>
</f:view>
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 class Traitement {
...
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/largeurVignette;
centreZoomY = image.getHeight()*y/hauteur;
ctx.renderResponse(); // ligne à rajouter
}
...
}
Téléchargement