6. Aplicación de ejemplo-03: rdvmedecins-pf-ejb
Recordemos la estructura de la aplicación de ejemplo desarrollada para el servidor Glassfish:
![]() |
No modificamos nada de esta arquitectura, salvo la capa web, que en este caso se implementará utilizando JSF y PrimeFaces.
![]() |
6.1. El proyecto de NetBeans
En la imagen anterior, las capas [métier] y [DAO] son las del ejemplo 01 JSF / EJB / Glassfish. Las reutilizamos.
![]() |
- [mv-rdvmedecins-ejb-dao-jpa]: proyecto EJB de las capas [DAO] y [JPA] del ejemplo 01,
- [mv-rdvmedecins-ejb-metier]: proyecto EJB de la capa [métier] del ejemplo 01,
- [mv-rdvmedecins-pf]: proyecto de la capa [web] / PrimeFaces – nuevo,
- [mv-rdvmedecins-app-ear]: proyecto empresarial para implementar la aplicación en el servidor Glassfish – nuevo.
6.2. El proyecto empresarial
El proyecto empresarial solo sirve para implementar los tres módulos [mv-rdvmedecins-ejb-dao-jpa], [mv-rdvmedecins-ejb-metier] y [mv-rdvmedecins-pf] en el servidor Glassfish. El proyecto de NetBeans es el siguiente:
![]() |
El proyecto existe únicamente para estas tres dependencias [1], definidas en el archivo [pom.xml] de la siguiente manera:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>mv-rdvmedecins-app</artifactId>
<groupId>istia.st</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>istia.st</groupId>
<artifactId>mv-rdvmedecins-app-ear</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>ear</packaging>
<name>mv-rdvmedecins-app-ear</name>
...
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
<type>ejb</type>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-metier</artifactId>
<version>${project.version}</version>
<type>ejb</type>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-pf</artifactId>
<version>${project.version}</version>
<type>war</type>
</dependency>
</dependencies>
</project>
- líneas 10-13: el artefacto Maven del proyecto empresarial,
- líneas 18-37: las tres dependencias del proyecto. Tenga en cuenta el tipo de estas (líneas 23, 29, 35).
Para ejecutar la aplicación web, será necesario ejecutar este proyecto empresarial.
6.3. El proyecto web PrimeFaces
El proyecto web de Primefaces es el siguiente:
![]() |
- en [1], las páginas del proyecto. La página [index.xhtml] es la única página del proyecto. Consta de tres fragmentos: [form1.xhtml], [form2.xhtml] y [erreur.xhtml]. Las demás páginas solo sirven para el diseño.
- En [2], los beans de Java. El bean [Application] tiene un ámbito application; el bean [Form] tiene un ámbito session. La clase [Erreur] encapsula un error. La clase [MyDataModel] sirve como modelo para una etiqueta <dataTable> de PrimeFaces,
- en [3], los archivos de mensajes para la internacionalización,
- en [4], las dependencias. El proyecto web depende del proyecto EJB de la capa [DAO], el proyecto EJB de la capa [métier] y Primefaces para la capa [web].
6.4. La configuración del proyecto
La configuración del proyecto es la misma que la de los proyectos de Primefaces o JSF que hemos analizado. Enumeramos los archivos de configuración sin volver a explicarlos.
![]() | ![]() |
[web.xml]: configura la aplicación web.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Production</param-value>
</context-param>
<context-param>
<param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
<error-page>
<error-code>500</error-code>
<location>/faces/exception.xhtml</location>
</error-page>
<error-page>
<exception-type>Exception</exception-type>
<location>/faces/exception.xhtml</location>
</error-page>
</web-app>
Cabe destacar, en la línea 30, que la página [index.xhtml] es la página de inicio de la aplicación.
[faces-config.xml]: configura la aplicación JSF
<?xml version='1.0' encoding='UTF-8'?>
<!-- =========== FULL CONFIGURATION FILE ================================== -->
<faces-config version="2.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
<application>
<resource-bundle>
<base-name>
messages
</base-name>
<var>msg</var>
</resource-bundle>
<message-bundle>messages</message-bundle>
</application>
</faces-config>
[beans.xml]: está vacía, pero es necesaria para la anotación @Named
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>
[styles.css]: la hoja de estilo de la aplicación
.col1{
background-color: #ccccff
}
.col2{
background-color: #ffcccc
}
La biblioteca PrimeFaces incluye sus propias hojas de estilo. La hoja de estilo anterior solo se utiliza para la página que se muestra en caso de excepción, una página que no es administrada por la aplicación. En ese caso, se muestra la página [exception.xhtml].
[messages_fr.properties]: el archivo de mensajes en francés
# diseño
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers - application propuls\u00e9e par PrimeFaces et JQuery
# excepción
exception.header=L'exception suivante s'est produite
exception.httpCode=Code HTTP de l'erreur
exception.message=Message de l'exception
exception.requestUri=Url demand\u00e9e lors de l'erreur
exception.servletName=Nom de la servlet demand\u00e9e lorsque l'erreur s'est produite
# formulario 1
form1.titre=R\u00e9servations
form1.medecin=M\u00e9decin
form1.jour=Jour
form1.options=Options
form1.francais=Fran\u00e7ais
form1.anglais=Anglais
form1.rafraichir=Rafra\u00eechir
form1.precedent=Jour pr\u00e9c\u00e9dent
form1.suivant=Jour suivant
form1.agenda=Affiche l'agenda du m\u00e9decin choisi pour le jour choisi
form1.today=Aujourd'hui
# formulario 2
form2.titre=Agenda de {0} {1} {2} le {3}
form2.titre_detail=Agenda de {0} {1} {2} le {3}
form2.creneauHoraire=Cr\u00e9neau horaire
form2.client=Client
form2.accueil=Accueil
form2.supprimer=Supprimer
form2.reserver=R\u00e9server
form2.valider=Valider
form2.annuler=Annuler
form2.erreur=Erreur
form2.emtyMessage=Pas de cr\u00e9neaux entr\u00e9s dans la base
form2.suppression.confirmation=Etes-vous s\u00fbr(e) ?
form2.suppression.message=Suppression d'un rendez-vous
form2.supprimer.oui=Oui
form2.supprimer.non=Non
form2.erreurClient=Client [{0}] inconnu
form2.erreurClient_detail=Client {0} inconnu
form2.erreurAction=Action non autoris\u00e9e
form2.erreurAction_detail=Action non autoris\u00e9e
# error
erreur.titre=Une erreur s'est produite.
erreur.exceptions=Cha\u00eene des exceptions
erreur.type=Type de l'exception
erreur.message=Message associ\u00e9
erreur.accueil=Accueil
[messages_en.properties]: el archivo de mensajes en inglés
# diseño
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university - Application powered by PrimeFaces and JQuery
# excepción
exception.header=The following exceptions occurred
exception.httpCode=Error HTTP code
exception.message=Exception message
exception.requestUri=Url targeted when error occurred
exception.servletName=Servlet targeted's name when error occurred
# formulario 1
form1.titre=Reservations
form1.medecin=Doctor
form1.jour=Date
form1.options=Options
form1.francais=French
form1.anglais=English
form1.rafraichir=Refresh
form1.precedent=Previous Day
form1.suivant=Next day
form1.agenda=Show the doctor's diary for the chosen doctor and the chosen day
form1.today=Today
# formulario 2
form2.titre={0} {1} {2}'' diary on {3}
form2.titre_detail={0} {1} {2}'' diary on {3}
form2.creneauHoraire=Time Period
form2.client=Client
form2.accueil=Welcome Page
form2.supprimer=Delete
form2.reserver=Reserve
form2.valider=Submit
form2.annuler=Cancel
form2.erreur=Error
form2.emtyMessage=No Time periods in the database
form2.suppression.confirmation=Are-you sure ?
form2.suppression.message=Booking deletion
form2.supprimer.oui=Yes
form2.supprimer.non=No
form2.erreurClient=Unknown Client {0}
form2.erreurClient_detail=Unknown Client [{0}]
form2.erreurAction=Unauthorized action
form2.erreurAction_detail=Action non autoris\u00e9e
# error
erreur.titre=The following exceptions occurred
erreur.exceptions=Exceptions' chain
erreur.type=Exception type
erreur.message=Associated Message
erreur.accueil=Welcome
6.5. La plantilla de las páginas [layout.xhtml]
![]() |
La plantilla [layout.xhtml] es la siguiente:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{form.locale}">
<h:head>
<title>JSF</title>
<h:outputStylesheet library="css" name="styles.css"/>
</h:head>
<h:body style="background-image: url('#{request.contextPath}/resources/images/standard.jpg');">
<h:form id="formulaire">
<table style="width: 1200px">
<tr>
<td colspan="2" bgcolor="#ccccff">
<ui:include src="entete.xhtml"/>
</td>
</tr>
<tr>
<td style="width: 10px;" bgcolor="#ffcccc">
<ui:include src="menu.xhtml"/>
</td>
<td>
<p:outputPanel id="contenu">
<ui:insert name="contenu">
<h2>Contenu</h2>
</ui:insert>
</p:outputPanel>
</td>
</tr>
<tr bgcolor="#ffcc66">
<td colspan="2">
<ui:include src="basdepage.xhtml"/>
</td>
</tr>
</table>
</h:form>
</h:body>
</f:view>
</html>
La única parte variable de esta plantilla es el área de las líneas 28 a 30. Esta área se encuentra en la zona de id:formulario:contenido (línea 27). Recordemos esto. Las llamadas a AJAX que actualizan esta área tendrán el atributo update=":formulario:contenido". Por otra parte, el formulario comienza en la línea 15. Por lo tanto, el fragmento insertado en las líneas 28 a 30 se inserta en este formulario.
El aspecto que presenta esta plantilla es el siguiente:
![]() |
La parte dinámica de la página se insertará en el área enmarcada arriba.
6.6. La página [index.xhtml]
![]() |
El proyecto siempre muestra la misma página, la siguiente página [index.xhtml]:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="contenu">
<ui:fragment rendered="#{form.form1Rendered}">
<ui:include src="form1.xhtml"/>
</ui:fragment>
<ui:fragment rendered="#{form.form2Rendered}">
<ui:include src="form2.xhtml"/>
</ui:fragment>
<ui:fragment rendered="#{form.erreurRendered}">
<ui:include src="erreur.xhtml"/>
</ui:fragment>
</ui:define>
</ui:composition>
</html>
- líneas 8-9: este fragmento XHTML se insertará en el área dinámica de la plantilla [layout.xhtml],
- La página contiene tres subfragmentos:
- [form1.xhtml], líneas 10-12;
- [form2.xhtml], líneas 13-15;
- [erreur.xhtml], líneas 16-18.
La presencia de estos fragmentos en [index.xhtml] se controla mediante variables booleanas del modelo [Form.java] asociado a la página. Por lo tanto, al modificar estas variables, la página que se muestra cambia.
El fragmento [form1.xhtml] se muestra de la siguiente manera:
El fragmento [form2.xhtml] se muestra de la siguiente manera:
![]() |
El fragmento [erreur.xhtml] se muestra así:
![]() |
6.7. Los beans del proyecto
![]() |
La clase del paquete [utils] ya se ha presentado: la clase [Messages] es una clase que facilita la internacionalización de los mensajes de una aplicación. Se analizó en el párrafo 2.8.5.7.
6.7.1. El bean Application
El bean [Application.java] es un bean de alcance application. Recordemos que este tipo de bean sirve para almacenar datos de solo lectura y disponibles para todos los usuarios de la aplicación. Este bean es el siguiente:
package beans;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import rdvmedecins.metier.service.IMetierLocal;
@Named(value = "application")
@ApplicationScoped
public class Application {
// capa de negocio
@EJB
private IMetierLocal metier;
public Application() {
}
// getters
public IMetierLocal getMetier() {
return metier;
}
}
- línea 8: se le asigna al bean el nombre «application»,
- línea 9: tiene alcance de aplicación,
- líneas 13-14: el contenedor EJB del servidor de aplicaciones le inyectará una referencia a la interfaz local de la capa [métier]. Recordemos la arquitectura de la aplicación:
![]() |
Las aplicaciones JSF, EJB y [Metier] se ejecutarán en la misma JVM (Máquina Virtual Java). Por lo tanto, la capa [JSF] utilizará la interfaz local de la EJB. Eso es todo. El bean [Application] no contiene nada más. Para tener acceso a la capa [métier], los demás beans la buscarán en este bean.
6.7.2. El bean [Erreur]
La clase [Erreur] es la siguiente:
- paquete beans;
public class Erreur {public Erreur() {// campoprivate String classe;private String message;// constructorpublic Erreur(String classe, String message){this.setClasse(classe);this.message=message;// getters y setters
- línea 9, el nombre de una clase de excepción si se lanzó una excepción,
- línea 10: un mensaje de error.
6.7.3. El bean [Form]
Su código es el siguiente:
package beans;
import java.io.IOException;
...
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// bean de aplicación
@Inject
private Application application;
// caché de sesión
private List<Medecin> medecins;
private List<Client> clients;
private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
private Map<Long, Client> hClients = new HashMap<Long, Client>();
private Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
// modelo
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneauChoisi;
private Medecin medecin;
private Long idClient;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
private Boolean erreur = false;
private String identiteClient;
private String action;
private String msgErreurClient;
private Boolean erreurClient;
private String msgErreurAction;
private Boolean erreurAction;
private String locale = "fr";
@PostConstruct
private void init() {
// se almacenan en caché los médicos y los clientes
try {
medecins = application.getMetier().getAllMedecins();
clients = application.getMetier().getAllClients();
} catch (Throwable th) {
// se registra el error
prepareVueErreur(th);
return;
}
// los diccionarios
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
hIdentitesClients.put(identite(c), c);
}
}
...
// visualización de la vista
private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean erreurRendered) {
this.form1Rendered = form1Rendered;
this.form2Rendered = form2Rendered;
this.erreurRendered = erreurRendered;
}
// preparación vueErreur
private void prepareVueErreur(Throwable th) {
// se crea la lista de errores
erreurs = new ArrayList<Erreur>();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
while (th.getCause() != null) {
th = th.getCause();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
}
// se muestra la vista de errores
setForms(true, false, true);
}
// getters y setters
...
}
- líneas 6-8: la clase [Form] es un bean de nombre «form» y de alcance de sesión. Cabe recordar que, en este caso, la clase debe ser serializable,
- líneas 14-15: el bean «form» tiene una referencia al bean «application». Esta será inyectada por el contenedor de servlets en el que se ejecuta la aplicación (presencia de la anotación @Inject).
- líneas 17-44: el modelo de las páginas [form1.xhtml, form2.xhtml, erreur.xhtml]. La visualización de estas páginas se controla mediante los valores booleanos de las líneas 27-29. Cabe señalar que, por defecto, se muestra la página [form1.xhtml] (línea 27),
- líneas 46-47: el método init se ejecuta inmediatamente después de la instanciación de la clase (presencia de la anotación @PostConstruct),
- líneas 50-51: se solicita a la capa [métier] la lista de médicos y clientes,
- líneas 59-65: si todo salió bien, se construyen los diccionarios de médicos y clientes. Se indexan por su número. A continuación, se mostrará la página [form1.xhtml] (línea 27),
- línea 54: en caso de error, se genera la plantilla de la página [erreur.xhtml]. Esta plantilla es la lista de errores de la línea 36,
- líneas 78-88: el método [prepareVueErreur] genera la lista de errores que se mostrará. La página [index.xhtml] muestra entonces los fragmentos [form1.xhtml] y [erreur.xhtml] (línea 87).
La página [erreur.xhtml] es la siguiente:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<p:panel header="#{msg['erreur.titre']}" closable="true" >
<hr/>
<p:dataTable value="#{form.erreurs}" var="erreur">
<f:facet name="header">
<h:outputText value="#{msg['erreur.exceptions']}"/>
</f:facet>
<p:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.type']}"/>
</f:facet>
<h:outputText value="#{erreur.classe}"/>
</p:column>
<p:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.message']}"/>
</f:facet>
<h:outputText value="#{erreur.message}"/>
</p:column>
</p:dataTable>
</p:panel>
</body>
</html>
Utiliza una etiqueta <p:dataTable> (líneas 12-28) para mostrar la lista de errores. Esto genera una página de error similar a la siguiente:
![]() |
Ahora definiremos las diferentes fases del ciclo de vida de la aplicación. Para cada acción del usuario, analizaremos las vistas involucradas y los controladores de eventos.
6.8. La visualización de la página de inicio
Si todo sale bien, la primera página que se muestra es [form1.xhtml]. Esto da como resultado la siguiente vista:
![]() |
La página [form1.xhtml] es la siguiente:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
...
</p:toolbarGroup>
</p:toolbar>
</html>
La barra de herramientas enmarcada en la captura de pantalla es el componente PrimeFaces Toolbar. Este se define en las líneas 8 a 14. Contiene dos grupos de componentes, cada uno definido por una etiqueta <toolbarGroup>, en las líneas 9 a 11 y 12 a 14. Uno de los grupos está alineado a la izquierda de la barra de herramientas (línea 9), y el otro a la derecha (línea 12).
Veamos algunos componentes del grupo de la izquierda:
<p:toolbar>
<p:toolbarGroup align="left">
<h:outputText value="#{msg['form1.medecin']}"/>
<p:selectOneMenu value="#{form.idMedecin}" effect="fade">
<f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>
<p:ajax update=":formulaire:contenu" listener="#{form.hideAgenda}" />
</p:selectOneMenu>
<p:separator/>
<h:outputText value="#{msg['form1.jour']}"/>
<p:calendar id="calendrier" value="#{form.jour}" readOnlyInputText="true">
<p:ajax event="dateSelect" listener="#{form.hideAgenda}" update=":formulaire:contenu"/>
</p:calendar>
<p:separator/>
<p:commandButton id="resa-agenda" icon="ui-icon-check" actionListener="#{form.getAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-agenda" value="#{msg['form1.agenda']}"/>
...
</p:toolbarGroup>
...
- líneas 4-7: el menú desplegable de médicos al que se le ha agregado un efecto (effect="fade"),
- línea 6: un comportamiento AJAX. Cuando haya un cambio en el menú desplegable, se ejecutará el método [Form].hideAgenda (listener="#{form.hideAgenda}") se ejecutará y el área dinámica :formulario:contenido (update=":formulaire:contenu") se actualizará,
- línea 8: incluye un separador en la barra de herramientas,
- líneas 10-12: el campo de entrada de la fecha. Aquí se utiliza el calendario de PrimeFaces. El campo de entrada es de solo lectura (readOnlyInputText="true"),
- línea 11: un comportamiento AJAX. Cuando haya un cambio de fecha, se ejecutará el método [Form].hideAgenda y se actualizará el campo dinámico :formulario:contenido,
- línea 14: un botón. Al hacer clic en él, se ejecuta una llamada AJAX al método [Form].getAgenda (), luego se modificará la plantilla y la respuesta del servidor se utilizará para actualizar el área dinámica :formulario:contenido,
- línea 15: la etiqueta <tooltip> permite asociar una burbuja de ayuda a un componente. El identificador de este último se designa mediante el atributo «for» de la etiqueta <tooltip>. Aquí, «for="resa-agenda"» se refiere al botón de la línea 14:
![]() | ![]() |
Esta página se genera a partir de la siguiente plantilla:
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// caché de la sesión
private List<Medecin> medecins;
private List<Client> clients;
// modelo
private Long idMedecin;
private Date jour = new Date();
// lista de médicos
public List<Medecin> getMedecins() {
return medecins;
}
// lista de clientes
public List<Client> getClients() {
return clients;
}
// agenda
public void getAgenda() {
...
}
- El campo de la línea 12 alimenta, en lectura y escritura, el valor de la lista de la línea 4 de la página. Al mostrarse la página por primera vez, establece el valor seleccionado en el menú desplegable. Al mostrarse por primera vez, idMedecin es igual a null, por lo que se seleccionará el primer médico,
- el método de las líneas 16 a 18 genera los elementos del menú desplegable de médicos (línea 5 de la página). Cada opción generada tendrá como etiqueta (itemLabel) el título, apellido y nombre del médico, y como valor (itemValue), el identificador del médico,
- el campo de la línea 13 alimenta, en lectura y escritura, el campo de entrada de la línea 10 de la página. Por lo tanto, en la visualización inicial se muestra la fecha de hoy,
- líneas 26-28: el método getAgenda gestiona el clic en el botón [Agenda] de la línea 14 de la página. Es prácticamente idéntico a lo que era en la versión JSF:
// bean de la aplicación
@Inject
private Application application;
// caché de la sesión
private List<Medecin> medecins;
private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
// plantilla
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneauChoisi;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
private Boolean erreur = false;
public void getAgenda() {
try {
// se busca al médico
medecin = hMedecins.get(idMedecin);
// la agenda del médico para un día determinado
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// se muestra el formulario 2
setForms(true, true, false);
} catch (Throwable th) {
// vista de errores
prepareVueErreur(th);
}
// por el momento no se ha seleccionado ningún horario
creneauChoisi = null;
}
No comentaremos este código. Ya se ha hecho.
6.9. Mostrar la agenda de un médico
6.9.1. Visión general de la agenda
Este es el siguiente caso de uso:
![]() |
- en [1], se selecciona un médico [1] y un día [2], y luego se solicita [3] la agenda del médico para el día elegido,
- en [4], esta aparece debajo de la barra de herramientas.
El código de la página [form2.xhtml] es el siguiente:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jsp/jstl/core">
<body>
<!-- menú contextual -->
<p:contextMenu for="agenda">
...
</p:contextMenu>
<!-- agenda -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- columna de horarios -->
<p:column style="width: 100px">
...
</p:column>
<!-- columna de clientes -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
<!-- confirmación de eliminación RV -->
<p:confirmDialog id="confirmDialog" message="#{msg['form2.suppression.confirmation']}"
header="#{msg['form2.suppression.message']}" severity="alert" widgetVar="confirmation">
...
</p:confirmDialog>
<!-- mensaje de error -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
...
</p:dialog>
<!-- gestión de la respuesta del servidor -->
<script type="text/javascript">
...
}
</script>
</body>
</html>
- líneas 16-26: el elemento principal de la página es la tabla <dataTable>, que muestra la agenda del médico,
![]() |
- líneas 12-14: utilizaremos un menú contextual para agregar o eliminar una cita:
![]() |
- líneas 29-32: se mostrará un cuadro de confirmación cuando el usuario quiera eliminar una cita:
![]() |
- líneas 35-37: se utilizará un cuadro de diálogo para notificar un error:
![]() |
- líneas 40-43: necesitaremos introducir un poco de JavaScript.
6.9.2. La tabla de citas
Aquí abordamos el modelo de una tabla de datos tal como se estudió en el párrafo 5.15, página 327.
Analicemos el elemento principal de la página, la tabla que muestra la agenda:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- columna de horarios -->
<p:column style="width: 100px">
...
</p:column>
<!-- columna de clientes -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
El resultado es el siguiente:
![]() |
Se trata de una tabla de dos columnas (líneas 4-6 y 8-10) alimentada por la fuente [Form].getMyDataModel() (value="#{form.myDataModel}"). Solo se puede seleccionar una fila a la vez (selectionMode="single"). Cada vez que se ejecuta POST, se asigna una referencia del elemento seleccionado a [Form].creneauChoisi (selection="#{form.creneauChoisi}").
Recordemos que el método getAgenda inicializó el siguiente campo en el modelo:
// plantilla
private AgendaMedecinJour agendaMedecinJour;
La plantilla de la tabla se obtiene al llamar al siguiente método [Form].getMyDataModel (atributo «value» de la etiqueta <dataTable>):
// la plantilla de dataTable
public MyDataModel getMyDataModel() {
return new MyDataModel(agendaMedecinJour.getCreneauxMedecinJour());
}
Analicemos la clase [MyDataModel], que sirve de modelo para la etiqueta <p:dataTable>:
package beans;
import javax.faces.model.ArrayDataModel;
import org.primefaces.model.SelectableDataModel;
import rdvmedecins.metier.entites.CreneauMedecinJour;
public class MyDataModel extends ArrayDataModel<CreneauMedecinJour> implements SelectableDataModel<CreneauMedecinJour> {
// fabricantes
public MyDataModel() {
}
public MyDataModel(CreneauMedecinJour[] creneauxMedecinJour) {
super(creneauxMedecinJour);
}
@Override
public Object getRowKey(CreneauMedecinJour creneauMedecinJour) {
return creneauMedecinJour.getCreneau().getId();
}
@Override
public CreneauMedecinJour getRowData(String rowKey) {
// lista de franjas horarias
CreneauMedecinJour[] creneauxMedecinJour = (CreneauMedecinJour[]) getWrappedData();
// la clave es un entero largo
long key = Long.parseLong(rowKey);
// se busca el horario seleccionado
for (CreneauMedecinJour creneauMedecinJour : creneauxMedecinJour) {
if (creneauMedecinJour.getCreneau().getId().longValue() == key) {
return creneauMedecinJour;
}
}
// nada
return null;
}
}
- línea 7: la clase [MyDataModel] es el modelo de la etiqueta <p:dataTable>. El propósito de esta clase es establecer el vínculo entre el elemento rowkey que se envía y el elemento asociado a esa fila,
- línea 7: la clase implementa la interfaz [SelectableDataModel] a través de la clase [ArrayDataModel]. Esto significa que el parámetro del constructor es un array. Es este array el que alimenta la etiqueta <dataTable>. Aquí, cada fila del array se asociará a un elemento de tipo [CreneauMedecinJour],
- líneas 13-15: el constructor pasa su parámetro a su clase padre,
- líneas 18-20: cada línea de la matriz corresponde a un horario y se identificará mediante el identificador del horario (línea 19). Es este identificador el que se enviará al servidor,
- línea 23: el código que se ejecutará del lado del servidor cuando se envíe el identificador de un horario. El objetivo de este método es obtener la referencia del objeto [CreneauMedecinJour] asociado a este identificador. Esta referencia se asignará al valor del atributo «selection» de la etiqueta <dataTable>:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
Por lo tanto, el campo [Form].creneauChoisi contendrá la referencia del objeto [CreneauMedecinJour] que se desea agregar o eliminar.
6.9.3. La columna de los horarios
![]() |
La columna de los intervalos horarios se obtiene con el siguiente código:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- columna de horarios -->
<p:column style="width: 100px">
<f:facet name="header">
<h:outputText value="#{msg['form2.creneauHoraire']}"/>
</f:facet>
<div align="center">
<h:outputFormat value="{0,number,#00}:{1,number,#00} - {2,number,#00}:{3,number,#00}">
<f:param value="#{creneauMedecinJour.creneau.hdebut}" />
<f:param value="#{creneauMedecinJour.creneau.mdebut}" />
<f:param value="#{creneauMedecinJour.creneau.hfin}" />
<f:param value="#{creneauMedecinJour.creneau.mfin}" />
</h:outputFormat>
</div>
</p:column>
<!-- columna de clientes -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
- líneas 5-7: el encabezado de la columna,
- líneas 8-15: el elemento actual de la columna. Cabe destacar, en la línea 9, el uso de la etiqueta <h:outputFormat>, que permite dar formato a los elementos que se mostrarán. El parámetro «value» indica la cadena de caracteres que se mostrará. La notación {i,tipo,formato} designa el parámetro n.º i, el tipo de este parámetro y su formato. Aquí hay 4 parámetros numerados del 0 al 3; su tipo es numérico y se mostrarán con dos dígitos,
- líneas 10-13: los cuatro parámetros que espera la etiqueta <h:outputFormat>.
6.9.4. La columna de clientes
![]() |
La columna de clientes se obtiene con el siguiente código:
<!-- agenda -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- columna de horarios -->
...
<!-- columna de clientes -->
<p:column style="width: 300px">
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
<ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
<h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}" />
</ui:fragment>
<ui:fragment rendered="#{creneauMedecinJour.rv==null and form.creneauChoisi!=null and form.creneauChoisi.creneau.id==creneauMedecinJour.creneau.id}">
...
</ui:fragment>
</p:column>
</p:dataTable>
- líneas 8-10: el encabezado de la columna,
- líneas 11-13: el elemento actual cuando hay una cita para ese horario. En este caso, se muestran el título, el nombre y el apellido del cliente para quien se programó esta cita,
- líneas 14-16: otro fragmento al que volveremos más adelante.
6.10. Eliminación de una cita
La eliminación de una cita sigue la siguiente secuencia:
![]() |
![]() |
La vista a la que se refiere esta acción es la siguiente:
<!-- menú contextual -->
<p:contextMenu for="agenda">
...
<p:menuitem value="#{msg['form2.supprimer']}" onclick="confirmation.show()"/>
</p:contextMenu>
<!-- agenda -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
...
</p:dataTable>
<!-- confirmación de eliminación RV -->
<p:confirmDialog id="confirmDialog" message="#{msg['form2.suppression.confirmation']}"
header="#{msg['form2.suppression.message']}" severity="alert" widgetVar="confirmation">
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
<f:setPropertyActionListener value="supprimer" target="#{form.action}"/>
</p:commandButton>
<p:commandButton value="#{msg['form2.supprimer.non']}" onclick="confirmation.hide()" type="button" />
</p:confirmDialog>
- líneas 2-5: un menú contextual vinculado a la tabla de datos (atributo «for»). Tiene dos opciones [1]:
![]() |
- línea 4: la opción [Supprimer] activa la visualización del cuadro de diálogo [2] de las líneas 13-20,
- línea 15: al hacer clic en [Oui] se ejecuta [Form.action], que eliminará la cita. Normalmente, el menú contextual no debería ofrecer la opción [Supprimer] si el elemento seleccionado no tiene una cita, ni la opción [Réserver] si el elemento seleccionado tiene una cita. No logramos que el menú contextual fuera tan sutil. Funciona para el primer elemento seleccionado, pero luego observamos que el menú contextual conserva la configuración establecida para esa primera selección. Por lo tanto, se vuelve incorrecto. Así que mantuvimos las dos opciones y decidimos proporcionar un mensaje de retroalimentación al usuario si eliminaba un elemento sin cita,
- línea 16: el atributo oncomplete, que permite definir código JavaScript para ejecutar después de la llamada a AJAX. Este código será el siguiente:
<!-- mensaje de error -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
<h:outputText value="#{form.msgErreur}" />
</p:dialog>
<!-- gestión de la respuesta del servidor -->
<script type="text/javascript">
function handleRequest(xhr, status, args) {
// ¿Error?
if(args.erreur) {
dlgErreur.show();
}
}
</script>
- línea 10: el código JavaScript verifica si el diccionario args tiene el atributo erreur. Si es así, muestra el cuadro de diálogo de la línea 2 (atributo widgetVar). Este cuadro muestra la plantilla [Form].msgErreur.
Veamos el código que se ejecuta para gestionar la eliminación de una cita:
<p:confirmDialog ...>
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
...>
<f:setPropertyActionListener value="supprimer" target="#{form.action}"/>
</p:commandButton>
...
</p:confirmDialog>
- línea 2: se ejecutará el método [Form].action,
- línea 4: antes de su ejecución, el campo action habrá recibido el valor 'supprimer'.
El método [action] es el siguiente:
// acción sobre RV
public void action() {
// según la acción deseada
if (action.equals("supprimer")) {
supprimer();
}
...
}
public void supprimer() {
// ¿Hay que hacer algo?
Rv rv = creneauChoisi.getRv();
if (rv == null) {
signalerActionIncorrecte();
return;
}
try {
// Eliminación de una cita
application.getMetier().supprimerRv(rv);
// se actualiza la agenda
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// se muestra el formulario 2
setForms(true, true, false);
} catch (Throwable th) {
// vista de errores
prepareVueErreur(th);
}
// borrar el horario seleccionado
creneauChoisi = null;
}
- línea 4: si la acción es «eliminar», se ejecuta el método [supprimer],
- línea 12: se recupera la cita del intervalo seleccionado. Cabe recordar que [creneauChoisi] se ha inicializado con la referencia del elemento [CreneauMedecinJour] seleccionado;
- si esta cita existe, se elimina (línea 19), se regenera la agenda (línea 21) y luego se vuelve a mostrar (línea 23),
- si la eliminación falló, se muestra la página de errores (línea 26),
- si el elemento seleccionado no tiene ninguna cita (línea 13), entonces nos encontramos en la situación en la que el usuario ha hecho clic en [Supprimer] en un intervalo que no tiene ninguna cita. Se reporta este error:
![]() |
El método [signalerActionIncorrecte] es el siguiente:
// notificar una acción incorrecta
private void signalerActionIncorrecte() {
// borrar el horario seleccionado
creneauChoisi = null;
// error
msgErreur = Messages.getMessage(null, "form2.erreurAction", null).getSummary();
RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
}
- línea 4: se elimina la selección,
- línea 6: se genera un mensaje de error internacionalizado,
- línea 7: se agrega al diccionario args de la llamada AJAX el atributo («error», true).
Volvamos al código XHTML del botón [Oui]:
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
- línea 2: tras la ejecución del método [Form].action, se ejecuta el método JavaScript handleRequest:
<!-- mensaje de error -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
<h:outputText value="#{form.msgErreur}" />
</p:dialog>
<!-- gestión de la respuesta del servidor -->
<script type="text/javascript">
function handleRequest(xhr, status, args) {
// ¿Error?
if(args.erreur) {
dlgErreur.show();
}
}
</script>
- línea 10: se verifica si el diccionario args tiene el atributo llamado «erreur». Si es así, se muestra el cuadro de diálogo de la línea 2,
- línea 3: muestra el mensaje de error generado por la plantilla.
6.11. Programación de citas
La programación de citas corresponde a la siguiente secuencia:
![]() |
La vista involucrada en esta acción es la siguiente:
<!-- menú contextual -->
<p:contextMenu for="agenda">
<p:menuitem value="#{msg['form2.reserver']}" update=":formulaire:contenu" action="#{form.action}" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="reserver" target="#{form.action}"/>
</p:menuitem>
...
</p:contextMenu>
<!-- agenda -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- columna de horarios -->
<p:column style="width: 100px">
...
</p:column>
<!-- columna de clientes -->
<p:column style="width: 300px">
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
...
<ui:fragment rendered="#{creneauMedecinJour.rv==null and form.creneauChoisi!=null and form.creneauChoisi.creneau.id==creneauMedecinJour.creneau.id}">
<p:autoComplete completeMethod="#{form.completeClients}" value="#{form.identiteClient}" size="30"/>
<p:spacer width="50px"/>
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
<p:spacer width="50px"/>
<p:commandLink action="#{form.action()}" value="#{msg['form2.annuler']}" update=":formulaire:contenu">
<f:setPropertyActionListener value="annuler" target="#{form.action}"/>
</p:commandLink>
</ui:fragment>
</p:column>
</p:dataTable>
...
- líneas 21-31: muestran lo siguiente:
![]() |
- línea 21: la visualización se produce si no hay citas, si se ha realizado una selección y si el ID de la franja horaria elegida coincide con el del elemento actual de la tabla. Si no se establece esta condición, el fragmento se muestra para todas las franjas horarias,
- línea 22: el campo de entrada será un campo de entrada asistida. Aquí se supone que puede haber muchos clientes,
- líneas 24-26: el enlace [Valider],
- líneas 28-30: el enlace [Annuler].
El campo de entrada asistida se genera mediante el siguiente código:
<p:autoComplete completeMethod="#{form.completeClients}" value="#{form.identiteClient}" size="30"/>
El método [Form].completeClients se encarga de ofrecer sugerencias al usuario a partir de los caracteres ingresados en el campo de entrada:
![]() |
Las propuestas tienen el formato [Nom prénom titre]. El código del método [Form].completeClients es el siguiente:
// el método de autocompletar texto
public List<String> completeClients(String query) {
List<String> identites = new ArrayList<String>();
// se buscan los clientes que coincidan
for (Client c : clients) {
String identite = identite(c);
if (identite.toLowerCase().startsWith(query.toLowerCase())) {
identites.add(identite);
}
}
return identites;
}
private String identite(Client c) {
return c.getNom() + " " + c.getPrenom() + " " + c.getTitre();
}
- línea 2: query es la cadena de caracteres ingresada por el usuario,
- línea 3: la lista de sugerencias. Inicialmente es una lista vacía,
- líneas 5-10: se construyen las identidades [Nom prénom titre] de los clientes. Si una identidad comienza con query (línea 7), se incluye en la lista de propuestas (línea 8).
6.12. Validación de una cita
La validación de una cita corresponde a la siguiente secuencia:
![]() |
El código del enlace [Valider] es el siguiente:
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
Por lo tanto, el método [Form].action() será el encargado de gestionar este evento. Mientras tanto, el modelo [Form].action habrá recibido la cadena «validar». El código es el siguiente:
// bean de aplicación
@Inject
private Application application;
// caché de sesión
...
private Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
// modelo
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
private AgendaMedecinJour agendaMedecinJour;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
private Boolean erreur = false;
private String identiteClient;
private String action;
private String msgErreur;
@PostConstruct
private void init() {
...
for (Client c : clients) {
hClients.put(c.getId(), c);
hIdentitesClients.put(identite(c), c);
}
}
// acción sobre RV
public void action() {
// según la acción deseada
...
if (action.equals("valider")) {
validerResa();
}
}
// validación de la cita
public void validerResa() {
// validación de la reserva
try {
// ¿existe el cliente?
Boolean erreur = !hIdentitesClients.containsKey(identiteClient);
if (erreur) {
msgErreur = Messages.getMessage(null, "form2.erreurClient", new Object[]{identiteClient}).getSummary();
RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
return;
}
// se agrega la cita
application.getMetier().ajouterRv(jour, creneauChoisi.getCreneau(), hIdentitesClients.get(identiteClient));
// se actualiza la agenda
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// se muestra el formulario 2
setForms(true, true, false);
} catch (Throwable th) {
// vista de errores
prepareVueErreur(th);
}
// Se borra el horario seleccionado
creneauChoisi = null;
// borrar datos del cliente
identiteClient = null;
}
- líneas 33-35: debido al valor del campo action, se ejecutará el método [validerResa],
- línea 43: primero se verifica que el cliente exista. De hecho, en el campo de entrada asistida, el usuario pudo haber ingresado el dato directamente sin utilizar las sugerencias que se le ofrecieron. La entrada asistida está asociada al modelo [Form].identiteClient. Por lo tanto, se comprueba si esta identidad existe en el diccionario identitesClients creado al instanciar el modelo (línea 20). Este diccionario asocia una identidad de cliente de tipo [Nom prénom titre] con el cliente mismo (línea 25),
- línea 44: si el cliente no existe, se devuelve un error al navegador,
- línea 45: un mensaje de error internacionalizado,
- línea 46: se agrega el atributo («error», true) al diccionario args de la llamada a AJAX. La llamada a AJAX se definió de la siguiente manera:
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
En la línea 3 anterior, se observa que el enlace [Valider] tiene un atributo oncomplete. Es este atributo el que hará que se muestre el mensaje de error según una técnica que ya hemos visto.
- Línea 50: se le solicita a la capa [métier] que agregue una cita para el día seleccionado (jour), el horario seleccionado (creneauChoisi.getCreneau()) y el cliente seleccionado (hIdentitesClients.get(identiteClient)),
- línea 52: se le pide a la capa [métier] que actualice la agenda del médico. Se verá la cita agregada, además de todos los cambios que otros usuarios de la aplicación hayan podido realizar,
- línea 54: se vuelve a mostrar la agenda [form2.xhtml],
- línea 57: se muestra la página de error si ocurre algún error.
6.13. Cancelación de una cita
Esto corresponde a la siguiente secuencia:
![]() |
El botón [Annuler] en la página [form2.xhtml] es el siguiente:
<p:commandLink action="#{form.action()}" value="#{msg['form2.annuler']}" update=":formulaire:contenu">
<f:setPropertyActionListener value="annuler" target="#{form.action}"/>
</p:commandLink>
Por lo tanto, se invoca el método [Form].action:
// acción en RV
public void action() {
// según la acción deseada
...
if (action.equals("annuler")) {
annulerRv();
}
}
// cancelación de la cita
public void annulerRv() {
// se muestra el formulario 2
setForms(true, true, false);
// borrar el horario seleccionado
creneauChoisi = null;
// borrar datos del cliente
identiteClient = null;
}
6.14. Navegación en el calendario
La barra de herramientas permite navegar por el calendario:
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
Aunque no se muestra en las capturas de pantalla anteriores, la agenda se actualiza con las citas del nuevo día seleccionado.
Las etiquetas de los tres botones en cuestión son las siguientes en [form1.xhtml]:
<p:toolbar>
<p:toolbarGroup align="left">
...
<h:outputText value="#{msg['form1.jour']}"/>
<p:calendar id="calendrier" value="#{form.jour}" readOnlyInputText="true">
<p:ajax event="dateSelect" listener="#{form.hideAgenda}" update=":formulaire:contenu"/>
</p:calendar>
<p:separator/>
<p:commandButton id="resa-agenda" icon="ui-icon-check" actionListener="#{form.getAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-agenda" value="#{msg['form1.agenda']}"/>
<p:commandButton id="resa-precedent" icon="ui-icon-seek-prev" actionListener="#{form.getPreviousAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-precedent" value="#{msg['form1.precedent']}"/>
<p:commandButton id="resa-suivant" icon="ui-icon-seek-next" actionListener="#{form.getNextAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-suivant" value="#{msg['form1.suivant']}"/>
<p:commandButton id="resa-today" icon="ui-icon-home" actionListener="#{form.today}" update=":formulaire:contenu"/>
<p:tooltip for="resa-today" value="#{msg['form1.today']}"/>
</p:toolbarGroup>
<p:toolbarGroup align="right">
...
</p:toolbarGroup>
</p:toolbar>
Los métodos [Form].getPreviousAgenda, [Form].getNextAgenda y [Form].today son los siguientes:
private Date jour = new Date();
public void getPreviousAgenda() {
// se pasa al día anterior
Calendar cal = Calendar.getInstance();
cal.setTime(jour);
cal.add(Calendar.DAY_OF_YEAR, -1);
jour = cal.getTime();
// agenda
if (form2Rendered) {
getAgenda();
}
}
public void getNextAgenda() {
// se pasa al día siguiente
Calendar cal = Calendar.getInstance();
cal.setTime(jour);
cal.add(Calendar.DAY_OF_YEAR, 1);
jour = cal.getTime();
// agenda
if (form2Rendered) {
getAgenda();
}
}
// agenda de hoy
public void today() {
jour = new Date();
// agenda
if (form2Rendered) {
getAgenda();
}
}
- línea 1: el día en que se muestra la agenda,
- línea 5: se utiliza un calendario,
- línea 6: que se inicializa con el día actual de la agenda,
- línea 7: se resta un día del calendario,
- línea 8: y se reinicializa con el día de visualización de la agenda,
- línea 11: se vuelve a mostrar la agenda si está visible. De hecho, el usuario puede utilizar la barra de herramientas sin que la agenda esté visible.
Los demás métodos son similares.
6.15. Cambio del idioma de visualización
El cambio de idioma se controla mediante el botón de menú de la barra de herramientas:
![]() |
![]() |
Las etiquetas del botón de menú son las siguientes:
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
<p:menuButton value="#{msg['form1.options']}">
<p:menuitem id="menuitem-francais" value="#{msg['form1.francais']}" actionListener="#{form.setFrenchLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-anglais" value="#{msg['form1.anglais']}" actionListener="#{form.setEnglishLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
</p:menuButton>
</p:toolbarGroup>
</p:toolbar>
Los métodos que se ejecutan en la plantilla son los siguientes:
private String locale = "fr";
public void setFrenchLocale() {
locale = "fr";
// se actualiza la página
redirect();
}
public void setEnglishLocale() {
locale = "en";
// se actualiza la página
redirect();
}
private void redirect() {
// se redirige al cliente al servlet
ExternalContext ctx = FacesContext.getCurrentInstance().getExternalContext();
try {
ctx.redirect(ctx.getRequestContextPath());
} catch (IOException ex) {
Logger.getLogger(Form.class.getName()).log(Level.SEVERE, null, ex);
}
}
Los métodos de las líneas 3 y 9 simplemente inicializan el campo locale de la línea 1 y luego redirigen el navegador del cliente a la misma página. Una redirección es una respuesta en la que el servidor le pide al navegador que cargue otra página. El navegador entonces realiza una GET hacia esa nueva página.
- línea 17: [ExternalContext] es una clase JSF que permite acceder al servlet que se está ejecutando,
- línea 19: se realiza la redirección. El parámetro del método redirect es el URL de la página a la que debe redirigirse el navegador del cliente. Aquí queremos redirigirnos a [/mv-rdvmedecins-pf], que es el nombre de nuestra aplicación:
![]() |
El método [getRequestContextPath] permite obtener ese nombre. Por lo tanto, se cargará la página de inicio [index.xhtml] de nuestra aplicación. Esta página está asociada al modelo [Form], que tiene un alcance de sesión. Este modelo maneja tres valores booleanos que controlan la apariencia de la página [index.xhtml]:
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
Dado que la plantilla tiene alcance de sesión, estas tres variables booleanas han conservado sus valores. Por lo tanto, la página [index.xhtml] aparecerá tal como estaba antes de la redirección. Esta página se formatea con la plantilla facelet [layout.xhtml] siguiente:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{form.locale}">
....
</f:view>
</html>
La etiqueta de la línea 9 establece el idioma de visualización de la página mediante su atributo «local». Por lo tanto, la página se mostrará en francés o en inglés, según el caso. Ahora bien, ¿por qué una redirección? Volvamos a las etiquetas de las opciones de cambio de idioma:
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
<p:menuButton value="#{msg['form1.options']}">
<p:menuitem id="menuitem-francais" value="#{msg['form1.francais']}" actionListener="#{form.setFrenchLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-anglais" value="#{msg['form1.anglais']}" actionListener="#{form.setEnglishLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
</p:menuButton>
</p:toolbarGroup>
</p:toolbar>
Inicialmente se habían escrito para actualizar, mediante una llamada a AJAX, el campo de identificación del formulario (atributo «update» de las líneas 7 y 8). Sin embargo, durante las pruebas, el cambio de idioma no funcionaba siempre. De ahí la redirección para resolver este problema. Quizás también se podría haber establecido el atributo ajax='false' en las etiquetas para provocar una recarga de la página. De esa manera se habría evitado la redirección.
6.16. Actualización de las listas
Esto corresponde a la siguiente acción:
![]() |
La etiqueta asociada a la opción [Rafraîchir] es la siguiente:
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
El método [Form].refresh es el siguiente:
public void refresh() {
// se actualizan las listas
init();
}
El método init es el que se ejecuta inmediatamente después de la construcción del bean [Form]. Su objetivo es almacenar en caché los datos de la base de datos en el modelo:
// bean de aplicación
@Inject
private Application application;
// caché de la sesión
private List<Medecin> medecins;
private List<Client> clients;
private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
private Map<Long, Client> hClients = new HashMap<Long, Client>();
private Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
...
@PostConstruct
private void init() {
// se almacenan en caché los médicos y los clientes
try {
medecins = application.getMetier().getAllMedecins();
clients = application.getMetier().getAllClients();
} catch (Throwable th) {
...
}
...
// los diccionarios
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
hIdentitesClients.put(identite(c), c);
}
}
El método init crea las listas y los diccionarios de las líneas 5 a 9. La desventaja de esta técnica es que estos elementos ya no reflejan los cambios en la base de datos (agregación de un cliente, un médico, etc.). El método refresh fuerza la reconstrucción de estas listas y diccionarios. Por lo tanto, se utilizará cada vez que se realice un cambio en la base de datos, como por ejemplo, la adición de un nuevo cliente.
6.17. Conclusion
Recordemos la arquitectura de la aplicación que acabamos de construir:
![]() |
Nos hemos basado en gran medida en la versión JSF2 ya construida:
- se conservaron las capas [métier], [DAO] y [JPA],
- se conservaron los beans [Application] y [Form] de la capa web, pero se les agregaron nuevas funcionalidades debido a la mejora de la interfaz de usuario,
- la interfaz de usuario se ha modificado profundamente. En particular, ahora cuenta con más funcionalidades y es más fácil de usar.
El cambio de JSF a PrimeFaces para desarrollar la interfaz web requiere cierta experiencia, ya que al principio uno se siente un poco abrumado ante la gran cantidad de componentes disponibles y, al final, no sabe muy bien cuáles utilizar. Por lo tanto, es necesario analizar la ergonomía deseada para la interfaz.
6.18. Pruebas en Eclipse
Al igual que hicimos con las versiones anteriores de la aplicación de ejemplo, mostramos cómo probar esta versión 03 con Eclipse. En primer lugar, importamos a Eclipse los proyectos Maven del ejemplo 03 [1]:
![]() |
- [mv-rdvmedecins-ejb-dao-jpa]: las capas [DAO] y [JPA],
- [mv-rdvmedecins-ejb-metier]: la capa [métier],
- [mv-rdvmedecins-pf]: la capa [web] implementada con JSF y Primefaces,
- [mv-rdvmedecins-app]: el proyecto padre del proyecto empresarial [mv-rdvmedecins-app-ear]. Al importar el proyecto padre, el proyecto hijo se importa automáticamente,
- en [2], se ejecuta el proyecto corporativo [mv-rdvmedecins-app-ear],
![]() |
- en [3], se selecciona el servidor Glassfish,
- en [4], en la pestaña [Servers], la aplicación se ha implementado. No se ejecuta por sí sola. Hay que solicitar su URL [http://localhost:8080/mv-rdvmedecins-pf/] en un navegador [5]:
![]() |
















































