Skip to content

3. Aplicación de ejemplo – 01: rdvmedecins-jsf2-ejb

El texto siguiente 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 conocer 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 analizará proviene de [ref9].

3.1. L'application

Una empresa de servicios de informática, [ISTIA-AGI], desea ofrecer un servicio de programación de citas. El primer mercado al que se dirige es el de los médicos que trabajan por cuenta propia. Por lo general, estos no cuentan con personal administrativo. Los clientes que desean concertar una cita llaman directamente al médico. Esto hace que el médico sea interrumpido con frecuencia 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 programación de citas que funcione según el siguiente principio:

  • una secretaría se encarga de la gestión de citas para un gran número de médicos. Esta secretaría puede estar a cargo de una sola persona. Su salario se comparte 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 por Internet tanto para la secretaría como para los médicos
  • La asignación de RV normalmente la realiza la secretaría. También pueden hacerlo los propios médicos. Este es el caso, en particular, cuando, al final de una consulta, el médico asigna él mismo 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 los RV. Si son suficientes, 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 una versión JSF / Spring / JPA Hibernate / servidor Tomcat:

3.2. Funcionamiento de la aplicación

Llamaremos a la aplicación [RdvMedecins]. A continuación presentamos 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 vista 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 que administra 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 título (Srta., Sra., Sr.)

3.3.2. La tabla [CLIENTS]

Los clientes 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 fila en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la fila.
  • NOM: el nombre del cliente
  • PRENOM: su nombre
  • TITRE: su tratamiento (Srta., Sra., Sr.)

3.3.3. La tabla [CRENEAUX]

Enumera los horarios en los que los RV son posibles:

  • ID: número que identifica el horario —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 del horario
  • MDEBUT: minutos de inicio del horario
  • HFIN: hora de finalización de la franja horaria
  • MFIN: minutos de fin del intervalo

La segunda línea de la tabla [CRENEAUX] (véase [1] más arriba) indica, por ejemplo, que el horario n.º 2 comienza a las 8:20 y termina a las 8:40, y pertenece 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 manera única al 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 en cuestión.
  • ID_CLIENT: número del cliente para quien se realiza la reservación – clave externa en el campo [ID] de la tabla [CLIENTS]

Esta tabla tiene una restricción de unicidad en la e sobre los valores de las columnas unidas (JOUR, ID_CRENEAU):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

Si una fila de la tabla [RV] tiene el valor (JOUR1, ID_CRENEAU1) para las columnas (JOUR, ID_CRENEAU), este valor no puede aparecer en ningún otro lugar. De lo contrario, significaría que se registraron 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 inicia un SQLException cuando ocurre este caso.

La línea de id igual a 3 (véase [1] más arriba) significa que se reservó un RV para el horario n.º 20 y el cliente n.º 4 el 23/08/2006. La tabla [CRENEAUX] nos indica que el horario n.º 20 corresponde al intervalo de 16:20 a 16:40 y pertenece a la doctora n.º 1 (la Sra. Marie PELISSIER). La tabla [CLIENTS] nos indica que el cliente n.º 4 es la Srta. Brigitte BISTROU.

3.3.5. Generación de la base de datos

Para crear las tablas y llenarlas, se puede utilizar el script [dbrdvmedecins2.sql], que se encuentra en el sitio de ejemplos. Con [WampServer] (véase el párrafo 1.3.3), se puede proceder de la siguiente manera:

  • en [1], haz clic en el ícono de [WampServer] y selecciona la opción [PhpMyAdmin] [2],
  • en [3], en la ventana que se abrió, 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], importamos 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. Sin embargo, invitamos al lector a seguir su evolución a lo largo de los programas, especialmente cuando algo 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 reunirá 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] se requieren conocimientos de Java EE. Para ello, se puede consultar [ref7] (véase el párrafo 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 párrafo 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], seleccione el controlador JDBC MySQL [3],
  • luego seleccione la opción [4] «Connect Using», que permite crear una conexión con una base de datos MySQL,
  • en [5], ingresa la información que se 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 verificar la información que se ha proporcionado,
  • en [9], el mensaje que se espera recibir cuando la información es correcta,
  • en [10], se establece la conexión. Aquí se ven 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 requiere la siguiente información:

  • las características JDBC de acceso a la base de datos (URL, usuario, contraseña),
  • las clases que representarán 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 agregar a GlassFish las bibliotecas de otra implementación.

NetBeans puede generar este archivo de persistencia mediante un asistente.

  • Haga clic con el botón derecho en el proyecto y elija 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 generó el archivo [persistence.xml] en la carpeta [META-INF],
  • en [12], se generó una carpeta [setup],
  • en [13], se agregaron 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>

Incluye la información proporcionada 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) administradas por el contenedor EJB3 del servidor GlassFish,
  • línea 4: el nombre JNDI de la fuente de datos.

Normalmente, en este archivo se encuentra el tipo de implementación JPA que se utiliza. En el asistente, indicamos EclipseLink. Como esta es 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 vista 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 ingresamos 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, generalmente mediante una herramienta de administración. Esta herramienta también existe para Glassfish.

Por último, se han agregado 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, 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, el servidor Glassfish los proporciona (provided),
  • líneas 41-48: definen un nuevo repositorio de artefactos de Maven, donde 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 de Java creadas por el asistente.

3.4.2.4. Las entidades JPA generadas

La entidad [Medecin] es la representación 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 a 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 corresponde a un campo en 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] corresponde al campo [ID] (línea 10) de la tabla [medecins],
  • líneas 13-14: el campo «título» de la clase [Medecin] corresponde al campo [TITRE] (línea 13) de la tabla [medecins],
  • líneas 16-17: el campo «nombre» de la clase [Medecin] corresponde al campo [NOM] (línea 16) de la tabla [medecins],
  • líneas 19-20: el campo «versión» de la clase [Medecin] corresponde al campo [VERSION] (línea 19) de la tabla [medecins]. En este caso, el asistente no reconoce que la columna es, de hecho, una columna de versión que debe incrementarse cada vez que se modifica la fila a la que pertenece. Para asignarle esta función, hay que agregar la anotación @Version. Lo haremos en un paso posterior,
  • líneas 22-23: el campo prenom de la clase [Medecin] corresponde al 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 aclaran 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]. Existen 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 externa en la tabla [medecins]. Un horario pertenece a un médico. A la inversa, un médico tiene varios horarios asociados a él. Por lo tanto, tenemos una relación de uno (médico) a varios (horarios), una relación calificada por la anotación @OneToMany por JPA (línea 25). El campo de la línea 26 contendrá todos los turnos del médico. Esto sin necesidad de programación. Para comprender completamente 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
...
// getters y setters
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}

Solo comentaremos las nuevas anotaciones:

  • Ya mencionamos que la tabla [creneaux] tiene una clave externa hacia la tabla [medecins]: cada horario está asociado a un médico. Se pueden asociar varios horarios al mismo médico. Existe una relación de la tabla [creneaux] hacia la tabla [medecins] que se califica como de varios (turnos) a uno (médico). La anotación @ManyToOne de la línea 32 sirve para definir la clave externa,
  • 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 del horario. Esto también se obtiene sin necesidad de programación.

La relación de clave externa entre la entidad [Creneau] y la entidad [Medecin] se materializa, por lo 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 externa de la tabla [creneaux] hacia la tabla [medecins]. Se dice que son inversas entre sí. Solo la relación @ManyToOne es indispensable. Ella identifica sin ambigüedad la relación de clave externa. La relación @OneToMany es opcional. Si está presente, simplemente hace 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 externa. 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 las entidades [Creneau] del campo de la línea 2 también deben insertarse,
  • 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.

Presentamos el código de las otras dos entidades sin comentarios particulares, 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 y 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 agregar 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 indispensables 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 después de 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 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 filas 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
  ...

  // getters y setters
  ...
 
  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    // TODO: Advertencia: este método no funcionará si los campos de identificación 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 varios turnos, y un turno pertenece a un solo médico.

La entidad [Rv] encapsula las filas 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 filas 29-31 modelan la relación «muchos a uno» que existe entre la tabla [rv] y la tabla [clients] (un cliente puede aparecer en varios Rv) de la base de datos, y las líneas 25 a 27, la relación «muchos a uno» que existe entre la tabla [rv] y la tabla [creneaux] (un horario puede aparecer en varios Rv).

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 manejarla 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 del 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 produjo en una excepción de tipo [EjbException] y la relanza. Es posible que no se desee esta encapsulación y se prefiera permitir que salga del contenedor EJB3 una excepción de tipo [RdvMedecinsException]. Esto es lo que permite la anotación @ApplicationException. Además, el atributo (rollback=true) de esta anotación le indica al contenedor EJB3 que, si la excepción de tipo [RdvMedecinsException] ocurre dentro de un método ejecutado en una transacción con un SGBD, esta debe revertirse. En términos técnicos, esto se conoce como 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 horarios 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);
  // agregar un RV
  public Rv ajouterRv(Date jour, Creneau creneau, Client client);
  // eliminar un RV
  public void supprimerRv(Rv rv);
}

Esta interfaz se creó 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 la lista desplegable de médicos,
  • línea 18: la lista de horarios disponibles de un médico. La necesitaremos para mostrar la agenda del médico para un día específico,
  • línea 20: la lista de citas de un médico para un día específico. Combinada con el método anterior, nos permitirá mostrar la agenda del médico para un día específico con sus horarios ya reservados,
  • 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 verificar previamente que realmente existe,
  • línea 28: busca un horario a partir de su número. Permite identificar el horario que un usuario desea agregar o eliminar,
  • línea 30: para agregar una cita,
  • línea 32: para eliminar una cita.

La interfaz local [IDaoLocal] de EJB simplemente deriva de 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 e 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 lleva el nombre «rdvmedecins.dao». Además, la anotación @Singleton (Java EE6) garantiza que solo se creará una instancia de EJB. La anotación @Stateless (Java EE5) define un EJB que puede crearse en 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 administrada por el contenedor EJB3,
  • la línea 7 muestra que EJB implementa las interfaces local y remota y también 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 horarios disponibles de un médico específico
  // 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 específico, en un día específico
  // 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);
    }
  }

  // Agregación de una cita
  // día: día de la cita
  // franja horaria: franja horaria de la cita
  // cliente: cliente para el que se programa 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 específico
  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 específico
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // recuperar una cita específica
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // recuperar un horario específico
  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 similar 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 su posterior almacenamiento 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 a un cliente específico,
  • línea 101: lo mismo para un médico,
  • línea 110: lo mismo para una cita,
  • línea 119: lo mismo para un horario,
  • todas las operaciones con el contexto de persistencia «em» de la línea 22 pueden presentar un problema con la base de datos. Por lo tanto, todas 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 requiere el controlador JDBC de MySQL. Este debe instalarse 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 donde se debe colocar el controlador JDBC de MySQL es <Carpeta de dominios>[1]/domain1/lib/ext [2]. Este controlador está disponible en URL [http://www.mysql.fr/downloads/connector/j/]. Una vez instalado, es necesario 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. Procedemos a hacerlo:

  • en [1], compilamos el proyecto Maven,
  • en [2], lo ejecutamos,
  • en [3], ya se ha implementado en el servidor Glassfish (pestaña [Services])

Quizás nos dé curiosidad revisar los registros de Glassfish:

En [1], los registros de Glassfish están disponibles en la pestaña [Output / Glassfish Server 3+]. Son los siguientes:

Config: The access type for the persistent class [class rdvmedecins.jpa.Personne] is set to [FIELD].
Config: The access type for the persistent class [class rdvmedecins.jpa.Client] is set to [FIELD].
Config: The access type for the persistent class [class rdvmedecins.jpa.Rv] is set to [FIELD].
Config: The target entity (reference) class for the many to one mapping element [field client] is being defaulted to: class rdvmedecins.jpa.Client.
Config: The target entity (reference) class for the many to one mapping element [field creneau] is being defaulted to: class rdvmedecins.jpa.Creneau.
Config: The access type for the persistent class [class rdvmedecins.jpa.Medecin] is set to [FIELD].
Config: The access type for the persistent class [class rdvmedecins.jpa.Creneau] is set to [FIELD].
Config: The target entity (reference) class for the many to one mapping element [field medecin] is being defaulted to: class rdvmedecins.jpa.Medecin.
Config: The alias name for the entity class [class rdvmedecins.jpa.Client] is being defaulted to: Client.
Config: The alias name for the entity class [class rdvmedecins.jpa.Rv] is being defaulted to: Rv.
Config: The alias name for the entity class [class rdvmedecins.jpa.Medecin] is being defaulted to: Medecin.
Config: The alias name for the entity class [class rdvmedecins.jpa.Creneau] is being defaulted to: Creneau.
Infos: rdvmedecins.jpa.Creneau actually got transformed
Infos: rdvmedecins.jpa.Medecin actually got transformed
Infos: rdvmedecins.jpa.Personne actually got transformed
Infos: rdvmedecins.jpa.Client actually got transformed
Infos: rdvmedecins.jpa.Rv actually got transformed
Infos: EclipseLink, version: Eclipse Persistence Services - 2.3.2.v20111125-r10461
Précis: Detected database platform: org.eclipse.persistence.platform.database.MySQLPlatform
Config: connecting(DatabaseLogin(
    platform=>DatabasePlatform
    user name=> ""
    connector=>JNDIConnector datasource name=>null
))
Config: Connected: jdbc:mysql://localhost:3306/dbrdvmedecins2
    User: root@localhost
    Database: MySQL  Version: 5.5.8-log
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-5.1.6 ( Revision: ${svn.Revision} )
Config: connecting(DatabaseLogin(
    platform=>MySQLPlatform
    user name=> ""
    connector=>JNDIConnector datasource name=>null
))
Config: Connected: jdbc:mysql://localhost:3306/dbrdvmedecins2
    User: root@localhost
    Database: MySQL  Version: 5.5.8-log
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-5.1.6 ( Revision: ${svn.Revision} )
Infos: file:/D:/data/istia-1112/netbeans/dvp/jsf2-pf-pfm/maven/netbeans/rdvmedecins-jsf2-ejb/mv-rdvmedecins-ejb-dao-jpa/target/classes/_dbrdvmedecins2-PU login successful
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/istia.st_mv-rdvmedecins-ejb-dao-jpa_ejb_1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal, java:global/istia.st_mv-rdvmedecins-ejb-dao-jpa_ejb_1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: istia.st_mv-rdvmedecins-ejb-dao-jpa_ejb_1.0-SNAPSHOT a été déployé en 270 ms.

Las líneas identificadas con [Config] y [Précis] son los registros de EclipseLink; las identificadas con [Infos] provienen 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 realizó normalmente,
  • 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 logró,
  • 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 logró,
  • línea 38: confirmación de que se pudo 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 realizaremos a continuación, utilizaremos el nombre «rdvmedecins.dao».

Las líneas 39 y 40 son importantes. Al 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 probará la interfaz remota del EJB [DAO] implementado en el servidor Glassfish.

Comenzamos 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 generó una clase [App.java]. La eliminaremos,
  • en [7], se generó una rama [Source Packages]. Aún no la habíamos visto. Podemos poner 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 agregar nuevas dependencias allí. La rama [Test Dependencies] reúne las dependencias necesarias para las pruebas. Aquí, la biblioteca utilizada es la del marco 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, intercambiará con el EJB entidades JPA. Por lo tanto, necesita la definición de estas entidades. Para que el proyecto de prueba del EJB tenga acceso a esta información, se agregará el proyecto del EJB [DAO] como dependencia del proyecto:

  • en [1], se agrega 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 agregada.

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 debe integrarse en las dependencias del cliente. La de Glassfish se llama [gf-client]. La agregamos:

  • en [1], agregamos una dependencia,
  • en [2], especificamos las características del artefacto deseado,
  • en [3], se agregan numerosas dependencias. Maven las descargará. Esto puede tardar varios minutos. Luego se almacenan en el repositorio local de Maven.

Ahora podemos crear la prueba JUnit:

  • en [2], hacemos clic con el botón derecho en [Test Packages] para crear una nueva prueba JUnit,
  • en [3], se le da un nombre a la clase de prueba y un paquete para ella, [4],
  • en [5], se elige el marco de trabajo 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:

  • 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 los horarios 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));
    // agregar 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));
    // agregar un RV en el mismo horario del mismo día
    // debe generar 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 verifica que haya habido 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 de utilidad: 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 dado el nombre JNDI «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: agregan una cita al primer médico, para su horario n.º 2 y el día de la línea 21,
  • línea 52: muestra, para 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 agregar,
  • líneas 55-70: se agrega 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 verificar, las citas del primer médico para el día de la línea 21. La que queríamos agregar no debe aparecer ahí,
  • líneas 74-76: se elimina la única cita que se había agregado,
  • línea 77: se muestran, para verificar, las citas del primer médico para el día de la línea 21. La que acabamos de eliminar no debe aparecer ahí.

Esta prueba es una prueba ficticia JUnit. Solo contiene una aserción (línea 70). Es una prueba visual con los defectos que ello conlleva.

Si todo va bien, las pruebas deben pasar:

  • 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:

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 15:34:15 CEST 2012]
Ajout d'un Rv le [Wed May 23 15:34:15 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 15:34:15 CEST 2012]
Rv[242, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Ajout d'un Rv le [Wed May 23 15:34:15 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]
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 15:34:15 CEST 2012]
Rv[242, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Suppression du Rv ajouté
Rv supprimé
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 15:34:15 CEST 2012]

Se invita al lector a leer estos registros junto con el código que los generó. Nos detendremos en la excepción que ocurrió al agregar 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 agregar una cita:


  // Agregar una cita
  // día: día de la cita
  // franja horaria: franja horaria de la cita
  // cliente: cliente para el que se programa 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);
    }
}

Veamos los registros de Glassfish al agregar las dos citas:

...
Infos: avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Infos: après persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Précis: INSERT INTO rv (JOUR, ID_CLIENT, ID_CRENEAU) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Précis: SELECT LAST_INSERT_ID()
Précis: SELECT t1.ID, t1.JOUR, t1.ID_CLIENT, t1.ID_CRENEAU FROM creneaux t0, rv t1 WHERE (((t0.ID_MEDECIN = ?) AND (t1.JOUR = ?)) AND (t0.ID = t1.ID_CRENEAU))
    bind => [2 parameters bound]
Infos: avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Infos: après persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Précis: INSERT INTO rv (JOUR, ID_CLIENT, ID_CRENEAU) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Précis: SELECT 1
Avertissement: Local Exception Stack: 
Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.2.v20111125-r10461): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '2012-05-23-3' for key 'UNQ1_RV'
Error Code: 1062
...
  • 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 ocurre 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, por lo tanto, normalmente tiene lugar al final de la transacción en la que se ejecuta el método,
  • línea 6: EclipseLink le pregunta a MySQL cuál es la última clave primaria utilizada. Obtendrá la clave primaria de la cita agregada. Este valor alimentará 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 salidas en pantalla de la segunda consulta persist,
  • líneas 11-12: la orden INSERT que se ejecutará. Debe provocar una excepción. Esta aparece en las líneas 15-16 y es clara. Es lanzada inicialmente por el controlador JDBC de MySQL por violar 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í:
1
2
3
4
5
6
7
8
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe

Recordemos la arquitectura cliente/servidor de la prueba:

Cuando EJB o [DAO] genera una excepción, esta debe ser serializada para llegar al cliente. Probablemente fue esta operación la que falló por alguna razón que no he podido entender. Como 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 creó 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 agrega una dependencia,
  • en [3], se selecciona el proyecto Maven de las capas [DAO] y [JPA],
  • en [4], se selecciona el alcance [provided]. Cabe recordar que esto significa que se necesita para la compilación, pero no para la ejecución del proyecto. De hecho, elEJB de la capa [métier] se implementará 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.

Ahora presentemos los códigos 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);

    // agregar 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 entender esta interfaz, hay que recordar la arquitectura del proyecto:

Hemos definido la interfaz de la capa [DAO] (párrafo 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] se comunica con la capa [DAO] únicamente a través de la capa [métier]. Esto explica por qué en la capa [métier] se encuentran todos los métodos de la capa [DAO]. Estos métodos simplemente delegarán 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, a fin de conocer los horarios ocupados 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 satisfacer 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]. Eso sería una mala idea. Porque si cambiamos la capa [web] por una capa [Swing], perderemos el método, aunque la necesidad siga existiendo.

El método recibe como parámetros el nombre del médico y el día para el que se desea la agenda de citas. Devuelve un objeto [AgendaMedecinJour] que representa la agenda del médico para ese 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 cuenta con constructores (líneas 17, 21) y 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: un horario disponible del médico,
  • línea 13: la cita asociada, null si el horario está disponible.

Así, vemos que el campo creneauxMedecinJour de la línea 14 de la clase [AgendaMedecinJour] nos permite obtener todos los horarios del médico con la información «ocupado» o «libre» para cada uno de ellos. 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 simplemente derivarán 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 horarios disponibles del médico
    List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
    // lista de citas 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 programadas
    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);
    // llenado 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 registra 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 comienza al inicio 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, el 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 a la capa [DAO] la llamada realizada a la capa [métier],
  • línea 84: el método getAgendaMedecinJour, que permite obtener 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 agregará 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. Aquí, 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 usarlo y, como no he profundizado en Maven, no sé para qué puede servir. Así que lo ignoraremos.

Ahora que el proyecto empresarial está creado, 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 este tipo no es adecuado aquí,
  • en [4], se utiliza el ámbito [compile],
  • en [5], el proyecto con su nueva dependencia,
  • en [6, 7, 8], se repite el proceso para agregar 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 implementado 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 la implementación de los dos EJB:

  • en [1], la pestaña de registros de Glassfish.

Allí se encuentran los siguientes registros:

Infos: rdvmedecins.jpa.Creneau actually got transformed
Infos: rdvmedecins.jpa.Medecin actually got transformed
Infos: rdvmedecins.jpa.Personne actually got transformed
Infos: rdvmedecins.jpa.Client actually got transformed
Infos: rdvmedecins.jpa.Rv actually got transformed
Infos: EclipseLink, version: Eclipse Persistence Services - 2.3.2.v20111125-r10461
Infos: file:/D:/data/istia-1112/netbeans/dvp/jsf2-pf-pfm/maven/netbeans/rdvmedecins-jsf2-ejb/mv-rdvmedecins-metier-dao/mv-rdvmedecins-metier-dao-ear/target/gfdeploy/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT_jar/_dbrdvmedecins2-PU login successful
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: EJB5181:Portable JNDI names for EJB 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, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]
  • líneas 1-5: se han reconocido las entidades JPA,
  • línea 7: indica que la construcción de la unidad de persistencia [dbrdvmedecins2-PU] se realizó con éxito y que se estableció 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 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] implementada 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 párrafo 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 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-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 contar con las dependencias descritas en las líneas 17 a 33. La prueba consistirá en una simple 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 horarios 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);
      // agregar 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 de utilidad: 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 obtenido 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 disponibles 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 agrega 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 verse la cita agregada,
  • líneas 55-57: se elimina la cita que acabamos de agregar,
  • 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:

 

Los mensajes de pantalla que se obtienen son los 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 horario reservado,
  • línea 39: se agrega una cita,
  • línea 42: la nueva agenda de la Sra. PELISSIER. Ahora hay un horario 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 horario reservado.

Ahora consideramos que las capas [DAO] y [métier] están operativas. Nos queda escribir la capa [web] con el marco JSF. Para ello, utilizaremos los conocimientos adquiridos al inicio 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

Estamos creando un proyecto de 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 agrega un marco de trabajo,
  • en [10], se selecciona Java Server Faces,
  • en [11], la configuración de Java Server Faces. Se dejan los valores predeterminados. 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.

Ahora presentamos el proyecto completo para luego detallar sus diferentes elementos.

  • a [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 de Maven que hemos creado, lo que explica las dependencias del proyecto [4]. Veamos, simplemente, cómo se agregan estas dependencias:

  • en [1], pondremos ejb para indicar que la dependencia es de un proyecto EJB,
  • en [2], se indicará [provided]. De hecho, el proyecto web se implementará 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 analizamos al inicio de este documento. 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. Las presentamos a continuación. 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 describió en el párrafo 2.11. Nos limitaremos 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 a 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 a 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 (párrafo 2.4.4).
  • La clase [Messages] es una clase que facilita la internacionalización de los mensajes de una aplicación. Se analizó en el párrafo 2.8.5.7.

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 alcance Application. Se crea una sola vez al inicio del ciclo de vida de la aplicación JSF y es accesible para todas las solicitudes de todos los usuarios. Por lo general, en ella se colocan datos de solo lectura. En este caso, colocaremos allí la lista de médicos y la de clientes. Por lo tanto, suponemos que estas no cambian con frecuencia. Las páginas XHTML tienen acceso a ellas a través del nombre de la aplicación,
  • 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 información que pueden compartir todas las solicitudes de todos los usuarios, por lo que es un dato de alcance Application.

  • líneas 34-35: el método init se ejecuta inmediatamente después de la instanciación de la clase [Application] (presencia de la anotación @PostConstruct),
  • En las líneas 36 a 73, el método crea los siguientes elementos: la lista de médicos en la línea 23, la de clientes en 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 ocurrir 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;
  }
  
  // getters y setters
...  
}
  • línea 9: el nombre de una clase de excepción si se lanzó una excepción,
  • línea 10: un mensaje de error.

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 realizó 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 alcance de sesión. Cabe recordar 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 señalar que, por defecto, se muestra la página [form1.xhtml],
  • líneas 33-34: el método init se ejecuta inmediatamente 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 primero: normalmente la página [form1.xhtml] (línea 19), a menos 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. Esto da como resultado una página similar a la siguiente:

Image

Ahora 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 sale bien, la primera página que se muestra es [form1.xhtml]. Esto da como resultado la siguiente vista:

 

La página [form1.xhtml] es la siguiente:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns: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, tanto en lectura como en escritura, el valor de la lista de la línea 18 de la página. Al mostrarse la página por primera vez, establece el valor seleccionado en el menú desplegable. Al mostrarse por primera vez, idMedecin es igual a null, por lo que se seleccionará el primer médico;
  • el método de las líneas 13 a 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 título, apellido y nombre del médico, y como valor (itemValue), el identificador del médico,
  • el campo de la línea 10 alimenta, en lectura y escritura, el campo de entrada 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. Como 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 valores: el valor seleccionado en el menú desplegable de médicos se registra 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 identificador, 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 toma del archivo de mensajes para que pueda ser internacionalizado. Esta técnica se describió en el párrafo 2.8.5.7, página 135.
  • línea 25: se utiliza 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 inicializó 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> de 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, en su caso, haya reservado el horario; de lo contrario, no muestra nada. Para realizar esta selección, 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 el horario está disponible, y el enlace [Supprimer] si el horario está ocupado.

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 señalar 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 era lo que se deseaba. Poner actionListener en lugar de action provocaba un mal funcionamiento,
  • el campo idCreneau de la línea 2 recuperará el ID del horario del enlace en el que se hizo 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 la 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 en RV
  public void action() {
    // se busca un horario disponible 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 encontró algo?
    if (!trouvé) {
      // qué raro — se vuelve a mostrar el formulario 2
      setForms(false, true, false, false);
      return;
    }
    // lo encontramos
    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 del horario seleccionado ya se ha enviado a idCreneau (línea 11),
  • líneas 18-26: se busca recuperar el horario 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 el horario buscado, se recupera una referencia y se almacena en la línea 12,
  • línea 36: se verifica si el horario seleccionado tenía una cita. Si es así, se elimina (línea 39); de lo contrario, se reserva una (línea 37),
  • línea 51: se elimina la cita del horario elegido. La capa [métier] se encarga de esta tarea,
  • línea 53: se solicita a la capa [métier] la nueva agenda del médico. Por supuesto, allí 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 llamado a 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. Programación de citas

La programación de citas sigue la siguiente secuencia:

El modelo involucrado 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 encontró
    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 el horario seleccionado no tiene ninguna cita, entonces 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 proviene de idClient. Establecemos 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 genera a partir de la siguiente plantilla:


// Bean de la 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 alimenta 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 alimenta 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 cita
  public void validerRv() {
    try {
      // se recupera una instancia del horario seleccionado
      Creneau creneau = application.getMetier().getCreneauById(idCreneau);
      // se agrega 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) {
      // vista 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 del horario guardado en un paso anterior (el bean tiene alcance de sesión), se le solicita a la capa [métier] una referencia al horario en sí,
  • línea 21: se le solicita a la capa [métier] que agregue una cita para el día seleccionado (día), el horario seleccionado (horario) y el cliente seleccionado (idClient),
  • línea 23: se le pide a la capa [métier] que actualice la agenda del médico. Se verá la cita agregada, además de todos los cambios que otros usuarios de la aplicación hayan podido realizar,
  • línea 25: se vuelve a mostrar la agenda [form2.xhtml],
  • línea 28: se muestra la página de error si ocurre algún error.

3.6.6.6. Cancelación de una cita

Esto corresponde a la siguiente secuencia:

El botón [Annuler] en 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. Regresar a la página de inicio

Queda una acción por revisar, 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 maneras:

  • se requiere autenticación. No todos tienen permiso para agregar o eliminar citas,
  • debería ser posible desplazarse hacia adelante y hacia atrás en la agenda al buscar un día con horarios disponibles,
  • debería ser posible solicitar la lista de días en los que hay horarios disponibles para un médico. De hecho, si se trata de un oftalmólogo, sus citas suelen reservarse con seis meses de anticipación,
  • ...

3.8. 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 ofrece el servidor Glassfish porque es el único que tiene un contenedor EJB,
  • en [6], se ha implementado el módulo EJB,
  • en [7], se muestran los registros:
1
2
3
4
5
6
7
8
Infos: Hibernate Validator 4.2.0.Final

Infos: Created EjbThreadPoolExecutor with thread-core-pool-size 16 thread-max-pool-size 32 thread-keep-alive-seconds 60 thread-queue-capacity 2147483647 allow-core-thread-timeout false 
...

Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/mv-rdvmedecins-ejb-dao-jpa/DaoJpa!rdvmedecins.dao.IDaoRemote, java:global/mv-rdvmedecins-ejb-dao-jpa/DaoJpa!rdvmedecins.dao.IDaoLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: mv-rdvmedecins-ejb-dao-jpa a été déployé en 5 523 ms.

Son los mismos que teníamos con NetBeans.

  • En [7A] y [7B] se ejecuta la prueba JUnit del cliente,
  • en [8], la prueba se realiza con éxito,
  • 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 corporativo y se ejecuta en [3], en un servidor Glassfish [4] [5],
  • en [6], el proyecto empresarial se ha implementado en Glassfish,
  • en [7], revisamos los registros de Glassfish,
1
2
3
4
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: EJB5181:Portable JNDI names for EJB Metier: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]

En la línea 3, tomamos nota del 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 implementar en Glassfish,
  • en [2], lo ejecutamos,
  • en el servidor GlassFish [3],
  • en [4], la aplicación empresarial se ha implementado correctamente,
  • en [5], solicitamos el URL de la aplicación en el navegador interno de Eclipse.