Skip to content

4. O Serviço Web J2EE para Agendamentos

Voltemos à arquitetura da aplicação a ser desenvolvida:

Nesta secção, vamos concentrar-nos na criação do serviço web J2EE [1] a ser executado num servidor Sun/Glassfish.

4.1. A base de dados

A base de dados, a que chamaremos [ dbrdvmedecins], é uma base de dados MySQL5 com quatro tabelas:

Image

4.1.1. A tabela [MEDECINS]

Contém informações sobre os médicos geridos pela aplicação [RdvMedecins].

  • ID: o número de identificação do médico — a chave primária da tabela
  • VERSÃO: um número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
  • LAST_NAME: o apelido do médico
  • FIRST_NAME: o nome próprio do médico
  • TITLE: o seu título (Sra., Sra., Sr.)

4.1.2. A tabela [CLIENTS]

Os clientes dos vários médicos estão armazenados na tabela [CLIENTS]:

  • ID: número de identificação do cliente - chave primária da tabela
  • VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
  • APELIDO: o apelido do cliente
  • NOME: o nome do cliente
  • TÍTULO: o seu título (Sra., Sra., Sr.)

4.1.3. A tabela [SLOTS]

Apresenta os horários disponíveis para marcação de consultas:

  • ID: Número de identificação do intervalo de tempo - chave primária da tabela (linha 8)
  • VERSION: Um número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
  • DOC_ID: Número de identificação que identifica o médico a quem este intervalo de tempo pertence – chave estrangeira na coluna DOCTORS(ID).
  • START_TIME: Hora de início do intervalo de tempo
  • MSTART: Minutos de início do intervalo de tempo
  • HFIN: hora de fim do intervalo
  • MFIN: Minutos de fim do intervalo

A segunda linha da tabela [SLOTS] (ver [1] acima) indica, por exemplo, que o intervalo n.º 2 começa às 8h20 e termina às 8h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER).

4.1.4. A tabela [RV]

Apresenta a lista de consultas marcadas para cada médico:

  • ID: identificador único da consulta – chave primária
  • DAY: dia da consulta
  • SLOT_ID: intervalo horário da consulta – chave estrangeira no campo [ID] da tabela [SLOTS] – determina tanto o intervalo horário como o médico envolvido.
  • CLIENT_ID: ID do cliente para quem a reserva é feita – chave estrangeira no campo [ID] da tabela [CLIENTS]

Esta tabela tem uma restrição de unicidade nos valores das colunas associadas (DAY, SLOT_ID):

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

Se uma linha na tabela [RV] tiver o valor (DAY1, SLOT_ID1) para as colunas (DAY, SLOT_ID), este valor não pode aparecer em mais nenhum outro local. Caso contrário, isso significaria que foram marcadas duas consultas ao mesmo tempo para o mesmo médico. Do ponto de vista da programação Java, o controlador JDBC da base de dados lança uma SQLException quando isto ocorre.

A linha com ID igual a 3 (ver [1] acima) significa que foi marcada uma consulta para o horário n.º 20 e o cliente n.º 4 em 23/08/2006. A tabela [SLOTS] indica-nos que o horário n.º 20 corresponde ao intervalo horário das 16h20 às 16h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER). A tabela [CLIENTS] indica-nos que o cliente n.º 4 é a Sra. Brigitte BISTROU.

4.2. Criação da base de dados

Crie a base de dados MySQL [dbrdvmedecins] utilizando a ferramenta da sua escolha. Para criar as tabelas e preenchê-las, pode utilizar o script [createbd.sql] fornecido. O seu conteúdo é o seguinte:

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. Componentes da arquitetura do lado do servidor

Voltemos à arquitetura da aplicação a ser construída:

No lado do servidor, a aplicação será composta por:

  1. uma camada JPA que permite a interação com a base de dados utilizando objetos
  1. um EJB responsável por gerir as operações com a camada JPA
  2. um serviço web responsável por expor a interface EJB a clientes remotos sob a forma de um serviço web.

Os elementos (b) e (c) implementam a camada [DAO] apresentada no diagrama anterior. Sabemos que uma aplicação pode aceder a um EJB remoto através dos protocolos RMI e JNDI. Na prática, isto limita os clientes a clientes Java. Um serviço web utiliza um protocolo de comunicação padronizado que várias linguagens implementam: .NET, PHP, C++, ... É isto que pretendemos demonstrar aqui utilizando um cliente .NET.

Para uma breve introdução aos serviços web, consulte o curso [ref1], parágrafo 14, página 109.

Um serviço web pode ser implementado de duas formas:

  • por uma classe anotada com @WebService que é executada num contentor web
  • por um EJB anotado com @WebService que é executado num contentor EJB

Vamos utilizar a primeira solução aqui:

No curso [ref1], parágrafo 14, página 109, encontrará um exemplo que utiliza a segunda solução.

4.4. , e a configuração do Hibernate para o servidor GlassFish

Dependendo da versão, o servidor GlassFish V2 incluído no NetBeans pode não ter as bibliotecas Hibernate necessárias para a camada JPA/Hibernate. Se, ao prosseguir com o tutorial, verificar que o GlassFish não fornece uma implementação JPA/Hibernate, ou se ocorrer uma exceção durante a implementação do serviço indicando que as bibliotecas Hibernate não foram encontradas, deve adicionar as bibliotecas à pasta [<glassfish>/domains/domain1/lib/ext] e, em seguida, reiniciar o servidor GlassFish:

  • em [1], a pasta <glassfish>/.../lib/ext
  • em [2], as bibliotecas Hibernate e alguns controladores JDBC
  • em [3], o controlador JDBC do MySQL

As bibliotecas Hibernate estão incluídas no ficheiro ZIP que acompanha o tutorial.

4.5. Ferramentas de geração automática do NetBeans

Voltemos à arquitetura que precisamos de construir:

Com o NetBeans, é possível gerar automaticamente a camada [JPA] e a camada [EJB] que controla o acesso às entidades JPA geradas. É útil familiarizar-se com estes métodos de geração automática, pois o código gerado fornece informações valiosas sobre como escrever entidades JPA ou o código EJB que as utiliza.

Vamos agora descrever algumas destas ferramentas de geração automática. Para compreender o código gerado, é necessário ter um conhecimento sólido das entidades JPA [ref1] e dos EJBs [ref2].

Criar uma ligação do NetBeans à base de dados

  • Inicie o SGBD MySQL 5 para que a base de dados fique disponível
  • Crie uma ligação do NetBeans à base de dados [dbrdvmedecins]
  • No separador [Ficheiros], na secção [Bases de dados] [1], selecione o controlador JDBC do MySQL [2]
  • Em seguida, selecione a opção [3] «Ligar utilizando» para criar uma ligação a uma base de dados MySQL
  • Em [4], introduza as informações solicitadas
  • e, em seguida, confirme em [5]
  • Em [6], a ligação é criada. Pode ver as quatro tabelas na base de dados ligada.

Criação de um projeto EJB

  • Em [1], crie uma nova aplicação, um módulo EJB
  • Em [2], selecione a categoria [Java EE] e, em [3], selecione o tipo [Módulo EJB]
  • Em [4], escolha uma pasta para o projeto e, em [5], atribua-lhe um nome — depois conclua o assistente
  • Em [6], o projeto gerado

Adicionar um recurso JDBC ao servidor GlassFish

Vamos adicionar um recurso JDBC ao servidor GlassFish.

  • No separador [Serviços], inicie o servidor GlassFish [2, 3]
  • No separador [Projectos], clique com o botão direito do rato no projecto EJB e, em [5], selecione a opção [Novo / Outro] para adicionar um elemento ao projecto.

Image

  • Em [6], selecione a categoria [GlassFish] e, em [7], especifique que pretende criar um recurso JDBC selecionando o tipo [Recurso JDBC]
  • Em [8], especifique que este recurso JDBC utilizará o seu próprio conjunto de ligações
  • Em [9], atribua um nome ao recurso JDBC
  • Em [10], avance para o passo seguinte
  • Em [11], defina as características do conjunto de ligações do recurso JDBC
  • Em [12], atribua um nome ao conjunto de conexões
  • Em [13], selecione a ligação do NetBeans [dbrdvmedecins] criada anteriormente
  • em [14], avance para o passo seguinte
  • Em [15], normalmente não há nada a alterar nesta página. As propriedades da ligação à base de dados MySQL [dbrdvmedecins] foram obtidas a partir das propriedades da ligação do NetBeans [dbrdvmedecins] criada anteriormente
  • Em [16], avance para o passo seguinte
  • em [17], mantenha os valores predefinidos fornecidos
  • em [18] e conclua o assistente. Isto cria o ficheiro [sun-resources.xml] [19] com o seguinte conteúdo:
<?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>

O ficheiro acima contém todas as informações introduzidas no assistente em formato XML. Será utilizado pelo IDE NetBeans para instruir o servidor GlassFish a criar o recurso «jdbc/dbrdvmedecins» definido na linha 4.

Criação de uma unidade de persistência

A unidade de persistência [persistence.xml] configura a camada JPA: especifica a implementação JPA utilizada (TopLink, Hibernate, etc.) e configura-a.

  • Em [1], clique com o botão direito do rato no projeto EJB e selecione [Novo / Outro] em [2]
  • Em [3], selecione a categoria [Persistência] e, em seguida, em [4], indique que pretende criar uma unidade de persistência JPA
  • Em [5], atribua um nome à unidade de persistência criada
  • Em [6], escolha [Hibernate] como implementação JPA
  • Em [7], selecione o recurso GlassFish «jdbc/dbrdvmedecins» que acabou de ser criado
  • em [8], especifique que nenhuma ação deve ser realizada na base de dados ao instanciar a camada JPA
  • Conclua o assistente
  • Em [9], o ficheiro [persistence.xml] criado pelo assistente

O seu conteúdo é o seguinte:

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>

Mais uma vez, converte as informações fornecidas no assistente para o formato XML. Este ficheiro não é suficiente para trabalhar com a base de dados MySQL5 «dbrdvmedecins». Teríamos de especificar o tipo de SGBD a ser gerido pelo Hibernate. Isto será feito mais tarde.

Criação de entidades JPA

 
  • Em [1], clique com o botão direito do rato no projeto e, em [2], selecione a opção [Novo / Outro]
  • Em [3], selecione a categoria [Persistência] e, em seguida, em [4], indique que pretende criar entidades JPA a partir de uma base de dados existente.
  • Em [5], selecione a fonte JDBC «jdbc/dbrdvmedecins» que criámos
  • Em [6], selecione as quatro tabelas da base de dados associada
  • Em [7,8], inclua-as todas na geração de entidades JPA
  • Em [9], continue com o assistente
  • Em [10], as entidades JPA que serão geradas
  • em [11], nomeie o pacote das entidades JPA
  • em [12], escolha o tipo Java que irá encapsular as listas de objetos devolvidas pela camada JPA
  • Conclua o assistente
  • em [13], as quatro entidades JPA geradas, uma para cada tabela da base de dados.

Aqui está, por exemplo, o código para a entidade [Rv], que representa uma linha na tabela [rv] da base de dados [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() {
  }

...
}

Criação da camada EJB para aceder a entidades JPA

  • Em [1], clique com o botão direito do rato no projeto e, em [2], selecione a opção [Novo / Outros]
  • em [3], selecione a categoria [Persistência] e, em seguida, em [4], selecione o tipo [Session Beans para classes de entidade]
  • em [5], são apresentadas as entidades JPA criadas anteriormente
  • Em [6], selecione todas
  • Em [7], elas foram selecionadas
  • Em [8], continue com o assistente
  • Em [9], atribua um nome ao pacote para os EJBs que serão gerados
  • Em [10], especifique que os EJBs devem implementar tanto uma interface local como uma interface remota
  • Conclua o assistente
  • em [11], os EJBs gerados

Aqui, por exemplo, está o código EJB que gere o acesso à entidade [Rv] e, consequentemente, à tabela [rv] na base de dados [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 mencionado, a geração automática de código pode ser muito útil para dar início a um projeto e aprender sobre entidades JPA e EJBs. Nas secções seguintes, reescreveremos as camadas JPA e EJB com o nosso próprio código, mas o leitor reconhecerá as informações que acabámos de abordar na geração automática das camadas.

4.6. O projeto NetBeans para o módulo EJB

Criamos um novo módulo EJB vazio (ver secção 4.5):

 
  • O pacote [rdvmedecins.entites] contém as entidades da camada JPA
  • o pacote [rdvmedecins.dao] implementa o EJB para a camada [dao]
  • o pacote [rdvmedecins.exceptions] implementa uma classe de exceção específica da aplicação

A seguir, partimos do princípio de que o leitor seguiu todos os passos da secção 4.5. Será necessário repetir alguns deles.

4.6.1. Configurar a camada JPA

Vamos rever a arquitetura da nossa aplicação cliente/servidor:

O projeto NetBeans:

 

A camada [JPA] é configurada pelos ficheiros [persistence.xml] e [sun-resources.xml] acima. Estes dois ficheiros são gerados pelos assistentes que já conhecemos:

  • A geração do ficheiro [sun-resources.xml] foi descrita na Secção 4.5.
  • A geração do ficheiro [persistence.xml] foi descrita na secção 4.5.

O ficheiro [persistence.xml] gerado deve ser modificado da seguinte forma:

<?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>
       <!-- Dialect -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>
  • Linha 3: O tipo de transação é JTA: as transações serão geridas pelo contentor EJB3 do GlassFish
  • linha 4: É utilizada uma implementação JPA/Hibernate. Para tal, a biblioteca Hibernate foi adicionada ao servidor GlassFish (ver secção 4.4).
  • linha 5: a fonte de dados JTA utilizada pela camada JPA tem o nome JNDI «jdbc/dbrdvmedecins».
  • Linha 8: Esta linha não é gerada automaticamente. Deve ser adicionada manualmente. Indica ao Hibernate que o SGBD utilizado é o MySQL5.

A fonte de dados «jdbc/dbrdvmedecins» está configurada no seguinte ficheiro [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>
  • Linhas 8–10: As propriedades JDBC da fonte de dados (URL da base de dados, nome de utilizador e palavra-passe). A base de dados MySQL `dbrdvmedecins` é a descrita na Secção 4.1.
  • Linha 7: As características do conjunto de ligações associado a esta fonte de dados

4.6.2. As entidades da camada JPA

Vamos rever a arquitetura da nossa aplicação cliente/servidor:

O projeto NetBeans:

O pacote [rdvmedecins.entites] implementa a camada [Jpa].

Na Secção 4.5, vimos como gerar automaticamente entidades JPA para uma aplicação. Não utilizaremos esta técnica aqui, mas definiremos as entidades nós próprios. No entanto, estas irão incorporar grande parte do código gerado na Secção 4.5. Aqui, queremos que as entidades [Medecin] e [Client] sejam classes filhas de uma classe [Personne].

A classe Person é utilizada para representar médicos e clientes:

package rdvmedecins.entites;
...
@MappedSuperclass
public class Personne implements Serializable {
   // characteristics of a person

  @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;

   // default builder
  public Personne() {
  }

   // builder with parameters
  public Personne(String titre, String nom, String prenom) {
     // we use setters
...
  }

   // copy builder
  public Personne(Personne personne) {
     // we use setters
 ...
  }

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

// getters and setters
....
}
  • Linha 3: Note que a classe [Person] não é, por si só, uma entidade (@Entity). Será a classe pai das entidades. A anotação @MappedSuperClass indica esta situação.

A entidade [Client] encapsula as linhas da tabela [clients]. Ela deriva da classe [Person] anterior:

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

   // default builder
  public Client() {
  }

   // builder with parameters
  public Client(String titre, String nom, String prenom) {
     // parent
    super(titre, nom, prenom);
  }

   // copy builder
  public Client(Client client) {
     // parent
    super(client);
  }
}
  • Linha 3: A classe [Client] é uma entidade JPA
  • linha 4: está associada à tabela [clients]
  • linha 5: deriva da classe [Person]

A entidade [Doctor], que encapsula as linhas da tabela [doctors], segue o mesmo padrão:

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

   // default builder
  public Medecin() {
  }

   // builder with parameters
  public Medecin(String titre, String nom, String prenom) {
     // parent
    super(titre, nom, prenom);
  }

   // copy builder
  public Medecin(Medecin medecin) {
     // parent
    super(medecin);
  }
}

A entidade [Creneau] encapsula as linhas da tabela [creneaux]:

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

   // characteristics of a RV slot
  @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;

   // default builder
  public Creneau() {

  }

   // builder with parameters
  public Creneau(Medecin medecin, Integer hDebut,Integer mDebut, Integer hFin, Integer mFin) {
     // we use setters
...
  }

   // copy builder
  public Creneau(Creneau creneau) {
     // we use setters
...
  }

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

   // setters - getters
...
}
  • As linhas 15–17 modelam a relação «um-para-muitos» entre a tabela [slots] e a tabela [doctors] na base de dados.

A entidade [Rv] encapsula as linhas da tabela [rv]:

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

  @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;

   // default builder
  public Rv() {
  }

   // builder with parameters
  public Rv(Date jour, Client client, Creneau creneau) {
     // we use setters
...
  }

   // copy builder
  public Rv(Rv rv) {
     // we use setters
...
  }

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

// getters and setters
...
}
  • As linhas 15–17 modelam a relação «um-para-muitos» entre a tabela [rv] e a tabela [clients] na base de dados, e as linhas 18–20 modelam a relação «um-para-muitos» entre a tabela [rv] e a tabela [slots]

4.6.3. A classe de exceção

A classe de exceção da aplicação [ RdvMedecinsException] é a seguinte:

package rdvmedecins.exceptions;

import javax.ejb.ApplicationException;

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

  private static final long serialVersionUID = 1L;

   // private fields
  private int code = 0;

   // manufacturers
  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
...
}
  • Linha 6: A classe estende a classe [RuntimeException]. Por isso, o compilador não exige que seja tratada com blocos try/catch.
  • Linha 5: a anotação @ApplicationException garante que a exceção não será «engolida» por uma [EjbException].

Para compreender a anotação @ApplicationException, vamos rever a arquitetura do lado do servidor:

A exceção [RdvMedecinsException] será lançada pelos métodos EJB na camada [dao] dentro do contentor EJB3 e interceptada por este. Sem a anotação @ApplicationException, o contentor EJB3 encapsula a exceção que ocorreu dentro de uma [EjbException] e relança-a. Pode não querer esta encapsulação e preferir deixar que uma exceção do tipo [RdvMedecinsException] escape do contentor EJB3. É isso que a anotação @ApplicationException permite. Além disso, o atributo (rollback=true) desta anotação instrui o contentor EJB3 para que, se ocorrer uma exceção do tipo [RdvMedecinsException] dentro de um método executado como parte de uma transação com um SGBD, a transação seja revertida. Em termos técnicos, isto é chamado de reversão da transação.

4.6.4. O EJB da camada [dao]

A interface Java [ IDao] da camada [dao] é a seguinte:

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

   // customer list
  public List<Client> getAllClients();
   // list of doctors
  public List<Medecin> getAllMedecins();
   // list of physician slots
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // list of doctor's appointments on a given day
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour);
   // find a customer identified by its id
  public Client getClientById(Long id);
   // find a customer identified by its id
  public Medecin getMedecinById(Long id);
   // find an Rv identified by its id
  public Rv getRvById(Long id);
   // find a time slot identified by its id
  public Creneau getCreneauById(Long id);
   // add a RV to the list
  public Rv ajouterRv(String jour, Creneau creneau, Client client);
   // delete a RV
  public void supprimerRv(Rv rv);
}

A interface local do EJB [IDaoLocal] simplesmente estende a interface anterior [IDao]:

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

import javax.ejb.Local;

@Local
public interface IDaoLocal extends IDao{
}

O mesmo se aplica à interface remota [IDaoRemote]:

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

import javax.ejb.Remote;

@Remote
public interface IDaoRemote extends IDao {
}

O EJB [DaoJpa] implementa tanto a interface local como a remota:

1
2
3
4
5
6
7
package rdvmedecins.dao;
...
@Stateless(mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal,IDaoRemote {
...
}
  • A linha 3 indica que o EJB remoto se chama "rdvmedecins.dao"
  • A linha 4 indica que todos os métodos do EJB são executados dentro de uma transação gerida pelo contentor EJB3.
  • A linha 5 mostra que o EJB implementa as interfaces local e remota.

O código EJB completo é o seguinte:

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

  @PersistenceContext
  private EntityManager em;

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

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

   // list of time slots for a given doctor
   // doctor: the doctor
  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);
    }
  }

   // list of appointments for a given doctor on a given day
   // doctor: the doctor
   // day: the day
  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);
    }
  }

   // add Rv
   // day : day of appointment
   // creneau: Rv time slot
   // customer: customer for whom the appointment is taken
  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);
    }
  }

   // deleting an appointment
   // rv: Rv deleted
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

   // retrieve a specific customer
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 7);
    }
  }

   // retrieve a specific doctor
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 8);
    }
  }

   // retrieve a given Rv
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 9);
    }
  }

   // retrieve a given slot
  public Creneau getCreneauById(Long id) {
    try {
      return (Creneau) em.find(Creneau.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 10);
    }
  }
}
  • Linha 8: o objeto EntityManager que gere o acesso ao contexto de persistência. Quando a classe for instanciada, este campo será inicializado pelo contentor EJB utilizando a anotação @PersistenceContext na linha 7.
  • linha 15: consulta JPQL que devolve todas as linhas da tabela [clients] como uma lista de objetos [Client].
  • linha 22: uma consulta semelhante para médicos
  • Linha 32: Uma consulta JPQL que realiza uma junção entre as tabelas [slots] e [doctors]. É parametrizada pelo ID do médico.
  • Linha 43: Uma consulta JPQL que realiza uma junção entre as tabelas [appointments], [slots] e [doctors] e que tem dois parâmetros: o ID do médico e a data da consulta.
  • Linhas 55–57: criação de uma consulta e a sua subsequente persistência na base de dados.
  • Linha 67: Elimina uma consulta da base de dados.
  • Linha 76: executa uma consulta SELECT na base de dados para encontrar um cliente específico
  • Linha 85: O mesmo para um médico
  • Linha 94: o mesmo para uma consulta
  • Linha 103: o mesmo para um intervalo de tempo
  • Todas as operações que envolvem o contexto de persistência da linha 9 são suscetíveis de encontrar um problema com a base de dados. Por isso, estão todas incluídas num bloco try/catch. Qualquer exceção é encapsulada na exceção personalizada RdvMedecinsException.

Depois de compilado, o módulo EJB gera um ficheiro .jar com o nome « »:

4.7. Implementação do EJB da camada [DAO] com o NetBeans

O NetBeans permite-lhe implementar facilmente o EJB criado anteriormente no servidor GlassFish.

  • Nas propriedades do projeto EJB, verifique as opções de tempo de execução [1].
  • Em [2], o nome do servidor no qual o EJB será implementado
  • No separador [Serviços] [3], inicie-o [4].
  • Em [5], o servidor GlassFish após ter sido iniciado. Ainda não possui um módulo EJB.
  • Inicie o servidor MySQL e certifique-se de que a base de dados [dbrdvmedecins] está online. Para tal, pode utilizar a ligação NetBeans criada na secção 4.5.
  • No separador [Projects] [6], implemente o módulo EJB [7]: o SGBD MySQL5 deve estar em execução para que o recurso JDBC «jdbc/dbrdvmedecins», utilizado pelo EJB, esteja acessível.
  • Em [8], o EJB implementado aparece na árvore do servidor GlassFish
  • Em [9], remova o EJB implementado
  • Em [10], o EJB já não aparece na árvore do servidor GlassFish.

4.8. Implantar o EJB a partir da camada [DAO] com o GlassFish

Aqui mostramos como implementar um EJB no servidor GlassFish a partir do seu arquivo .jar.

  • Inicie o servidor MySQL e certifique-se de que a base de dados [dbrdvmedecins] está online. Para tal, pode utilizar a ligação do NetBeans criada na Secção 4.5.

Vamos rever a configuração JPA do módulo EJB a ser implementado. Esta configuração está definida no ficheiro [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>
       <!-- Dialect -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>

A linha 5 indica que a camada JPA utiliza uma fonte de dados JTA, ou seja, uma fonte gerida pelo contentor EJB3, denominada «jdbc/dbrdvmedecins».

Vimos na Secção 4.5 como criar este recurso JDBC utilizando o NetBeans. Aqui, mostramos como fazê-lo diretamente com o GlassFish. Seguimos o procedimento descrito na Secção 13.1.2, página 79 de [ref1].

Começamos por eliminar o recurso para que possamos recriá-lo. Fazemos isto a partir do NetBeans:

  • em [1], os recursos JDBC do servidor GlassFish
  • em [2], o recurso «jdbc/dbrdvmedecins» do nosso EJB
  • em [3], o conjunto de ligações para este recurso JDBC
  • em [4], eliminamos o conjunto de ligações. Isto terá como efeito a eliminação de todos os recursos JDBC que o utilizam, incluindo o recurso «jdbc/dbrdvmedecins».
  • em [5] e [6], o recurso JDBC e o conjunto de ligações foram destruídos.

Agora, utilizamos a consola de administração do servidor GlassFish para criar o recurso JDBC e implementar o EJB.

  • No separador [Serviços] [1] do NetBeans, inicie o servidor GlassFish [2] e, em seguida, aceda [3] à sua consola de administração
  • Em [4], inicie sessão como administrador (palavra-passe: adminadmin, caso não a tenha alterado durante a instalação ou posteriormente).
  • Em [5], selecione o ramo [Connection Pools] dos recursos do GlassFish
  • Em [6], crie um novo conjunto de conexões. Note que um conjunto de conexões é uma técnica para limitar o número de conexões abertas e fechadas com um SGBD. Quando o servidor é iniciado, N conexões — um número definido pela configuração — são abertas para o SGBD. Estas conexões abertas são então disponibilizadas aos EJBs que as solicitam para realizar uma operação com o SGBD. Assim que a operação é concluída, o EJB devolve a conexão ao conjunto. A ligação nunca é encerrada; é partilhada entre os vários threads que acedem ao SGBD
  • em [7], atribua um nome ao pool
  • em [8], a classe que modela a fonte de dados é a classe [javax.sql.DataSource]
  • em [9], o SGBD que contém a fonte de dados é o MySQL neste caso.
  • em [10], avance para o passo seguinte
  • em [11], o atributo «Validação de ligação necessária» garante que, antes de conceder uma ligação, o conjunto verifique se esta está operacional. Caso contrário, cria uma nova. Isto permite que uma aplicação continue a funcionar após uma interrupção temporária do SGBD. Durante a interrupção, nenhuma ligação está disponível e são lançadas exceções para o cliente. Quando a interrupção termina, os clientes que continuam a solicitar ligações obtêm-nas novamente: graças ao atributo «Validação de ligação necessária», todas as ligações no conjunto serão recriadas. Sem este atributo, o conjunto detetaria que as ligações iniciais foram perdidas, mas não tentaria recriar novas ligações.
  • Em [12], o nível de isolamento «Read Committed» é especificado para transações. Este nível garante que uma transação T2 não pode ler dados modificados por uma transação T1 até que esta última esteja totalmente concluída.
  • Em [13], especificamos que todas as transações devem utilizar o nível de isolamento especificado em [12]
  • Em [14] e [15], especifique o URL da base de dados cujas ligações são geridas pelo pool
  • Em [16], o utilizador será o root
  • Em [17], adicione uma propriedade
  • Em [18], adicione a propriedade «Password» com o valor () em [19]. Embora a captura de ecrã [19] não o mostre, não introduza uma cadeia de caracteres vazia; em vez disso, introduza () (parêntese de abertura, parêntese de fecho) para indicar uma palavra-passe vazia. Se o utilizador root do seu SGBD MySQL tiver uma palavra-passe não vazia, introduza essa palavra-passe.
  • Em [20], conclua o assistente de criação do conjunto de ligações para a base de dados MySQL [dbrdvmedecins].
  • Em [21], o conjunto foi criado. Clique no seu link.
  • Em [22], o botão [Ping] permite estabelecer uma ligação com a base de dados [dbrdvmedecins]
  • Em [23], se tudo correr bem, uma mensagem indica que a ligação foi bem-sucedida

Depois de criado o conjunto de conexões, pode criar um recurso JDBC:

  • Em [1], selecione o ramo [Recursos JDBC] na árvore de objetos do servidor
  • Em [2], crie um novo recurso JDBC
  • Em [3], atribua um nome ao recurso JDBC. Este deve corresponder ao nome utilizado no ficheiro [persistence.xml]:
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
  • Em [4], especifique o conjunto de ligações que o novo recurso JDBC deve utilizar: aquele que acabou de criar
  • Em [5], concluímos o assistente de criação
  • Em [6], o novo recurso JDBC

Agora que o recurso JDBC foi criado, pode implementar o ficheiro JAR do EJB:

  • Em [1], selecione o ramo [Aplicações Empresariais]
  • Em [2], clique no botão [Implantar] para indicar que pretende implantar uma nova aplicação
  • Em [3], especifique que a aplicação é um módulo EJB
  • Em [4], selecione o ficheiro JAR EJB [serveur-ejb-dao-jpa-hibernate.jar] que lhe foi fornecido para o laboratório.
  • Em [5], pode alterar o nome do módulo EJB, se desejar
  • Em [6], conclua o assistente de implementação do módulo EJB
  • Em [7], o módulo EJB foi implementado. Agora já pode ser utilizado.

4.9. Testar o EJB da camada [DAO]

Agora que o EJB para a camada [DAO] da nossa aplicação foi implementado, podemos testá-lo. Faremos isso utilizando o seguinte cliente Java:

A classe [MainTestsDaoRemote] [1] é uma classe de teste JUnit 4. As bibliotecas em [2] consistem em:

  • o ficheiro JAR EJB da camada [DAO] [3] (ver secção 4.6.4).
  • as bibliotecas GlassFish [4] necessárias para clientes EJB remotos.

A classe de teste é a seguinte:

package dao;
...
public class MainTestsDaoRemote {

   // layer [dao] tested
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
     // environment initialization JNDI
    InitialContext initialContext = new InitialContext();
     // dao layer instantiation
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }

  @Test
  public void test1() {
     // tEST DATA
    String jour = "2006:08:23";
     // customer display
    List<Client> clients = null;
    try {
      clients = dao.getAllClients();
      display("Liste des clients :", clients);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // physician display
    List<Medecin> medecins = null;
    try {
      medecins = dao.getAllMedecins();
      display("Liste des médecins :", medecins);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // display doctor's slots
    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);
    }
     // list of doctor's appointments on a given day
    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);
    }
     // add a RV to the list
    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);
    }
     // add a RV in the same slot on the same day
     // must trigger an exception
    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);
    }
     // delete a 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);
    }
  }

   // utility method - displays items in a collection
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • Linha 13: Observe a instanciação do proxy EJB remoto. Utilizamos o seu nome JNDI "rdvmedecins.dao".
  • Os métodos de teste utilizam os métodos expostos pelo EJB (ver secção 4.6.4).

Se tudo correr bem, os testes devem ser aprovados:

 

Agora que o EJB da camada [dao] está operacional, podemos prosseguir para o expor publicamente através de um serviço web.

4.10. O serviço web da camada [dao]

Para uma breve introdução ao conceito de serviço web, consulte a secção 14, página 111 de [ref1].

Voltemos à arquitetura do servidor da nossa aplicação cliente/servidor:

Estamos a concentrar-nos aqui no serviço web da camada [DAO]. O único objetivo deste serviço é disponibilizar a interface EJB da camada [DAO] a clientes multiplataforma capazes de comunicar com um serviço web.

Lembre-se de que existem duas formas de implementar um serviço web:

  • utilizando uma classe anotada com @WebService que é executada num contentor web
  • utilizando um EJB anotado com @WebService que é executado num contentor EJB

Aqui, utilizamos a primeira solução. No IDE NetBeans, precisamos de criar um projeto empresarial com dois módulos:

  • o módulo EJB que será executado no contentor EJB: o EJB da camada [DAO].
  • o módulo web que será executado no contentor web: o serviço web que estamos atualmente a construir.

Vamos construir este projeto empresarial de duas maneiras.

4.10.1. Projeto NetBeans - Versão 1

Primeiro, criamos um projeto NetBeans do tipo «Aplicação Web»:

  • Em [1], crie um novo projeto na categoria «Java Web» [2] do tipo «Aplicação Web» [3].
  • Em [4], nomeamos o projeto e, em [5], especificamos a pasta onde ele deve ser gerado
  • Em [6], especificamos o servidor de aplicações que irá executar a aplicação web
  • Em [7], defina o contexto da aplicação
  • Em [8], valide a configuração do projeto.
  • Em [9], o projeto gerado. O serviço web que estamos a construir irá utilizar o EJB do projeto anterior [10]. Por conseguinte, é necessário referenciar o ficheiro .jar do módulo EJB [10].
  • Em [11], adicione um projeto NetBeans às bibliotecas do projeto web [12]
  • Em [13], selecione a pasta do módulo EJB no sistema de ficheiros e confirme.
  • Em [14], o módulo EJB foi adicionado às bibliotecas do projeto web.

Em [15], implementamos o serviço web com a seguinte classe [WsDaoJpa]:

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

  @EJB
  private IDaoLocal dao;

   // customer list
  @WebMethod
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }

   // list of doctors
  @WebMethod
  public List<Medecin> getAllMedecins() {
    return dao.getAllMedecins();
  }

   // list of time slots for a given doctor
   // doctor: the doctor
  @WebMethod
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    return dao.getAllCreneaux(medecin);
  }

   // list of appointments for a given doctor on a given day
   // doctor: the doctor
   // day: the day
  @WebMethod
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour) {
    return dao.getRvMedecinJour(medecin, jour);
  }

   // add Rv
   // day : day of appointment
   // creneau: Rv time slot
   // customer: customer for whom the appointment is taken
  @WebMethod
  public Rv ajouterRv(String jour, Creneau creneau, Client client) {
    return dao.ajouterRv(jour, creneau, client);
  }

   // deleting an appointment
   // rv: Rv deleted
  @WebMethod
  public void supprimerRv(Rv rv) {
    dao.supprimerRv(rv);
  }

   // retrieve a specific customer
  @WebMethod
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }

   // retrieve a specific doctor
  @WebMethod
  public Medecin getMedecinById(Long id) {
    return dao.getMedecinById(id);
  }

   // retrieve a given Rv
  @WebMethod
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }

   // retrieve a given slot
  @WebMethod
  public Creneau getCreneauById(Long id) {
    return dao.getCreneauById(id);
  }
}
  • Linha 4: A classe [WsdaoJpa] implementa a interface [IDao]. Recorde-se que esta interface está definida no arquivo EJB da camada [dao] da seguinte forma:
package rdvmedecins.dao;
...
public interface IDao {

   // customer list
  public List<Client> getAllClients();
   // list of doctors
  public List<Medecin> getAllMedecins();
   // list of physician slots
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // list of doctor's appointments on a given day
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour);
   // find a customer identified by its id
  public Client getClientById(Long id);
   // find a customer identified by its id
  public Medecin getMedecinById(Long id);
   // find an Rv identified by its id
  public Rv getRvById(Long id);
   // find a time slot identified by its id
  public Creneau getCreneauById(Long id);
   // add a RV to the list
  public Rv ajouterRv(String jour, Creneau creneau, Client client);
   // delete a RV
  public void supprimerRv(Rv rv);
}
  • Linha 3: A anotação @WebService torna a classe [WsDaoJpa] um serviço web.
  • Linhas 6–7: A referência ao EJB na camada [DAO] será injetada pelo servidor de aplicações no campo da linha 7. Note-se que é sempre a implementação local (IDaoLocal, neste caso) que é injetada. Esta injeção é possível porque o serviço web é executado na mesma JVM que o EJB.
  • Todos os métodos do serviço web são marcados com a anotação @WebMethod para torná-los visíveis aos clientes remotos. Um método não marcado com a anotação @WebMethod seria interno ao serviço web e não visível aos clientes remotos. Cada método M do serviço web simplesmente chama o método M correspondente do EJB injetado na linha 7.

A criação deste serviço web é refletida por um novo ramo no projeto NetBeans:

Vemos em [1] o serviço web WsDaoJpa e em [2] os métodos que este expõe aos clientes remotos.

Vamos rever a arquitetura do serviço web em desenvolvimento:

Os componentes do serviço web que vamos implementar são:

  • [1]: o módulo web que acabámos de criar
  • [2]: o módulo EJB que criámos numa etapa anterior, do qual o serviço web depende

Para os implementar em conjunto, precisamos de combinar os dois módulos num projeto «enterprise» do NetBeans:

Em [1], crie um novo projeto empresarial [2, 3].

  • Em [4,5], nomeamos o projeto e especificamos o diretório de criação
  • Em [6], selecione o servidor de aplicações no qual a aplicação empresarial será implementada
  • Em [7], um projeto empresarial pode ter três componentes: aplicação web, módulo EJB e aplicação cliente. Aqui, o projeto é criado sem quaisquer componentes. Estes serão adicionados posteriormente.
  • Em [8], a aplicação empresarial recém-criada.
  • Em [9], clique com o botão direito do rato em [Módulos Java EE] e adicione um novo módulo
  • Em [10], são apresentados apenas os módulos do NetBeans atualmente abertos no IDE. Aqui, selecionamos o módulo web [web-service-server-1-ejb-dao-jpa-hibernate] e o módulo EJB [ejb-server-dao-jpa-hibernate] que criámos.
  • Em [11], os dois módulos adicionados ao projeto empresarial.

Precisamos agora de implementar esta aplicação empresarial no servidor GlassFish. Em seguida, o SGBD MySQL deve ser iniciado para que a fonte de dados JDBC «jdbc/dbrdvmedecins» utilizada pelo módulo EJB fique acessível.

  • Em [1], iniciamos o servidor GlassFish
  • Se o módulo EJB [ejb-server-dao-jpa-hibernate] estiver implementado, descarregamo-lo [2]
  • Em [3], implemente a aplicação empresarial
  • Em [4], está implementado. Podemos ver que contém ambos os módulos: Web e EJB.

4.10.2. Projeto NetBeans - Versão 2

Vamos agora mostrar como implementar o serviço Web quando não se dispõe do código-fonte do módulo EJB, mas apenas do seu arquivo .jar.

O novo projeto NetBeans para o serviço web será o seguinte:

Os elementos mais importantes do projeto são os seguintes:

  • [1]: O serviço web é implementado por um projeto NetBeans do tipo [Aplicação Web].
  • [2]: O serviço web é implementado pela classe [WsDaoJpa], que já estudámos
  • [3]: o arquivo EJB para a camada [DAO], que permite à classe [WsDaoJpa] aceder às definições das várias classes, interfaces e entidades nas camadas [DAO] e [JPA].

Em seguida, criamos o projeto empresarial necessário para implementar o serviço web:

  • [1] Criamos uma aplicação empresarial [ea-rdvmedecins], inicialmente sem quaisquer módulos.
  • Em [2], adicionamos o módulo web anterior [webservice-ejb-dao-jpa-hibernate]
  • em [3], o resultado.

Tal como está, a aplicação empresarial [ea-rdvmedecins] não pode ser implementada no servidor GlassFish a partir do NetBeans. Ocorre um erro. Por conseguinte, deve implementar manualmente o arquivo EAR da aplicação [ea-rdvmedecins]:

  • O arquivo [ea-rdvmedecins.ear] está localizado na pasta [dist] [2] do separador [Arquivos] no NetBeans.
  • Neste arquivo [3], encontrará os dois componentes da aplicação empresarial:
  • o arquivo EJB [ejb-server-dao-jpa-hibernate]. Este arquivo está presente porque fazia parte das bibliotecas referenciadas pelo serviço web.
  • o arquivo do serviço web [webservice-server-ejb-dao-jpa-hibernate].
  • O arquivo [ea-rdvmedecins.ear] é criado por uma simples compilação [4] da aplicação empresarial.
  • Em [5], a operação de implementação falha.

Para implementar o arquivo da aplicação empresarial [ea-rdvmedecins.ear], procedemos da mesma forma que na implementação do arquivo EJB [ejb-server-dao-jpa-hibernate.jar] na secção 4.2. Utilizamos novamente o cliente de administração web do servidor GlassFish. Não repetiremos os passos já descritos.

Primeiro, começaremos por «descarregar» a aplicação empresarial implementada na secção 4.10.1:

  • [1]: Selecione o ramo [Aplicações Empresariais] do servidor GlassFish
  • em [2] selecione a aplicação empresarial a descarregar e, em seguida, em [3] descarregue-a
  • em [4], a aplicação empresarial foi descarregada
  • Em [1], selecione o ramo [Aplicações Empresariais] do servidor GlassFish
  • em [2], implemente uma nova aplicação empresarial
  • Em [3], selecione o tipo [Aplicação Empresarial]
  • Em [4], especifique o ficheiro .ear para o projeto NetBeans [ea-rdvmedecins]
  • Em [5], implemente este arquivo
  • Em [6], a aplicação foi implementada
  • em [7], o serviço web [WsDaoJpa] aparece no ramo [Web Services] do servidor GlassFish. Selecione-o.
  • Em [8], tem vários detalhes sobre o serviço web. A informação mais relevante para um cliente é [9]: o URI do serviço web.
  • Em [10], pode testar o serviço web
  • Em [11], o URI do serviço web ao qual foi adicionado o parâmetro ?test. Este URI apresenta uma página de teste. Todos os métodos (@WebMethod) expostos pelo serviço web são apresentados e podem ser testados. Aqui, testamos o método [13], que recupera a lista de clientes.
  • Em [14], mostramos apenas uma vista parcial da página de resposta. Mas podemos ver que o método getAllClients devolveu, de facto, a lista de clientes. A captura de ecrã mostra que envia a sua resposta em formato XML.

Um serviço web é descrito na íntegra por um ficheiro XML denominado ficheiro WSDL:

  • Em [1], na ferramenta web de administração do servidor GlassFish, selecione o serviço web [WsDaoJpa]
  • Em [2], siga o link [Ver WSDL]
  • Em [3]: o URI do ficheiro WSDL. Esta é uma informação importante a saber. É necessária para configurar os clientes para este serviço web.
  • Em [4], a descrição XML do serviço web. Não iremos comentar este conteúdo complexo.

4.10.3. Testes JUnit para o serviço web

Criamos um projeto NetBeans para «executar» os testes anteriormente realizados com um cliente EJB, desta vez utilizando um cliente para o serviço web recentemente implementado. Aqui, seguimos um procedimento semelhante ao descrito na secção 14.2.1, página 115 de [ref1].

  • em [1], um projeto Java clássico
  • Em [2], a classe de teste
  • em [3], o cliente utiliza o arquivo EJB para aceder às definições da interface da camada [DAO] e às entidades JPA. Note-se que este arquivo se encontra na subpasta [dist] da pasta do módulo EJB.

Para aceder ao serviço web remoto, é necessário gerar classes proxy:

No diagrama acima, a camada [2] [C=Cliente] comunica com a camada [1] [S=Servidor]. Para interagir com a camada [S], o cliente [C] deve estabelecer uma ligação de rede com a camada [S] e comunicar com ela utilizando um protocolo específico. As ligações de rede são ligações TCP e o protocolo de transporte é HTTP. A camada [S], que representa o serviço web, é implementada por um servlet Java em execução no servidor GlassFish. Não escrevemos este servlet. A sua geração é automatizada pelo GlassFish com base nas anotações @WebService e @WebMethod na classe [WsDaoJpa] que escrevemos. Da mesma forma, iremos automatizar a geração da camada cliente [C]. A camada [C] é por vezes designada por camada proxy para o serviço web remoto, sendo que o termo «proxy» se refere a um elemento intermediário numa cadeia de software. Aqui, o proxy C atua como intermediário entre o cliente que estamos prestes a escrever e o serviço web que implementámos.

Com o NetBeans 6.5, o proxy C pode ser gerado da seguinte forma (para o resto do processo, o serviço web deve estar a ser executado no servidor GlassFish):

  • Em [1], adicione um novo elemento ao projeto Java
  • em [2], selecione o ramo [Serviços Web]
  • em [3], selecione [Cliente de Serviço Web]
  • em [4], forneça o URI do ficheiro WSDL do serviço Web. Este URI foi apresentado na secção 4.10.2.
  • Em [5], mantenha o valor padrão [JAX-WS]. O outro valor possível é [JAX-RPC]
  • Após confirmar o assistente de criação do proxy do serviço web, o projeto NetBeans foi expandido para incluir um ramo [Referências de Serviço Web] [6]. Este ramo exibe os métodos expostos pelo serviço web remoto.
  • No separador [Ficheiros] [7], foi adicionado código-fonte Java [8]. Este corresponde ao proxy C gerado.
  • Em [9] encontra-se o código de uma das classes. Podemos ver [10] que foram colocadas num pacote [rdvmedecins.ws]. Não iremos comentar o código destas classes, que é, mais uma vez, bastante complexo.

Para o cliente Java que estamos a construir, o proxy C gerado atua como intermediário. Para aceder ao método M do serviço web remoto, o cliente Java chama o método M do proxy C. O cliente Java chama, assim, métodos locais (executados na mesma JVM) e, de forma transparente para ele, estas chamadas locais são traduzidas em chamadas remotas.

Ainda precisamos de saber como chamar os métodos M do proxy C. Voltemos à nossa classe de teste JUnit:

Em [1], a classe de teste [MainTestsDaoRemote] é a mesma já utilizada ao testar o EJB da camada [dao]:

package dao;
...
public class MainTestsDaoRemote {

   // layer [dao] tested
  private static IDaoRemote dao;

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

  @Test
  public void test1() {
...
  }
}
  • linha [13], o teste test1 permanece inalterado.
  • linha [9], o conteúdo do método [init] foi eliminado.

Nesta altura, o projeto contém erros porque o método de teste [test1] utiliza as entidades [Client], [Medecin], [Creneau] e [Rv], que já não se encontram nos mesmos pacotes que antes. Estão agora no pacote proxy C gerado. Removemos as instruções de importação relevantes e regeneramo-las utilizando a operação Fix Imports.

Voltemos ao código da classe de teste [MainTestsDaoRemote]:

package dao;
...

public class MainTestsDaoRemote {

   // layer [dao] tested
  private static IDaoRemote dao;

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

O método [init] na linha 10 deve inicializar a referência à camada [dao] na linha 7. Precisamos de saber como utilizar o proxy C gerado no nosso código. O NetBeans ajuda-nos neste processo.

  • Selecione o método [getAllClients] do serviço web em [1] com o rato, depois arraste esse método e solte-o no método [init] da classe de teste.

Obtemos o resultado [2]. Este esqueleto de código mostra-nos como utilizar o proxy C gerado:

1
2
3
4
5
6
7
8
9
    try { // Call Web Service Operation
      rdvmedecins.ws.WsDaoJpaService service = new rdvmedecins.ws.WsDaoJpaService();
      rdvmedecins.ws.WsDaoJpa port = service.getWsDaoJpaPort();
       // TODO process result here
      java.util.List<rdvmedecins.ws.Client> result = port.getAllClients();
      System.out.println("Result = "+result);
    } catch (Exception ex) {
       // TODO handle custom exceptions here
}
  • A linha [5] mostra que o método [getAllClients] é um método do objeto [WsDaoJpa] definido na linha 3. O tipo [WsDaoJpa] é uma interface que expõe os mesmos métodos que aqueles expostos pelo serviço web remoto.
  • Na linha [3], o objeto [WsDaoJpaPort] é obtido a partir de outro objeto do tipo [WsDaoJpaService] definido na linha 2. O tipo [WsDaoJpaService] representa o proxy C gerado localmente.
  • O acesso ao serviço web remoto pode falhar, pelo que todo o bloco de código está incluído num bloco try/catch.
  • Os objetos proxy C encontram-se no pacote [rdvmedecins.ws]

Uma vez compreendido este código, vemos que a referência local ao serviço web remoto pode ser obtida utilizando o seguinte código:

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

O código para a classe de teste JUnit passa então a ser o seguinte:

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 {

   // layer [dao] tested
  private static WsDaoJpa dao;

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

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

   // utility method - displays items in a collection
  private static void display(String message, List elements) {
 ...
  }
}

Estamos agora prontos para o teste:

Em [1], o teste JUnit é executado. Em [2], o teste é aprovado. Se observarmos a saída na consola do NetBeans, vemos linhas como as seguintes:

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

No lado do servidor, a entidade [Client] possui um método toString que exibe os vários campos de um objeto [Client]. Durante a geração automática do proxy C, as entidades são criadas no proxy C, mas apenas com os campos privados acompanhados pelos seus métodos get/set. Assim, o método toString não foi gerado na entidade [Client] do proxy C. Isto explica a exibição anterior. Isto não prejudica o teste JUnit: este foi aprovado. Vamos agora considerar que temos um serviço web operacional.