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á mediante JSF y Primefaces.
![]() |
6.1. El proyecto 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 de empresa
El proyecto de empresa 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 solo existe 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. Fíjate bien en el tipo de estas (líneas 23, 29, 35).
Para ejecutar la aplicación web, habrá que 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. Contiene 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, y el bean [Form] tiene un ámbito session. La clase [Erreur] encapsula un error. La clase [MyDataModel] sirve de plantilla 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 Primefaces o JSF que hemos estudiado. 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 gestiona 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-30. Esta área se encuentra en la zona de id:formulario:contenido (línea 27). Hay que tenerlo en cuenta. Las llamadas AJAX que actualizan esta zona 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-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 anteriormente.
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 la zona dinámica de la plantilla [layout.xhtml],
- La página consta de 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 resultante varía.
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 ha analizado en el apartado 2.8.5.7.
6.7.1. El bean Application
El bean [Application.java] es un bean de ámbito 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 da 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 EJB. Eso es todo. El bean [Application] no contiene nada más. Para acceder 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:
- package 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 ha lanzado 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 ámbito de sesión. Recordemos 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: la plantilla 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 destacar que, por defecto, se muestra la página [form1.xhtml] (línea 27),
- líneas 46-47: el método init se ejecuta justo 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 ha ido bien, se crean 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 va a mostrar. A continuación, la página [index.xhtml] muestra 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 da como resultado una página de error similar a la siguiente:
![]() |
Ahora vamos a definir las diferentes fases del ciclo de vida de la aplicación. Para cada acción del usuario, analizaremos las vistas correspondientes y los gestores de eventos.
6.8. Visualización de la página de inicio
Si todo va bien, la primera página que se muestra es [form1.xhtml]. El resultado es 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 que aparece enmarcada en la captura de pantalla es el componente Primefaces Toolbar. Este se define en las líneas 8-14. Contiene dos grupos de componentes, cada uno de ellos definido por una etiqueta <toolbarGroup>, en las líneas 9-11 y 12-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 añadido un efecto (effect="fade"),
- línea 6: un comportamiento AJAX. Cuando se produzca un cambio en el menú desplegable, se ejecutará el método [Form].hideAgenda (listener="#{form.hideAgenda}") se ejecutará y se actualizará el área dinámica :formulario:contenido (update=":formulaire:contenu"),
- línea 8: incluye un separador en la barra de herramientas,
- líneas 10-12: el campo de introducción de la fecha. Aquí se utiliza el calendario de Primefaces. El campo de introducción es de solo lectura (readOnlyInputText="true"),
- línea 11: un comportamiento AJAX. Cuando se produzca 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 (), tras lo cual se modificará la plantilla y se utilizará la respuesta del servidor para actualizar el área dinámica :formulario:contenido,
- línea 15: la etiqueta <tooltip> permite asociar una ventana emergente de ayuda a un componente. El identificador de este último se indica mediante el atributo «for» de la etiqueta <tooltip>. En este caso, «for="resa-agenda"» hace referencia 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;
// plantilla
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 cargarse la página por primera vez, establece el valor seleccionado en el cuadro combinado. Al cargarse la página, idMedecin es igual a null, por lo que se seleccionará el primer médico,
- el método de las líneas 16-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 apellido, el nombre y el nombre de pila del médico y, como valor (itemValue), el ID del médico,
- el campo de la línea 13 alimenta, en lectura y escritura, el campo de introducción de datos 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 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 hay franjas horarias seleccionadas
creneauChoisi = null;
}
No vamos a comentar este código. Ya se ha hecho.
6.9. Mostrar la agenda de un médico
6.9.1. Vista 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, a continuación, 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 añadir 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: tendremos que introducir un poco de JavaScript.
6.9.2. La tabla de citas
Aquí abordamos el modelo de una tabla de datos tal y como se estudió en el apartado 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 ha inicializado el siguiente campo en la plantilla:
// plantilla
private AgendaMedecinJour agendaMedecinJour;
La plantilla de la tabla se obtiene llamando al método [Form].getMyDataModel (atributo «value» de la etiqueta <dataTable>) siguiente:
// la plantilla de dataTable
public MyDataModel getMyDataModel() {
return new MyDataModel(agendaMedecinJour.getCreneauxMedecinJour());
}
Analicemos la clase [MyDataModel], que sirve de plantilla 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 la franja horaria seleccionada
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 objetivo de esta clase es establecer el vínculo entre el elemento rowkey que se envía y el elemento asociado a esa línea,
- 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 una matriz. Es esta matriz la que alimenta la etiqueta <dataTable>. Aquí, cada fila de la matriz 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 una franja horaria y se identificará mediante el ID de la franja horaria (línea 19). Es este ID el que se enviará al servidor,
- línea 23: el código que se ejecutará en el servidor cuando se envíe el identificador de un intervalo horario. El objetivo de este método es obtener la referencia del objeto [CreneauMedecinJour] asociado a este id. 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 añadir o eliminar.
6.9.3. La columna de franjas horarias
![]() |
La columna de franjas horarias 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 van a mostrar. El parámetro «value» indica la cadena de caracteres que se va a mostrar. La notación {i,tipo,formato} designa el parámetro n.º i, el tipo de dicho parámetro y su formato. Aquí hay cuatro parámetros numerados del 0 al 3; su tipo es numérico y se mostrarán con dos cifras,
- 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 intervalo horario. En este caso, se muestran el cargo, el nombre y los apellidos del cliente para el que se ha concertado dicha 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 secuencia siguiente:
![]() |
![]() |
La vista afectada por 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 ninguna cita, ni la opción [Réserver] si el elemento seleccionado tiene una cita. No hemos conseguido que el menú contextual sea tan sutil. Lo conseguimos para el primer elemento seleccionado, pero luego observamos que el menú contextual mantiene la configuración adquirida para esa primera selección. Entonces se vuelve incorrecto. Por lo tanto, hemos mantenido las dos opciones y hemos decidido proporcionar una respuesta al usuario si elimina un elemento sin cita,
- línea 16: el atributo oncomplete, que permite definir código JavaScript que se ejecutará tras 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 comprueba si el diccionario args tiene el atributo erreur. En caso afirmativo, 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);
}
// borrado de la franja horaria seleccionada
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 dicha cita existe, se elimina (línea 19), se regenera la agenda (línea 21) y se vuelve a mostrar (línea 23),
- si la eliminación ha fallado, 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 de tiempo que no tiene ninguna cita. Se señala este error:
![]() |
El método [signalerActionIncorrecte] es el siguiente:
// notificar una acción incorrecta
private void signalerActionIncorrecte() {
// borrar el intervalo 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 añade 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 comprueba si el diccionario args tiene el atributo denominado «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. Concertación de citas
La concertación de citas corresponde a la siguiente secuencia:
![]() |
La vista implicada 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 tiene lugar si no hay ninguna cita, se ha realizado una selección y 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. Se supone aquí 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 introducidos 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 autocompletado de 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 introducida por el usuario,
- línea 3: la lista de propuestas. Al principio es una lista vacía,
- líneas 5-10: se construyen las identidades [Nom prénom titre] de los clientes. Si una identidad comienza por 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 sigue 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, será el método [Form].action() el que gestione 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>();
// plantilla
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 añade 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 la franja horaria seleccionada
creneauChoisi = null;
// borrar cliente
identiteClient = null;
}
- líneas 33-35: debido al valor del campo action, se ejecutará el método [validerResa],
- línea 43: primero se comprueba que el cliente exista. De hecho, en el campo de entrada asistida, el usuario ha podido introducir un valor manualmente sin valerse de las sugerencias que se le han ofrecido. 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 propio cliente (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 añade el atributo («error», true) al diccionario args de la llamada a AJAX. La llamada a AJAX se ha definido 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 solicita a la capa [métier] que añada una cita para el día seleccionado (jour), la franja horaria seleccionada (creneauChoisi.getCreneau()) y el cliente seleccionado (hIdentitesClients.get(identiteClient)),
- línea 52: se solicita a la capa [métier] que actualice la agenda del médico. Se verá la cita añadida, además de todos los cambios que hayan podido realizar otros usuarios de la aplicación,
- línea 54: se vuelve a mostrar la agenda [form2.xhtml],
- línea 57: se muestra la página de error si se produce algún error.
6.13. Cancelación de una cita
Esto corresponde a la siguiente secuencia:
![]() |
El botón [Annuler] de 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);
// borrado de la franja horaria seleccionada
creneauChoisi = null;
// borrar cliente
identiteClient = null;
}
6.14. Navegación por 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 de visualización de 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 vuelve a inicializar 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 gestiona 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 recarga 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 se limitan a inicializar el campo locale de la línea 1 y, a continuación, redirigen el navegador del cliente a la misma página. Una redirección es una respuesta en la que el servidor solicita al navegador que cargue otra página. A continuación, el navegador 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. En este caso, queremos redirigirnos a [/mv-rdvmedecins-pf], que es el nombre de nuestra aplicación:
![]() |
El método [getRequestContextPath] permite obtener este 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 ámbito de sesión. Este modelo gestiona tres valores booleanos que controlan el aspecto de la página [index.xhtml]:
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
Dado que la plantilla tiene un ámbito de sesión, estas tres variables booleanas han conservado sus valores. Por lo tanto, la página [index.xhtml] aparecerá tal y como estaba antes de la redirección. Esta página se maquetará 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é se produce 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 «id» del formulario (atributo «update» de las líneas 7 y 8). Pero, durante las pruebas, el cambio de idioma no funcionaba siempre. De ahí la redirección para resolver este problema. Quizá también se podría haber añadido el atributo ajax='false' a las etiquetas para provocar una recarga de la página. De este modo, 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 justo después de la creació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. El inconveniente de esta técnica es que estos elementos ya no tienen en cuenta los cambios en la base de datos (adición de un cliente, de 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 incorporación de un nuevo cliente.
6.17. Conclusion
Recordemos la arquitectura de la aplicación que acabamos de crear:
![]() |
Nos hemos basado en gran medida en la versión JSF2 ya creada:
- se han conservado las capas [métier], [DAO] y [JPA],
- se han conservado los beans [Application] y [Form] de la capa web, pero se les han añadido 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 intuitiva.
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, hay que centrarse en la ergonomía deseada para la interfaz.
6.18. Pruebas con 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 principal de la empresa [mv-rdvmedecins-app-ear]. Al importar el proyecto principal, el proyecto secundario se importa automáticamente,
- en [2], se ejecuta el proyecto empresarial [mv-rdvmedecins-app-ear],
![]() |
- en [3], se selecciona el servidor Glassfish,
- en [4], en la pestaña [Servers], la aplicación se ha desplegado. No se ejecuta por sí sola. Hay que solicitar su URL [http://localhost:8080/mv-rdvmedecins-pf/] en un navegador [5]:
![]() |
















































