9. Aplicación de ejemplo 05: rdvmedecins-pfm-ejb
Recordemos la estructura de la aplicación de ejemplo 01 JSF / EJB desarrollada para el servidor Glassfish:
![]() |
No modificamos nada de esta arquitectura, salvo la capa web, que en este caso se implementará utilizando JSF, PrimeFaces y PrimeFaces Mobile. El navegador de destino será el de un dispositivo móvil.
![]() |
Hemos desarrollado dos aplicaciones con Glassfish:
- la aplicación 01 utilizaba JSF / EJB y tenía una interfaz bastante sencilla,
- la aplicación 03 utilizaba PF / EJB y tenía una interfaz rica.
Debido al escaso número de componentes disponibles en PrimeFaces Mobile, volveremos a la interfaz sencilla de la aplicación 01. Además, las vistas deberán poder adaptarse al tamaño reducido de las pantallas de los dispositivos móviles.
9.1. Las vistas
Para que se hagan una idea, les mostramos algunas vistas de la aplicación ejecutada en el simulador de un iPhone 4:
![]() |
- en [1], la página de inicio. Cabe señalar que esta vez fue necesario indicar el nombre de la máquina (también se puede indicar su dirección IP), ya que con la máquina localhost no se mostraba nada,
- en [2], la lista de médicos,
- en [3], la fecha deseada para la cita,
- en [4], el botón para consultar la agenda del día,
- en [5], la nueva vista muestra los horarios disponibles del médico,
- en [6], una serie de botones para navegar por el calendario,
- en [7], un mensaje que recuerda el nombre del médico y el día,
- en [8], un horario disponible para reservar. Lo hacemos,
![]() |
- en [9], la vista de selección del cliente,
- en [10], un mensaje que recuerda el médico, el día y el horario de la cita,
- en [11], el menú desplegable de clientes,
- en [12], el botón de validación,
- en [13], al validar se nos redirige al calendario,
- en [14], el horario ya está reservado. Ahora vamos a eliminar la reserva,
![]() |
- en [15], seguimos en la misma vista,
- pero en [16], la cita se ha eliminado,
En la página de inicio es posible cambiar el idioma: [17], [18], [19]:
![]() |
Por último, se ha incluido una vista de errores:
![]() |
9.2. El proyecto de NetBeans
El proyecto de NetBeans es el siguiente:
![]() |
- [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-pfmobile]: proyecto de la capa [web] / PrimeFaces Mobile – nuevo,
- [mv-rdvmedecins-pfmobile-app-ear]: proyecto empresarial para implementar la aplicación en el servidor Glassfish – nuevo.
9.3. El proyecto corporativo
El proyecto empresarial solo sirve para implementar los tres módulos [mv-rdvmedecins-ejb-dao-jpa], [mv-rdvmedecins-ejb-metier] y [mv-rdvmedecins-pfmobile] 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>
...
<groupId>istia.st</groupId>
<artifactId>mv-rdvmedecins-pfmobile-app-ear</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>ear</packaging>
<name>mv-rdvmedecins-pfmobile-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-pfmobile</artifactId>
<version>${project.version}</version>
<type>war</type>
</dependency>
</dependencies>
</project>
- líneas 6-9: el artefacto Maven del proyecto empresarial,
- líneas 14-33: las tres dependencias del proyecto. Tenga en cuenta el tipo de estas (líneas 19, 25, 31).
Para ejecutar la aplicación web, será necesario ejecutar este proyecto empresarial.
9.4. El proyecto web PrimeFaces Mobile
El proyecto web Primefaces Mobile es el siguiente:
![]() |
- en [1], las páginas del proyecto. La página [index.xhtml] es la única página del proyecto. Consta de cinco vistas: [vue1.xhtml], [vue2.xhtml], [vue3.xhtml], [vueErreurs.xhtml] y [config.xhtml],
- 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,
- 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 Mobile para la capa [web].
9.5. La configuración del proyecto
La configuración del proyecto es la misma que la de los proyectos 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.PROJECT_STAGE</param-name>
<param-value>Development</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 26, 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>
<default-render-kit-id>PRIMEFACES_MOBILE</default-render-kit-id>
</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>
[messages_fr.properties]: el archivo de mensajes en francés
# página
page.titre=Les M\u00e9decins Associ\u00e9s
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
# vista1
vue1.header=Les M\u00e9decins Associ\u00e9s - R\u00e9servations
# formulario 1
form1.titre=R\u00e9servations
form1.medecin=M\u00e9decin
form1.jour=Jour (jj/mm/aaaa)
form1.date.requise=La date est n\u00e9cessaire
form1.date.invalide=La date est invalide
form1.date.invalide_detail=La date est invalide
form1.agenda=Agenda
form1.options=Options
# formulario 2
form2.titre={0} {1} {2}<br/>{3}
form2.titre_detail={0} {1} {2}<br/>{3}
form2.retour=Retour
form2.supprimer=Supprimer
form2.reserver=R\u00e9server
form2.precedent=Jour pr\u00e9c\u00e9dent
form2.suivant=Jour suivant
form2.today=Aujourd'hui
# formulario 3
form3.client=Client
form3.valider=Valider
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}
form3.retour=Retour
# error
erreur.titre=Une erreur s'est produite.
# configuración
config.retour=Retour
config.titre=Configuration
config.langue=Langue
config.langue.francais=Fran\u00e7ais
config.langue.anglais=Anglais
config.valider=Valider
#excepción
exception.titre=Application indisponible. Veuillez recommencer ult\u00e9rieurement.
[messages_en.properties]: el archivo de mensajes en inglés
# página
page.titre=The Associated Doctors
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
# vista1
vue1.header=The Associated Doctors - Reservations
# formulario 1
form1.titre=Reservations
form1.medecin=Doctor
form1.jour=day (dd/mm/yyyy)
form1.date.requise=The date is necessary
form1.date.invalide=Invalid date
form1.date.invalide_detail=invalid date
form1.agenda=Diary
form1.options=Options
# formulario 2
form2.titre={0} {1} {2}<br/>{3}
form2.titre_detail={0} {1} {2}<br/>{3}
form2.retour=Back
form2.supprimer=Delete
form2.reserver=Reserve
form2.precedent=Previous Day
form2.suivant=Next Day
form2.today=Today
# formulario 3
form3.client=Patient
form3.valider=Validate
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}
form3.retour=Back
# error
erreur.titre=Some error happened
# configuración
config.retour=Back
config.titre=Configuration
config.langue=Language
config.langue.francais=French
config.langue.anglais=English
config.valider=Validate
#excepción
exception.titre=Application not available. Please try again later.
9.6. La página [index.xhtml]
![]() |
El proyecto siempre muestra la misma página, la siguiente página [index.xhtml]:
<f:view xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:pm="http://primefaces.org/mobile"
contentType="text/html"
locale="#{form.locale}">
<pm:page title="#{msg['page.titre']}">
<pm:view id="vue1">
<ui:fragment rendered="#{form.form1Rendered}">
<ui:include src="vue1.xhtml"/>
</ui:fragment>
<ui:fragment rendered="#{form.erreurInit}">
<ui:include src="vueErreurs.xhtml"/>
</ui:fragment>
</pm:view>
<pm:view id="vue2">
<ui:fragment rendered="#{form.form2Rendered}">
<ui:include src="vue2.xhtml"/>
</ui:fragment>
</pm:view>
<pm:view id="vue3">
<ui:fragment rendered="#{form.form3Rendered}">
<ui:include src="vue3.xhtml"/>
</ui:fragment>
</pm:view>
<pm:view id="vueErreurs">
<ui:fragment rendered="#{form.erreurRendered}">
<ui:include src="vueErreurs.xhtml"/>
</ui:fragment>
</pm:view>
<pm:view id="config">
<ui:include src="config.xhtml"/>
</pm:view>
</pm:page>
</f:view>
- línea 8: la página está internacionalizada (atributo «locale»),
- línea 10: la página contiene cinco vistas: vista1 en la línea 11, vista2 en la línea 19, vista3 en la línea 24, vueErreurs en la línea 29, config en la línea 34. En un momento dado, solo una de estas vistas es visible. Al iniciar la aplicación, se muestra la vista vista1. Aquí nos encontramos con la siguiente dificultad: si la inicialización de la aplicación se realizó correctamente, se debe mostrar [vue1.xhtml]; de lo contrario, se debe mostrar [vueErreurs.xhtml]. Resolvimos el problema asegurándonos de que el contenido de la vista vue1 sea administrado por el modelo, el cual asigna valores a los booleanos [Form].form1rendered (línea 12) y [Form].erreurInit (línea 15) para fijar el contenido de vista1 (línea 11),
En un simulador, la vista [vue1.xhtml] tiene la representación [1], la vista [vue2.xhtml] tiene la representación [2], y la vista [vue3.xhtml] tiene la representación [3]:
![]() |
la vista [vueErreurs.xhtml], la representación [4]; la vista [config.xhtml], la representación [5]:
![]() |
9.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.
9.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 PFM, EJB y [Metier] se ejecutarán en la misma JVM (Máquina Virtual Java). Por lo tanto, la capa [PFM] utilizará la interfaz local de 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.
9.7.2. El bean [Erreur]
La clase [Erreur] es la siguiente:
package beans;
public class Erreur {
public Erreur() {
}
// campo
private String classe;
private String message;
// constructor
public 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.
9.7.3. El bean [Form]
Su código es el siguiente:
package beans;
...
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// bean de aplicación
@Inject
private Application application;
private IMetierLocal metier;
// 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>();
// modelo
private Long idMedecin;
private Date jour = new Date();
private String strJour;
private Boolean form1Rendered;
private Boolean form2Rendered;
private Boolean form3Rendered;
private Boolean erreurRendered;
private String form2Titre;
private String form3Titre;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneauChoisi;
private Medecin medecin;
private Long idClient;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
private Boolean erreurInit = false;
private String action;
private String locale = "fr";
private String msgErreurDate = "";
private SimpleDateFormat dateFormatter;
private Boolean erreurDate;
@PostConstruct
private void init() {
System.out.println("init");
// al principio no hay error
erreurInit = false;
// el formato de las fechas
dateFormatter = new SimpleDateFormat(Messages.getMessage(null, "format.date", null).getSummary());
dateFormatter.setLenient(false);
// el día actual
strJour = dateFormatter.format(jour);
// se recupera la capa de negocio
metier = application.getMetier();
// se almacenan en caché los médicos y los clientes
try {
medecins = metier.getAllMedecins();
clients = metier.getAllClients();
} catch (Throwable th) {
// se registra el error
erreurInit = true;
prepareVueErreur(th);
return;
}
// verificación de las listas
if (medecins.size() == 0) {
// se registra el error
erreurInit = true;
erreurs = new ArrayList<Erreur>();
erreurs.add(new Erreur("", "La liste des médecins est vide"));
}
if (clients.size() == 0) {
// se anota el error
erreurInit = true;
erreurs = new ArrayList<Erreur>();
erreurs.add(new Erreur("", "La liste des clients est vide"));
}
// ¿Error?
if (erreurInit) {
// Se muestra la vista de errores
setForms(false, false, false, true);
return;
}
// los diccionarios
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
}
// la vista 1
setForms(true, false, false, false);
}
// visualización de la vista
private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
this.form1Rendered = form1Rendered;
this.form2Rendered = form2Rendered;
this.form3Rendered = form3Rendered;
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(false, false, false, true);
}
// getters y setters
..
}
- líneas 5-7: 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 12-13: 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 24-27: controlan la visualización de las vistas vue1 (línea 24), vue2 (línea 25), vue3 (línea 26) y vueErreurs (línea 27),
- líneas 43-44: el método init se ejecuta inmediatamente después de la instanciación de la clase (presencia de la anotación @PostConstruct),
- líneas 49-50: gestionan el formato de las fechas. PrimeFaces Mobile no ofrece un calendario. Además, los validadores JSF no se pueden utilizar en una página PFM. Por lo tanto, tendremos que gestionar manualmente la entrada de la fecha de la agenda,
- línea 49: establece el formato de las fechas. Este formato se toma de los archivos internacionalizados:
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
- línea 50: se indica que no se debe utilizar el formato «laxiste» para las fechas. Si no se hace así, una entrada como 32/12/2011, que es incorrecta, se considera la fecha válida 01/01/2012,
- línea 54: se obtiene una referencia de la capa [métier] a través del bean [Application],
- líneas 57-58: se solicita a la capa [métier] la lista de médicos y clientes,
- líneas 66-91: si todo salió bien, se generan los diccionarios de médicos y clientes. Estos se indexan por su número. A continuación, se mostrará la vista [vue1.xhtml] (línea 93),
- línea 59: en caso de error, se genera la plantilla de la página [vueErreurs.xhtml]. Esta plantilla es la lista de errores de la línea 35,
- líneas 105-115: el método [prepareVueErreur] genera la lista de errores que se mostrará. La página [index.xhtml] muestra entonces la vista [vueErreurs.xhtml] (línea 114).
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:pm="http://primefaces.org/mobile"
xmlns:ui="http://java.sun.com/jsf/facelets">
<!-- Vista de errores -->
<pm:header title="#{msg['page.titre']}" swatch="b">
<f:facet name="left">
<p:button icon="home" value=" " href="#vue1?reverse=true" />
</f:facet>
</pm:header>
<pm:content>
<div align="center">
<h1><h:outputText value="#{msg['erreur.titre']}" style="color: blue"/></h1>
</div>
<p:dataList value="#{form.erreurs}" var="erreur">
<b>#{erreur.classe}</b> : <i>#{erreur.message}</i>
</p:dataList>
</pm:content>
</html>
Utiliza una etiqueta <p:dataList> (líneas 21-23) para mostrar la lista de errores. El botón de la línea 13 permite regresar a la vista vista1.
![]() |
- el botón [1] se genera en la línea 13. El atributo icon establece el ícono del botón. El botón lleva de vuelta a la vista vue1 (atributo href),
- el encabezado [2] se genera mediante las líneas 11-15,
- el título [3] se genera en la línea 18,
- el texto [4] se genera a partir de la plantilla #{erreur.classe} de la línea 22,
- el texto [5] se genera a partir de la plantilla #{erreur.message} de la línea 22.
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 que se producen en ellas.
9.8. La visualización de la página de inicio
Si todo sale bien, la primera vista que se muestra es [vue1.xhtml]. Esto da como resultado la siguiente vista:
![]() |
El código de la vista [vue1.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:pm="http://primefaces.org/mobile"
xmlns:ui="http://java.sun.com/jsf/facelets">
<!-- Vista 1 -->
<pm:header title="#{msg['page.titre']}" swatch="b">
<f:facet name="left">
<p:button icon="gear" value=" " href="#config" />
</f:facet>
</pm:header>
<pm:content>
<h:form id="form1">
<div align="center">
<h1><h:outputText value="#{msg['form1.titre']}" style="color: blue"/></h1>
</div>
<pm:field>
<h:outputLabel value="#{msg['form1.medecin']}" for="choixMedecin"/>
<h:selectOneMenu id="choixMedecin" value="#{form.idMedecin}">
<f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>
</h:selectOneMenu>
</pm:field>
<pm:field>
<h:outputLabel value="#{msg['form1.jour']}" for="jour"/>
<p:inputText id="jour" value="#{form.strJour}"/>
<ui:fragment rendered="#{form.erreurDate}">
<p:spacer width="50px"/>
<h:outputText id="msgErreurDate" value="#{form.msgErreurDate}" style="color: red"/>
</ui:fragment>
</pm:field>
<p:commandButton value="#{msg['form1.agenda']}" update=":form1, :vue2, :vueErreurs" action="#{form.getAgenda}" />
</h:form>
</pm:content>
</html>
- líneas 11-15: generan el encabezado [1],
- línea 13: genera el botón [2]. Al hacer clic en él, se muestra la vista de configuración (atributo href),
- línea 19: genera el título [3],
- líneas 21-26: generan el menú desplegable de médicos [4],
- líneas 27-34: generan el campo de entrada de fecha [5]. Esta entrada se realiza con una etiqueta <p:inputText> sin validador. La validación de la fecha se realizará del lado del servidor. Si la fecha es incorrecta, este mostrará un mensaje de error que aparecerá en las líneas 30-34,
![]() |
- línea 35: el botón que valida el formulario. Actualiza tres campos: form1 (el formulario en vista1), vista2 y vueErreurs. De hecho, si la fecha no es válida, es form1 el que debe actualizarse. Si la fecha es correcta, es vista2 el que debe actualizarse. Por último, si se produce una excepción (por ejemplo, una conexión a la base de datos fallida), se debe mostrar vueErreurs. Podría ser tentador poner vista1 en lugar de form1 (se actualiza toda la vista). En ese caso, la aplicación presenta un error.
Esta vista se basa en el siguiente modelo:
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// Bean de aplicación
@Inject
private Application application;
private IMetierLocal metier;
// Caché de sesión
private List<Medecin> medecins;
private List<Client> clients;
// plantilla
private Long idMedecin;
private Date jour = new Date();
private String strJour;
private Boolean form1Rendered;
private Boolean form2Rendered;
private Boolean form3Rendered;
private Boolean erreurRendered;
private String msgErreurDate = "";
private Boolean erreurDate;
// lista de médicos
public List<Medecin> getMedecins() {
return medecins;
}
// agenda
public String getAgenda() {
...
}
}
- el campo de la línea 16 alimenta, tanto en lectura como en escritura, el valor de la lista de la línea 23 de la página. Al mostrarse la página por primera vez, establece el valor seleccionado en el combo,
- el método de las líneas 27 a 29 genera los elementos del menú desplegable de médicos (línea 24 de la vista). 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 18 alimenta, en lectura y escritura, el campo de entrada de la línea 29 de la página,
- líneas 32-34: el método getAgenda gestiona el clic en el botón [Agenda] de la línea 35 de la página. Su código es el siguiente:
// agenda
public String getAgenda() {
try {
// se verifica el día
jour = dateFormatter.parse(strJour);
// sin errores
erreurDate=false;
msgErreurDate = "";
// se crea la agenda
return getAgenda(jour);
} catch (ParseException ex) {
// mensaje de error
erreurDate=true;
msgErreurDate = Messages.getMessage(null, "form1.date.invalide", null).getSummary();
// vista1
setForms(true, false, false, false);
return "pm:vue1";
}
}
- el método comienza por verificar la validez de la fecha ingresada por el usuario,
- línea 5: la fecha es parsée según el formato de fecha inicializado por el método init al instanciar el modelo,
- línea 11: si se produce una excepción, se establece un error (línea 13), se genera un mensaje de error internacionalizado (línea 14), se prepara la vista vue1 (línea 16) y se muestra la vista vue1 (línea 17),
- línea 10: si la fecha es correcta, se ejecuta el siguiente método:
// agenda
public String getAgenda(Date jour) {
// por el momento no se ha seleccionado ningún horario
creneauChoisi = null;
try {
// se recupera el médico seleccionado
medecin = hMedecins.get(idMedecin);
// título del formulario 2
form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
// la agenda del médico para un día determinado
agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
// se muestra la vista 2
setForms(false, true, false, false);
return "pm:vue2";
} catch (Throwable th) {
System.out.println(th);
// vista de errores
prepareVueErreur(th);
return "pm:vueErreurs";
}
}
Aquí encontramos un código que ya hemos visto en varias ocasiones. En la línea 9, se genera un mensaje internacionalizado para la vista vue2:
form2.titre={0} {1} {2}<br/>{3}
form2.titre_detail={0} {1} {2}<br/>{3}
Cabe señalar que hemos incluido XHTML en el mensaje. Se mostrará de la siguiente manera:
![]() |
9.9. Mostrar la agenda de un médico
La agenda del médico se muestra mediante la vista [vue2.xhtml]:
![]() |
El código de la vista [vue2.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:pm="http://primefaces.org/mobile"
xmlns:ui="http://java.sun.com/jsf/facelets">
<!-- Vista 2 -->
<pm:header title="#{msg['page.titre']}" swatch="b"/>
<pm:content>
<h:form id="form2">
<div align="center">
<pm:buttonGroup orientation="horizontal">
<p:commandButton inline="true" icon="back" value=" " action="#{form.showVue1}" update=":vue1"/>
<p:commandButton inline="true" icon="minus" value=" " action="#{form.getPreviousAgenda}" update=":form2"/>
<p:commandButton inline="true" icon="home" value=" " action="#{form.getTodayAgenda}" update=":form2"/>
<p:commandButton inline="true" icon="plus" value=" " action="#{form.getNextAgenda}" update=":form2"/>
</pm:buttonGroup>
<h3><h:outputText value="#{form.form2Titre}" style="color: blue" escape="false"/></h3>
</div>
<p:dataList id="creneaux" type="inset" value="#{form.agendaMedecinJour.creneauxMedecinJour}" var="creneauMedecinJour">
<p:column>
<div align="center">
<h2>
<h:outputFormat value="{0,number,#00}h:{1,number,#00} - {2,number,#00}h:{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>
<ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
<br/>
<h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}" style="color: blue"/>
</ui:fragment>
</h2>
</div>
<div align="center">
<ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
<p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.supprimer']}" icon="minus" update=":form2, :vue3, :vueErreurs">
<f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
</p:commandButton>
</ui:fragment>
<ui:fragment rendered="#{creneauMedecinJour.rv==null}">
<p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.reserver']}" icon="plus" update=":form2, :vue3, :vueErreurs">
<f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
</p:commandButton>
</ui:fragment>
</div>
</p:column>
</p:dataList>
</h:form>
</pm:content>
</html>
- líneas 11: genera el encabezado [1],
- líneas 15-20: genera el grupo de botones [2],
- línea 21: genera el título [3]. Cabe destacar el valor del atributo «escape». Es lo que permite interpretar el código XHTML que hemos incluido en form2Titre,
- línea 24: la visualización de los horarios se realiza mediante un dataList,
- líneas 28-33: generan el título del intervalo horario [4],
- líneas 34-37: muestran un fragmento si hay una cita en ese horario,
- línea 36: muestra la identidad del cliente que programó la cita,
- líneas 41-45: muestran el botón [Supprimer] si hay una cita,
- líneas 46-50: muestran el botón [Réserver] si no hay ninguna cita.
Esta vista se alimenta principalmente del siguiente modelo:
private AgendaMedecinJour agendaMedecinJour;
que alimenta el campo dataList de la línea 24. Este campo se generó mediante el método getAgenda, al pasar de la vista vista1 a la vista vista2.
9.10. Eliminación de una cita
La eliminación de una cita corresponde a la siguiente secuencia:
![]() | ![]() |
La vista a la que se refiere esta acción es la siguiente:
<ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
<p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.supprimer']}" icon="minus" update=":form2, :vueErreurs">
<f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
</p:commandButton>
</ui:fragment>
- línea 2: el botón [Supprimer] está asociado al método [Form].action (atributo «action»),
- línea 3: el ID del intervalo en el que se encuentra el usuario se enviará al modelo [Form].idCreneauChoisi,
- línea 2: la llamada AJAX actualizará los campos de form2 (formulario de vista2) y la vista vueErreurs. De hecho, hay dos casos: si todo sale bien, se volverá a mostrar la vista vista2; de lo contrario, se mostrará la vista vueErreurs.
El método [action] es el siguiente:
// Acción en RV
public String action() {
// se busca el horario disponible en la agenda
int i = 0;
Boolean trouvé = false;
while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneauChoisi) {
trouvé = true;
} else {
i++;
}
}
// ¿Se encontró algo?
if (!trouvé) {
// Qué raro — se vuelve a mostrar el formulario 2
setForms(false, true, false, false);
return "pm:vue2";
} else {
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
}
// lo encontramos
// según la acción deseada
if (creneauChoisi.getRv() == null) {
return reserver();
} else {
return supprimer();
}
}
// reserva
public String reserver() {
...
}
public String supprimer() {
try {
// eliminación de una cita
metier.supprimerRv(creneauChoisi.getRv());
// se actualiza la agenda
agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
// se muestra el formulario 2
setForms(false, true, false, false);
return "pm:vue2";
} catch (Throwable th) {
// vista de errores
prepareVueErreur(th);
return "pm:vueErreurs";
}
}
- líneas 3-12: se busca el horario del que se recibió el ID (línea 7),
- si no se encuentra, lo cual es inusual, se vuelve a mostrar la vista2 (líneas 16-17),
- línea 19: si se encuentra, se almacena el objeto [CreneauMedecinJour] correspondiente. Este es el que nos da acceso a la cita que se va a eliminar,
- línea 26: lo eliminamos,
- líneas 35-48: el método «suprimir» devuelve la vista vue2 si la eliminación se realizó correctamente (líneas 42-43) o la vista vueErreurs si hubo algún problema (líneas 46-47).
9.11. Programación de citas
La creación de una cita sigue la siguiente secuencia:
![]() | ![]() |
Se pasa de la vista vue2 a la vista vue3. El código relacionado con esta acción es el siguiente:
<ui:fragment rendered="#{creneauMedecinJour.rv==null}">
<p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.reserver']}" icon="plus" update=":vue3, :vueErreurs">
<f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
</p:commandButton>
</ui:fragment>
- línea 2: el botón [Réserver] está asociado al método [Form].action (atributo action), es decir, el mismo que para el botón [Supprimer]. La llamada AJAX actualiza las vistas vue3 y vueErreurs dependiendo de si se producen o no errores durante el procesamiento de la llamada.
- línea 3: al igual que en el botón [Supprimer], el identificador del horario se pasa a la plantilla.
La plantilla que procesa esta acción es la siguiente:
// acción en RV
public String action() {
...
// según la acción deseada
if (creneauChoisi.getRv() == null) {
return reserver();
} else {
return supprimer();
}
}
// reserva
public String reserver() {
try {
// título del formulario 3
form3Titre = Messages.getMessage(null, "form3.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour),
creneauChoisi.getCreneau().getHdebut(), creneauChoisi.getCreneau().getMdebut(), creneauChoisi.getCreneau().getHfin(), creneauChoisi.getCreneau().getMfin()}).getSummary();
// se muestra el formulario 3
setForms(false, false, true, false);
return "pm:vue3";
} catch (Throwable th) {
// vista de errores
prepareVueErreur(th);
return "pm:vueErreurs";
}
}
- líneas 2-10: el método action determinará la referencia creneauChoisi del objeto [CreneauMedecinJour] que es objeto de una reservación y luego llamará al método reserver,
- línea 16: se genera un mensaje internacionalizado. Es el siguiente:
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}
Este será el título de la vista vue3. Al igual que en la vista vue2, este título incluye XML. También incluye parámetros formateados para mostrar los horarios del intervalo,
![]() |
- líneas 19-20: se muestra la vista vue3,
- líneas 23-24: se muestra la vista vueErreurs si se han detectado problemas.
9.12. Validación de una cita
La validación de una cita sigue la siguiente secuencia:
![]() |
En [1], la vista vue3, y en [2], la vista vue2 después de agregar una cita.
El código [vue3.xhtml] de vue3 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:pm="http://primefaces.org/mobile"
xmlns:ui="http://java.sun.com/jsf/facelets">
<!-- Vista 3 -->
<pm:header title="#{msg['page.titre']}" swatch="b"/>
<pm:content>
<h:form id="form3">
<p:commandButton inline="true" value=" " icon="back" action="#{form.showVue2}" update=":vue2"/>
<div align="center">
<h3><h:outputText value="#{form.form3Titre}" style="color: blue" escape="false"/></h3>
</div>
<pm:field>
<h:outputLabel value="#{msg['form3.client']}" for="choixClient"/>
<h:selectOneMenu id="choixClient" value="#{form.idClient}">
<f:selectItems value="#{form.clients}" var="client" itemLabel="#{client.titre} #{client.prenom} #{client.nom}" itemValue="#{client.id}"/>
</h:selectOneMenu>
</pm:field>
<div align="center">
<p:commandButton inline="true" value="#{msg['form3.valider']}" action="#{form.validerRv}" update=":vue2, :vueErreurs" icon="check"/>
</div>
</h:form>
</pm:content>
</html>
- línea 16: genera el título de la vista [3]. Cabe destacar el valor del atributo «escape», que permite la interpretación de los caracteres XHTML en el título,
- líneas 18-23: generan el menú desplegable de clientes [4],
- línea 25: genera el botón [Valider] [5]. El método [Form].validerRv está asociado a este botón:
// validación de cita
public String validerRv() {
try {
// se recupera una instancia del horario seleccionado
Creneau creneau = metier.getCreneauById(idCreneauChoisi);
// se agrega la cita
metier.ajouterRv(jour, creneau, hClients.get(idClient));
// se actualiza la agenda
agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
// se muestra form2
setForms(false, true, false, false);
return "pm:vue2";
} catch (Throwable th) {
// vista de errores
prepareVueErreur(th);
return "pm:vueErreurs";
}
}
Este código ya apareció en la versión 01. Solo cabe destacar la visualización de las vistas:
- la vista vue2 (líneas 11-12) si todo salió bien,
- la vista vueErreurs (líneas 15-16) en caso contrario.
9.13. Cancelación de una cita
Esto corresponde a la siguiente secuencia:
![]() |
El botón [1] en la vista [vue3.xhtml] es el siguiente:
<p:commandButton inline="true" value=" " icon="back" action="#{form.showVue2}" update=":vue2"/>
Por lo tanto, se invoca el método [Form].showVue2. Este método simplemente muestra la vista 2:
public String showVue2() {
// vista2
setForms(false, true, false, false);
return "pm:vue2?reverse=true";
}
9.14. Navegación en el calendario
En vista2, hay botones que permiten navegar por el calendario:
Día anterior:
![]() |
Día siguiente:
![]() |
Hoy:
![]() |
Aunque no se muestra en las capturas de pantalla anteriores, la agenda se actualiza y muestra las citas del nuevo día seleccionado.
Las etiquetas de los tres botones en cuestión son las siguientes en [vue2.xhtml]:
<pm:buttonGroup orientation="horizontal">
<p:commandButton inline="true" icon="back" value=" " action="#{form.showVue1}" update=":vue1"/>
<p:commandButton inline="true" icon="minus" value=" " action="#{form.getPreviousAgenda}" update=":form2"/>
<p:commandButton inline="true" icon="home" value=" " action="#{form.getTodayAgenda}" update=":form2"/>
<p:commandButton inline="true" icon="plus" value=" " action="#{form.getNextAgenda}" update=":form2"/>
</pm:buttonGroup>
Los métodos [Form].getPreviousAgenda, [Form].getNextAgenda, [Form].today se analizaron en el ejemplo 03.
9.15. Cambio del idioma de visualización
El cambio de idioma se controla mediante un botón en la página de inicio:
![]() |
El código del botón es el siguiente:
<p:button icon="gear" value=" " href="#config" />
Por lo tanto, cambia a la vista de configuración [2]. La vista [config.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:pm="http://primefaces.org/mobile"
xmlns:ui="http://java.sun.com/jsf/facelets">
<!-- Vista 1 -->
<pm:header title="#{msg['page.titre']}" swatch="b">
<f:facet name="left">
<p:button icon="back" value=" " href="#vue1?reverse=true" />
</f:facet>
</pm:header>
<pm:content>
<h:form id="frmConfig">
<div align="center">
<h3><h:outputText value="#{msg['config.titre']}" style="color: blue"/></h3>
</div>
<pm:field>
<h:outputLabel value="#{msg['config.langue']}" for="langue"/>
<h:selectOneRadio id="langue" value="#{form.locale}">
<f:selectItem itemLabel="#{msg['config.langue.francais']}" itemValue="fr"/>
<f:selectItem itemLabel="#{msg['config.langue.anglais']}" itemValue="en" />
</h:selectOneRadio>
</pm:field>
<p:commandButton value="#{msg['config.valider']}" action="#{form.configurer}" update=":vue1"/>
</h:form>
</pm:content>
</html>
- línea 11: muestra [3],
- línea 13: muestra el botón [4]. Este botón permite regresar a la vista vista1,
- línea 17: el formulario de la vista,
- línea 19: muestra el título de la vista [5],
- líneas 21-27: muestran los botones de opción. El valor (itemValue) del botón de opción seleccionado se enviará al modelo [Form].locale (atributo «value» de la línea 23),
- línea 28: muestra el botón [Valider]. La llamada a AJAX actualiza la vista vue1 (atributo update). El método llamado es [Form].configurer:
public String configurer(){
// después de la configuración: se vuelve a mostrar la vista 1
redirect();
return null;
}
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);
}
}
El método «configurar» (línea 1) simplemente redirige el navegador del celular a la página URL de la aplicación. Por lo tanto, se cargará la página [index.xhtml]:
<f:view xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:pm="http://primefaces.org/mobile"
contentType="text/html"
locale="#{form.locale}">
<pm:page title="#{msg['page.titre']}">
<pm:view id="vue1">
...
</pm:view>
...
</pm:page>
</f:view>
- línea 8: la vista utilizará el idioma que acaba de cambiarse (atributo «locale») y mostrará la vista 1 (línea 11).
9.16. Conclusion
Recordemos la arquitectura de la aplicación que acabamos de construir:
![]() |
El cambio a una interfaz para dispositivos móviles requirió reescribir las páginas XHTML. El modelo, por el contrario, apenas ha cambiado. Las capas inferiores [métier], [DAO] y [JPA], por su parte, no han cambiado en absoluto.
9.17. Pruebas en Eclipse
Al igual que hicimos con las versiones anteriores de la aplicación de ejemplo, mostramos cómo probar esta versión 05 con Eclipse. En primer lugar, importamos a Eclipse los proyectos Maven del ejemplo 05 [1]:
![]() |
- [mv-rdvmedecins-ejb-dao-jpa]: las capas [DAO] y [JPA],
- [mv-rdvmedecins-ejb-metier]: la capa [métier],
- [mv-rdvmedecins-pfmobile]: la capa [web] implementada por PrimeFaces Mobile,
- [mv-rdvmedecins-pfmobile-app]: el proyecto padre del proyecto empresarial [mv-rdvmedecins-pfmobile-app-ear]. Al importar el proyecto padre, el proyecto hijo se importa automáticamente,
- en [2], se ejecuta el proyecto empresarial [mv-rdvmedecins-pfmobile-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 abrirla en un navegador o en un simulador de celular:
![]() |




































