3. Aplicación de ejemplo – 01: rdvmedecins-jsf2-ejb
El texto que sigue hace referencia, , a los siguientes documentos:
- [ref7]: Introducción a Java EE 5 (junio de 2010) [http://tahe.developpez.com/java/javaee]. Este documento permite descubrir JSF 1 y los EJB3.
- [ref8]: Persistencia en Java a través de la práctica (junio de 2007) [http://tahe.developpez.com/java/jpa]. Este documento permite descubrir la persistencia de datos con JPA (Java Persistence API).
- [ref9]: Creación de un servicio web Java EE con NetBeans y el servidor GlassFish (enero de 2009) [http://tahe.developpez.com/java/webservice-jee]. Este documento analiza la creación de un servicio web.
La aplicación de ejemplo que se va a estudiar procede de [ref9].
3.1. L'application
Una empresa de servicios informáticos, [ISTIA-AGI], desea ofrecer un servicio de gestión de citas. El primer mercado al que se dirige es el de los médicos que ejercen por cuenta propia. Estos, por lo general, no cuentan con personal de secretaría. Los clientes que desean concertar una cita llaman directamente por teléfono al médico. Esto supone una interrupción frecuente a lo largo del día, lo que reduce su disponibilidad para atender a sus pacientes. La empresa [ISTIA-AGI] desea ofrecerles un servicio de gestión de citas que funcione según el siguiente principio:
- una secretaría se encarga de gestionar las citas de un gran número de médicos. Esta secretaría puede reducirse a una sola persona. Su salario se reparte entre todos los médicos que utilizan el servicio.
- La secretaría y todos los médicos están conectados a Internet
- las citas se registran en una base de datos centralizada, accesible a través de Internet tanto para la secretaría como para los médicos
- La asignación de RV la realiza normalmente la secretaría. También pueden hacerlo los propios médicos. Este es el caso, en particular, cuando, al final de una consulta, el propio médico asigna un nuevo RV a su paciente.
La arquitectura del servicio de asignación de RV es la siguiente:
![]() |
Los médicos ganan en eficiencia al no tener que gestionar ya los RV. Si son en número suficiente, su contribución a los gastos de funcionamiento de la secretaría será mínima.
La empresa [ISTIA-AGI] decide desarrollar la aplicación en dos versiones:
- una versión JSF / EJB3 / JPA EclipseLink / servidor Glassfish:
![]() |
- y otra versión JSF / Spring / JPA Hibernate / servidor Tomcat:
![]() |
3.2. Funcionamiento de la aplicación
Denominaremos a la aplicación [RdvMedecins]. A continuación, presentamos unas capturas de pantalla de su funcionamiento.
La página de inicio de la aplicación es la siguiente:
![]() |
Desde esta primera página, el usuario (secretaría, médico) realizará una serie de acciones. A continuación las presentamos. La vista de la izquierda muestra la pantalla desde la que el usuario realiza una solicitud; la de la derecha, la respuesta enviada por el servidor.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Por último, también puede aparecer una página de errores:
![]() |
3.3. La base de datos
Volvamos a la arquitectura de la aplicación que vamos a desarrollar:
![]() |
La base de datos, a la que llamaremos [dbrdvmedecins2] , es una base de datos MySQL5 con cuatro tablas:
![]() |
3.3.1. La tabla [MEDECINS]
Contiene información sobre los médicos gestionados por la aplicación [RdvMedecins].
![]() | ![]() |
- ID: número que identifica al médico —clave primaria de la tabla
- VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la fila.
- NOM: el apellido del médico
- PRENOM: su nombre
- TITRE: su tratamiento (Srta., Sra., Sr.)
3.3.2. La tabla [CLIENTS]
Los pacientes de los distintos médicos se registran en la tabla [CLIENTS]:
![]() | ![]() |
- ID: número de identificación del cliente —clave primaria de la tabla
- VERSION: número que identifica la versión de la línea en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la línea.
- NOM: el nombre del cliente
- PRENOM: su nombre
- TITRE: su tratamiento (Srta., Sra., Sr.)
3.3.3. La tabla [CRENEAUX]
En ella se enumeran las franjas horarias en las que son posibles los RV:
![]() |
![]() |
- ID: número que identifica la franja horaria —clave primaria de la tabla (línea 8)
- VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la fila.
- ID_MEDECIN: número que identifica al médico al que pertenece este horario – clave externa en la columna MEDECINS (ID).
- HDEBUT: hora de inicio de la franja horaria
- MDEBUT: minutos de inicio de la franja horaria
- HFIN: hora de finalización de la franja horaria
- MFIN: minutos de fin de franja
La segunda línea de la tabla [CRENEAUX] (véase [1] más arriba) indica, por ejemplo, que el turno n.º 2 comienza a las 8:20 y termina a las 8:40, y corresponde a la doctora n.º 1 (la Sra. Marie PELISSIER).
3.3.4. La tabla [RV]
Enumera los RV asignados a cada médico:
![]() |
- ID: número que identifica de forma única el RV – clave primaria
- JOUR: día del RV
- ID_CRENEAU: franja horaria del RV —clave externa en el campo [ID] de la tabla [CRENEAUX]—; determina tanto la franja horaria como el médico correspondiente.
- ID_CLIENT: número del cliente para el que se realiza la reserva – clave externa en el campo [ID] de la tabla [CLIENTS]
Esta tabla tiene una restricción de unicidad de « » sobre los valores de las columnas unidas (JOUR, ID_CRENEAU):
Si una fila de la tabla [RV] tiene el valor (JOUR1, ID_CRENEAU1) en las columnas (JOUR, ID_CRENEAU), dicho valor no puede aparecer en ningún otro sitio. De lo contrario, significaría que se han registrado dos RV al mismo tiempo para el mismo médico. Desde el punto de vista de la programación en Java, el controlador JDBC de la base de datos lanza un SQLException cuando se produce este caso.
La línea de id igual a 3 (véase [1] más arriba) significa que se ha reservado un RV para la franja horaria n.º 20 y el cliente n.º 4 el 23/08/2006. La tabla [CRENEAUX] nos indica que la franja n.º 20 corresponde al horario de 16:20 a 16:40 y pertenece a la médica n.º 1 (la Sra. Marie PELISSIER). La tabla [CLIENTS] nos indica que el cliente n.º 4 es la Srta. Brigitte BISTROU.
3.3.5. Creación de la base de datos
Para crear las tablas y rellenarlas, se puede utilizar el script [dbrdvmedecins2.sql], que se encuentra en la página web de ejemplos. Con [WampServer] (véase el apartado 1.3.3), se puede proceder de la siguiente manera:
![]() |
- en [1], se hace clic en el icono de [WampServer] y se selecciona la opción [PhpMyAdmin] [2],
- en [3], en la ventana que se ha abierto, selecciona el enlace [Bases de données],
![]() |
- a [2], se crea una base de datos a la que se le ha asignado el nombre [4] y la codificación [5],
- en [7], la base de datos ya se ha creado. Hacemos clic en su enlace,
![]() |
- en [8], se importa un archivo SQL,
- que se selecciona en el sistema de archivos con el botón [9],
![]() |
- en [11], se selecciona el script SQL y en [12] se ejecuta,
- en [13], se han creado las cuatro tablas de la base de datos. Seguimos uno de los enlaces,
![]() |
- en [14], el contenido de la tabla.
A partir de ahora, no volveremos a referirnos a esta base de datos. No obstante, invitamos al lector a seguir su evolución a lo largo de los programas, sobre todo cuando no funcione.
3.4. Las capas [DAO] y [JPA]
Volvamos a la arquitectura que debemos construir:
![]() |
Vamos a crear cuatro proyectos Maven:
- un proyecto para las capas [DAO] y [JPA],
- un proyecto para la capa [métier],
- un proyecto para la capa [web],
- un proyecto empresarial que agrupará los tres proyectos anteriores.
Ahora vamos a compilar el proyecto Maven de las capas [DAO] y [JPA].
Nota: para comprender las capas [métier], [DAO] y [JPA] es necesario tener conocimientos de Java EE. Para ello, se puede consultar [ref7] (véase el apartado 3).
3.4.1. El proyecto NetBeans
Es el siguiente:
![]() |
- en [1], se crea un proyecto Maven de tipo [EJB Module] [2],
- en [3], se le da un nombre al proyecto,
![]() |
- en [4], se elige como servidor el servidor Glassfish,
- en [5], el proyecto generado.
3.4.2. Generación de la capa [JPA]
Volvamos a la arquitectura que debemos construir:
![]() |
Con NetBeans, es posible generar automáticamente la capa [JPA] y la capa [EJB], que controla el acceso a las entidades JPA generadas. Es interesante conocer estos métodos de generación automática, ya que el código generado ofrece valiosas indicaciones sobre cómo escribir entidades JPA o el código EJB que las utiliza.
A continuación describimos algunas de estas herramientas de generación automática. Para comprender el código generado, es necesario tener buenos conocimientos sobre las entidades JPA, [ref8] y las EJB, [ref7] (véase el apartado 3).
3.4.2.1. Creación de una conexión de NetBeans a la base de datos
- ejecuta SGBD y MySQL 5 para que BD esté disponible,
- crear una conexión de NetBeans a la base de datos [dbrdvmedecins2],
![]() |
- en la pestaña [Services] [1], en la rama [Databases] [2], seleccionar el controlador JDBC MySQL [3],
- y, a continuación, seleccione la opción [4] «Connect Using», que permite crear una conexión con una base de datos MySQL,
- en [5], introduce la información que se te solicita. En [6], el nombre de la base de datos; en [7], el usuario de la base de datos y su contraseña;
- en [8], se puede comprobar la información introducida,
- en [9], el mensaje que se espera recibir cuando los datos son correctos,
![]() |
- en [10], se establece la conexión. Aquí se pueden ver las cuatro tablas de la base de datos conectada.
3.4.2.2. Creación de una unidad de persistencia
Volvamos a la arquitectura que estamos construyendo:
![]() |
Estamos construyendo la capa [JPA]. Su configuración se realiza en un archivo [persistence.xml] en el que se definen las unidades de persistencia. Cada una de ellas necesita la siguiente información:
- las características JDBC de acceso a la base de datos (URL, usuario, contraseña),
- las clases que servirán de representaciones de las tablas de la base de datos,
- la implementación JPA utilizada. De hecho, JPA es una especificación implementada por diversos productos. En este caso, utilizaremos EclipseLink, que es la implementación predeterminada utilizada por el servidor GlassFish. Esto nos evita tener que añadir a GlassFish las bibliotecas de otra implementación.
NetBeans puede generar este archivo de persistencia mediante un asistente.
![]() |
- haz clic con el botón derecho del ratón sobre el proyecto y selecciona la opción para crear una unidad de persistencia [1],
- en [2], asignar un nombre a la unidad de persistencia que se está creando,
- en [3], seleccionar la implementación JPA EclipseLink (JPA 2.0),
- en [4], indicar que las transacciones con la base de datos serán gestionadas por el contenedor EJB del servidor Glassfish,
- en [5], indicar que las tablas de BD ya están creadas y que, por lo tanto, no se crearán,
![]() |
- en [6], crear una nueva fuente de datos para el servidor Glassfish,
- en [7], asignar un nombre JNDI (Java Naming Directory Interface),
- en [8], vincular este nombre a la conexión MySQL creada en el paso anterior,
![]() |
- en [9], finalizar el asistente,
- en [10], el nuevo proyecto,
- en [11], se ha generado el archivo [persistence.xml] en la carpeta [META-INF],
- en [12], se ha generado una carpeta [setup],
- en [13], se han añadido nuevas dependencias al proyecto Maven.
El archivo [META-INF/persistence.xml] generado es el siguiente:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
<jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties/>
</persistence-unit>
</persistence>
Recoge la información introducida en el asistente:
- línea 3: el nombre de la unidad de persistencia,
- línea 3: el tipo de transacciones con la base de datos; en este caso, transacciones JTA (Java Transaction API) gestionadas por el contenedor EJB3 del servidor GlassFish,
- línea 4: el nombre JNDI de la fuente de datos.
Normalmente, en este archivo aparece el tipo de implementación JPA que se utiliza. En el asistente, hemos indicado EclipseLink. Dado que se trata de la implementación JPA que utiliza por defecto el servidor Glassfish, no aparece mencionada en el archivo [persistence.xml].
En la pestaña [Design], se puede obtener una visión general del archivo [persistence.xml]:
![]() |
Para obtener los registros de EclipseLink, utilizaremos el siguiente archivo [persistence.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="eclipselink.logging.level" value="FINE"/>
</properties>
</persistence-unit>
</persistence>
- línea 4: se indica que se utiliza la implementación JPA de EclipseLink,
- líneas 7-9: recogen las propiedades de configuración del proveedor JPA, en este caso EclipseLink,
- línea 8: esta propiedad permite registrar las órdenes SQL que emitirá EclipseLink.
El archivo [glassfish-resources.xml] que se ha creado es el siguiente:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
<jdbc-connection-pool allow-non-component-callers="false" ... steady-pool-size="8" validate-atmost-once-period-in-seconds="0" wrap-jdbc-objects="false">
<property name="serverName" value="localhost"/>
<property name="portNumber" value="3306"/>
<property name="databaseName" value="dbrdvmedecins2"/>
<property name="User" value="root"/>
<property name="Password" value=""/>
<property name="URL" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
</jdbc-connection-pool>
<jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins2" object-type="user" pool-name="mysql_dbrdvmedecins2_rootPool"/>
</resources>
Este archivo recoge la información que hemos introducido en los dos asistentes utilizados anteriormente:
- líneas 5-11: las características JDBC de la base de datos MySQL5 [dbrdvmedecins2],
- línea 13: el nombre JNDI de la fuente de datos.
Este archivo se utilizará para crear la fuente de datos JNDI [jdbc/dbrdvmedecins2] del servidor Glassfish. Es específico de este servidor. Para otro servidor, habría que proceder de otra manera, normalmente mediante una herramienta de administración. Esta herramienta también existe para Glassfish.
Por último, se han añadido dependencias al proyecto. El archivo [pom.xml] es el siguiente:
<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-ejb-dao-jpa</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>ejb</packaging>
<name>mv-rdvmedecins-ejb-dao-jpa</name>
...
<dependencies>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>javax.persistence</artifactId>
<version>2.0.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>6.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
...
<repositories>
<repository>
<url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
<id>eclipselink</id>
<layout>default</layout>
<name>Repository for library Library[eclipselink]</name>
</repository>
</repositories>
</project>
- líneas 32-37: una capa [JPA] requiere el artefacto [javaee-api];
- líneas 16, 22 y 28: los artefactos requeridos por la implementación JPA / EclipseLink utilizada aquí.
- líneas 18, 24, 30 y 36: todos los artefactos tienen el atributo provided. Cabe recordar que esto significa que son necesarios para la compilación, pero no para la ejecución. De hecho, durante la ejecución, los proporciona (provided) el servidor Glassfish,
- líneas 41-48: definen un nuevo repositorio de artefactos de Maven, en el que se pueden encontrar los artefactos EclipseLink.
3.4.2.3. Generación de las entidades JPA
Las entidades JPA se pueden generar mediante un asistente de NetBeans:
![]() |
- en [1], se crean entidades JPA a partir de una base de datos,
- en [2], se selecciona la fuente de datos [jdbc / dbrdvmedecins2] creada anteriormente,
- en [3], la lista de tablas de esta fuente de datos,
- en [4], se seleccionan todas,
![]() |
- en [5], las tablas seleccionadas,
- en [6], se asigna un nombre a las clases Java asociadas a las cuatro tablas,
- así como un nombre de paquete [7],
- en [8], JPA agrupa las filas de las tablas de BD en colecciones. Elegimos la lista como colección,
![]() |
- en [9], las clases Java creadas por el asistente.
3.4.2.4. Las entidades JPA generadas
La entidad [Medecin] es la imagen de la tabla [medecins]. La clase Java está repleta de anotaciones que hacen que el código resulte poco legible a primera vista. Si nos quedamos solo con lo esencial para comprender la función de la entidad, obtenemos el siguiente código:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "medecins")
public class Medecin implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
// constructores
....
// getters y setters
....
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- En la línea 4, la anotación @Entity convierte la clase [Medecin] en una entidad JPA, c.a.d. una clase vinculada a una tabla de BD a través de API y JPA,
- línea 5, el nombre de la tabla BD asociada a la entidad JPA. Cada campo de la tabla se corresponde con un campo de la clase Java,
- línea 6, la clase implementa la interfaz Serializable. Esto es necesario en aplicaciones cliente/servidor, donde las entidades se serializan entre el cliente y el servidor.
- líneas 10-11: el campo id de la clase [Medecin] se corresponde con el campo [ID] (línea 10) de la tabla [medecins],
- líneas 13-14: el campo «título» de la clase [Medecin] se corresponde con el campo [TITRE] (línea 13) de la tabla [medecins],
- líneas 16-17: el campo «nombre» de la clase [Medecin] se corresponde con el campo [NOM] (línea 16) de la tabla [medecins],
- líneas 19-20: el campo «versión» de la clase [Medecin] se corresponde con el campo [VERSION] (línea 19) de la tabla [medecins]. En este caso, el asistente no reconoce que la columna es, en realidad, una columna de versión que debe incrementarse cada vez que se modifica la línea a la que pertenece. Para asignarle esta función, hay que añadir la anotación @Version. Lo haremos en un paso posterior,
- líneas 22-23: el campo «prenom» de la clase [Medecin] se corresponde con el campo [PRENOM] de la tabla [medecins],
- líneas 10-11: el campo «id» corresponde a la clave primaria [ID] de la tabla. Las anotaciones de las líneas 8-9 precisan este punto,
- línea 8: la anotación @Id indica que el campo anotado está asociado a la clave primaria de la tabla,
- Línea 9: la capa [JPA] generará la clave primaria de las líneas que insertará en la tabla [Medecins]. Hay varias estrategias posibles. En este caso, la estrategia GenerationType.IDENTITY indica que la capa JPA utilizará el modo auto_increment de la tabla MySQL,
- líneas 25-26: la tabla [creneaux] tiene una clave foránea sobre la tabla [medecins]. Una franja horaria pertenece a un médico. A la inversa, un médico tiene varias franjas horarias asociadas a él. Por lo tanto, tenemos una relación de uno (médico) a varios (franjas horarias), una relación calificada por la anotación @OneToMany por JPA (línea 25). El campo de la línea 26 contendrá todas las franjas horarias del médico. Esto sin necesidad de programación. Para comprender plenamente la línea 25, debemos presentar la clase [Creneau].
Esta es la siguiente:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "MDEBUT")
private int mdebut;
@Column(name = "HFIN")
private int hfin;
@Column(name = "HDEBUT")
private int hdebut;
@Column(name = "MFIN")
private int mfin;
@Column(name = "VERSION")
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
private List<Rv> rvList;
// constructores
...
// getter y setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
Solo comentaremos las nuevas anotaciones:
- ya hemos dicho que la tabla [creneaux] tiene una clave foránea hacia la tabla [medecins]: cada franja horaria está asociada a un médico. Se pueden asociar varias franjas horarias al mismo médico. Existe una relación de la tabla [creneaux] a la tabla [medecins] que se califica como de varios (franjas horarias) a uno (médico). La anotación @ManyToOne de la línea 32 sirve para definir la clave foránea,
- la línea 31, con la anotación @JoinColumn, especifica la relación de clave externa: la columna [ID_MEDECIN] de la tabla [creneaux] es una clave externa sobre la columna [ID] de la tabla [medecins],
- línea 33: una referencia al médico titular de la franja horaria. De nuevo, se obtiene sin necesidad de programación.
La relación de clave externa entre la entidad [Creneau] y la entidad [Medecin] se materializa, por tanto, mediante dos anotaciones:
- en la entidad [Creneau]:
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
- en la entidad [Medecin]:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
Ambas anotaciones reflejan la misma relación: la de la clave foránea de la tabla [creneaux] hacia la tabla [medecins]. Se dice que son inversas entre sí. Solo la relación @ManyToOne es imprescindible. Esta califica sin ambigüedad la relación de clave externa. La relación @OneToMany es opcional. Si está presente, se limita a hacer referencia a la relación @ManyToOne a la que está asociada. Este es el significado del atributo mappedBy de la línea 1 de la entidad [Medecin]. El valor de este atributo es el nombre del campo de la entidad [Creneau] que tiene la anotación @ManyToOne, la cual especifica la clave foránea. También en esta misma línea 1 de la entidad [Medecin], el atributo cascade=CascadeType.ALL establece el comportamiento de la entidad [Medecin] con respecto a la entidad [Creneau]:
- si se inserta una nueva entidad [Medecin] en la base de datos, entonces también deben insertarse las entidades [Creneau] del campo de la línea 2;
- si se modifica una entidad [Medecin] en la base de datos, entonces las entidades [Creneau] del campo de la línea 2 también deben modificarse,
- si se elimina una entidad [Medecin] de la base de datos, entonces también deben eliminarse las entidades [Creneau] del campo de la línea 2.
Proporcionamos el código de las otras dos entidades sin comentarios específicos, ya que no introducen nuevas notaciones.
La entidad [Client]
package rdvmedecins.jpa;
...
@Entity
@Table(name = "clients")
public class Client implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idClient")
private List<Rv> rvList;
// constructores
...
// getters y setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- Las líneas 24-25 reflejan la relación de clave externa entre la tabla [rv] y la tabla [clients].
La entidad [Rv]:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau idCreneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client idClient;
// constructores
...
// getters y setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- la línea 13 describe el campo «día» de tipo Java Date. Se indica que, en la tabla [rv], la columna [JOUR] (línea 12) es de tipo fecha (sin hora),
- líneas 16-18: definen la relación de clave externa que tiene la tabla [rv] con la tabla [creneaux],
- líneas 20-22: definen la relación de clave externa que tiene la tabla [rv] con la tabla [clients].
La generación automática de las entidades JPA nos permite obtener una base de trabajo. A veces es suficiente, otras veces no. Este es el caso aquí:
- hay que añadir la anotación @Version a los distintos campos de versión de las entidades,
- hay que escribir métodos toString más explícitos que los generados,
- las entidades [Medecin] y [Client] son análogas. Las derivaremos de una clase [Personne],
- vamos a eliminar las relaciones @OneToMany inversas a las relaciones @ManyToOne. No son imprescindibles y complican la programación,
- eliminamos la validación @NotNull sobre las claves primarias. Cuando se persiste una entidad JPA con MySQL, la entidad inicial tiene una clave primaria null. Solo tras la persistencia en la base de datos, la clave primaria del elemento persistido tiene un valor.
Con estas especificaciones, las diferentes clases quedan así:
La clase «Persona» se utiliza para representar a los médicos y a los clientes:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@MappedSuperclass
public class Personne implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@Size(min = 1, max = 5)
@Column(name = "TITRE")
private String titre;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 30)
@Column(name = "NOM")
private String nom;
@Basic(optional = false)
@NotNull
@Column(name = "VERSION")
@Version
private int version;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 30)
@Column(name = "PRENOM")
private String prenom;
// constructores
...
// getters y setters
...
@Override
public String toString() {
return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
}
}
- línea 8: cabe señalar que la clase [Personne] no es en sí misma una entidad (@Entity). Será la clase padre de las entidades. La anotación @MappedSuperClass indica esta situación.
La entidad [Client] encapsula las filas de la tabla [clients]. Deriva de la clase anterior [Personne]:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// constructores
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
- línea 6: la clase [Client] es una entidad JPA,
- línea 7: está asociada a la tabla [clients],
- línea 8: deriva de la clase [Personne].
La entidad [Medecin], que encapsula las filas de la tabla [medecins], sigue el mismo patrón:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// constructores
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Médecin[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
La entidad [Creneau] encapsula las líneas de la tabla [creneaux]:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@NotNull
@Column(name = "MDEBUT")
private int mdebut;
@Basic(optional = false)
@NotNull
@Column(name = "HFIN")
private int hfin;
@Basic(optional = false)
@NotNull
@Column(name = "HDEBUT")
private int hdebut;
@Basic(optional = false)
@NotNull
@Column(name = "MFIN")
private int mfin;
@Basic(optional = false)
@NotNull
@Column(name = "VERSION")
@Version
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin medecin;
// constructores
...
// métodos getter y setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
// TODO: Advertencia: este método no funcionará si los campos de ID no están definidos
...
}
@Override
public String toString() {
return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
}
}
- Las líneas 45-47 modelan la relación «muchos a uno» que existe entre la tabla [creneaux] y la tabla [medecins] de la base de datos: un médico tiene varias franjas horarias, y una franja horaria pertenece a un solo médico.
La entidad [Rv] encapsula las líneas de la tabla [rv]:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@NotNull
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau creneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client client;
// constructores
...
// métodos getter y setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Rv[%s, %s, %s]", id, creneau, client);
}
}
- las líneas 29-31 modelan la relación «muchos a uno» que existe entre la tabla [rv] y la tabla [clients] (un cliente puede aparecer en varias citas) de la base de datos, y las líneas 25-27, la relación «muchos a uno» que existe entre la tabla [rv] y la tabla [creneaux] (una franja horaria puede aparecer en varias citas).
3.4.3. La clase de excepción
![]() |
La clase de excepción [RdvMedecinsException] de la aplicación es la siguiente:
package rdvmedecins.exceptions;
import java.io.Serializable;
import javax.ejb.ApplicationException;
@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException implements Serializable{
// campos privados
private int code = 0;
// constructores
public RdvMedecinsException() {
super();
}
public RdvMedecinsException(String message) {
super(message);
}
public RdvMedecinsException(String message, Throwable cause) {
super(message, cause);
}
public RdvMedecinsException(Throwable cause) {
super(cause);
}
public RdvMedecinsException(String message, int code) {
super(message);
setCode(code);
}
public RdvMedecinsException(Throwable cause, int code) {
super(cause);
setCode(code);
}
public RdvMedecinsException(String message, Throwable cause, int code) {
super(message, cause);
setCode(code);
}
// getter y setter
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- línea 7: la clase deriva de la clase [RuntimeException]. Por lo tanto, el compilador no obliga a gestionarla con try / catch.
- Línea 6: la anotación @ApplicationException hace que la excepción no sea «absorbida» por una excepción de tipo [EjbException].
Para entender la anotación @ApplicationException, volvamos a la arquitectura utilizada en el lado del servidor:
![]() |
La excepción de tipo [RdvMedecinsException] será lanzada por los métodos de EJB de la capa [DAO] dentro del contenedor EJB3 y será interceptada por este. Sin la anotación @ApplicationException, el contenedor EJB3 encapsula la excepción que se ha producido en una excepción de tipo [EjbException] y la vuelve a lanzar. Es posible que no se desee esta encapsulación y que se quiera permitir que salga del contenedor EJB3 una excepción de tipo [RdvMedecinsException]. Esto es lo que permite la anotación @ApplicationException. Por otra parte, el atributo (rollback=true) de esta anotación indica al contenedor EJB3 que, si se produce una excepción de tipo [RdvMedecinsException] dentro de un método ejecutado en una transacción con un SGBD, esta debe anularse. En términos técnicos, esto se denomina realizar un rollback de la transacción.
3.4.4. El EJB de la capa [DAO]
![]() |
![]() |
La interfaz Java [IDao] de la capa [DAO] es la siguiente:
package rdvmedecins.dao;
import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
public interface IDao {
// lista de clientes
public List<Client> getAllClients();
// lista de médicos
public List<Medecin> getAllMedecins();
// lista de franjas horarias de un médico
public List<Creneau> getAllCreneaux(Medecin medecin);
// lista de citas de un médico en un día determinado
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// buscar un cliente identificado por su ID
public Client getClientById(Long id);
// buscar un cliente identificado por su ID
public Medecin getMedecinById(Long id);
// buscar una cita identificada por su ID
public Rv getRvById(Long id);
// buscar una franja horaria identificada por su ID
public Creneau getCreneauById(Long id);
// añadir un RV
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// eliminar un RV
public void supprimerRv(Rv rv);
}
Esta interfaz se ha creado tras identificar las necesidades de la capa [web]:
- línea 14: la lista de clientes. La necesitaremos para alimentar la lista desplegable de clientes,
- línea 16: la lista de médicos. La necesitaremos para alimentar el menú desplegable de médicos,
- línea 18: la lista de franjas horarias de un médico. La necesitaremos para mostrar la agenda del médico para un día determinado,
- línea 20: la lista de citas de un médico para un día determinado. Combinada con el método anterior, nos permitirá mostrar la agenda del médico para un día determinado con sus franjas horarias ya reservadas,
- línea 22: permite buscar a un cliente a partir de su número. El método nos permitirá buscar a un cliente a partir de una selección en la lista desplegable de clientes,
- línea 24: lo mismo para los médicos,
- línea 26: busca una cita por su número. Se puede utilizar al eliminar una cita para comprobar previamente que realmente existe,
- línea 28: busca un hueco a partir de su número. Permite identificar el hueco que un usuario quiere añadir o eliminar,
- línea 30: para añadir una cita,
- línea 32: para eliminar una cita.
La interfaz local [IDaoLocal] de EJB se limita a derivar la interfaz anterior [IDao]:
package rdvmedecins.dao;
import javax.ejb.Local;
@Local
public interface IDaoLocal extends IDao{
}
Lo mismo ocurre con la interfaz remota [IDaoRemote]:
package rdvmedecins.dao;
import javax.ejb.Remote;
@Remote
public interface IDaoRemote extends IDao{
}
La clase EJB [DaoJpa] implementa ambas interfaces, la local y la remota:
package rdvmedecins.dao;
...
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
- La línea 5 indica que la interfaz remota EJB recibe el nombre de «rdvmedecins.dao». Por otra parte, la anotación @Singleton (Java EE6) garantiza que solo se creará una instancia de EJB. La anotación @Stateless (Java EE5) define un EJB del que se pueden crear múltiples instancias para alimentar un grupo de EJB,
- La línea 6 indica que todos los métodos de EJB se ejecutan dentro de una transacción gestionada por el contenedor EJB3,
- la línea 7 muestra que EJB implementa las interfaces local y remota y que, además, es serializable.
El código completo de EJB es el siguiente:
package rdvmedecins.dao;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import rdvmedecins.exceptions.RdvMedecinsException;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
@PersistenceContext
private EntityManager em;
// lista de clientes
public List<Client> getAllClients() {
try {
return em.createQuery("select rc from Client rc").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 1);
}
}
// lista de médicos
public List<Medecin> getAllMedecins() {
try {
return em.createQuery("select rm from Medecin rm").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 2);
}
}
// lista de franjas horarias de un médico concreto
// médico: el médico
public List<Creneau> getAllCreneaux(Medecin medecin) {
try {
return em.createQuery("select rc from Creneau rc join rc.medecin m where m.id=:idMedecin").setParameter("idMedecin", medecin.getId()).getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 3);
}
}
// lista de citas de un médico concreto, en un día determinado
// médico: el médico
// día: el día
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
try {
return em.createQuery("select rv from Rv rv join rv.creneau c join c.idMedecin m where m.id=:idMedecin and rv.jour=:jour").setParameter("idMedecin", medecin.getId()).setParameter("jour", jour).getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 3);
}
}
// Añadir una cita
// día: día de la cita
// franja horaria: franja horaria de la cita
// cliente: cliente para el que se ha concertado la cita
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
try {
Rv rv = new Rv(null, jour);
rv.setClient(client);
rv.setCreneau(creneau);
em.persist(rv);
return rv;
} catch (Throwable th) {
throw new RdvMedecinsException(th, 4);
}
}
// eliminación de una cita
// cita: la cita eliminada
public void supprimerRv(Rv rv) {
try {
em.remove(em.merge(rv));
} catch (Throwable th) {
throw new RdvMedecinsException(th, 5);
}
}
// buscar un cliente concreto
public Client getClientById(Long id) {
try {
return (Client) em.find(Client.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// recuperar un médico determinado
public Medecin getMedecinById(Long id) {
try {
return (Medecin) em.find(Medecin.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// Recuperar una cita concreta
public Rv getRvById(Long id) {
try {
return (Rv) em.find(Rv.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// recuperar una franja horaria concreta
public Creneau getCreneauById(Long id) {
try {
return (Creneau) em.find(Creneau.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
}
- línea 22: el objeto EntityManager, que gestiona el acceso al contexto de persistencia. Al instanciar la clase, este campo será inicializado por el contenedor EJB gracias a la anotación @PersistenceContext de la línea 21,
- línea 27: consulta JPQL (Java Persistence Query Language) que devuelve todas las filas de la tabla [clients] en forma de una lista de objetos [Client],
- línea 36: consulta análoga para los médicos,
- línea 46: una consulta JPQL que realiza una unión entre las tablas [creneaux] y [medecins]. Se configura mediante el ID del médico,
- línea 57: una consulta JPQL que realiza una unión entre las tablas [rv], [creneaux] y [medecins] y que tiene dos parámetros: el ID del médico y el día de la cita,
- líneas 69-73: creación de una cita y posterior almacenamiento de la misma en la base de datos,
- línea 83: eliminación de una cita de la base de datos,
- línea 92: ejecuta un select en la base de datos para buscar un cliente concreto,
- línea 101: lo mismo para un médico,
- línea 110: lo mismo para una cita,
- línea 119: lo mismo para una franja horaria,
- todas las operaciones con el contexto de persistencia «em» de la línea 22 pueden presentar un problema con la base de datos. Por ello, todas ellas están rodeadas por un try / catch. La posible excepción se encapsula en la excepción «propia» RdvMedecinsException.
3.4.5. Implementación del controlador JDBC a partir de MySQL
En la arquitectura que se muestra a continuación:
![]() |
EclipseLink necesita el controlador JDBC de MySQL. Hay que instalarlo en las bibliotecas del servidor Glassfish, en la carpeta <glassfish>/domains/domain1/lib/ext, donde <glassfish> es la carpeta de instalación del servidor Glassfish. Se puede obtener de la siguiente manera:
![]() |
La carpeta en la que se debe colocar el controlador JDBC de MySQL es <carpeta Domains>[1]/domain1/lib/ext [2]. Este controlador está disponible en URL [http://www.mysql.fr/downloads/connector/j/]. Una vez instalado, hay que reiniciar el servidor Glassfish para que reconozca esta nueva biblioteca.
3.4.6. Implementación de la capa EJB de la capa [DAO]
Volvamos a la arquitectura construida hasta ahora:
![]() |
El conjunto [web, métier, DAO, JPA] debe implementarse en el servidor Glassfish. Lo hacemos así:
![]() |
- en [1], compilamos el proyecto Maven,
- en [2], lo ejecutamos,
- en [3], se ha desplegado en el servidor Glassfish (pestaña [Services])
Quizá nos interese echar un vistazo a los registros de Glassfish:
![]() |
En [1], los registros de Glassfish están disponibles en la pestaña [Output / Glassfish Server 3+]. Son los siguientes:
Las líneas identificadas con [Config] y [Précis] son los registros de EclipseLink, mientras que las identificadas con [Infos] proceden de Glassfish.
- líneas 1-12: EclipseLink procesa las entidades JPA que ha detectado,
- líneas 13-17: información que indica que el procesamiento de las entidades JPA se ha realizado con normalidad,
- línea 18: EclipseLink se identifica,
- línea 19: EclipseLink reconoce que se trata de SGBD MySQL,
- líneas 20-24: EclipseLink intenta conectarse con BD,
- líneas 25-28: lo ha conseguido,
- líneas 29-33: intenta volver a conectarse, esta vez utilizando específicamente una plataforma MySQL (línea 30),
- líneas 34-37: también se ha realizado con éxito,
- línea 38: confirmación de que se ha podido instanciar la unidad de persistencia [dbrdvmedecins-PU],
- línea 39: los nombres portables de las interfaces remota y local de EJB y [DaoJpa], donde «portable» significa que son reconocidas por todos los servidores de aplicaciones Java EE 6,
- línea 40: los nombres de las interfaces remota y local de EJB y [DaoJpa], específicos de Glassfish. En la prueba que vamos a realizar, utilizaremos el nombre «rdvmedecins.dao».
Las líneas 39 y 40 son importantes. A la hora de escribir el cliente de un EJB en Glassfish, es necesario conocerlas.
3.4.7. Pruebas del EJB de la capa [DAO]
Ahora que se ha implementado el EJB de la capa [DAO] de nuestra aplicación, podemos probarlo. Lo haremos en el marco de una aplicación cliente/servidor:
![]() |
El cliente va a probar la interfaz remota del EJB [DAO] implementado en el servidor Glassfish.
Empezamos creando un nuevo proyecto Maven « »:
![]() |
- En [1], creamos un nuevo proyecto,
- en [2,3], creamos un proyecto Maven de tipo [Java Application],
- en [4], le damos un nombre y lo colocamos en la misma carpeta que el EJB y el [DAO],
![]() |
- en [5], el proyecto generado,
- en [6], se ha generado una clase [App.java]. La eliminaremos,
- en [7], se ha generado una rama [Source Packages]. Aún no la habíamos visto. Podemos incluir pruebas JUnit en esta rama. Lo haremos. No conservaremos la clase de prueba [AppTest] generada,
- en [8], las dependencias del proyecto Maven. La rama [Dependencies] está vacía. Tendremos que añadir nuevas dependencias en ella. La rama [Test Dependencies] reúne las dependencias necesarias para las pruebas. Aquí, la biblioteca utilizada es la del framework JUnit 3.8. Tendremos que cambiarla.
El proyecto evoluciona de la siguiente manera:
![]() |
- a [1], el proyecto en el que se han eliminado las dos clases generadas, así como la dependencia JUnit.
Volvamos a la arquitectura cliente/servidor que se utilizará para la prueba:
![]() |
El cliente necesita conocer la interfaz remota que ofrece el EJB [DAO]. Además, va a intercambiar con el EJB entidades JPA. Por lo tanto, necesita la definición de dichas entidades. Para que el proyecto de prueba del EJB tenga acceso a esta información, vamos a añadir el proyecto del EJB y del [DAO] como dependencia del proyecto:
![]() |
- en [1], añadimos una dependencia de la rama [Test Dependencies],
- en [2], se selecciona la pestaña [Open Projects],
- en [3], se selecciona el proyecto Maven de EJB [DAO],
![]() |
- en [4], la dependencia añadida.
Volvamos a la arquitectura cliente/servidor de la prueba:
![]() |
Durante la ejecución, el cliente y el servidor se comunican a través de la red TCP-IP. No vamos a programar estos intercambios. Para cada servidor de aplicaciones, existe una biblioteca que hay que integrar en las dependencias del cliente. La de Glassfish se llama [gf-client]. La añadimos:
![]() |
- en [1], añadimos una dependencia,
- en [2], especificamos las características del artefacto deseado,
- en [3], se añaden numerosas dependencias. Maven las descargará. Esto puede tardar varios minutos. A continuación, se almacenan en el repositorio local de Maven.
Ahora ya podemos crear la prueba JUnit:
![]() |
- en [2], hacemos clic con el botón derecho sobre [Test Packages] para crear una nueva prueba JUnit,
![]() |
- en [3], se le da un nombre a la clase de prueba y se le asigna un paquete, [4],
- en [5], se elige el framework JUnit 4.x,
- en [6], la clase de prueba generada,
- en [7], las nuevas dependencias del proyecto Maven.
El archivo [pom.xml] queda entonces así:
<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-client-rdvmedecins-ejb-dao</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-client-rdvmedecins-ejb-dao</name>
<url>http://maven.apache.org</url>
<repositories>
<repository>
<url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
<id>eclipselink</id>
<layout>default</layout>
<name>Repository for library Library[eclipselink]</name>
</repository>
<repository>
<url>http://repo1.maven.org/maven2/</url>
<id>junit_4</id>
<layout>default</layout>
<name>Repository for library Library[junit_4]</name>
</repository>
</repositories>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.appclient</groupId>
<artifactId>gf-client</artifactId>
<version>3.1.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Cabe destacar:
- en las líneas 32-51, las dependencias del proyecto;
- líneas 13-26: se han definido dos repositorios de Maven, uno para EclipseLink (líneas 14-19) y otro para JUnit4 (líneas 20-25).
La clase de prueba será la siguiente:
package rdvmedecins.tests.dao;
import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import junit.framework.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import rdvmedecins.dao.IDaoRemote;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
public class JUnitTestDao {
// capa [dao] probada
private static IDaoRemote dao;
// fecha de hoy
Date jour = new Date();
@BeforeClass
public static void init() throws NamingException {
// inicialización del entorno JNDI
InitialContext initialContext = new InitialContext();
// instanciación de la capa DAO
dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
}
@Test
public void test1() {
// visualización de clientes
List<Client> clients =dao.getAllClients();
display("Liste des clients :", clients);
// visualización de médicos
List<Medecin> medecins =dao.getAllMedecins();
display("Liste des médecins :", medecins);
// Visualización de franjas horarias de un médico
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = dao.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// lista de citas de un médico en un día determinado
display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// Añadir un RV
Rv rv = null;
Creneau creneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
rv = dao.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// Añadir un RV en el mismo horario del mismo día
// debe provocar una excepción
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
Boolean erreur = false;
try {
rv = dao.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// se registra el error
erreur=true;
}
// se comprueba que se ha producido un error
Assert.assertTrue(erreur);
// lista de RV
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// eliminar un RV
System.out.println("Suppression du Rv ajouté");
dao.supprimerRv(rv);
System.out.println("Rv supprimé");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
}
// método auxiliar: muestra los elementos de una colección
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- líneas 23-29: el método etiquetado con @BeforeClass se ejecuta antes que todos los demás. Aquí se crea una referencia a la interfaz remota de EJB [DaoJpa]. Recordemos que le habíamos asignado el nombre «JNDI» a «rdvmedecins.dao»,
- líneas 34-35: muestran la lista de clientes,
- líneas 37-38: muestran la lista de médicos,
- líneas 40-42: muestran los horarios disponibles del primer médico,
- línea 44: muestra las citas del primer médico para el día de la línea 21,
- líneas 46-51: añaden una cita al primer médico, para su franja horaria n.º 2 y el día de la línea 21,
- línea 52: muestra, a modo de verificación, las citas del primer médico para el día de la línea 21. Debe haber al menos una, la que acabamos de añadir,
- líneas 55-70: se añade la misma cita. Como la tabla [RV] tiene una restricción de unicidad, esta adición debe provocar una excepción. Nos aseguramos de ello en la línea 70,
- línea 72: se muestran, para comprobación, las citas del primer médico correspondientes al día de la línea 21. La que queríamos añadir no debe aparecer ahí,
- líneas 74-76: se elimina la única cita que se ha añadido,
- línea 77: se muestran, a modo de verificación, las citas del primer médico para el día de la línea 21. La que acabamos de eliminar no debe aparecer allí.
Esta prueba es una prueba ficticia JUnit. Solo contiene una aserción (línea 70). Se trata de una prueba visual con los defectos que ello conlleva.
Si todo va bien, las pruebas deben superarse:
![]() |
- en [1], se crea el proyecto de prueba,
- en [2], se ejecuta la prueba,
- en [3], la prueba se ha superado.
Veamos más de cerca los resultados de la prueba:
Se invita al lector a leer estos registros junto con el código que los ha generado. Nos detendremos en la excepción que se produjo al añadir una cita ya existente, líneas 41-49. La pila de excepciones se reproduce en las líneas 42-48. Es inesperada. Volvamos al código del método para añadir una cita:
// Añadir una cita
// día: día de la cita
// franja horaria: franja horaria de la cita
// cliente: cliente para el que se ha concertado la cita
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
try {
Rv rv = new Rv(null, jour);
rv.setClient(client);
rv.setCreneau(creneau);
System.out.println(String.format("avant persist : %s",rv));
em.persist(rv);
System.out.println(String.format("après persist : %s",rv));
return rv;
} catch (Throwable th) {
throw new RdvMedecinsException(th, 4);
}
}
Echemos un vistazo a los registros de Glassfish al añadir las dos citas:
- línea 2: antes del primer persist,
- línea 3: después del primer persist,
- línea 4: la orden INSERT que se va a ejecutar. Cabe señalar que no tiene lugar al mismo tiempo que la operación persist. Si fuera así, este registro habría aparecido antes de la línea 2. La operación INSERT tiene lugar, por lo tanto, normalmente al final de la transacción en la que se ejecuta el método,
- línea 6: EclipseLink pregunta a MySQL cuál es la última clave primaria utilizada. Obtendrá la clave primaria de la cita añadida. Este valor se introducirá en el campo id de la entidad [Rv] persistida,
- líneas 7-8: la consulta SELECT, que mostrará las citas del médico,
- líneas 9-10: las pantallas de visualización de la segunda consulta persist,
- líneas 11-12: la orden INSERT que se va a ejecutar. Debe provocar una excepción. Esta aparece en las líneas 15-16 y es clara. La excepción la lanza inicialmente el controlador JDBC de MySQL por incumplimiento de la restricción de unicidad de las citas. De ello se deduce que deberíamos ver estas excepciones en los registros de la prueba JUnit. Sin embargo, no es así:
Recordemos la arquitectura cliente/servidor de la prueba:
![]() |
Cuando EJB o [DAO] lanzan una excepción, esta debe serializarse para llegar al cliente. Probablemente sea esta operación la que ha fallado por alguna razón que no he comprendido. Dado que nuestra aplicación completa no funcionará en modo cliente/servidor, podemos ignorar este problema.
Ahora que el EJB de la capa [DAO] está operativo, podemos pasar al EJB de la capa [métier].
3.5. La capa [métier]
Volvamos a la arquitectura de la aplicación que se está desarrollando:
![]() |
Vamos a crear un nuevo proyecto Maven para EJB y [métier]. Como se ve arriba, tendrá una dependencia del proyecto Maven que se ha creado para las capas [DAO] y [JPA].
3.5.1. El proyecto NetBeans
Creamos un nuevo proyecto Maven de tipo EJB. Para ello, basta con seguir el procedimiento ya utilizado y descrito en la página 174.
![]() |
- en [1], el proyecto Maven de la capa [métier],
- en [2], se añade una dependencia,
- en [3], se selecciona el proyecto Maven de las capas [DAO] y [JPA],
- en [4], se selecciona el ámbito [provided]. Cabe recordar que esto significa que es necesario para la compilación, pero no para la ejecución del proyecto. De hecho, elEJB de la capa [métier] se va a desplegar en el servidor Glassfish junto con el EJB de las capas [DAO] y [JPA]. Por lo tanto, cuando se ejecute, el EJB de las capas [DAO] y [JPA] ya estará presente,
![]() |
- en [6], el nuevo proyecto con su dependencia.
Veamos ahora el código fuente de la capa [métier]:
![]() |
El EJB [Metier] tendrá la siguiente interfaz [IMetier]:
package rdvmedecins.metier.service;
import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
public interface IMetier {
// capa DAO
// lista de clientes
public List<Client> getAllClients();
// lista de médicos
public List<Medecin> getAllMedecins();
// lista de franjas horarias de un médico
public List<Creneau> getAllCreneaux(Medecin medecin);
// lista de citas de un médico en un día determinado
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// buscar un cliente identificado por su ID
public Client getClientById(Long id);
// buscar un cliente identificado por su ID
public Medecin getMedecinById(Long id);
// buscar una cita identificada por su ID
public Rv getRvById(Long id);
// Buscar un horario identificado por su ID
public Creneau getCreneauById(Long id);
// añadir un RV
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// eliminar un RV
public void supprimerRv(Rv rv);
// profesión
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
}
Para comprender esta interfaz, hay que tener en cuenta la arquitectura del proyecto:
![]() |
Hemos definido la interfaz de la capa [DAO] (apartado 3.4.4) y hemos indicado que esta responde a las necesidades de la capa [web], es decir, a las necesidades del usuario. La capa [web] solo se comunica con la capa [DAO] a través de la capa [métier]. Esto explica que en la capa [métier] se encuentren todos los métodos de la capa [DAO]. Estos métodos se limitarán a delegar la solicitud de la capa [web] a la capa [DAO]. Nada más.
Durante el análisis de la aplicación, surge una necesidad: poder mostrar en una página web la agenda de un médico para un día determinado, con el fin de conocer las franjas horarias ocupadas y libres de ese día. Esto suele ocurrir cuando la secretaria atiende una solicitud por teléfono. Se le pide una cita para tal día con tal médico. Para dar respuesta a esta necesidad, la capa [métier] ofrece el método de la línea 46.
// profesión
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
Cabe preguntarse dónde ubicar este método:
- se podría colocar en la capa [DAO]. Sin embargo, este método no responde realmente a una necesidad de acceso a los datos, sino más bien a una necesidad de negocio,
- se podría colocar en la capa [web]. Sin embargo, esto sería una mala idea. Porque si se cambia la capa [web] por una capa [Swing], se perderá el método, aunque la necesidad siga existiendo.
El método recibe como parámetros el médico y el día para el que se desea la agenda de reservas. Devuelve un objeto [AgendaMedecinJour] que representa la agenda del médico y del día:
package rdvmedecins.metier.entites;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.jpa.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// campos
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// constructores
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// getters y setters
...
}
- línea 12: el médico al que pertenece la agenda,
- línea 13: el día de la agenda,
- línea 14: los horarios disponibles del médico para ese día.
- La clase presenta constructores (líneas 17 y 21), así como un método toString adaptado (línea 27).
La clase [CreneauMedecinJour] (línea 14) es la siguiente:
package rdvmedecins.metier.entites;
import java.io.Serializable;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// campos
private Creneau creneau;
private Rv rv;
// constructores
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau,rv);
}
// getters y setters
...
}
- línea 12: una franja horaria del médico,
- línea 13: la cita asociada, null si la franja horaria está libre.
Así, vemos que el campo creneauxMedecinJour de la línea 14 de la clase [AgendaMedecinJour] nos permite obtener todas las franjas horarias del médico con la información «ocupado» o «libre» para cada una de ellas. Ese era el objetivo del nuevo método [getAgendaMedecinJour] de la interfaz [IMetier].
Nuestro EJB [Metier] tendrá una interfaz local y una interfaz remota que se limitarán a derivar de la interfaz principal [IMetier]:
package rdvmedecins.metier.service;
import javax.ejb.Local;
@Local
public interface IMetierLocal extends IMetier{
}
package rdvmedecins.metier.service;
import javax.ejb.Remote;
@Remote
public interface IMetierRemote extends IMetier{
}
El EJB [Metier] implementa estas interfaces de la siguiente manera:
package rdvmedecins.metier.service;
import java.io.Serializable;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import javax.ejb.EJB;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import rdvmedecins.dao.IDaoLocal;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.entites.CreneauMedecinJour;
@Singleton
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote, Serializable {
// capa DAO
@EJB
private IDaoLocal dao;
public Metier() {
}
@Override
public List<Client> getAllClients() {
return dao.getAllClients();
}
@Override
public List<Medecin> getAllMedecins() {
return dao.getAllMedecins();
}
@Override
public List<Creneau> getAllCreneaux(Medecin medecin) {
return dao.getAllCreneaux(medecin);
}
@Override
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
return dao.getRvMedecinJour(medecin, jour);
}
@Override
public Client getClientById(Long id) {
return dao.getClientById(id);
}
@Override
public Medecin getMedecinById(Long id) {
return dao.getMedecinById(id);
}
@Override
public Rv getRvById(Long id) {
return dao.getRvById(id);
}
@Override
public Creneau getCreneauById(Long id) {
return dao.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return dao.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(Rv rv) {
dao.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour) {
// lista de franjas horarias del médico
List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
// lista de reservas de ese mismo médico para ese mismo día
List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
// se crea un diccionario a partir de las citas concertadas
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// se crea la agenda para el día solicitado
AgendaMedecinJour agenda = new AgendaMedecinJour();
// el médico
agenda.setMedecin(medecin);
// el día
agenda.setJour(jour);
// los horarios de reserva
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// cumplimentación de los horarios de reserva
for (int i = 0; i < creneauxHoraires.size(); i++) {
// línea i de la agenda
creneauxMedecinJour[i] = new CreneauMedecinJour();
// ID de la franja horaria
creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
// ¿El horario está libre o reservado?
if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
// El horario está ocupado: se anota la reserva
Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
creneauxMedecinJour[i].setRv(resa);
}
}
// se devuelve el resultado
return agenda;
}
}
- En la línea 22, la clase [Metier] es un singleton de EJB,
- línea 23, cada método de EJB se ejecuta dentro de una transacción. Esto significa que la transacción se inicia al comienzo del método, en la capa [métier]. Esta llamará a métodos de la capa [DAO]. Estos se ejecutarán dentro de la misma transacción,
- línea 24: EJB implementa sus interfaces local y remota y, además, es serializable,
- línea 27: una referencia a EJB desde la capa [DAO],
- línea 29: esta será inyectada por el contenedor EJB del servidor Glassfish, gracias a la anotación @EJB. Por lo tanto, cuando se ejecutan los métodos de la clase [Metier], la referencia a EJB de la capa [DAO] ya se ha inicializado,
- líneas 33-81: esta referencia se utiliza para delegar en la capa [DAO] la llamada realizada a la capa [métier],
- línea 84: el método getAgendaMedecinJour, que permite consultar la agenda de un médico para un día determinado. Dejamos que el lector siga los comentarios.
3.5.2. Implementación de la capa [métier]
La capa [métier] depende de la capa [DAO]. Cada capa se ha implementado con un EJB. Para probar el EJB y el [métier], debemos implementar ambos EJB. Para ello, necesitamos un proyecto empresarial.
![]() |
- [1], creamos un nuevo proyecto,
- de tipo Maven [2] y Aplicación empresarial [3],
- y le damos un nombre: [4]. El sufijo ear se añadirá automáticamente,
![]() |
- en [5], se elige el servidor Glassfish y Java EE 6,
- en [6], una aplicación empresarial contiene módulos, por lo general módulos EJB y módulos web. En este caso, la aplicación empresarial contendrá los módulos de los dos EJB que hemos creado. Como estos módulos ya existen, no marcamos las casillas,
- en [7,8] se han creado dos proyectos. [8] es el proyecto empresarial que vamos a utilizar. [7] es un proyecto cuya función desconozco. No he tenido que utilizarlo y, como no he profundizado en Maven, no sé para qué puede servir. Así que lo ignoraremos.
Ahora que ya se ha creado el proyecto de la empresa, podemos definir sus módulos.
![]() |
- En [1], creamos una nueva dependencia,
- en [2], seleccionamos el proyecto de EJB [DAO],
- en [3], se declara que es un EJB. No dejes el tipo en blanco, ya que, en ese caso, se utilizará el tipo jar y, aquí, ese tipo no es adecuado,
- en [4], se utiliza el ámbito [compile],
- en [5], el proyecto con su nueva dependencia,
![]() |
- en [6, 7, 8], volvemos a empezar para añadir el EJB de la capa [métier],
- en [9], las dos dependencias,
- en [10], se compila el proyecto,
![]() |
- en [11], se ejecuta,
- en [12], en la pestaña [Services], se ve que el proyecto se ha desplegado en el servidor Glassfish. Esto significa que los dos EJB ya están presentes en el servidor.
En los registros del servidor Glassfish, encontramos información sobre el despliegue de los dos EJB:
![]() |
- en [1], la pestaña de registros de Glassfish.
Allí se encuentran los siguientes registros:
- líneas 1-5: se han reconocido las entidades JPA,
- línea 7: indica que la creación de la unidad de persistencia [dbrdvmedecins2-PU] se ha realizado con éxito y que se ha establecido la conexión con la base de datos asociada,
- línea 8: los nombres portables de las interfaces remota y local de EJB, [DaoJpa] y portable, lo que significa que son reconocidos por todos los servidores de aplicaciones,
- línea 9: lo mismo, pero con nombres propios de GlassFish,
- líneas 10-11: lo mismo para EJB y [Metier].
Retendremos el nombre portátil de la interfaz remota de EJB y [Metier]:
java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote
Lo necesitaremos durante las pruebas de la capa [métier].
3.5.3. Prueba de la capa [métier]
Al igual que hicimos con la capa [DAO], vamos a probar la capa [métier] en el marco de una aplicación cliente/servidor:
![]() |
El cliente va a probar la interfaz remota de la capa EJB [Metier] desplegada en el servidor Glassfish.
Comenzamos creando un nuevo proyecto Maven. Para ello, seguimos el procedimiento utilizado para crear el proyecto de prueba de la capa [dao] (véase el apartado 3.4.7), excluyendo la creación de la prueba JUnit. El proyecto así creado es el siguiente
![]() |
- en [1], el proyecto creado con sus dependencias: respecto al EJB de la capa [dao], respecto al EJB de la capa [métier], de la biblioteca [gf-client].
En este punto, el archivo [pom.xml] del proyecto queda así:
<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-client-rdvmedecins-ejb-metier</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-client-rdvmedecins-ejb-metier</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.glassfish.appclient</groupId>
<artifactId>gf-client</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-metier</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
Nos aseguraremos de que se cumplan las dependencias descritas en las líneas 17-33. La prueba consistirá en una sencilla clase de consola:
![]() |
El código de la clase [ClientRdvMedecinsMetier] es el siguiente:
package istia.st.client;
import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.service.IMetierRemote;
public class ClientRdvMedecinsMetier {
// el nombre de la interfaz remota de EJB [Metier]
private static String IDaoRemoteName = "java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
// fecha de hoy
private static Date jour = new Date();
public static void main(String[] args) {
try {
// contexto JNDI del servidor Glassfish
InitialContext initialContext = new InitialContext();
// referencia en la capa remota [metier]
IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
// visualización de clientes
List<Client> clients = metier.getAllClients();
display("Liste des clients :", clients);
// visualización de médicos
List<Medecin> medecins = metier.getAllMedecins();
display("Liste des médecins :", medecins);
// visualización de franjas horarias de un médico
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = metier.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// lista de citas de un médico en un día determinado
display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// visualización de la agenda
AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// Añadir un RV
Rv rv = null;
Creneau creneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
rv = metier.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// visualización de la agenda
agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// eliminar un RV
System.out.println("Suppression du Rv ajouté");
metier.supprimerRv(rv);
System.out.println("Rv supprimé");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// visualización del calendario
agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
} catch (Throwable ex) {
System.out.println("Erreur...");
while (ex != null) {
System.out.println(String.format("%s : %s", ex.getClass().getName(), ex.getMessage()));
ex = ex.getCause();
}
}
}
// método auxiliar: muestra los elementos de una colección
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- línea 18: el nombre portátil de la interfaz remota del EJB [Metier] se ha extraído de los registros de GlassFish,
- líneas 24-27: se obtiene una referencia a la interfaz remota del EJB [Metier],
- líneas 29-30: muestran los clientes,
- líneas 32-33: muestran los médicos,
- líneas 35-37: muestran los horarios de un médico,
- línea 39: muestra las citas de un médico en un día determinado,
- líneas 41-42: la agenda de ese mismo médico para ese mismo día,
- líneas 44-49: se añade una cita,
- línea 50: se muestran las citas del médico. Debe haber una más,
- líneas 52-53: se muestra la agenda del médico. Debe aparecer la cita añadida,
- líneas 55-57: se elimina la cita que acabamos de añadir,
- línea 58: esto debe reflejarse en la lista de citas del médico,
- líneas 60-61: y en su agenda.
Ejecutamos la prueba:
![]() | ![]() |
Las visualizaciones en pantalla obtenidas son las siguientes:
Liste des clients :
Client[1,Mr,Jules,MARTIN]
Client[2,Mme,Christine,GERMAN]
Client[3,Mr,Jules,JACQUARD]
Client[4,Melle,Brigitte,BISTROU]
Liste des médecins :
Médecin[1,Mme,Marie,PELISSIER]
Médecin[2,Mr,Jacques,BROMARD]
Médecin[3,Mr,Philippe,JANDOT]
Médecin[4,Melle,Justine,JACQUEMOT]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER]
Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Ajout d'un Rv le [Wed May 23 16:25:26 CEST 2012] dans le créneau Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] pour le client Client[1,Mr,Jules,MARTIN]
Rv ajouté
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Suppression du Rv ajouté
Rv supprimé
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
- línea 37: la agenda de la Sra. PELISSIER, el 23 de mayo de 2012. No hay ningún hueco reservado,
- línea 39: se añade una cita,
- línea 42: la nueva agenda de la Sra. PELISSIER. Ahora hay un hueco reservado para el Sr. MARTIN,
- línea 44: se ha eliminado la cita,
- línea 46: la agenda de la Sra. PELISSIER muestra que no hay ningún hueco reservado.
Consideramos ahora que las capas [DAO] y [métier] están operativas. Nos queda por escribir la capa [web] con el marco JSF. Para ello, vamos a utilizar los conocimientos adquiridos al principio de este documento.
3.6. La capa [web]
Volvamos a la arquitectura que estamos construyendo:
![]() |
Vamos a construir la última capa, la capa [web].
3.6.1. El proyecto NetBeans
Creamos un proyecto Maven:
![]() |
- En [1], creamos un nuevo proyecto,
- en [2, 3], un proyecto Maven de tipo [Web Application],
- en [4], le damos un nombre,
![]() |
- en [5], se elige el servidor Glassfish y Java EE 6 Web,
- en [6], el proyecto así creado,
- en [7], el proyecto una vez eliminadas la página [index.jsp] y el paquete presente en [Source Packages],
![]() |
- en [8, 9], en las propiedades del proyecto, se añade un framework,
- en [10], se selecciona Java Server Faces,
![]() |
- en [11], la configuración de Java Server Faces. Se mantienen los valores por defecto. Cabe señalar que se utiliza JSF 2,
- en [12], el proyecto se modifica entonces en dos puntos: se genera un archivo [web.xml], así como una página [index.html].
El archivo [web.xml] es el siguiente:
<?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>
<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>
</web-app>
Ya nos hemos encontrado con este archivo.
- líneas 7-11: definen el servlet que procesará todas las solicitudes realizadas a la aplicación. Se trata del servlet de JSF,
- líneas 12-15: definen los URL que procesa este servlet. Son los URL con el formato /faces/*,
- líneas 21-23: definen la página [index.xhtml] como página de inicio.
Esta página 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">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
Hello from Facelets
</h:body>
</html>
Ya la hemos visto antes. Podemos ejecutar este proyecto:
![]() |
- en [1], ejecutamos el proyecto y obtenemos el resultado [2] en el navegador.
A continuación, presentamos el proyecto completo para, después, detallar sus distintos elementos.
![]() |
- en [1], las páginas XHTML del proyecto,
- en [2], los códigos Java,
- en [3], los archivos de mensajes, ya que la aplicación está internacionalizada,
![]() |
- en [4], las dependencias del proyecto.
3.6.2. Las dependencias del proyecto
Volvamos a la arquitectura del proyecto:
![]() |
La capa JSF se basa en las capas [métier], [DAO] y [JPA]. Estas tres capas están encapsuladas en los dos proyectos Maven que hemos creado, lo que explica las dependencias del proyecto [4]. Veamos, de forma sencilla, cómo se añaden estas dependencias:
![]() |
- en [1], pondremos «ejb» para indicar que la dependencia corresponde a un proyecto EJB,
- en [2], se indicará [provided]. De hecho, el proyecto web se va a desplegar al mismo tiempo que los dos proyectos EJB. Por lo tanto, no es necesario incluir los archivos JAR de EJB.
3.6.3. La configuración del proyecto
La configuración del proyecto es la misma que la de los proyectos JSF que hemos analizado al principio de este documento. A continuación 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>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 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>
</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
.reservationsHeaders {
text-align: center;
font-style: italic;
color: Snow;
background: Teal;
}
.creneau {
height: 25px;
text-align: center;
background: MediumTurquoise;
}
.client {
text-align: left;
background: PowderBlue;
}
.action {
width: 6em;
text-align: left;
color: Black;
background: MediumTurquoise;
}
.erreursHeaders {
background: Teal;
background-color: #ff6633;
color: Snow;
font-style: italic;
text-align: center
}
.erreurClasse {
background: MediumTurquoise;
background-color: #ffcc66;
height: 25px;
text-align: center
}
.erreurMessage {
background: PowderBlue;
background-color: #ffcc99;
text-align: left
}
[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
layout.entete.langue1=Fran\u00e7ais
layout.entete.langue2=Anglais
# 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 (jj/mm/aaaa)
form1.button.agenda=Agenda
form1.jour.required=date requise
form1.jour.erreur=date erron\u00e9e
# 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
# formulario 3
form3.titre=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.titre_detail=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.client=Client
form3.valider=Valider
form3.annuler=Annuler
# error
erreur.titre=Une erreur s'est produite.
erreur.message=Message d'erreur
erreur.accueil=Page d'accueil
erreur.classe=Cause
[messages_en.properties]: el archivo de mensajes en inglés
# diseño
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university
layout.entete.langue1=French
layout.entete.langue2=English
# 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 (dd/mm/yyyy)
form1.button.agenda=Diary
form1.jour.required=The date is required
form1.jour.erreur=The date is invalid
# 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
# formulario 3
form3.titre=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.titre_detail=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.client=Client
form3.valider=Submit
form3.annuler=Cancel
# error
erreur.titre=An error occurred
erreur.message=Error message
erreur.accueil=Welcome Page
erreur.classe=Cause
3.6.4. Las vistas del proyecto
Recordemos cómo funciona la aplicación. La página de inicio es la siguiente:
![]() |
Desde esta primera página, el usuario (secretaría, médico) realizará una serie de acciones. A continuación las presentamos. La vista de la izquierda muestra la pantalla desde la que el usuario realiza una solicitud; la de la derecha, la respuesta enviada por el servidor.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Por último, también puede aparecer una página de errores:
![]() |
Estas diferentes vistas se obtienen con las siguientes páginas del proyecto web:
![]() |
- en [1], las páginas [basdepage, entete, layout] se encargan del formato de todas las vistas,
- en [2], la vista generada por [layout.xhtml].
Aquí se ha utilizado la tecnología de facelets. Esta se ha descrito en el apartado 2.11. Nos limitamos a proporcionar el código de las páginas XHTML utilizadas para el diseño:
[entete.xhtml]
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['layout.entete']}"/></h2>
<div align="left">
<h:commandLink value="#{msg['layout.entete.langue1']}" actionListener="#{changeLocale.setFrenchLocale}"/>
<h:outputText value=" "/>
<h:commandLink value="#{msg['layout.entete.langue2']}" actionListener="#{changeLocale.setEnglishLocale}"/>
</div>
</body>
</html>
Cabe destacar, en las líneas 10-12, los dos enlaces para cambiar el idioma de la aplicación.
[basdepage.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">
<body>
<h:outputText value="#{msg['layout.basdepage']}"/>
</body>
</html>
[layout.xhtml]
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{changeLocale.locale}">
<h:head>
<title>RdvMedecins</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: 100px; height: 200px" bgcolor="#ffcccc">
</td>
<td>
<ui:insert name="contenu" >
<h2>Contenu</h2>
</ui:insert>
</td>
</tr>
<tr bgcolor="#ffcc66">
<td colspan="2">
<ui:include src="basdepage.xhtml"/>
</td>
</tr>
</table>
</h:form>
</h:body>
</f:view>
</html>
Esta página es la plantilla de la 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:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="contenu">
<h:panelGroup rendered="#{form.form1Rendered}">
<ui:include src="form1.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.form2Rendered}">
<ui:include src="form2.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.form3Rendered}">
<ui:include src="form3.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.erreurRendered}">
<ui:include src="erreur.xhtml"/>
</h:panelGroup>
</ui:define>
</ui:composition>
</html>
Las líneas 8-21 definen el área denominada «contenido» (línea 8) en [layout.xhtml] (línea 7). Se trata del área central de las vistas:
![]() |
La página [index.xhtml] es la única página de la aplicación. Por lo tanto, no habrá navegación entre páginas. Muestra una de las cuatro páginas [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. Esta visualización está controlada por cuatro valores booleanos [form1Rendered, form2Rendered, form3Rendered, erreurRendered] del bean de formulario que describiremos a continuación.
3.6.5. Los beans del proyecto
![]() |
Las clases del paquete [utils] ya se han presentado:
- la clase [ChangeLocale] es la que se encarga del cambio de idioma. Ya se ha analizado (apartado 2.4.4).
- 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.
3.6.5.1. El bean Application
El bean [Application] es el siguiente:
package beans;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.metier.service.IMetierLocal;
@Named(value = "application")
@ApplicationScoped
public class Application implements Serializable{
// capa de negocio
@EJB
private IMetierLocal metier;
// caché
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>();
// errores
private List<Erreur> erreurs = new ArrayList<Erreur>();
private Boolean erreur = false;
public Application() {
}
@PostConstruct
public void init() {
// se almacenan en caché los médicos y los clientes
try {
medecins = metier.getAllMedecins();
clients = metier.getAllClients();
} catch (Throwable th) {
// se registra el error
erreur = true;
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()));
}
return;
}
// verificación de las listas
if (medecins.size() == 0) {
// se anota el error
erreur = true;
erreurs.add(new Erreur("", "La liste des médecins est vide"));
}
if (clients.size() == 0) {
// se anota el error
erreur = true;
erreurs.add(new Erreur("", "La liste des clients est vide"));
}
// ¿Error?
if (erreur) {
return;
}
// los diccionarios
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
}
}
// getters y setters
...
}
- líneas 15-16: la clase [Application] es un bean de ámbito «Application». Se crea una vez al inicio del ciclo de vida de la aplicación JSF y es accesible para todas las solicitudes de todos los usuarios. En ella se suelen colocar datos de solo lectura. En este caso, almacenaremos en ella la lista de médicos y la de clientes. Por lo tanto, partimos de la hipótesis de que estas no cambian con frecuencia. Las páginas XHTML acceden a ella a través del nombre «application»,
- líneas 20-21: el contenedor EJB de Glassfish inyectará una referencia a la interfaz local de EJB y [Metier]. Recordemos la arquitectura de la aplicación:
![]() |
La aplicación JSF y las aplicaciones 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. En este caso, el bean de la aplicación utiliza EJB y [Metier]. Aunque no fuera así, lo normal sería encontrar una referencia a la capa [métier]. De hecho, se trata de una información que pueden compartir todas las solicitudes de todos los usuarios, por lo que es un dato de ámbito Application.
- líneas 34-35: el método init se ejecuta justo después de la instanciación de la clase [Application] (presencia de la anotación @PostConstruct),
- En las líneas 36-73, el método crea los siguientes elementos: la lista de médicos de la línea 23, la de clientes de la línea 24, un diccionario de médicos indexado por su ID en la línea 25 y lo mismo para los clientes en la línea 26. Pueden producirse errores. Estos se registran en la lista de la línea 28.
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;
}
// getter y setter
...
}
- 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.
3.6.5.2. 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;
// modelo
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private String form3Titre;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneau;
private Medecin medecin;
private Client client;
private Long idClient;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
@PostConstruct
private void init() {
// ¿Se ha realizado correctamente la inicialización?
if (application.getErreur()) {
// se recupera la lista de errores
erreurs = application.getErreurs();
// se muestra la vista de errores
setForms(false, false, false, true);
}
}
// 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;
}
.................................................
}
- líneas 5-7: 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 13-14: 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-31: el modelo de las páginas [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. La visualización de estas páginas se controla mediante los valores booleanos de las líneas 19-22. Cabe destacar que, por defecto, se muestra la página [form1.xhtml],
- líneas 33-34: el método init se ejecuta justo después de la instanciación de la clase (presencia de la anotación @PostConstruct),
- líneas 35-41: el método init se utiliza para determinar qué página debe mostrarse en primer lugar: normalmente la página [form1.xhtml] (línea 19), salvo que la inicialización de la aplicación haya fallado (línea 36), en cuyo caso se mostrará la página [erreur.xhtml] (línea 40).
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:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['erreur.titre']}"/></h2>
<p>
<h:commandButton value="#{msg['erreur.accueil']}" actionListener="#{form.accueil()}"/>
</p>
<hr/>
<h:dataTable value="#{form.erreurs}" var="erreur" headerClass="erreursHeaders" columnClasses="erreurClasse,erreurMessage">
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.classe']}"/>
</f:facet>
<h:outputText value="#{erreur.classe}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.message']}"/>
</f:facet>
<h:outputText value="#{erreur.message}"/>
</h:column>
</h:dataTable>
</body>
</html>
Utiliza una etiqueta <h:dataTable> (líneas 14-27) para mostrar la lista de errores. El resultado es una página similar a la siguiente:

A continuación, definiremos las diferentes fases del ciclo de vida de la aplicación.
3.6.6. Interacciones entre las páginas y el modelo
3.6.6.1. 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:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['form1.titre']}"/></h2>
<h:panelGrid columns="3">
<h:panelGroup>
<div align="center"><h3><h:outputText value="#{msg['form1.medecin']}"/></h3></div>
</h:panelGroup>
<h:panelGroup>
<div align="center"><h3><h:outputText value="#{msg['form1.jour']}"/></h3></div>
</h:panelGroup>
<h:panelGroup/>
<h:selectOneMenu value="#{form.idMedecin}">
<f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>
</h:selectOneMenu>
<h:inputText id="jour" value="#{form.jour}" required="true" requiredMessage="#{msg['form1.jour.required']}" converterMessage="#{msg['form1.jour.erreur']}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:inputText>
<h:message for="jour" styleClass="error"/>
</h:panelGrid>
<h:commandButton value="#{msg['form1.button.agenda']}" actionListener="#{form.getAgenda}"/>
</body>
</html>
Esta página se genera a partir de la siguiente plantilla:
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
// Bean de aplicación
@Inject
private Application application;
// plantilla
private Long idMedecin;
private Date jour = new Date();
// lista de médicos
public List<Medecin> getMedecins() {
return application.getMedecins();
}
// agenda
public void getAgenda() {
...
}
- El campo de la línea 9 alimenta, en lectura y escritura, el valor de la lista de la línea 18 de la página. Al mostrarse la página por primera vez, se establece el valor seleccionado en el cuadro combinado. Al mostrarse por primera vez, idMedecin es igual a null, por lo que se seleccionará el primer médico;
- el método de las líneas 13-15 genera los elementos del menú desplegable de médicos (línea 19 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 10 alimenta, en lectura y escritura, el campo de introducción de datos de la línea 21 de la página. Por lo tanto, en la visualización inicial se muestra la fecha de hoy,
- líneas 17-19: el método getAgenda gestiona el clic en el botón [Agenda] de la línea 26 de la página. Dado que no hay navegación (siempre se solicita la página [index.html]), a menudo se utilizará el atributo actionListener en lugar del atributo action. En este caso, el método llamado en la plantilla no devuelve ningún resultado.
Al hacer clic en el botón [Agenda],
- se envían los siguientes valores: el valor seleccionado en el menú desplegable de médicos se guarda en el campo idMedecin del modelo y el día elegido en el campo «día»,
- se invoca el método getAgenda del modelo.
El método getAgenda es el siguiente:
// Bean de aplicación
@Inject
private Application application;
// plantilla
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
private Medecin medecin;
private List<Erreur> erreurs;
// agenda
public void getAgenda() {
try {
// se busca al médico
medecin = application.gethMedecins().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 = application.getMetier().getAgendaMedecinJour(medecin, jour);
// se muestra el formulario 2
setForms(false, true, false, false);
} catch (Throwable th) {
// vista de errores
prepareVueErreur(th);
}
}
// 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);
}
Recordemos lo que debe mostrar el método getAgenda:
![]() |
- línea 21: se recupera el médico seleccionado del diccionario de médicos que se ha almacenado en el bean application. Para ello, se utiliza su ID, que se ha enviado en idMedecin,
- línea 23: se prepara el título de la página [form2.xhtml] que se va a mostrar. Este mensaje se extrae del archivo de mensajes para que pueda internacionalizarse. Esta técnica se ha descrito en el apartado 2.8.5.7, página 135.
- línea 25: se recurre a la capa [métier] para calcular la agenda del médico seleccionado para el día elegido,
- línea 27: se muestra [form2.xhtml],
- línea 28: si se produce una excepción, se genera una lista de errores (líneas 37-42) y se muestra la página [erreur.xhtml] (línea 44).
3.6.6.2. Mostrar la agenda de un médico
La página [form2.xhtml] corresponde a la siguiente vista:
![]() |
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: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>
<h2><h:outputText value="#{form.form2Titre}"/></h2>
<h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
<h:dataTable value="#{form.agendaMedecinJour.creneauxMedecinJour}" var="creneauMedecinJour" headerClass="reservationsHeaders" columnClasses="creneau,client,action">
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['form2.creneauHoraire']}"/>
</f:facet>
<h:outputText value="#{creneauMedecinJour.creneau.hdebut}:#{creneauMedecinJour.creneau.mdebut} - #{creneauMedecinJour.creneau.hfin}:#{creneauMedecinJour.creneau.mfin}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
<c:if test="#{creneauMedecinJour.rv==null}">
<h:outputText value=""/>
<c:otherwise>
<h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}"/>
</c:otherwise>
</c:if>
</h:column>
<h:column>
<f:facet name="header"/>
<h:commandLink action="#{form.action()}" value="#{creneauMedecinJour.rv==null ? msg['form2.reserver'] : msg['form2.supprimer']}">
<f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneau}"/>
</h:commandLink>
</h:column>
</h:dataTable>
</body>
</html>
Recordemos que el método getAgenda ha inicializado dos campos en el modelo:
// plantilla
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
Estos dos campos alimentan la página [form2.xhtml]:
- línea 10, el título de la página,
- línea 12: la agenda del médico se muestra mediante una etiqueta <h:dataTable> con tres columnas,
- líneas 13-18: la primera columna muestra los horarios disponibles,
- líneas 19-30: la segunda columna muestra el nombre del cliente que haya reservado la franja horaria, o nada si no es así. Para ello, se utilizan las etiquetas de la biblioteca JSTL Core a la que se hace referencia en la línea 7,
- líneas 30-35: la tercera columna muestra el enlace [Réserver] si la franja horaria está libre, y el enlace [Supprimer] si está ocupada.
Los enlaces de la tercera columna están vinculados a la siguiente plantilla:
// plantilla
private Long idCreneau;
// acción en RV
public void action() {
...
}
- El método action se invoca cuando el usuario hace clic en el enlace «Reservar / Eliminar» (línea 32). Cabe destacar que aquí se ha utilizado el atributo action. El método al que apunta este atributo debería tener la firma String action(), ya que el método debe devolver una clave de navegación. Sin embargo, aquí es void action(). Esto no ha provocado ningún error y se puede suponer que, en este caso, no hay navegación. Eso es lo que se pretendía. Poner actionListener en lugar de action provocaba un fallo de funcionamiento,
- el campo idCreneau de la línea 2 recuperará el ID de la franja horaria del enlace en el que se ha hecho clic (línea 33 de la página).
3.6.6.3. Eliminación de una cita
Analicemos el código que gestiona la eliminación de una cita. Esto corresponde a la siguiente secuencia de vistas:
![]() |
El código relacionado con esta operación es el siguiente:
// bean de aplicación
@Inject
private Application application;
// plantilla
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneau;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
// acción sobre RV
public void action() {
// se busca un hueco en la agenda
int i = 0;
Boolean trouvé = false;
while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
trouvé = true;
} else {
i++;
}
}
// ¿Se ha encontrado?
if (!trouvé) {
// qué raro: volvemos a mostrar el formulario 2
setForms(false, true, false, false);
return;
}
// Lo hemos encontrado
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// según la acción deseada
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// reserva
public void reserver() {
...
}
public void supprimer() {
try {
// eliminación de una cita
application.getMetier().supprimerRv(creneauChoisi.getRv());
// se actualiza la agenda
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// se muestra el formulario 2
setForms(false, true, false, false);
} catch (Throwable th) {
// vista de errores
prepareVueErreur(th);
}
}
- línea 16: cuando se inicia el método action, el ID de la franja horaria seleccionada se ha enviado a idCreneau (línea 11),
- líneas 18-26: se intenta recuperar la franja horaria a partir de su id (línea 21). Se busca en la agenda actual, agendaMedecinJour de la línea 10. Normalmente debería encontrarse. Si no es así, no se hace nada (líneas 28-32),
- línea 34: si se ha encontrado la franja horaria buscada, se recupera una referencia que se almacena en la línea 12,
- línea 36: se comprueba si el intervalo elegido tenía una cita. Si es así, se elimina (línea 39); si no, se reserva una (línea 37),
- línea 51: se elimina la cita del intervalo elegido. Esta tarea la realiza la capa [métier],
- línea 53: se solicita a la capa [métier] la nueva agenda del médico. Por supuesto, en ella se verá una cita menos. Pero, como la aplicación es multiusuario, se pueden ver los cambios realizados por otros usuarios,
- línea 55: se vuelve a mostrar la página [form2.xhtml],
- línea 58: dado que se ha solicitado la capa [métier], pueden surgir excepciones. En ese caso, se almacena la pila de excepciones en la lista de errores de la línea 13 y se muestran mediante la vista [erreur.xhtml].
3.6.6.4. Concertación de citas
La concertación de citas sigue la secuencia siguiente:
![]() |
El modelo implicado en esta acción es el siguiente:
// plantilla
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form3Titre;
private AgendaMedecinJour agendaMedecinJour;
private Medecin medecin;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
// acción en RV
public void action() {
...
// se ha encontrado
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// según la acción deseada
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// reserva
public void 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();
// cliente seleccionado en el menú desplegable
idClient=null;
// se muestra el formulario 3
setForms(false, false, true, false);
} catch (Throwable th) {
// vista de errores
prepareVueErreur(th);
}
}
- línea 14: si la franja horaria elegida no tiene ninguna cita, se trata de una reserva;
- línea 30: se prepara el título de la página [form3.xhtml] con la misma técnica que la utilizada para el título de la página [form2.xhtml],
- línea 34: en este formulario hay un menú desplegable cuyo valor se obtiene de idClient. Se establece el valor de este campo en null para no seleccionar a nadie,
- línea 36: se muestra la página [form3.xhtml],
- línea 39: o la página de errores si se ha producido una excepción.
La página [form3.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:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{form.form3Titre}"/></h2>
<h:panelGrid columns="2">
<h:outputText value="#{msg['form3.client']}"/>
<h:selectOneMenu value="#{form.idClient}">
<f:selectItems value="#{form.clients}" var="client" itemLabel="#{client.titre} #{client.prenom} #{client.nom}" itemValue="#{client.id}"/>
</h:selectOneMenu>
<h:panelGroup>
<h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
<h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
</h:panelGroup>
</h:panelGrid>
</body>
</html>
Esta página se alimenta del siguiente modelo:
// Bean de aplicación
@Inject
private Application application;
// plantilla
private Long idClient;
// lista de clientes
public List<Client> getClients() {
return application.getClients();
}
- línea 6: el número de cliente rellena el atributo value del menú desplegable de clientes de la línea 12 de la página. Establece el elemento seleccionado del menú desplegable,
- líneas 9-11: el método getClients rellena el contenido del menú desplegable (línea 13). El texto (itemLabel) de cada opción es [Titre Prénom Nom] del cliente, y el valor asociado (itemValue) es el ID del cliente. Por lo tanto, es este valor el que se enviará.
3.6.6.5. Validación de una cita
La validación de una cita sigue la siguiente secuencia:
![]() |
y corresponde al clic en el botón [Valider]:
<h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
Por lo tanto, el método [Form].validerRv es el que gestionará este evento. Su código es el siguiente:
// Bean de aplicación
@Inject
private Application application;
// plantilla
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private Long idCreneau;
private Long idClient;
private List<Erreur> erreurs;
// validación de la cita
public void validerRv() {
try {
// se recupera una instancia de la franja horaria elegida
Creneau creneau = application.getMetier().getCreneauById(idCreneau);
// se añade la cita
application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
// se actualiza la agenda
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// se muestra el formulario 2
setForms(false, true, false, false);
} catch (Throwable th) {
// comprobación de errores
prepareVueErreur(th);
}
}
- línea 12: antes de que se ejecute el método validerRv, el campo idClient ha recibido el ID del cliente seleccionado por el usuario,
- línea 19: a partir del ID de la franja horaria almacenado en un paso anterior (el bean tiene alcance de sesión), se solicita a la capa [métier] una referencia a la propia franja horaria,
- línea 21: se solicita a la capa [métier] que añada una cita para el día elegido (día), la franja horaria elegida (franja) y el cliente elegido (idClient),
- línea 23: 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 25: se vuelve a mostrar la agenda [form2.xhtml],
- línea 28: se muestra la página de error si se produce algún error.
3.6.6.6. Cancelación de una cita
Esto corresponde a la siguiente secuencia:
![]() |
El botón [Annuler] de la página [form3.xhtml] es el siguiente:
<h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
Por lo tanto, se invoca el método [Form].annulerRv:
// cancelación de la cita
public void annulerRv() {
// se muestra el formulario 2
setForms(false, true, false, false);
}
3.6.6.7. Volver a la página de inicio
Queda una acción por ver, la de la siguiente secuencia:
![]() |
El código del botón [Accueil] en la página [form2.xhtml] es el siguiente:
<h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
El método [Form].accueil es el siguiente:
public void accueil() {
// se muestra la página de inicio
setForms(true, false, false, false);
}
3.7. Conclusion
Hemos creado la siguiente aplicación:
![]() |
Nos hemos centrado más en las funcionalidades de la aplicación que en su aspecto para el usuario. Este se mejorará con el uso de la biblioteca de componentes PrimeFaces. Hemos creado una aplicación básica, pero representativa de una arquitectura Java EE en capas que utiliza EJB. La aplicación se puede mejorar de diversas formas:
- es necesaria la autenticación. No todo el mundo está autorizado a añadir o eliminar citas,
- debería ser posible desplazarse hacia adelante y hacia atrás por la agenda al buscar un día con franjas horarias libres,
- debería poder consultarse la lista de días en los que hay franjas horarias libres para un médico. De hecho, si se trata de un oftalmólogo, sus citas suelen reservarse con seis meses de antelación,
- ...
3.8. Las pruebas con Eclipse
3.8.1. La capa [DAO]
![]() |
- en [1], se importa el proyecto EJB de la capa [DAO] y su cliente,
- en [2], se selecciona el proyecto EJB de la capa [DAO] y se ejecuta en [3],
- en [4], se ejecuta en un servidor,
![]() |
- en [5], solo se propone el servidor Glassfish, ya que es el único que tiene un contenedor EJB,
- en [6], se ha implementado el módulo EJB,
![]() |
- en [7], se muestran los registros:
Son los mismos que teníamos con NetBeans.
![]() |
- En [7A] y [7B] se ejecuta la prueba JUnit del cliente,
![]() |
- en [8], la prueba se supera,
- en [9], los registros de la consola.
![]() |
En [10], se descarga la aplicación EJB.
3.8.2. La capa [métier]
![]() |
- en [1], se importan los cuatro proyectos Maven de la capa [métier],
- en [2], se selecciona el proyecto de empresa y se ejecuta en [3], en un servidor Glassfish [4] [5],
![]() |
- en [6], el proyecto empresarial se ha implementado en Glassfish,
![]() |
- en [7], se revisan los registros de Glassfish,
En la línea 3, anotamos el nombre portátil de EJB [Metier] y lo pegamos en la consola de este EJB:
public class ClientRdvMedecinsMetier {
// el nombre de la interfaz remota de EJB [Metier]
private static String IDaoRemoteName = "java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
// fecha de hoy
private static Date jour = new Date();
![]() |
- En el [8], ejecutamos el cliente de consola,
- en [9], sus registros.
![]() |
- en [10], se descarga la aplicación empresarial;
3.8.3. La capa [web]
![]() |
- en [1], se importan los tres proyectos Maven de la capa [web]. El que tiene el sufijo «ear» es el proyecto empresarial que hay que desplegar en Glassfish,
- en [2], se ejecuta,
![]() |
- en el servidor Glassfish [3],
- en [4], la aplicación empresarial se ha desplegado correctamente,
![]() |
- en [5], se solicita el URL de la aplicación en el navegador interno de Eclipse.


























































































































