Skip to content

4. El servicio web J2EE de citas

Volvamos a la arquitectura de la aplicación que vamos a construir:

En esta parte nos centramos en la construcción del servicio web J2EE [1] ejecutado en un servidor Sun / Glassfish.

4.1. La base de datos

La base de datos, que llamaremos [dbrdvmedecins] , es una base de datos MySQL5 con cuatro tablas:

Image

4.1.1. La tabla [MEDECINS]

Contiene información sobre los médicos gestionados por la aplicación [RdvMedecins].

  • ID: número que identifica al médico - clave primaria de la tabla
  • VERSION: número que identifica la versión de la línea en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la línea.
  • NOM: el apellido del médico
  • PRENOM: su nombre
  • TITRE: su tratamiento (Srta., Sra., Sr.)

4.1.2. La tabla [CLIENTS]

Los clientes de los distintos médicos se registran en la tabla [CLIENTS]:

  • ID: n.º de identificación del cliente - clave primaria de la tabla
  • VERSION: número que identifica la versión de la línea en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la línea.
  • NOM: el nombre del cliente
  • PRENOM: su nombre
  • TITRE: su tratamiento (Srta., Sra., Sr.)

4.1.3. La tabla [CRENEAUX]

Enumera los intervalos horarios en los que son posibles los RV:

  • ID: número que identifica el intervalo horario - clave primaria de la tabla (línea 8)
  • VERSION: número que identifica la versión de la línea en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la línea.
  • ID_MEDECIN: número que identifica al médico al que pertenece este intervalo de tiempo – clave externa en la columna MEDECINS (ID).
  • HDEBUT: hora de inicio de la franja horaria
  • MDEBUT: minutos de inicio del horario
  • HFIN: hora de fin de la franja
  • MFIN: minutos de fin de franja

La segunda línea de la tabla [CRENEAUX] (véase [1] más arriba) indica, por ejemplo, que la franja n.º 2 comienza a las 8:20 y termina a las 8:40 y pertenece al médico n.º 1 (Sra. Marie PELISSIER).

4.1.4. La tabla [RV]

Enumera los RV asignados a cada médico:

  • ID: n.º que identifica el RV de forma única – clave primaria
  • JOUR: día del RV
  • ID_CRENEAU: franja horaria del RV – clave externa en el campo [ID] de la tabla [CRENEAUX] – determina tanto la franja horaria como el médico correspondiente.
  • ID_CLIENT: n.º del cliente para el que se realiza la reserva – clave externa en el campo [ID] de la tabla [CLIENTS]

Esta tabla tiene una restricción de unicidad en la e 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 han tomado dos RV al mismo tiempo para el mismo médico. Desde el punto de vista de la programación Java, el controlador JDBC de la base de datos lanza un SQLException cuando se produce este caso.

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

4.2. Generación de la base de datos

Cree la base de datos MySql [dbrdvmedecins] con la herramienta que prefiera. Para crear las tablas y rellenarlas, puede utilizar el script [createbd.sql] que se le proporcionará. Su contenido es el siguiente:

create table CLIENTS (
        ID bigint not null auto_increment,
        VERSION integer not null,
        TITRE varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(30) not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table CRENEAUX (
        ID bigint not null auto_increment,
        VERSION integer not null,
        HDEBUT integer not null,
        MDEBUT integer not null,
        HFIN integer not null,
        MFIN integer not null,
        ID_MEDECIN bigint not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table MEDECINS (
        ID bigint not null auto_increment,
        VERSION integer not null,
        TITRE varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(30) not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table RV (
        ID bigint not null auto_increment,
        JOUR date not null,
        ID_CLIENT bigint not null,
        ID_CRENEAU bigint not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    alter table CRENEAUX 
        add index FK9BD7A197FE16862 (ID_MEDECIN), 
        add constraint FK9BD7A197FE16862 
        foreign key (ID_MEDECIN) 
        references MEDECINS (ID);

    alter table RV 
        add index FKA4494D97AD2 (ID_CLIENT), 
        add constraint FKA4494D97AD2 
        foreign key (ID_CLIENT) 
        references CLIENTS (ID);

    alter table RV 
        add index FKA441A673246 (ID_CRENEAU), 
        add constraint FKA441A673246 
        foreign key (ID_CRENEAU) 
        references CRENEAUX (ID);

INSERT INTO CLIENTS ( VERSION, NOM, PRENOM, TITRE) VALUES (1, 'MARTIN', 'Jules', 'Mr');
...

INSERT INTO MEDECINS ( VERSION, NOM, PRENOM, TITRE) VALUES (1, 'PELISSIER', 'Marie', 'Mme');
...

INSERT INTO CRENEAUX ( VERSION, ID_MEDECIN, HDEBUT, MDEBUT, HFIN, MFIN) VALUES (1, 1, 8, 0, 8, 20);
...

INSERT INTO RV ( JOUR, ID_CRENEAU, ID_CLIENT) VALUES ('2006-08-22', 1, 2);
...

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

COMMIT WORK;

4.3. Los elementos de la arquitectura del lado del servidor

Volvamos a la arquitectura de la aplicación que vamos a construir:

En el lado del servidor, la aplicación estará formada por:

  1. una capa JPA que permite trabajar con la BD mediante objetos
  2. un EJB encargado de gestionar las operaciones con la capa JPA
  3. un servicio web encargado de exponer a los clientes remotos la interfaz del EJB en forma de servicio web.

Los elementos (b) y (c) implementan la capa [dao] representada en el esquema anterior. Se sabe que una aplicación puede acceder a un EJB remoto a través de los protocolos RMI y JNDI. En la práctica, esto limita los clientes a clientes Java. Un servicio web utiliza un protocolo de comunicación estandarizado que implementan diversos lenguajes: .NET, PHP, C++, ... Esto es lo que queremos mostrar aquí utilizando un cliente .NET.

Para una breve introducción a los servicios web, puede consultarse el curso [ref1], párrafo 14, página 109.

Un servicio web se puede implementar de dos maneras:

  • mediante una clase anotada con @WebService que se ejecuta en un contenedor web
  • mediante un EJB anotado con @WebService que se ejecuta en un contenedor EJB

Aquí utilizaremos la primera solución:

En el curso [ref1], párrafo 14, página 109, se encuentra un ejemplo que utiliza la segunda solución.

4.4. Configuración de Hibernate para el servidor Glassfish

Dependiendo de la versión, es posible que el servidor Glassfish V2 incluido con NetBeans no cuente con las bibliotecas Hibernate que necesita la capa JPA/Hibernate. Si, al continuar con el tutorial, descubre que Glassfish no le ofrece una implementación JPA/Hibernate o que, al implementar los servicios, una excepción indica que no se encuentran las bibliotecas de Hibernate, debe añadir las bibliotecas en la carpeta [<glassfish>/domains/domain1/lib/ext] y, a continuación, reiniciar el servidor Glassfish:

  • en [1], la carpeta <glassfish>/.../lib/ext
  • en [2], las bibliotecas de Hibernate y algunos controladores JDBC
  • en [3], el controlador JDBC de MySQL

Las bibliotecas de Hibernate se encuentran en el archivo zip que acompaña al tutorial.

4.5. Las herramientas de generación automática de NetBeans

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, [ref1] y las EJB, [ref2].

Creación de una conexión de NetBeans a la base de datos

  • Ejecutar SGBD y MySQL 5 para que BD esté disponible
  • crear una conexión NetBeans a la base de datos [dbrdvmedecins]
  • en la pestaña [Files], en la rama [Databases] [1], seleccionar el controlador Jdbc MySQL [2]
  • y, a continuación, seleccione la opción [3] «Connect Using», que permite crear una conexión con una base de datos MySQL
  • en [4], introduzca la información que se le solicita
  • y, a continuación, confirme en [5]
  • en [6], se crea la conexión. Se pueden ver las cuatro tablas de la base de datos conectada.

Creación de un proyecto EJB

  • en [1], crear una nueva aplicación, un módulo EJB
  • en [2], elija la categoría [Java EE] y en [3] el tipo [EJB Module]
  • en [4], elija una carpeta para el proyecto y, en [5], asígnele un nombre; a continuación, finalice el asistente
  • en [6] el proyecto generado

Añadir un recurso JDBC al servidor Glassfish

Vamos a añadir un recurso JDBC al servidor Glassfish.

  • En la pestaña [Services], iniciar el servidor Glassfish [2, 3]
  • en la pestaña [Projects], haga clic con el botón derecho del ratón en el proyecto EJB y, en [5], seleccione la opción [New / Other] que permite añadir un elemento al proyecto.

Image

  • en [6], seleccione la categoría [Glassfish] y en [7] indique que desea crear un recurso JDBC seleccionando el tipo [JDBC Resource]
  • en [8], indicar que este recurso JDBC utilizará su propio grupo de conexiones
  • en [9], asigne un nombre al recurso JDBC
  • en [10], pasar al siguiente paso
  • en [11], se definen las características del grupo de conexiones del recurso JDBC
  • en [12], asigne un nombre al grupo de conexiones
  • en [13], seleccione la conexión NetBeans [dbrdvmedecins] creada anteriormente
  • En [14], pasa al siguiente paso
  • en [15], normalmente no hay nada que cambiar en esta página. Las propiedades de la conexión a la base de datos MySQL [dbrdvmedecins] se han extraído de las de la conexión Netbeans [dbrdvmedecins] creada anteriormente
  • en [16], pasar al siguiente paso
  • en [17], mantenga los valores predeterminados propuestos
  • en [18], finalice el asistente. Este crea el archivo [sun-resources.xml] [19] cuyo contenido es el siguiente:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Resource Definitions //EN" "http://www.sun.com/software/appserver/dtds/sun-resources_1_3.dtd">
<resources>
  <jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins" object-type="user" pool-name="dbrdvmedecinsPool">
    <description/>
  </jdbc-resource>
  <jdbc-connection-pool ...">
    <property name="URL" value="jdbc:mysql://localhost:3306/dbrdvmedecins"/>
    <property name="User" value="root"/>
    <property name="Password" value="()"/>
  </jdbc-connection-pool>
</resources>

El archivo anterior recoge toda la información introducida en el asistente en formato XML. Será utilizado por IDE Netbeans para solicitar al servidor Glassfish que cree el recurso «jdbc/dbrdvmedecins» definido en la línea 4.

Creación de una unidad de persistencia

La unidad de persistencia [persistence.xml] configura la capa JPA: indica la implementación JPA utilizada (Toplink, Hibernate, etc.) y la configura.

  • en [1], haz clic con el botón derecho del ratón en el proyecto EJB y selecciona [New / Other] en [2]
  • en [3], seleccione la categoría [Persistence] y, a continuación, en [4], indique que desea crear una unidad de persistencia JPA
  • en [5], asigne un nombre a la unidad de persistencia creada
  • en [6], elija [Hibernate] como implementación JPA
  • en [7], seleccionar el recurso Glassfish «jdbc/dbrdvmedecins» que se acaba de crear
  • en [8], indique que no se debe realizar ninguna acción en la base de datos al instanciar la capa JPA
  • finalice el asistente
  • en [9], el archivo [persistence.xml] creado por el asistente

Su contenido es el siguiente:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.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_1_0.xsd">
  <persistence-unit name="serveur-ejb-dao-jpa-hibernate-generePU" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties/>
  </persistence-unit>
</persistence>

De nuevo, recoge en un formato XML la información proporcionada en el asistente. Este archivo no es suficiente para trabajar con la base de datos MySQL5 «dbrdvmedecins». Deberíamos indicar a Hibernate el tipo de SGBD que hay que gestionar. Esto se hará más adelante.

Creación de las entidades JPA

 
  • en [1], hacer clic con el botón derecho del ratón sobre el proyecto y en [2] seleccionar la opción [New / Other]
  • en [3], seleccione la categoría [Persistence] y, a continuación, en [4], indique que desea crear entidades JPA a partir de una base de datos existente.
  • en [5], seleccione la fuente JDBC «jdbc/dbrdvmedecins» que hemos creado
  • en [6], las cuatro tablas de la base de datos asociada
  • en [7,8], incluirlas todas en la generación de entidades JPA
  • en [9], continuar con el asistente
  • en [10], las entidades JPA que se van a generar
  • en [11], asigne un nombre al paquete de las entidades JPA
  • en [12], elegir el tipo Java que encapsulará las listas de objetos devueltas por la capa JPA
  • finalizar el asistente
  • en [13], las cuatro entidades JPA generadas, una para cada tabla de la base de datos.

A continuación se muestra, por ejemplo, el código de la entidad [Rv], que representa una fila de la tabla [rv] de la base de datos [dbrdvmedecins].

package jpa;
...
@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)
  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;
  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneaux idCreneau;
  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Clients idClient;

  public Rv() {
  }

...
}

Creación de la capa EJB de acceso a las entidades JPA

  • en [1], haga clic con el botón derecho del ratón en el proyecto y en [2], seleccione la opción [New / Other]
  • en [3], seleccione la categoría [Persistence] y, a continuación, en [4], el tipo [Session Beans for Entity Classes]
  • en [5], las entidades JPA creadas anteriormente se muestran
  • en [6], selecciónalas todas
  • en [7], ya han sido seleccionadas
  • en [8], continuar con el asistente
  • en [9], asigne un nombre al paquete de los EJB que se van a generar
  • en [10], indicar que los EJB deben implementar tanto una interfaz local como una remota
  • cerrar el asistente
  • en [11], los EJB generados

A continuación se muestra, por ejemplo, el código del EJB que gestiona el acceso a la entidad [Rv], es decir, a la tabla [rv] de la base de datos [dbrdvmedecins]:

package ejb;
...
@Stateless
public class RvFacade implements RvFacadeLocal, RvFacadeRemote {
  @PersistenceContext
  private EntityManager em;

  public void create(Rv rv) {
    em.persist(rv);
  }

  public void edit(Rv rv) {
    em.merge(rv);
  }

  public void remove(Rv rv) {
    em.remove(em.merge(rv));
  }

  public Rv find(Object id) {
    return em.find(Rv.class, id);
  }

  public List<Rv> findAll() {
    return em.createQuery("select object(o) from Rv as o").getResultList();
  }

}

Como se ha dicho, la generación automática de código puede ser muy útil para iniciar un proyecto y familiarizarse con las entidades JPA y EJB. A continuación, reescribiremos las capas JPA y EJB con nuestro propio código, pero el lector encontrará en ellas información que acabamos de ver en la generación automática de las capas.

4.6. El proyecto NetBeans del módulo EJB

Creamos un nuevo módulo EJB en blanco (véase el apartado 4.5):

 
  • el paquete [rdvmedecins.entites] agrupa las entidades de la capa JPA
  • el paquete [rdvmedecins.dao] implementa el EJB de la capa [dao]
  • el paquete [rdvmedecins.exceptions] implementa una clase de excepción específica de la aplicación

A partir de ahora, damos por hecho que el lector ha seguido todos los pasos del apartado 4.5. Tendrá que repetir algunos de ellos.

4.6.1. Configuración de la capa JPA

Recordemos la arquitectura de nuestra aplicación cliente/servidor:

El proyecto NetBeans:

 

La capa [JPA] se configura mediante los archivos [persistence.xml] y [sun-resources.xml] mencionados anteriormente. Estos dos archivos son generados por los asistentes que ya hemos visto:

  • la generación del archivo [sun-resources.xml] se ha descrito en el apartado 4.5.
  • La generación del archivo [persistence.xml] se ha descrito en el apartado 4.5.

El archivo [persistence.xml] generado debe modificarse de la siguiente manera:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.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_1_0.xsd">
  <persistence-unit name="dbrdvmedecins" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <properties>
       <!-- Dialecto -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>
  • línea 3: el tipo de transacciones es JTA: las transacciones serán gestionadas por el contenedor EJB3 de Glassfish
  • línea 4: se utiliza una implementación JPA/Hibernate. Para ello, se ha añadido la biblioteca Hibernate al servidor Glassfish (véase el apartado 4.4).
  • línea 5: la fuente de datos JTA utilizada por la capa JPA tiene el nombre JNDI «jdbc/dbrdvmedecins».
  • línea 8: esta línea no se genera automáticamente. Debe añadirse manualmente. Indica a Hibernate que el SGBD utilizado es MySQL5.

La fuente de datos «jdbc/dbrdvmedecins» se configura en el siguiente archivo [sun-resources.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Resource Definitions //EN" "http://www.sun.com/software/appserver/dtds/sun-resources_1_3.dtd">
<resources>
  <jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins" object-type="user" pool-name="dbrdvmedecinsPool">
    <description/>
  </jdbc-resource>
  <jdbc-connection-pool ...>
    <property name="URL" value="jdbc:mysql://localhost/dbrdvmedecins"/>
    <property name="User" value="root"/>
    <property name="Password" value="()"/>
  </jdbc-connection-pool>
</resources>
  • líneas 8-10: las características JDBC de la fuente de datos (URL de la base de datos, nombre y contraseña del usuario). La base de datos MySQL dbrdvmedecins es la descrita en el apartado 4.1.
  • línea 7: las características del grupo de conexiones asociado a esta fuente de datos

4.6.2. Las entidades de la capa JPA

Recordemos la arquitectura de nuestra aplicación cliente/servidor:

El proyecto NetBeans:

El paquete [rdvmedecins.entites] implementa la capa [Jpa].

En el apartado 4.5 vimos cómo generar automáticamente las entidades JPA de una aplicación. Aquí no utilizaremos esta técnica, sino que definiremos nosotros mismos las entidades. No obstante, estas retomarán gran parte del código generado en el apartado 4.5. En este caso, queremos que las entidades [Medecin] y [Client] sean clases hijas de una clase [Personne].

La clase Persona se utiliza para representar a los médicos y a los clientes:

package rdvmedecins.entites;
...
@MappedSuperclass
public class Personne implements Serializable {
   // características de una persona

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Version
  @Column(name = "VERSION", nullable = false)
  private Integer version;

  @Column(name = "TITRE", length = 5, nullable = false)
  private String titre;
  @Column(name = "NOM", length = 30, nullable = false)
  private String nom;
  @Column(name = "PRENOM", length = 30, nullable = false)
  private String prenom;

   // constructor por defecto
  public Personne() {
  }

   // constructor con parámetros
  public Personne(String titre, String nom, String prenom) {
     // se pasa por los setters
...
  }

   // constructor por copia
  public Personne(Personne personne) {
     // se pasa por los setters
 ...
  }

   // toString
  @Override
  public String toString() {
    return "[" + titre + "," + prenom + "," + nom + "]";
  }

// getters y setters
....
}
  • línea 3: cabe señalar que la clase [Personne] no es en sí misma una entidad (@Entity). Va a ser la clase padre de entidades. La anotación @MappedSuperClass designa esta situación.

La entidad [Client] encapsula las filas de la tabla [clients]. Deriva de la clase [Personne] anterior:

package rdvmedecins.entites;
....
@Entity
@Table(name = "CLIENTS")
public class Client extends Personne implements Serializable {

   // constructor por defecto
  public Client() {
  }

   // constructor con parámetros
  public Client(String titre, String nom, String prenom) {
     // padre
    super(titre, nom, prenom);
  }

   // constructor por copia
  public Client(Client client) {
     // padre
    super(client);
  }
}
  • línea 3: la clase [Client] es una entidad JPA
  • línea 4: está asociada a la tabla [clients]
  • línea 5: deriva de la clase [Personne]

La entidad [Medecin], que encapsula las filas de la tabla [medecins], sigue el mismo patrón:

package rdvmedecins.entites;
...
@Entity
@Table(name = "MEDECINS")
public class Medecin extends Personne implements Serializable {

   // constructor por defecto
  public Medecin() {
  }

   // constructor con parámetros
  public Medecin(String titre, String nom, String prenom) {
     // padre
    super(titre, nom, prenom);
  }

   // constructor por copia
  public Medecin(Medecin medecin) {
     // padre
    super(medecin);
  }
}

La entidad [Creneau] encapsula las líneas de la tabla [creneaux]:

package rdvmedecins.entites;
....
@Entity
@Table(name = "CRENEAUX")
public class Creneau implements Serializable {

   // características de un intervalo de RV
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Version
  @Column(name = "VERSION", nullable = false)
  private Integer version;
  @ManyToOne
  @JoinColumn(name = "ID_MEDECIN", nullable = false)
  private Medecin medecin;
  @Column(name = "HDEBUT", nullable = false)
  private Integer hdebut;
  @Column(name = "MDEBUT", nullable = false)
  private Integer mdebut;
  @Column(name = "HFIN", nullable = false)
  private Integer hfin;
  @Column(name = "MFIN", nullable = false)
  private Integer mfin;

   // fabricante por defecto
  public Creneau() {

  }

   // constructor con parámetros
  public Creneau(Medecin medecin, Integer hDebut,Integer mDebut, Integer hFin, Integer mFin) {
     // se pasa por los setters
...
  }

   // constructor por copia
  public Creneau(Creneau creneau) {
     // se pasa por los setters
...
  }

   // toString
  @Override
  public String toString() {
    return "[" + getId() + "," + getVersion() + "," + getMedecin() + "," + getHdebut() + ":" + getMdebut() + "," + getHfin() + ":" + getMfin() + "]";
  }

   // setters - getters
...
}
  • las líneas 15-17 modelan la relación «uno a varios» que existe entre la tabla [creneaux] y la tabla [medecins] de la base de datos.

La entidad [Rv] encapsula las filas de la tabla [rv]:

package rdvmedecins.entites;
...
@Entity
@Table(name = "RV")
public class Rv implements Serializable {
   // características

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Column(name = "JOUR", nullable = false)
  @Temporal(TemporalType.DATE)
  private Date jour;
  @ManyToOne
  @JoinColumn(name = "ID_CLIENT", nullable = false)
  private Client client;
  @ManyToOne
  @JoinColumn(name = "ID_CRENEAU", nullable = false)
  private Creneau creneau;

   // constructor por defecto
  public Rv() {
  }

   // constructor con parámetros
  public Rv(Date jour, Client client, Creneau creneau) {
     // se pasa por los setters
...
  }

   // constructor por copia
  public Rv(Rv rv) {
     // se pasa por los setters
...
  }

   // toString
  @Override
  public String toString() {
    return "[" + getId() + "," + new SimpleDateFormat("dd/MM/yyyy").format(getJour()) + "," + getClient() + "," + getCreneau() + "]";
  }

// getters y setters
...
}
  • las líneas 15-17 modelan la relación «uno a varios» que existe entre la tabla [rv] y la tabla [clients] de la base de datos, y las líneas 18-20 la relación «uno a varios» que existe entre la tabla [rv] y la tabla [creneaux]

4.6.3. La clase de excepción

La clase de excepción [RdvMedecinsException] de la aplicación es la siguiente:

package rdvmedecins.exceptions;

import javax.ejb.ApplicationException;

@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException {

  private static final long serialVersionUID = 1L;

   // 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);
  }

   // getters - setters
...
}
  • línea 6: la clase deriva de la clase [RuntimeException]. Por lo tanto, el compilador no obliga a gestionarla con try / catch.
  • línea 5: la anotación @ApplicationException hace que la excepción no sea «absorbida» por una excepción de tipo [EjbException].

Para entender la anotación @ApplicationException, volvamos a la arquitectura utilizada en el lado del servidor:

La excepción de tipo [RdvMedecinsException] será lanzada por los métodos del EJB de la capa [dao] dentro del contenedor EJB3 y será interceptada por este. Sin la anotación @ApplicationException, el contenedor EJB3 encapsula la excepción que se ha producido en una excepción de tipo [EjbException] y la relanza. Es posible que no se desee esta encapsulación y se quiera dejar que salga del contenedor Ejb3 una excepción de tipo [RdvMedecinsException]. Esto es lo que permite la anotación @ApplicationException. Por otra parte, el atributo (rollback=true) de esta anotación indica al contenedor EJB3 que, si la excepción de tipo [RdvMedecinsException] se produce dentro de un método ejecutado en una transacción con un SGBD, esta debe ser revertida. En términos técnicos, esto se denomina realizar un rollback de la transacción.

4.6.4. El EJB de la capa [dao]

La interfaz Java [IDao] de la capa [dao] es la siguiente:

package rdvmedecins.dao;
...
public interface IDao {

   // lista de clientes
  public List<Client> getAllClients();
   // lista de médicos
  public List<Medecin> getAllMedecins();
   // lista de franjas horarias de un médico
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // lista de citas de un médico en un día determinado
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour);
   // buscar un cliente identificado por su ID
  public Client getClientById(Long id);
   // buscar un cliente identificado por su ID
  public Medecin getMedecinById(Long id);
   // buscar una cita identificada por su ID
  public Rv getRvById(Long id);
   // buscar un horario identificado por su ID
  public Creneau getCreneauById(Long id);
   // añadir un RV
  public Rv ajouterRv(String jour, Creneau creneau, Client client);
   // eliminar un RV
  public void supprimerRv(Rv rv);
}

La interfaz local [IDaoLocal] del EJB se limita a derivar la interfaz [IDao] anterior:

1
2
3
4
5
6
7
package rdvmedecins.dao;

import javax.ejb.Local;

@Local
public interface IDaoLocal extends IDao{
}

Lo mismo ocurre con la interfaz remota [IDaoRemote]:

1
2
3
4
5
6
7
package rdvmedecins.dao;

import javax.ejb.Remote;

@Remote
public interface IDaoRemote extends IDao {
}

El EJB [DaoJpa] implementa ambas interfaces, la local y la remota:

1
2
3
4
5
6
7
package rdvmedecins.dao;
...
@Stateless(mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal,IDaoRemote {
...
}
  • la línea 3 indica que el EJB remoto se llama «rdvmedecins.dao»
  • la línea 4 indica que todos los métodos del EJB se ejecutan dentro de una transacción gestionada por el contenedor EJB3.
  • La línea 5 muestra que el EJB implementa las interfaces local y remota.

El código completo del EJB es el siguiente:

package rdvmedecins.dao;
...
@Stateless(mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal,IDaoRemote {

  @PersistenceContext
  private EntityManager em;

   // lista de clientes
  public List<Client> getAllClients() {
    try {
      return em.createQuery("select c from Client c").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 1);
    }
  }

   // lista de médicos
  public List<Medecin> getAllMedecins() {
    try {
      return em.createQuery("select m from Medecin m").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 2);
    }
  }

   // lista de franjas horarias de un médico determinado
   // médico: el médico
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    try {
      return em.createQuery("select c from Creneau c join c.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 determinado, en un día determinado
   // médico: el médico
   // día: el día
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour) {
    try {
      return em.createQuery("select rv from Rv rv join rv.creneau c join c.medecin m where m.id=:idMedecin and rv.jour=:jour").setParameter("idMedecin", medecin.getId()).setParameter("jour", new SimpleDateFormat("yyyy:MM:dd").parse(jour)).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
  }

   // Añadir una cita
   // día: día de la cita
   // franja horaria: franja horaria de la cita
   // cliente: cliente para el que se ha concertado la cita
  public Rv ajouterRv(String jour, Creneau creneau, Client client) {
    try {
      Rv rv = new Rv(new SimpleDateFormat("yyyy:MM:dd").parse(jour), client, creneau);
      em.persist(rv);
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }

   // 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, 6);
    }
  }

   // buscar un cliente determinado
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 7);
    }
  }

   // recuperar un médico determinado
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 8);
    }
  }

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

   // recuperar una franja horaria determinada
  public Creneau getCreneauById(Long id) {
    try {
      return (Creneau) em.find(Creneau.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 10);
    }
  }
}
  • línea 8: 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 7.
  • línea 15: consulta JPQL que devuelve todas las filas de la tabla [clients] en forma de una lista de objetos [Client].
  • línea 22: consulta análoga para los médicos
  • línea 32: una consulta JPQL que realiza una unión entre las tablas [creneaux] y [medecins]. Se configura mediante el ID del médico.
  • línea 43: 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 55-57: creación de una cita y posterior almacenamiento de la misma en la base de datos.
  • línea 67: eliminación de una cita de la base de datos.
  • línea 76: realiza una consulta en la base de datos para encontrar a un cliente determinado
  • línea 85: lo mismo para un médico
  • línea 94: lo mismo para una cita
  • línea 103: lo mismo para un horario
  • Todas las operaciones con el contexto de persistencia em de la línea 9 pueden encontrar 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.

Una vez compilado, el módulo EJB da lugar a un archivo .jar de tipo « »:

4.7. Implementación del EJB de la capa [dao] con NetBeans

NetBeans permite implementar fácilmente en el servidor GlassFish el EJB creado anteriormente.

  • En las propiedades del proyecto EJB, compruebe las opciones de ejecución [1].
  • En [2], el nombre del servidor en el que se va a implementar el EJB
  • en la pestaña [Services] [3], se inicia [4].
  • en [5], el servidor Glassfish una vez iniciado. Todavía no tiene ningún módulo EJB.
  • Inicie el servidor MySQL y asegúrese de que la base de datos [dbrdvmedecins] está en línea. Para ello, puede utilizar la conexión de NetBeans creada en el apartado 4.5.
  • En la pestaña [Projects] [6], se implementa el módulo EJB [7]: es necesario que se ejecute SGBD MySQL5 para que el recurso JDBC «jdbc/dbrdvmedecins» utilizado por el EJB sea accesible.
  • En [8], el EJB desplegado aparece en el árbol del servidor Glassfish
  • En [9], se elimina el EJB desplegado
  • En [10], el EJB ya no aparece en el árbol del servidor Glassfish.

4.8. Implementación del EJB de la capa [dao] con Glassfish

Aquí mostramos cómo implementar en el servidor Glassfish un EJB a partir de su archivo .jar.

  • Inicie el servidor MySQL y asegúrese de que la base de datos [dbrdvmedecins] está en línea. Para ello, puede utilizar la conexión de NetBeans creada en el apartado 4.5.

Recordemos la configuración JPA del módulo EJB que se va a implementar. Esta configuración se realiza en el archivo [persistence.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.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_1_0.xsd">
  <persistence-unit name="dbrdvmedecins" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <properties>
       <!-- Dialecto -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>

La línea 5 indica que la capa JPA utiliza una fuente de datos JTA, c.a.d, gestionada por el contenedor EJB3, denominada «jdbc/dbrdvmedecins».

En el apartado 4.5 vimos cómo crear este recurso JDBC desde NetBeans. Aquí mostramos cómo hacerlo directamente con GlassFish. Seguimos el procedimiento descrito en el apartado 13.1.2, página 79 de [ref1].

Comenzamos eliminando el recurso para poder volver a crearlo. Lo hacemos desde NetBeans:

  • en [1], los recursos JDBC del servidor Glassfish
  • en [2], el recurso «jdbc/dbrdvmedecins» de nuestro EJB
  • en [3], el grupo de conexiones de este recurso JDBC
  • en [4], eliminamos el grupo de conexiones. Esto tendrá como efecto la eliminación de todos los recursos JDBC que lo utilizan, es decir, el recurso «jdbc/dbrdvmedecins».
  • En [5] y [6], el recurso JDBC y el grupo de conexiones han sido eliminados.

Ahora utilizamos la consola de administración del servidor Glassfish para crear el recurso JDBC e implementar el EJB.

  • en la pestaña [services] [1] de NetBeans, inicie el servidor Glassfish [2] y, a continuación, acceda a su consola de administración
  • en [4], inicie sesión como administrador (contraseña: adminadmin si no la ha cambiado durante la instalación o posteriormente).
  • en [5], seleccione la rama [Connection Pools] de los recursos de Glassfish
  • en [6], cree un nuevo grupo de conexiones. Recuerde que un grupo de conexiones es una técnica para limitar el número de aperturas/cierres de conexiones con un SGBD. Al iniciar el servidor, N, un número definido por configuración, se abren conexiones con el SGBD. Estas conexiones abiertas se ponen a disposición de los EJB que las solicitan para realizar una operación con el SGBD. Tan pronto como esta finaliza, el EJB devuelve la conexión al grupo. La conexión nunca se cierra. Se comparte entre los diferentes subprocesos que acceden al SGBD
  • en [7], asigne un nombre al grupo
  • en [8], la clase que modela la fuente de datos es la clase [javax.sql.DataSource]
  • en [9], el SGBD que contiene la fuente de datos es aquí MySQl.
  • en [10], pase al siguiente paso
  • En [11], el atributo «Connection Validation Required» hace que, antes de proporcionar una conexión, el grupo compruebe que está operativa. Si no es así, crea una nueva. Esto permite que una aplicación siga funcionando tras una interrupción momentánea con SGBD. Durante la interrupción, ninguna conexión es utilizable y se envían excepciones al cliente. Cuando finaliza la interrupción, los clientes que siguen solicitando conexiones las obtienen de nuevo: gracias al atributo «Connection Validation Required», se volverán a crear todas las conexiones del grupo. Sin este atributo, el grupo detectaría que las conexiones iniciales se han interrumpido, pero no intentaría crear otras nuevas.
  • En [12], se solicita el nivel de aislamiento «Read Committed» para las transacciones. Este nivel garantiza que una transacción T2 no pueda leer datos modificados por una transacción T1 hasta que esta última haya finalizado por completo.
  • En [13], se solicita que todas las transacciones utilicen el nivel de aislamiento especificado en [12]
  • En [14] y [15], especifique la URL de BD cuyo pool gestiona las conexiones
  • en [16], el usuario será root
  • en [17], añada una propiedad
  • en [18], añada la propiedad «Password» con el valor () en [19]. Aunque la captura de pantalla [19] no lo muestra, no se debe poner la cadena vacía, sino () (paréntesis de apertura, paréntesis de cierre) para indicar una contraseña vacía. Si el usuario root de su SGBD MySQL tiene una contraseña que no está vacía, introduzca dicha contraseña.
  • en [20], finalice el asistente de creación del grupo de conexiones para la base de datos MySQL [dbrdvmedecins].
  • en [21], el grupo se ha creado. Haga clic en su enlace.
  • En [22], el botón [Ping] permite crear una conexión con la base de datos [dbrdvmedecins]
  • en [23], si todo va bien, aparecerá un mensaje indicando que la conexión se ha establecido correctamente

Una vez creado el grupo de conexiones, se puede crear un recurso Jdbc:

  • en [1], se selecciona la rama [JDBC Resources] del árbol de objetos del servidor
  • en [2], se crea un nuevo recurso JDBC
  • en [3], se le da un nombre al recurso JDBC. Este debe coincidir con el nombre utilizado en el archivo [persistence.xml]:
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
  • en [4], se especifica el grupo de conexiones que debe utilizar el nuevo recurso JDBC: el que acabamos de crear
  • en [5], se finaliza el asistente de creación
  • en [6] el nuevo recurso JDBC

Ahora que se ha creado el recurso JDBC, podemos implementar el archivo jar del EJB:

  • en [1], seleccione la rama [Enterprise Applications]
  • en [2], con el botón [Deploy], indique que desea implementar una nueva aplicación
  • en [3], indique que la aplicación es un módulo EJB
  • en [4], seleccione el archivo JAR del EJB [serveur-ejb-dao-jpa-hibernate.jar] que se le habrá proporcionado para el TP.
  • En [5], puede cambiar el nombre del módulo EJB si lo desea
  • En [6], finalice el asistente de implementación del módulo EJB
  • En [7], se ha implementado el módulo EJB. Ya está listo para su uso.

4.9. 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 mediante el siguiente cliente Java:

La clase [MainTestsDaoRemote] [1] es una clase de prueba JUnit 4. Las bibliotecas en [2] constan, por un lado:

  • el archivo JAR del EJB de la capa [dao] [3] (véase el apartado 4.6.4).
  • las bibliotecas Glassfish [4] necesarias para los clientes remotos de los EJB.

La clase de prueba es la siguiente:

package dao;
...
public class MainTestsDaoRemote {

   // capa [dao] probada
  private static IDaoRemote dao;

  @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() {
     // datos de la prueba
    String jour = "2006:08:23";
     // visualización de clientes
    List<Client> clients = null;
    try {
      clients = dao.getAllClients();
      display("Liste des clients :", clients);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // visualización de médicos
    List<Medecin> medecins = null;
    try {
      medecins = dao.getAllMedecins();
      display("Liste des médecins :", medecins);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // visualización de franjas horarias de un médico
    Medecin medecin = medecins.get(0);
    List<Creneau> creneaux = null;
    try {
      creneaux = dao.getAllCreneaux(medecin);
      display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // lista de citas de un médico en un día determinado
    try {
      display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // añadir un RV
    Rv rv = null;
    Creneau creneau = creneaux.get(2);
    Client client = clients.get(0);
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    try {
      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, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // añadir un RV en el mismo horario del mismo día
     // debe provocar una excepción
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    try {
      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, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // eliminar un RV
    System.out.println("Suppression du Rv ajouté");
    try {
      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, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
  }

   // 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 13: cabe destacar la instanciación del proxy del EJB remoto. Se utiliza su nombre JNDI «rdvmedecins.dao».
  • Los métodos de prueba utilizan los métodos expuestos por el EJB (véase el apartado 4.6.4).

Si todo va bien, las pruebas deben pasar:

 

Ahora que el EJB de la capa [dao] está operativo, podemos pasar a su exposición pública a través de un servicio web.

4.10. El servicio web de la capa [dao]

Para una breve introducción al concepto de servicio web, consulte el apartado 14, página 111 de [ref1].

Volvamos a la arquitectura del servidor de nuestra aplicación cliente/servidor:

Nos centramos aquí en el servicio web de la capa [dao]. La única función de este servicio es poner a disposición la interfaz del EJB de la capa [dao] a clientes multiplataforma capaces de interactuar con un servicio web.

Recordemos que hay dos formas de implementar un servicio web:

  • mediante una clase anotada con @WebService que se ejecuta en un contenedor web
  • mediante un EJB anotado con @WebService que se ejecuta en un contenedor EJB

Aquí utilizamos la primera solución. En NetBeans, debemos crear un proyecto de empresa con dos módulos:

  • el módulo EJB que se ejecutará en el contenedor EJB: el EJB de la capa [dao].
  • el módulo web que se ejecutará en el contenedor web: el servicio web que estamos creando.

Vamos a crear este proyecto empresarial de dos maneras.

4.10.1. Proyecto NetBeans - Versión 1

En primer lugar, creamos un proyecto NetBeans de tipo «Aplicación web»:

  • En [1], creamos un nuevo proyecto en la categoría «Java Web» [2] de tipo «Aplicación web» [3].
  • en [4], se le da un nombre al proyecto y en [5] se especifica la carpeta en la que debe generarse
  • en [6], se establece el servidor de aplicaciones que ejecutará la aplicación web
  • en [7], se establece el contexto de la aplicación
  • en [8], se valida la configuración del proyecto.
  • en [9], el proyecto generado. El servicio web que estamos creando utilizará el EJB del proyecto anterior [10]. Por lo tanto, necesita hacer referencia al archivo .jar del módulo EJB [10].
  • En [11], añadimos un proyecto NetBeans a las bibliotecas del proyecto web [12]
  • en [13], se selecciona la carpeta del módulo EJB en el sistema de archivos y se confirma.
  • En [14], el módulo EJB se ha añadido a las bibliotecas del proyecto web.

En [15], implementamos el servicio web con la siguiente clase [WsDaoJpa]:

package rdvmedecins.ws;
...
@WebService()
public class WsDaoJpa implements IDao {

  @EJB
  private IDaoLocal dao;

   // lista de clientes
  @WebMethod
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }

   // lista de médicos
  @WebMethod
  public List<Medecin> getAllMedecins() {
    return dao.getAllMedecins();
  }

   // lista de franjas horarias de un médico determinado
   // médico: el médico
  @WebMethod
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    return dao.getAllCreneaux(medecin);
  }

   // lista de citas de un médico determinado, en un día determinado
   // médico: el médico
   // día: el día
  @WebMethod
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour) {
    return dao.getRvMedecinJour(medecin, jour);
  }

   // Añadir una cita
   // día: día de la cita
   // franja horaria: franja horaria de la cita
   // cliente: cliente para el que se ha concertado la cita
  @WebMethod
  public Rv ajouterRv(String jour, Creneau creneau, Client client) {
    return dao.ajouterRv(jour, creneau, client);
  }

   // eliminación de una cita
   // cita: la cita eliminada
  @WebMethod
  public void supprimerRv(Rv rv) {
    dao.supprimerRv(rv);
  }

   // recuperar un cliente determinado
  @WebMethod
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }

   // recuperar un médico determinado
  @WebMethod
  public Medecin getMedecinById(Long id) {
    return dao.getMedecinById(id);
  }

   // recuperar una cita determinada
  @WebMethod
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }

   // recuperar una franja horaria determinada
  @WebMethod
  public Creneau getCreneauById(Long id) {
    return dao.getCreneauById(id);
  }
}
  • En la línea 4, la clase [WsdaoJpa] implementa la interfaz [IDao]. Recordemos que esta interfaz se define en el archivo EJB de la capa [dao] de la siguiente forma:
package rdvmedecins.dao;
...
public interface IDao {

   // lista de clientes
  public List<Client> getAllClients();
   // lista de médicos
  public List<Medecin> getAllMedecins();
   // lista de franjas horarias de un médico
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // lista de citas de un médico en un día determinado
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour);
   // buscar un cliente identificado por su ID
  public Client getClientById(Long id);
   // buscar un cliente identificado por su ID
  public Medecin getMedecinById(Long id);
   // buscar una cita identificada por su ID
  public Rv getRvById(Long id);
   // buscar un horario identificado por su ID
  public Creneau getCreneauById(Long id);
   // añadir un RV
  public Rv ajouterRv(String jour, Creneau creneau, Client client);
   // eliminar un RV
  public void supprimerRv(Rv rv);
}
  • línea 3: la anotación @WebService convierte la clase [WsDaoJpa] en un servicio web.
  • Líneas 6-7: la referencia del EJB de la capa [dao] será inyectada por el servidor de aplicaciones en el campo de la línea 7. Recordemos que siempre es la implementación local (IDaoLocal en este caso) la que se inyecta. Esta inyección es posible porque el servicio web se ejecuta en la misma JVM que el EJB.
  • Todos los métodos del servicio web están etiquetados con la anotación @WebMethod para que sean visibles para los clientes remotos. Un método no etiquetado con la anotación @WebMethod sería interno al servicio web y no visible para los clientes remotos. Cada método M del servicio web se limita a llamar al método M correspondiente del EJB inyectado en la línea 7.

La creación de este servicio web se refleja en una nueva rama del proyecto NetBeans:

En [1] vemos el servicio web WsDaoJpa y en [2] los métodos que expone a los clientes remotos.

Recordemos la arquitectura del servicio web en desarrollo:

Los componentes del servicio web que vamos a implementar son:

  • [1]: el módulo web que acabamos de construir
  • [2]: el módulo EJB que creamos en una etapa anterior y del que depende el servicio web

Para implementarlos juntos, hay que reunir ambos módulos en un proyecto de NetBeans denominado «empresarial»:

En [1] creamos un nuevo proyecto empresarial [2, 3].

  • En [4,5], se le da un nombre al proyecto y se establece su carpeta de creación
  • En [6], se selecciona el servidor de aplicaciones en el que se implementará la aplicación empresarial
  • en [7], un proyecto empresarial puede tener tres componentes: aplicación web, módulo EJB, aplicación cliente. Aquí, el proyecto se crea sin ningún componente. Estos se añadirán posteriormente.
  • En [8], la aplicación empresarial recién creada.
  • en [9], haz clic con el botón derecho en [Java EE Modules] y añade un nuevo módulo
  • en [10], solo se muestran los módulos de NetBeans actualmente abiertos en IDE. Aquí seleccionamos el módulo web [serveur-webservice-1-ejb-dao-jpa-hibernate] y el módulo EJB [serveur-ejb-dao-jpa-hibernate] que hemos creado.
  • En [11], los dos módulos añadidos al proyecto empresarial.

Ahora nos queda implementar esta aplicación empresarial en el servidor Glassfish. A continuación, hay que iniciar SGBD y MySQL para que sea accesible la fuente de datos JDBC «jdbc/dbrdvmedecins» utilizada por el módulo EJB.

  • en [1], se inicia el servidor Glassfish
  • si el módulo EJB [serveur-ejb-dao-jpa-hibernate] está implementado, se descarga [2]
  • en [3], se implementa la aplicación de empresa
  • en [4], ya está implementada. Se ve que contiene los dos módulos: Web y EJB.

4.10.2. Proyecto NetBeans - versión 2

Ahora mostramos cómo implementar el servicio web cuando no se dispone del código fuente del módulo EJB, sino solo de su archivo .jar.

El nuevo proyecto NetBeans del servicio web será el siguiente:

Los elementos destacados del proyecto son los siguientes:

  • [1]: el servicio web se implementa mediante un proyecto NetBeans de tipo [Web Application].
  • [2]: el servicio web se implementa mediante la clase [WsDaoJpa] ya estudiada
  • [3]: el archivo EJB de la capa [dao], que permite a la clase [WsDaoJpa] acceder a las definiciones de las diferentes clases, interfaces y entidades de las capas [dao] y [jpa].

A continuación, creamos el proyecto empresarial necesario para la implementación del servicio web:

  • [1], creamos una aplicación empresarial [ea-rdvmedecins], inicialmente sin ningún módulo.
  • En [2], añadimos el módulo web [serveur-webservice-ejb-dao-jpa-hibernate] anterior
  • en [3], el resultado.

Tal cual, la aplicación empresarial [ea-rdvmedecins] no se puede implementar en el servidor Glassfish desde NetBeans. Se produce un error. Por lo tanto, hay que implementar manualmente el archivo EAR de la aplicación [ea-rdvmedecins]:

  • El archivo [ea-rdvmedecins.ear] se encuentra en la carpeta [dist] [2] de la pestaña [Files] de NetBeans.
  • En este archivo [3] se encuentran los dos elementos de la aplicación empresarial:
  • el archivo del EJB [serveur-ejb-dao-jpa-hibernate]. Este archivo está presente porque formaba parte de las bibliotecas a las que hacía referencia el servicio web.
  • el archivo del servicio web [serveur-webservice- ejb-dao-jpa-hibernate].
  • El archivo [ea-rdvmedecins.ear] se crea a partir de un simple Build [4] de la aplicación empresarial.
  • En [5], la operación de implementación falla.

Para implementar el archivo [ea-rdvmedecins.ear] de la aplicación empresarial, procedemos tal y como se mostró al implementar el archivo EJB [serveur-ejb-dao-jpa-hibernate.jar] en el apartado 4.2. Volvemos a utilizar el cliente web de administración del servidor Glassfish. No repetiremos los pasos ya descritos.

En primer lugar, comenzaremos por «descargar» la aplicación empresarial implementada en el apartado 4.10.1:

  • [1]: seleccione la rama [Enterprise Applications] del servidor Glassfish
  • en [2], seleccione la aplicación empresarial que desea descargar y, a continuación, en [3], descárguela
  • en [4], la aplicación empresarial se ha descargado
  • en [1], elija la rama [Enterprise Applications] del servidor Glassfish
  • en [2], implemente una nueva aplicación empresarial
  • en [3], seleccione el tipo [Enterprise Application]
  • en [4], especifique el archivo .ear del proyecto NetBeans [ea-rdvmedecins]
  • en [5], implemente este archivo
  • en [6], la aplicación se ha implementado
  • en [7], el servicio web [WsDaoJpa] aparece en la rama [Web Services] del servidor Glassfish. Lo seleccionamos.
  • En [8], se dispone de diversa información sobre el servicio web. La más interesante para un cliente es la información [9]: la URI del servicio web.
  • En [10], se puede probar el servicio web
  • En [11], la URI del servicio web a la que se ha añadido el parámetro ?tester. Esta URI muestra una página de prueba. Todos los métodos (@WebMethod) expuestos por el servicio web se muestran y pueden probarse. Aquí se prueba el método [13], que solicita la lista de clientes.
  • En [14], solo mostramos una vista parcial de la página de respuesta. Pero se puede ver que el método getAllClients sí ha devuelto la lista de clientes. La captura de pantalla nos muestra que envía su respuesta en formato XML.

Un servicio web se describe íntegramente mediante un archivo XML denominado archivo WSDL:

  • en [1] en la herramienta de administración web del servidor Glassfish, seleccione el servicio web [WsDaoJpa]
  • en [2], siga el enlace [View WSDL]
  • en [3]: la URI del archivo WSDL. Es importante conocer esta información. Es necesaria para configurar los clientes de este servicio web.
  • en [4], la descripción XML del servicio web. No comentaremos este contenido complejo.

4.10.3. Pruebas JUnit del servicio web

Creamos un proyecto NetBeans para «ejecutar» las pruebas ya realizadas con un cliente EJB, esta vez con un cliente para el servicio web recientemente implementado. Seguimos aquí un procedimiento análogo al descrito en el apartado 14.2.1, página 115 de [ref1].

  • en [1], un proyecto Java clásico
  • en [2], la clase de prueba
  • en [3], el cliente utiliza el archivo del EJB para acceder a las definiciones de la interfaz de la capa [dao] y de las entidades JPA. Recordemos que este archivo se encuentra en la subcarpeta [dist] de la carpeta del módulo EJB.

Para acceder al servicio web remoto, es necesario generar clases proxy:

En el esquema anterior, la capa [2] [C=Client] se comunica con la capa [1] [S=Serveur]. Para comunicarse con la capa [S], el cliente [C] debe establecer una conexión de red con la capa [S] y comunicarse con ella siguiendo un protocolo específico. Las conexiones de red son conexiones TCP y el protocolo de transporte es HTTP. La capa [S], que representa el servicio web, está implementada por un servlet Java ejecutado por el servidor Glassfish. No hemos escrito este servlet. Su generación está automatizada por Glassfish a partir de las anotaciones @Webservice y @WebMethod de la clase [WsDaoJpa] que hemos escrito. Del mismo modo, vamos a automatizar la generación de la capa [C] del cliente. A veces se denomina a la capa [C], una capa proxy del servicio web remoto, ya que el término proxy designa un elemento intermediario en una cadena de software. En este caso, el proxy C es el intermediario entre el cliente que vamos a escribir y el servicio web que hemos desplegado.

Con NetBeans 6.5, el proxy C se puede generar de la siguiente manera (para continuar, es necesario que el servicio web esté activo en el servidor GlassFish):

  • en [1], añadir un nuevo elemento al proyecto Java
  • en [2], seleccionar la rama [Web services]
  • en [3], seleccione [Web Service Client]
  • en [4], introduzca la URI del archivo WSDL del servicio web. Esta URI se ha presentado en el apartado 4.10.2.
  • en [5], dejar el valor por defecto [JAX-WS]. El otro valor posible es [JAX-RPC]
  • tras validar el asistente de creación del proxy del servicio web, el proyecto NetBeans se ha enriquecido con una rama [Web Service References] [6]. Esta rama muestra los métodos expuestos por el servicio web remoto.
  • En la pestaña [Files] [7], se han añadido códigos fuente Java [8]. Estos corresponden al proxy C generado.
  • En [9], el código de una de las clases. Se puede ver [10] que se han colocado en un paquete [rdvmedecins.ws]. No comentaremos el código de estas clases, que vuelve a ser bastante complejo.

Para el cliente Java que estamos construyendo, el proxy C generado actúa como intermediario. Para acceder al método M del servicio web remoto, el cliente Java llama al método M del proxy C. De este modo, el cliente Java llama a métodos locales (ejecutados en la misma JVM) y, de forma transparente para él, estas llamadas locales se traducen en llamadas remotas.

Nos queda por ver cómo llamar a los métodos M del proxy C. Volvamos a nuestra clase de prueba JUnit:

En [1], la clase de prueba [MainTestsDaoRemote] es la que ya se utilizó durante la prueba del EJB de la capa [dao]:

package dao;
...
public class MainTestsDaoRemote {

   // capa [dao] probada
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
  }

  @Test
  public void test1() {
...
  }
}
  • En la línea [13], la prueba test1 se mantiene tal cual.
  • línea [9], se ha eliminado el contenido del método [init].

En este punto, el proyecto presenta errores porque el método de prueba [test1] utiliza las entidades [Client], [Medecin], [Creneau], [Rv], que ya no se encuentran en los mismos paquetes que antes. Ahora están en el paquete del proxy C generado. Se eliminan las instrucciones import afectadas y se regeneran mediante la operación «Fix Imports».

Volvamos al código de la clase de prueba [MainTestsDaoRemote]:

package dao;
...

public class MainTestsDaoRemote {

   // Capa [dao] probada
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
}

El método [init] de la línea 10 debe inicializar la referencia de la capa [dao] de la línea 7. Necesitamos saber cómo utilizar el proxy C generado en nuestro código. NetBeans nos ayuda en este proceso.

  • Seleccionar en [1] el método [getAllClients] del servicio web con el ratón y, a continuación, arrastrar este método para colocarlo dentro del método [init] de la clase de prueba.

Se obtiene el resultado [2]. Este esqueleto de código nos muestra cómo utilizar el proxy C generado:

1
2
3
4
5
6
7
8
9
    try { // Operación de llamada al servicio web
      rdvmedecins.ws.WsDaoJpaService service = new rdvmedecins.ws.WsDaoJpaService();
      rdvmedecins.ws.WsDaoJpa port = service.getWsDaoJpaPort();
       // TODO procesar el resultado aquí
      java.util.List<rdvmedecins.ws.Client> result = port.getAllClients();
      System.out.println("Result = "+result);
    } catch (Exception ex) {
       // TODO gestionar excepciones personalizadas aquí
}
  • La línea [5] nos muestra que el método [getAllClients] es un método del objeto de tipo [WsDaoJpa] definido en la línea 3. El tipo [WsDaoJpa] es una interfaz que presenta los mismos métodos que los expuestos por el servicio web remoto.
  • En la línea [3], el objeto [WsDaoJpa port] se obtiene a partir de otro objeto de tipo [WsDaoJpaService] definido en la línea 2. El tipo [WsDaoJpaService] representa el proxy C generado localmente.
  • El acceso al servicio web remoto puede fallar, por lo que todo el código está rodeado por un try / catch.
  • Los objetos del proxy C se encuentran en el paquete [rdvmedecins.ws]

Una vez entendido este código, se ve que la referencia local del servicio web remoto se puede obtener mediante el código:

WsDaoJpa dao=new WsDaoJpaService().getWsDaoJpaPort();

El código de la clase de prueba JUnit queda entonces así:

package dao;

import rdvmedecins.ws.Client;
import rdvmedecins.ws.Creneau;
import rdvmedecins.ws.Medecin;
import rdvmedecins.ws.Rv;
import rdvmedecins.ws.WsDaoJpa;
import rdvmedecins.ws.WsDaoJpaService;
...

public class MainTestsDaoRemote {

   // Capa [dao] probada
  private static WsDaoJpa dao;

  @BeforeClass
  public static void init(){
    dao=new WsDaoJpaService().getWsDaoJpaPort();
  }

  @Test
  public void test1() {
...
  }

   // método de utilidad: muestra los elementos de una colección
  private static void display(String message, List elements) {
 ...
  }
}

Ya estamos listos para las pruebas:

En [1], se ejecuta la prueba JUnit. En [2], se supera. Si observamos los mensajes que aparecen en la consola de NetBeans, encontramos líneas como las siguientes:

Liste des clients :
rdvmedecins.ws.Client@1982fc1
rdvmedecins.ws.Client@676437
rdvmedecins.ws.Client@1e4853f
rdvmedecins.ws.Client@1e808ca

En el lado del servidor, la entidad [Client] tiene un método toString que muestra los diferentes campos de un objeto de tipo [Client]. Durante la generación automática del proxy C, las entidades se crean en el proxy C, pero solo con los campos privados acompañados de sus métodos get / set. Por lo tanto, el método toString no se ha generado en la entidad [Client] del proxy C. Esto explica la visualización anterior. Esto no resta valor a la prueba JUnit: se ha superado. A partir de ahora se considerará que se dispone de un servicio web operativo.