Skip to content

3. Aplicação de exemplo – 01: rdvmedecins-jsf2-ejb

O texto que se segue faz referência aos seguintes documentos:

  • [ref7]: Introdução ao Java EE 5 (junho de 2010) [http://tahe.developpez.com/java/javaee]. Este documento permite conhecer o JSF 1 e os EJB3.
  • [ref8]: Persistência em Java na prática (junho de 2007) [http://tahe.developpez.com/java/jpa]. Este documento permite conhecer a persistência de dados com JPA (Java Persistence API).
  • [ref9]: Criar um serviço web Java EE com o NetBeans e o servidor GlassFish (janeiro de 2009) [http://tahe.developpez.com/java/webservice-jee]. Este documento aborda a criação de um serviço web.

A aplicação de exemplo que será analisada provém de [ref9].

3.1. L'application

Uma empresa de serviços informáticos, a [ISTIA-AGI], pretende oferecer um serviço de marcação de consultas. O primeiro mercado-alvo é o dos médicos que exercem a profissão de forma independente. Estes, em geral, não dispõem de secretariado. Os clientes que pretendem marcar uma consulta ligam, então, diretamente para o médico. Este é, assim, frequentemente incomodado ao longo do dia, o que diminui a sua disponibilidade para com os doentes. A empresa [ISTIA-AGI] pretende propor-lhes um serviço de marcação de consultas que funcione segundo o seguinte princípio:

  • um secretariado assegura a marcação de consultas para um grande número de médicos. Este secretariado pode ser reduzido a uma única pessoa. O salário desta pessoa é partilhado entre todos os médicos que utilizam o serviço.
  • O secretariado e todos os médicos estão ligados à Internet
  • as marcações são registadas numa base de dados centralizada, acessível pela Internet, tanto pela secretaria como pelos médicos
  • A emissão de RV é normalmente efetuada pela secretaria. Pode também ser efetuada pelos próprios médicos. É o caso, nomeadamente, quando, no final de uma consulta, o próprio médico atribui um novo RV ao seu doente.

A arquitetura do serviço de atribuição do RV é a seguinte:

Os médicos ganham em eficiência se deixarem de ter de gerir os RV. Se forem em número suficiente, a sua contribuição para as despesas de funcionamento do secretariado será reduzida.

A empresa [ISTIA-AGI] decide desenvolver a aplicação em duas versões:

  • uma versão JSF / EJB3 / JPA EclipseLink / servidor Glassfish:
  • e uma versão JSF / Spring / JPA Hibernate / servidor Tomcat:

3.2. Funcionamento da aplicação

Chamaremos à aplicação [RdvMedecins]. Apresentamos abaixo capturas de ecrã do seu funcionamento.

A página inicial da aplicação é a seguinte:

A partir desta primeira página, o utilizador (Secretariado, Médico) irá realizar uma série de ações. Apresentamo-las abaixo. A imagem à esquerda mostra a vista a partir da qual o utilizador efetua um pedido; a imagem à direita mostra a resposta enviada pelo servidor.

Por fim, também é possível obter uma página de erros:

3.3. A base de dados

Voltemos à arquitetura da aplicação a construir:

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

  

3.3.1. A tabela [MEDECINS]

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

  • ID: número de identificação do médico — chave primária da tabela
  • VERSION: 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.
  • NOM: o nome do médico
  • PRENOM: o seu nome próprio
  • TITRE: o seu título (Menina, Sra., Sr.)

3.3.2. A tabela [CLIENTS]

Os clientes dos diferentes médicos estão registados 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 sempre que é feita uma alteração na linha.
  • NOM: o nome do cliente
  • PRENOM: o seu nome próprio
  • TITRE: o seu título (Menina, Sra., Sr.)

3.3.3. A tabela [CRENEAUX]

Esta tabela lista os intervalos horários em que os RV são possíveis:

  • ID: número que identifica o intervalo horário — chave primária da tabela (linha 8)
  • VERSION: 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.
  • ID_MEDECIN: número que identifica o médico a quem pertence este intervalo horário – chave estrangeira na coluna MEDECINS (ID).
  • HDEBUT: hora de início do intervalo
  • MDEBUT: minutos de início do intervalo
  • HFIN: hora de fim do intervalo
  • MFIN: minutos de fim do intervalo

A segunda linha da tabela [CRENEAUX] (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).

3.3.4. A tabela [RV]

Esta tabela lista os RV atribuídos a cada médico:

  • ID: número que identifica o RV de forma única – chave primária
  • JOUR: dia do RV
  • ID_CRENEAU: intervalo horário do RV – chave estrangeira no campo [ID] da tabela [CRENEAUX] – define simultaneamente o intervalo horário e o médico em questão.
  • ID_CLIENT: número do cliente para quem é feita a reserva – chave estrangeira no campo [ID] da tabela [CLIENTS]

Esta tabela possui uma restrição de unicidade na sobre os valores das colunas associadas (JOUR, ID_CRENEAU):

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

Se uma linha da tabela [RV] tiver o valor (JOUR1, ID_CRENEAU1) para as colunas (JOUR, ID_CRENEAU), esse valor não pode aparecer em mais nenhum outro local. Caso contrário, isso significaria que dois RV foram registados 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 um SQLException quando esta situação ocorre.

A linha de id igual a 3 (ver [1] acima) significa que um RV foi marcado para o intervalo n.º 20 e o cliente n.º 4 em 23/08/2006. A tabela [CRENEAUX] 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 Srta. Brigitte BISTROU.

3.3.5. Criação da base de dados

Para criar as tabelas e preenchê-las, pode utilizar-se o script [dbrdvmedecins2.sql], disponível no site de exemplos. Com o [WampServer] (ver parágrafo 1.3.3), pode proceder-se da seguinte forma:

  • no [1], clica-se no ícone do [WampServer] e seleciona-se a opção [PhpMyAdmin] [2],
  • em [3], na janela que se abriu, selecione o link [Bases de données],
  • em [2], cria-se uma base de dados à qual se atribuiu o nome [4] e a codificação [5],
  • em [7], a base de dados foi criada. Clica-se no respetivo link,
  • em [8], importa-se um ficheiro SQL,
  • que se seleciona no sistema de ficheiros com o botão [9],
  • em [11], seleciona-se o script SQL e, em [12], executa-se o mesmo,
  • em [13], as quatro tabelas da base de dados foram criadas. Seguimos uma das ligações,
  • em [14], o conteúdo da tabela.

A partir daqui, não voltaremos a abordar esta base de dados. No entanto, convidamos o leitor a acompanhar a sua evolução ao longo dos programas, sobretudo quando algo não funcionar.

3.4. As camadas [DAO] e [JPA]

Voltemos à arquitetura que temos de construir:

Vamos criar quatro projetos Maven:

  • um projeto para as camadas [DAO] e [JPA],
  • um projeto para a camada [métier],
  • um projeto para a camada [web],
  • um projeto empresarial que irá reunir os três projetos anteriores.

Vamos agora compilar o projeto Maven das camadas [DAO] e [JPA].

Nota: a compreensão das camadas [métier], [DAO] e [JPA] requer conhecimentos de Java EE. Para tal, pode consultar-se o [ref7] (ver parágrafo 3).

3.4.1. O projeto NetBeans

É o seguinte:

  • em [1], cria-se um projeto Maven do tipo [EJB Module] [2],
  • em [3], atribui-se um nome ao projeto,
  • em [4], escolhe-se o servidor Glassfish como servidor,
  • em [5], o projeto gerado.

3.4.2. Geração da camada [JPA]

Voltemos à arquitetura que temos de construir:

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

Passamos agora a descrever algumas dessas ferramentas de geração automática. Para compreender o código gerado, é necessário ter bons conhecimentos sobre as entidades JPA, [ref8] e as EJB, [ref7] (ver parágrafo 3).

3.4.2.1. Criação de uma ligação do NetBeans à base de dados

  • executar o SGBD e o MySQL 5 para que o BD fique disponível,
  • criar uma ligação do NetBeans à base de dados [dbrdvmedecins2],
  • no separador [Services] [1], no ramo [Databases] [2], selecione o controlador JDBC MySQL [3],
  • depois selecione a opção [4] «Connect Using», que permite criar uma ligação a uma base de dados MySQL,
  • em [5], introduza as informações solicitadas. Em [6], o nome da base de dados; em [7], o utilizador da base de dados e a sua palavra-passe;
  • em [8], é possível testar as informações fornecidas,
  • em [9], a mensagem esperada quando estas estão corretas,
  • em [10], a ligação é estabelecida. Aqui podem ver-se as quatro tabelas da base de dados à qual se ligou.

3.4.2.2. Criação de uma unidade de persistência

Voltemos à arquitetura em fase de construção:

Estamos a construir a camada [JPA]. A sua configuração é feita num ficheiro [persistence.xml], no qual se definem as unidades de persistência. Cada uma delas necessita das seguintes informações:

  • as características JDBC de acesso à base de dados (URL, utilizador, palavra-passe),
  • as classes que servirão de representação das tabelas da base de dados,
  • a implementação JPA utilizada. Com efeito, JPA é uma especificação implementada por vários produtos. Aqui, utilizaremos o EclipseLink, que é a implementação predefinida utilizada pelo servidor Glassfish. Isto evita-nos ter de adicionar ao Glassfish as bibliotecas de outra implementação.

O NetBeans pode gerar este ficheiro de persistência através de um assistente.

  • clique com o botão direito do rato no projeto e selecione a criação de uma unidade de persistência [1],
  • em [2], atribuir um nome à unidade de persistência que se está a criar,
  • em [3], selecione a implementação JPA EclipseLink (JPA 2.0),
  • em [4], indicar que as transações com a base de dados serão geridas pelo contentor EJB do servidor Glassfish,
  • em [5], indicar que as tabelas do BD já estão criadas e que, por isso, não serão criadas,
  • no [6], criar uma nova fonte de dados para o servidor Glassfish,
  • em [7], atribuir um nome JNDI (Java Naming Directory Interface),
  • em [8], associar esse nome à ligação MySQL criada na etapa anterior,
  • em [9], concluir o assistente,
  • em [10], o novo projeto,
  • em [11], o ficheiro [persistence.xml] foi gerado na pasta [META-INF],
  • em [12], foi gerada uma pasta [setup],
  • no [13], foram adicionadas novas dependências ao projeto Maven.

O ficheiro [META-INF/persistence.xml] gerado é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
    <jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties/>
  </persistence-unit>
</persistence>

Este ficheiro retoma as informações fornecidas no assistente:

  • linha 3: o nome da unidade de persistência,
  • linha 3: o tipo de transações com a base de dados, neste caso transações JTA (Java Transaction API) geridas pelo contentor EJB3 do servidor GlassFish,
  • linha 4: o nome JNDI da fonte de dados.

Normalmente, neste ficheiro encontra-se o tipo de implementação JPA utilizado. No assistente, indicámos EclipseLink. Como se trata da implementação JPA utilizada por predefinição pelo servidor Glassfish, esta não é mencionada no ficheiro [persistence.xml].

No separador [Design], é possível ter uma visão geral do ficheiro [persistence.xml]:

Para obter os registos do EclipseLink, utilizaremos o seguinte ficheiro [persistence.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/> 
    </properties>
  </persistence-unit>
</persistence>
  • linha 4: indica-se que se utiliza a implementação JPA do EclipseLink,
  • linhas 7-9: reúnem as propriedades de configuração do provedor JPA, neste caso EclipseLink,
  • linha 8: esta propriedade permite registar as ordens SQL que serão emitidas pelo EclipseLink.

O ficheiro [glassfish-resources.xml] que foi criado é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
    <jdbc-connection-pool allow-non-component-callers="false" ... steady-pool-size="8" validate-atmost-once-period-in-seconds="0" wrap-jdbc-objects="false">
        <property name="serverName" value="localhost"/>
        <property name="portNumber" value="3306"/>
        <property name="databaseName" value="dbrdvmedecins2"/>
        <property name="User" value="root"/>
        <property name="Password" value=""/>
        <property name="URL" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
    </jdbc-connection-pool>
    <jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins2" object-type="user" pool-name="mysql_dbrdvmedecins2_rootPool"/>
</resources>

Este ficheiro contém as informações que introduzimos nos dois assistentes utilizados anteriormente:

  • linhas 5-11: as características JDBC da base de dados MySQL5 [dbrdvmedecins2],
  • linha 13: o nome JNDI da fonte de dados.

Este ficheiro será utilizado para criar a fonte de dados JNDI [jdbc/dbrdvmedecins2] do servidor Glassfish. Trata-se de um processo exclusivo deste servidor. Para outro servidor, seria necessário proceder de forma diferente, geralmente através de uma ferramenta de administração. Esta ferramenta também existe para o Glassfish.

Por fim, foram adicionadas dependências ao projeto. O ficheiro [pom.xml] é o seguinte:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>istia.st</groupId>
    <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>ejb</packaging>

    <name>mv-rdvmedecins-ejb-dao-jpa</name>

    ...
    <dependencies>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>eclipselink</artifactId>
            <version>2.3.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>javax.persistence</artifactId>
            <version>2.0.3</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
            <version>2.3.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

...
    <repositories>
        <repository>
            <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
            <id>eclipselink</id>
            <layout>default</layout>
            <name>Repository for library Library[eclipselink]</name>
        </repository>
    </repositories>
</project>
  • nas linhas 32-37, uma camada [JPA] requer o artefacto [javaee-api],
  • linhas 16, 22 e 28: os artefactos necessários para a implementação JPA / EclipseLink aqui utilizada.
  • linhas 18, 24, 30, 36: todos os artefactos têm o atributo provided. Recorde-se que isto significa que são necessários para a compilação, mas não para a execução. Com efeito, durante a execução, são fornecidos (provided) pelo servidor Glassfish,
  • linhas 41-48: definem um novo repositório de artefactos Maven, onde os artefactos EclipseLink podem ser encontrados.

3.4.2.3. Geração das entidades JPA

As entidades JPA podem ser geradas por um assistente do NetBeans:

  • em [1], criam-se entidades JPA a partir de uma base de dados,
  • no [2], seleciona-se a fonte de dados [jdbc / dbrdvmedecins2] criada anteriormente,
  • em [3], a lista de tabelas dessa fonte de dados,
  • em [4], selecionam-se todas,
  • em [5], as tabelas selecionadas,
  • em [6], atribuímos um nome às classes Java associadas às quatro tabelas,
  • bem como um nome de pacote [7],
  • em [8], JPA reúne as linhas das tabelas de BD em coleções. Escolhemos a lista como coleção,
  • em [9], as classes Java criadas pelo assistente.

3.4.2.4. As entidades JPA geradas

A entidade [Medecin] é a representação da tabela [medecins]. A classe Java está repleta de anotações que tornam o código pouco legível à primeira vista. Se mantivermos apenas o que é essencial para compreender a função da entidade, obtemos o seguinte código:


package rdvmedecins.jpa;

...
@Entity
@Table(name = "medecins")
public class Medecin implements Serializable {
  
@Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;
  
  @Column(name = "TITRE")
  private String titre;

  @Column(name = "NOM")
  private String nom;

  @Column(name = "VERSION")
  private int version;

  @Column(name = "PRENOM")
  private String prenom;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
  private List<Creneau> creneauList;

// construtores
....

   // getters e setters
....

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

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

  @Override
  public String toString() {
    ...
  }
  
}
  • na linha 4, a anotação @Entity transforma a classe [Medecin] numa entidade JPA, c.a.d. uma classe associada a uma tabela de BD através de API e JPA,
  • na linha 5, o nome da tabela BD associada à entidade JPA. Cada campo da tabela corresponde a um campo na classe Java,
  • linha 6, a classe implementa a interface Serializable. Isto é necessário em aplicações cliente/servidor, onde as entidades são serializadas entre o cliente e o servidor.
  • linhas 10-11: o campo id da classe [Medecin] corresponde ao campo [ID] (linha 10) da tabela [medecins],
  • linhas 13-14: o campo «título» da classe [Medecin] corresponde ao campo [TITRE] (linha 13) da tabela [medecins],
  • linhas 16-17: o campo «nome» da classe [Medecin] corresponde ao campo [NOM] (linha 16) da tabela [medecins],
  • linhas 19-20: o campo «versão» da classe [Medecin] corresponde ao campo [VERSION] (linha 19) da tabela [medecins]. Aqui, o assistente não reconhece que a coluna é, na verdade, uma coluna de versão que deve ser incrementada sempre que a linha a que pertence for alterada. Para lhe atribuir essa função, é necessário adicionar a anotação @Version. Faremos isso numa etapa seguinte,
  • linhas 22-23: o campo prenom da classe [Medecin] corresponde ao campo [PRENOM] da tabela [medecins],
  • linhas 10-11: o campo id corresponde à chave primária [ID] da tabela. As anotações das linhas 8-9 esclarecem este ponto,
  • linha 8: a anotação @Id indica que o campo anotado está associado à chave primária da tabela,
  • linha 9: a camada [JPA] irá gerar a chave primária das linhas que irá inserir na tabela [Medecins]. Existem várias estratégias possíveis. Aqui, a estratégia GenerationType.IDENTITY indica que a camada JPA irá utilizar o modo auto_increment da tabela MySQL,
  • linhas 25-26: a tabela [creneaux] possui uma chave estrangeira na tabela [medecins]. Um horário pertence a um médico. Por outro lado, um médico tem vários horários que lhe estão associados. Temos, portanto, uma relação de um (médico) para vários (intervalos), uma relação qualificada pela anotação @OneToMany por JPA (linha 25). O campo da linha 26 conterá todos os horários do médico. Isto sem qualquer programação. Para compreender totalmente a linha 25, é necessário apresentar a classe [Creneau].

Esta é a seguinte:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "MDEBUT")
  private int mdebut;

  @Column(name = "HFIN")
  private int hfin;

  @Column(name = "HDEBUT")
  private int hdebut;

  @Column(name = "MFIN")
  private int mfin;

  @Column(name = "VERSION")
  private int version;

  @JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Medecin idMedecin;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
  private List<Rv> rvList;

// construtores
...
// getters e setters
...

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

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

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

Apenas comentamos as novas anotações:

  • já referimos que a tabela [creneaux] possui uma chave estrangeira para a tabela [medecins]: um horário está associado a um médico. Podem estar associados vários horários ao mesmo médico. Existe uma relação da tabela [creneaux] para a tabela [medecins], que é qualificada como «muitos (horários) para um (médico)». É a anotação @ManyToOne da linha 32 que serve para definir a chave estrangeira,
  • a linha 31, com a anotação @JoinColumn, especifica a relação da chave estrangeira: a coluna [ID_MEDECIN] da tabela [creneaux] é uma chave estrangeira na coluna [ID] da tabela [medecins],
  • linha 33: uma referência ao médico titular do horário. Mais uma vez, obtém-se esta informação sem necessidade de programação.

A ligação de chave estrangeira entre a entidade [Creneau] e a entidade [Medecin] é, portanto, concretizada por duas anotações:

  • na entidade [Creneau]:

@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
private Medecin idMedecin;
  • na entidade [Medecin]:

@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;

Ambas as anotações refletem a mesma relação: a da chave estrangeira da tabela [creneaux] para a tabela [medecins]. Diz-se que são inversas uma da outra. Apenas a relação @ManyToOne é indispensável. Esta qualifica, sem ambiguidade, a relação de chave estrangeira. A relação @OneToMany é opcional. Se estiver presente, limita-se a referenciar a relação @ManyToOne à qual está associada. É este o significado do atributo mappedBy da linha 1 da entidade [Medecin]. O valor deste atributo é o nome do campo da entidade [Creneau] que possui a anotação @ManyToOne, a qual especifica a chave estrangeira. Ainda nesta mesma linha 1 da entidade [Medecin], o atributo cascade=CascadeType.ALL define o comportamento da entidade [Medecin] em relação à entidade [Creneau]:

  • se for inserida uma nova entidade [Medecin] na base de dados, então as entidades [Creneau] do campo da linha 2 também devem ser inseridas,
  • se se alterar uma entidade [Medecin] na base de dados, então as entidades [Creneau] do campo da linha 2 também devem ser alteradas,
  • se se eliminar uma entidade [Medecin] da base de dados, então as entidades [Creneau] do campo da linha 2 também devem ser eliminadas.

Apresentamos o código das outras duas entidades sem comentários específicos, uma vez que não introduzem novas notações.

A entidade [Client]


package rdvmedecins.jpa;

...
@Entity
@Table(name = "clients")
public class Client implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "TITRE")
  private String titre;

  @Column(name = "NOM")
  private String nom;

  @Column(name = "VERSION")
  private int version;

  @Column(name = "PRENOM")
  private String prenom;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idClient")
  private List<Rv> rvList;

// construtores
...
// getters e setters
...

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

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

  @Override
  public String toString() {
    ...
  }
  
}
  • as linhas 24-25 refletem a relação de chave estrangeira entre a tabela [rv] e a tabela [clients].

A entidade [Rv]:


package rdvmedecins.jpa;

...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;

  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneau idCreneau;

  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client idClient;

   // construtores
...

   // getters e setters
...

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

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

  @Override
  public String toString() {
    ...
  }
  
}
  • a linha 13 define o campo «dia» de tipo Java Date. Indica-se que, na tabela [rv], a coluna [JOUR] (linha 12) é do tipo data (sem hora),
  • linhas 16-18: definem a relação de chave estrangeira da tabela [rv] com a tabela [creneaux],
  • linhas 20-22: definem a relação de chave estrangeira da tabela [rv] com a tabela [clients].

A geração automática das entidades JPA permite-nos obter uma base de trabalho. Por vezes é suficiente, outras vezes não. É o caso aqui:

  • é necessário adicionar a anotação @Version aos diferentes campos de versão das entidades,
  • é necessário escrever métodos toString mais explícitos do que os gerados,
  • as entidades [Medecin] e [Client] são análogas. Vamos fazê-las derivar de uma classe [Personne],
  • vamos eliminar as relações @OneToMany inversas às relações @ManyToOne. Estas não são indispensáveis e causam complicações na programação,
  • eliminamos a validação @NotNull nas chaves primárias. Quando se persiste uma entidade JPA com MySQL, a entidade inicial tem uma chave primária null. Só após a persistência na base de dados é que a chave primária do elemento persistido assume um valor.

Com estas especificações, as diferentes classes passam a ser as seguintes:

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


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@MappedSuperclass
public class Personne implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Basic(optional = false)
  @Size(min = 1, max = 5)
  @Column(name = "TITRE")
  private String titre;

  @Basic(optional = false)
  @NotNull
  @Size(min = 1, max = 30)
  @Column(name = "NOM")
  private String nom;

  @Basic(optional = false)
  @NotNull
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @Basic(optional = false)
  @NotNull
  @Size(min = 1, max = 30)
  @Column(name = "PRENOM")
  private String prenom;
// construtores
...

// getters e setters
  ...

  @Override
  public String toString() {
    return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
  }
  
}
  • linha 8: note-se que a classe [Personne] não é, por si só, uma entidade (@Entity). Será a classe pai de entidades. A anotação @MappedSuperClass indica esta situação.

A entidade [Client] encapsula as linhas da tabela [clients]. Deriva da classe anterior [Personne]:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
  private static final long serialVersionUID = 1L;

// construtores
...

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

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

  @Override
  public String toString() {
    return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
  }
  
}
  • linha 6: a classe [Client] é uma entidade JPA,
  • linha 7: está associada à tabela [clients],
  • linha 8: deriva da classe [Personne].

A entidade [Medecin], que encapsula as linhas da tabela [medecins], segue o mesmo modelo:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@Table(name = "medecins")
public class Medecin extends Personne implements Serializable {
  private static final long serialVersionUID = 1L;

  // construtores
...

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

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

  @Override
  public String toString() {
    return String.format("Médecin[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
  }
  
}

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


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {

  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "MDEBUT")
  private int mdebut;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "HFIN")
  private int hfin;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "HDEBUT")
  private int hdebut;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "MFIN")
  private int mfin;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Medecin medecin;

  // construtores
  ...

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

  @Override
  public boolean equals(Object object) {
    // TODO: Aviso - este método não funcionará caso os campos de ID não estejam definidos
    ...
  }

  @Override
  public String toString() {
    return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
  }
}
  • as linhas 45-47 modelam a relação «muitos para um» que existe entre a tabela [creneaux] e a tabela [medecins] da base de dados: um médico tem vários horários, um horário pertence a um único médico.

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


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "rv")
public class Rv implements Serializable {

  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;
  
  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneau creneau;
  
  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client client;

   // construtores
...

   // getters e setters
...

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

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

  @Override
  public String toString() {
    return String.format("Rv[%s, %s, %s]", id, creneau, client);
  }
}
  • as linhas 29-31 modelam a relação «muitos para um» que existe entre a tabela [rv] e a tabela [clients] (um cliente pode aparecer em vários Rv) da base de dados e as linhas 25-27 a relação «muitos para um» que existe entre a tabela [rv] e a tabela [creneaux] (um intervalo de tempo pode aparecer em vários Rv).

3.4.3. A classe de exceção

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


package rdvmedecins.exceptions;

import java.io.Serializable;
import javax.ejb.ApplicationException;

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

  // campos privados
  private int code = 0;

  // construtores
  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 e setters
  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • linha 7: a classe deriva da classe [RuntimeException]. O compilador não obriga, portanto, a tratá-la com try/catch.
  • linha 6: a anotação @ApplicationException faz com que a exceção não seja «engolida» por uma exceção do tipo [EjbException].

Para compreender a anotação @ApplicationException, voltemos à arquitetura utilizada no lado do servidor:

A exceção do tipo [RdvMedecinsException] será lançada pelos métodos do EJB da camada [DAO] no interior do contentor EJB3 e interceptada por este. Sem a anotação @ApplicationException, o contentor EJB3 encapsula a exceção ocorrida numa exceção do tipo [EjbException] e relança-a. Pode não se desejar este encapsulamento e permitir que uma exceção do tipo [RdvMedecinsException] saia do contentor EJB3. É isso que a anotação @ApplicationException permite. Além disso, o atributo (rollback=true) desta anotação indica ao contentor EJB3 que, se a exceção do tipo [RdvMedecinsException] ocorrer no interior de um método executado no âmbito de uma transação com um SGBD, esta deve ser revertida. Em termos técnicos, isto denomina-se efetuar um rollback da transação.

3.4.4. A EJB da camada [DAO]

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


package rdvmedecins.dao;


import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;

public interface IDao {

  // lista de clientes
  public List<Client> getAllClients();
  // lista de médicos
  public List<Medecin> getAllMedecins();
  // lista de horários disponíveis de um médico
  public List<Creneau> getAllCreneaux(Medecin medecin);
  // lista de consultas de um médico, num determinado dia
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
  // encontrar um cliente identificado pelo seu ID
  public Client getClientById(Long id);
  // encontrar um cliente identificado pelo seu ID
  public Medecin getMedecinById(Long id);
  // encontrar uma consulta identificada pelo seu ID
  public Rv getRvById(Long id);
  // encontrar um intervalo horário identificado pelo seu ID
  public Creneau getCreneauById(Long id);
  // adicionar um RV
  public Rv ajouterRv(Date jour, Creneau creneau, Client client);
  // eliminar um RV
  public void supprimerRv(Rv rv);
}

Esta interface foi criada após a identificação das necessidades da camada [web]:

  • linha 14: a lista de clientes. Vamos precisar dela para preencher a lista suspensa de clientes,
  • linha 16: a lista de médicos. Precisaremos dela para preencher a lista suspensa de médicos,
  • linha 18: a lista dos horários disponíveis de um médico. Precisaremos dela para apresentar a agenda do médico para um determinado dia,
  • linha 20: a lista de consultas de um médico para um determinado dia. Combinada com o método anterior, permitirá exibir a agenda do médico para um determinado dia com os seus horários já reservados,
  • linha 22: permite localizar um cliente a partir do seu número. O método permitirá localizar um cliente a partir de uma escolha na lista suspensa de clientes,
  • linha 24: o mesmo se aplica aos médicos,
  • linha 26: procura uma consulta pelo seu número. Pode ser utilizado quando se elimina uma consulta para verificar previamente se esta existe efetivamente,
  • linha 28: procura um intervalo de tempo a partir do seu número. Permite identificar o intervalo que um utilizador pretende adicionar ou eliminar,
  • linha 30: para adicionar uma consulta,
  • linha 32: para eliminar uma consulta.

A interface local [IDaoLocal] do EJB limita-se a derivar da interface anterior [IDao]:


package rdvmedecins.dao;

import javax.ejb.Local;

@Local
public interface IDaoLocal extends IDao{

}

O mesmo se aplica à interface remota [IDaoRemote]:


package rdvmedecins.dao;

import javax.ejb.Remote;

@Remote
public interface IDaoRemote extends IDao{

}

A interface EJB [DaoJpa] implementa ambas as interfaces, a local e a remota:


package rdvmedecins.dao;

...

@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
  • a linha 5 indica que a interface remota EJB tem o nome «rdvmedecins.dao». Além disso, a anotação @Singleton (Java EE6) garante que apenas será criada uma instância do EJB. A anotação @Stateless (Java EE5) define um EJB que pode ser criado em múltiplas instâncias para alimentar um conjunto de EJB,
  • a linha 6 indica que todos os métodos do EJB são executados no âmbito de uma transação gerida pelo contentor EJB3,
  • a linha 7 mostra que o EJB implementa as interfaces local e remota e é também serializável.

O código completo do EJB é o seguinte:


package rdvmedecins.dao;

import java.io.Serializable;
import java.util.Date;
import java.util.List;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import rdvmedecins.exceptions.RdvMedecinsException;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;

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

  @PersistenceContext
  private EntityManager em;

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

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

  // lista dos horários disponíveis de um determinado médico
  // médico: o médico
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    try {
      return em.createQuery("select rc from Creneau rc join rc.medecin m where m.id=:idMedecin").setParameter("idMedecin", medecin.getId()).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 3);
    }
  }

  // lista de consultas de um determinado médico, num determinado dia
  // médico: o médico
  // dia: o dia
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
    try {
      return em.createQuery("select rv from Rv rv join rv.creneau c join c.idMedecin m where m.id=:idMedecin and rv.jour=:jour").setParameter("idMedecin", medecin.getId()).setParameter("jour", jour).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 3);
    }
  }

  // adição de uma consulta
  // dia: dia da consulta
  // intervalo: intervalo horário da consulta
  // cliente: cliente para quem foi marcada a consulta
  public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
    try {
      Rv rv = new Rv(null, jour);
      rv.setClient(client);
      rv.setCreneau(creneau);
      em.persist(rv);
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
  }

  // eliminação de uma consulta
  // consulta: a consulta eliminada
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }

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

  // recuperar um médico específico
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

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

  // recuperar um horário específico
  public Creneau getCreneauById(Long id) {
    try {
      return (Creneau) em.find(Creneau.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
}
  • linha 22: o objeto EntityManager que gere o acesso ao contexto de persistência. Ao instanciar a classe, este campo será inicializado pelo contentor EJB graças à anotação @PersistenceContext da linha 21,
  • linha 27: consulta JPQL (Java Persistence Query Language) que devolve todas as linhas da tabela [clients] sob a forma de uma lista de objetos [Client],
  • linha 36: consulta análoga para os médicos,
  • linha 46: uma consulta JPQL que realiza uma junção entre as tabelas [creneaux] e [medecins]. É parametrizada pelo ID do médico,
  • linha 57: uma consulta JPQL que realiza uma junção entre as tabelas [rv], [creneaux] e [medecins] e que tem dois parâmetros: o ID do médico e o dia da consulta,
  • linhas 69-73: criação de uma consulta e, em seguida, o seu registo na base de dados,
  • linha 83: eliminação de uma consulta da base de dados,
  • linha 92: executa um select na base de dados para encontrar um determinado cliente,
  • linha 101: o mesmo para um médico,
  • linha 110: o mesmo para uma consulta,
  • linha 119: o mesmo para um intervalo horário,
  • todas as operações com o contexto de persistência em da linha 22 estão sujeitas a encontrar um problema com a base de dados. Por isso, estão todas rodeadas por um try / catch. A eventual exceção é encapsulada na exceção «própria» RdvMedecinsException.

3.4.5. Implementação do controlador JDBC a partir de MySQL

Na arquitetura abaixo:

O EclipseLink necessita do controlador JDBC do MySQL. É necessário instalar este controlador nas bibliotecas do servidor Glassfish, na pasta <glassfish>/domains/domain1/lib/ext, em que <glassfish> é a pasta de instalação do servidor Glassfish. Pode-se obtê-lo da seguinte forma:

A pasta onde deve ser colocado o controlador JDBC do MySQL é <Pasta Domains>[1]/domain1/lib/ext [2]. Este controlador está disponível no URL [http://www.mysql.fr/downloads/connector/j/]. Após a sua instalação, é necessário reiniciar o servidor Glassfish para que este reconheça esta nova biblioteca.

3.4.6. Implantação da camada EJB da camada [DAO]

Voltemos à arquitetura construída até ao momento:

O conjunto [web, métier, DAO, JPA] deve ser implementado no servidor Glassfish. Procedemos da seguinte forma:

  • em [1], compilamos o projeto Maven,
  • em [2], executamo-lo,
  • em [3], foi implementado no servidor Glassfish (separador [Services])

Pode ser interessante consultar os registos do Glassfish:

Em [1], os registos do Glassfish estão disponíveis no separador [Output / Glassfish Server 3+]. São os seguintes:

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

As linhas identificadas por [Config] e [Précis] são os registos de EclipseLink; as identificadas por [Infos] provêm do Glassfish.

  • linhas 1-12: o EclipseLink processa as entidades JPA que detetou,
  • linhas 13-17: informações que indicam que o processamento das entidades JPA decorreu normalmente,
  • linha 18: o EclipseLink dá sinal de si,
  • linha 19: EclipseLink reconhece que se trata de SGBD MySQL,
  • linhas 20-24: o EclipseLink tenta estabelecer ligação com o BD,
  • linhas 25-28: conseguiu,
  • linhas 29-33: tenta voltar a ligar-se, desta vez utilizando especificamente uma plataforma MySQL (linha 30),
  • linhas 34-37: também bem-sucedido,
  • linha 38: confirmação de que a unidade de persistência [dbrdvmedecins-PU] pôde ser instanciada,
  • linha 39: os nomes portáveis das interfaces remota e local do EJB [DaoJpa], sendo que «portável» significa que são reconhecidos por todos os servidores de aplicações Java EE 6,
  • linha 40: os nomes das interfaces remota e local do EJB [DaoJpa], específicos do Glassfish. No teste que se segue, utilizaremos o nome «rdvmedecins.dao».

As linhas 39 e 40 são importantes. Ao escrever o cliente de um EJB no Glassfish, é necessário conhecê-las.

3.4.7. Testes do EJB da camada [DAO]

Agora que o EJB da camada [DAO] da nossa aplicação foi implementado, podemos testá-lo. Vamos fazê-lo no âmbito de uma aplicação cliente/servidor:

O cliente irá testar a interface remota do EJB [DAO] implementado no servidor Glassfish.

Começamos por criar um novo projeto Maven:

  • No [1], criamos um novo projeto,
  • em [2,3], criamos um projeto Maven do tipo [Java Application],
  • em [4], atribuímos-lhe um nome e colocamo-lo na mesma pasta que o EJB e o [DAO],
  • em [5], o projeto gerado,
  • em [6], foi gerada uma classe [App.java]. Vamos eliminá-la,
  • em [7], foi gerado um ramo [Source Packages]. Ainda não o tínhamos encontrado. Podemos colocar testes JUnit neste ramo. É o que faremos. Não iremos manter a classe de teste [AppTest] gerada,
  • em [8], as dependências do projeto Maven. O ramo [Dependencies] está vazio. Teremos de adicionar novas dependências a este ramo. O ramo [Test Dependencies] reúne as dependências necessárias para os testes. Aqui, a biblioteca utilizada é a do framework JUnit 3.8. Teremos de a alterar.

O projeto evolui da seguinte forma:

  • para [1], o projeto onde as duas classes geradas foram eliminadas, bem como a dependência JUnit.

Voltemos à arquitetura cliente/servidor que será utilizada para o teste:

O cliente precisa de conhecer a interface remota disponibilizada pelo EJB e pelo [DAO]. Além disso, irá trocar entidades JPA com o EJB. Por isso, necessita da definição dessas entidades. Para que o projeto de teste do EJB tenha acesso a estas informações, vamos adicionar o projeto do EJB [DAO] como dependência ao projeto:

  • no [1], adicionamos uma dependência do ramo [Test Dependencies],
  • no [2], seleciona-se o separador [Open Projects],
  • em [3], seleciona-se o projeto Maven de EJB [DAO],
  • em [4], a dependência adicionada.

Voltemos à arquitetura cliente/servidor do teste:

Durante a execução, o cliente e o servidor comunicam através da rede TCP-IP. Não vamos programar estas trocas de dados. Para cada servidor de aplicações, existe uma biblioteca a integrar nas dependências do cliente. A biblioteca para o Glassfish chama-se [gf-client]. Adicionamo-la:

  • no [1], adicionamos uma dependência,
  • no [2], definimos as características do artefacto pretendido,
  • no [3], são adicionadas inúmeras dependências. O Maven irá descarregá-las. Este processo pode demorar vários minutos. Em seguida, são armazenadas no repositório local do Maven.

Podemos agora criar o teste JUnit:

  • em [2], clicamos com o botão direito do rato em [Test Packages] para criar um novo teste JUnit,
  • em [3], atribua um nome à classe de teste, bem como um pacote para a mesma [4],
  • em [5], escolhe-se o framework JUnit 4.x,
  • em [6], a classe de teste gerada,
  • em [7], as novas dependências do projeto Maven.

O ficheiro [pom.xml] fica então da seguinte forma:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-client-rdvmedecins-ejb-dao</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-client-rdvmedecins-ejb-dao</name>
  <url>http://maven.apache.org</url>

  <repositories>
    <repository>
      <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
      <id>eclipselink</id>
      <layout>default</layout>
      <name>Repository for library Library[eclipselink]</name>
    </repository>
    <repository>
      <url>http://repo1.maven.org/maven2/</url>
      <id>junit_4</id>
      <layout>default</layout>
      <name>Repository for library Library[junit_4]</name>
    </repository>
  </repositories>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish.appclient</groupId>
      <artifactId>gf-client</artifactId>
      <version>3.1.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

De notar:

  • nas linhas 32-51, as dependências do projeto,
  • linhas 13-26: foram definidos dois repositórios Maven, um para o EclipseLink (linhas 14-19) e outro para o JUnit4 (linhas 20-25).

A classe de teste será a seguinte:


package rdvmedecins.tests.dao;

import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import junit.framework.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import rdvmedecins.dao.IDaoRemote;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;

public class JUnitTestDao {

  // camada [dao] testada
  private static IDaoRemote dao;
  // data de hoje
  Date jour = new Date();

  @BeforeClass
  public static void init() throws NamingException {
    // inicialização do ambiente JNDI
    InitialContext initialContext = new InitialContext();
    // instanciação da camada DAO
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }

  @Test
  public void test1() {
    // exibição de clientes
    List<Client> clients =dao.getAllClients();
    display("Liste des clients :", clients);
    // exibição de médicos
    List<Medecin> medecins =dao.getAllMedecins();
    display("Liste des médecins :", medecins);
    // exibição dos horários disponíveis de um médico
    Medecin medecin = medecins.get(0);
    List<Creneau> creneaux = dao.getAllCreneaux(medecin);
    display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
    // lista de consultas de um médico, num determinado dia
    display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // adicionar um RV
    Rv rv = null;
    Creneau creneau = creneaux.get(2);
    Client client = clients.get(0);
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    rv = dao.ajouterRv(jour, creneau, client);
    System.out.println("Rv ajouté");
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // adicionar um RV no mesmo horário do mesmo dia
    // deve provocar uma exceção
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    Boolean erreur = false;
    try {
      rv = dao.ajouterRv(jour, creneau, client);
      System.out.println("Rv ajouté");
    } catch (Exception ex) {
      Throwable th = ex;
      while (th != null) {
        System.out.println(ex.getMessage());
        th = th.getCause();
      }
      // regista-se o erro
      erreur=true;
    }
    // verifica-se se ocorreu um erro
    Assert.assertTrue(erreur);
    // lista de RV
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // eliminar um RV
    System.out.println("Suppression du Rv ajouté");
    dao.supprimerRv(rv);
    System.out.println("Rv supprimé");
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
  }

  // método utilitário — apresenta os elementos de uma coleção
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • linhas 23-29: o método marcado com @BeforeClass é executado antes de todos os outros. Aqui, cria-se uma referência à interface remota do EJB [DaoJpa]. Recorde-se que lhe tínhamos atribuído o nome JNDI «rdvmedecins.dao»,
  • linhas 34-35: apresentam a lista de clientes,
  • linhas 37-38: apresentam a lista de médicos,
  • linhas 40-42: apresentam os horários disponíveis do primeiro médico,
  • linha 44: exibe as consultas do primeiro médico para o dia da linha 21,
  • linhas 46-51: adicionam uma consulta ao primeiro médico, para o seu horário n.º 2 e no dia da linha 21,
  • linha 52: exibe, para verificação, as consultas do primeiro médico para o dia da linha 21. Deve haver pelo menos uma, a que acabou de ser adicionada,
  • linhas 55-70: adiciona-se a mesma consulta. Como a tabela [RV] tem uma restrição de unicidade, esta adição deve provocar uma exceção. Verifica-se isso na linha 70,
  • linha 72: exibem, para verificação, as consultas do primeiro médico para o dia da linha 21. A consulta que se pretendia adicionar não deve constar aí,
  • linhas 74-76: elimina-se a única consulta que foi adicionada,
  • linha 77: exibem, para verificação, as consultas do primeiro médico para o dia da linha 21. A consulta que acabámos de eliminar não deve constar aí.

Este teste é um teste falso JUnit. Contém apenas uma asserção (linha 70). Trata-se de um teste visual com os defeitos que lhe são inerentes.

Se tudo correr bem, os testes devem ser bem-sucedidos:

  • em [1], cria-se o projeto de teste,
  • em [2], executa-se o teste,
  • em [3], o teste foi bem-sucedido.

Vamos analisar mais detalhadamente os resultados do teste:

Liste des clients :
Client[1,Mr,Jules,MARTIN]
Client[2,Mme,Christine,GERMAN]
Client[3,Mr,Jules,JACQUARD]
Client[4,Melle,Brigitte,BISTROU]
Liste des médecins :
Médecin[1,Mme,Marie,PELISSIER]
Médecin[2,Mr,Jacques,BROMARD]
Médecin[3,Mr,Philippe,JANDOT]
Médecin[4,Melle,Justine,JACQUEMOT]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER]
Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 15:34:15 CEST 2012]
Ajout d'un Rv le [Wed May 23 15:34:15 CEST 2012] dans le créneau Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] pour le client Client[1,Mr,Jules,MARTIN]
Rv ajouté
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 15:34:15 CEST 2012]
Rv[242, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Ajout d'un Rv le [Wed May 23 15:34:15 CEST 2012] dans le créneau Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] pour le client Client[1,Mr,Jules,MARTIN]
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 15:34:15 CEST 2012]
Rv[242, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Suppression du Rv ajouté
Rv supprimé
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 15:34:15 CEST 2012]

O leitor é convidado a ler estes registos em simultâneo com o código que os gerou. Vamos debruçar-nos sobre a exceção que ocorreu ao adicionar um compromisso já existente, nas linhas 41-49. A pilha de exceções é reproduzida nas linhas 42-48. Trata-se de uma exceção inesperada. Voltemos ao código do método de adição de um compromisso:


  // adicionar um compromisso
  // dia: dia do Rv
  // intervalo: intervalo horário da consulta
  // cliente: cliente para quem foi marcada a consulta
  public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
    try {
      Rv rv = new Rv(null, jour);
      rv.setClient(client);
      rv.setCreneau(creneau);
      System.out.println(String.format("avant persist : %s",rv));
      em.persist(rv);
      System.out.println(String.format("après persist : %s",rv));
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
}

Vejamos os registos do Glassfish durante a adição dos dois compromissos:

...
Infos: avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Infos: après persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Précis: INSERT INTO rv (JOUR, ID_CLIENT, ID_CRENEAU) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Précis: SELECT LAST_INSERT_ID()
Précis: SELECT t1.ID, t1.JOUR, t1.ID_CLIENT, t1.ID_CRENEAU FROM creneaux t0, rv t1 WHERE (((t0.ID_MEDECIN = ?) AND (t1.JOUR = ?)) AND (t0.ID = t1.ID_CRENEAU))
    bind => [2 parameters bound]
Infos: avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Infos: après persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Précis: INSERT INTO rv (JOUR, ID_CLIENT, ID_CRENEAU) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Précis: SELECT 1
Avertissement: Local Exception Stack: 
Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.2.v20111125-r10461): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '2012-05-23-3' for key 'UNQ1_RV'
Error Code: 1062
...
  • linha 2: antes do primeiro persist,
  • linha 3: após o primeiro persist,
  • linha 4: o comando INSERT que vai ser executado. Note-se que não ocorre ao mesmo tempo que a operação persist. Se fosse esse o caso, este registo teria aparecido antes da linha 2. A operação INSERT ocorre, então, normalmente no final da transação em que o método é executado,
  • linha 6: EclipseLink pergunta a MySQL qual é a última chave primária utilizada. Obterá a chave primária da consulta adicionada. Este valor irá preencher o campo id da entidade [Rv] persistida,
  • linhas 7-8: a consulta SELECT que irá apresentar os compromissos do médico,
  • linhas 9-10: as exibições no ecrã da segunda consulta persist,
  • linhas 11-12: a ordem INSERT que será executada. Deve provocar uma exceção. Esta aparece nas linhas 15-16 e é clara. É inicialmente lançada pelo controlador JDBC do MySQL devido à violação da restrição de unicidade das consultas. Deduz-se que estas exceções deveriam aparecer nos registos do teste JUnit. No entanto, não é esse o caso:
1
2
3
4
5
6
7
8
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe

Recorde-se a arquitetura cliente/servidor do teste:

Quando o EJB [DAO] lança uma exceção, esta tem de ser serializada para chegar ao cliente. Provavelmente foi esta operação que falhou por uma razão que não compreendi. Como a nossa aplicação completa não funcionará em modo cliente/servidor, podemos ignorar este problema.

Agora que o EJB da camada [DAO] está operacional, podemos passar para o EJB da camada [métier].

3.5. A camada [métier]

Voltemos à arquitetura da aplicação em desenvolvimento:

Vamos criar um novo projeto Maven para a camada EJB [métier]. Como se pode ver acima, este terá uma dependência do projeto Maven que foi criado para as camadas [DAO] e [JPA].

3.5.1. O projeto NetBeans

Criamos um novo projeto Maven do tipo EJB. Para tal, basta seguir o procedimento já utilizado e descrito na página 174.

  • no [1], o projeto Maven da camada [métier],
  • em [2], adiciona-se uma dependência,
  • no [3], seleciona-se o projeto Maven das camadas [DAO] e [JPA],
  • em [4], seleciona-se o escopo [provided]. Recorde-se que isto significa que é necessário para a compilação, mas não para a execução do projeto. Com efeito, oEJB da camada [métier] será implementado no servidor Glassfish juntamente com o EJB das camadas [DAO] e [JPA]. Assim, quando for executado, o EJB das camadas [DAO] e [JPA] já estará presente,
  • no [6], o novo projeto com a sua dependência.

Apresentemos agora os códigos-fonte da camada [métier]:

O EJB [Metier] terá a seguinte interface [IMetier]:


package rdvmedecins.metier.service;

import java.util.Date;
import java.util.List;

import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;

public interface IMetier {

    // camada DAO
    // lista de clientes
    public List<Client> getAllClients();

    // lista de médicos
    public List<Medecin> getAllMedecins();

    // lista de horários de um médico
    public List<Creneau> getAllCreneaux(Medecin medecin);

    // lista das consultas de um médico, num determinado dia
    public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);

    // encontrar um cliente identificado pelo seu ID
    public Client getClientById(Long id);

    // encontrar um cliente identificado pelo seu ID
    public Medecin getMedecinById(Long id);

    // encontrar uma consulta identificada pelo seu ID
    public Rv getRvById(Long id);

    // encontrar um intervalo horário identificado pelo seu ID
    public Creneau getCreneauById(Long id);

    // adicionar um RV
    public Rv ajouterRv(Date jour, Creneau creneau, Client client);

    // eliminar um RV
    public void supprimerRv(Rv rv);
    
    // função
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);

}

Para compreender esta interface, é necessário ter em conta a arquitetura do projeto:

Definimos a interface da camada [DAO] (parágrafo 3.4.4) e indicámos que esta respondia às necessidades da camada [web], ou seja, às necessidades do utilizador. A camada [web] comunica com a camada [DAO] apenas através da camada [métier]. Isto explica por que razão se encontram na camada [métier] todos os métodos da camada [DAO]. Estes métodos limitar-se-ão a delegar o pedido da camada [web] à camada [DAO]. Nada mais do que isso.

Durante a análise da aplicação, surge uma necessidade: ser capaz de apresentar numa página web a agenda de um médico para um determinado dia, de modo a saber os horários ocupados e livres desse dia. É tipicamente o que acontece quando a secretária responde a um pedido por telefone. Pede-se-lhe uma consulta para um determinado dia com um determinado médico. Para responder a esta necessidade, a camada [métier] disponibiliza o método da linha 46.


    // função
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);

Pode-se questionar onde colocar este método:

  • poderia ser colocado na camada [DAO]. No entanto, este método não responde propriamente a uma necessidade de acesso aos dados, mas sim a uma necessidade de negócio,
  • poderia ser colocado na camada [web]. Isso seria uma má ideia. Pois se alterarmos a camada [web] para uma camada [Swing], perderemos o método, embora a necessidade continue a existir.

O método recebe como parâmetros o médico e o dia para o qual se pretende a agenda de marcações. Devolve um objeto [AgendaMedecinJour] que representa a agenda do médico para esse dia:


package rdvmedecins.metier.entites;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.jpa.Medecin;

public class AgendaMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // campos
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;

    // construtores
    public AgendaMedecinJour() {

    }

    public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
        this.medecin = medecin;
        this.jour = jour;
        this.creneauxMedecinJour = creneauxMedecinJour;
    }

    public String toString() {
        StringBuffer str = new StringBuffer("");
        for (CreneauMedecinJour cr : creneauxMedecinJour) {
            str.append(" ");
            str.append(cr.toString());
        }
        return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
    }

    // getters e setters
...
  
}
  • linha 12: o médico a quem pertence a agenda,
  • linha 13: o dia da agenda,
  • linha 14: os horários disponíveis do médico para esse dia.
  • A classe apresenta construtores (linhas 17, 21), bem como um método toString adaptado (linha 27).

A classe [CreneauMedecinJour] (linha 14) é a seguinte:


package rdvmedecins.metier.entites;

import java.io.Serializable;
import rdvmedecins.jpa.Creneau;

import rdvmedecins.jpa.Rv;

public class CreneauMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // campos
    private Creneau creneau;
    private Rv rv;

    // construtores
    public CreneauMedecinJour() {

    }

    public CreneauMedecinJour(Creneau creneau, Rv rv) {
        this.creneau=creneau;
    this.rv=rv;
    }

    // toString
    @Override
    public String toString() {
        return String.format("[%s %s]", creneau,rv);
    }

    // getters e setters

  ...
}
  • linha 12: um horário disponível do médico,
  • linha 13: a consulta associada, null, se o horário estiver disponível.

Vemos, assim, que o campo creneauxMedecinJour da linha 14 da classe [AgendaMedecinJour] permite-nos obter todos os horários do médico com a informação «ocupado» ou «disponível» para cada um deles. Este era o objetivo do novo método [getAgendaMedecinJour] da interface [IMetier].

O nosso EJB [Metier] terá uma interface local e uma interface remota que se limitarão a derivar da interface principal [IMetier]:


package rdvmedecins.metier.service;
import javax.ejb.Local;

@Local
public interface IMetierLocal extends IMetier{

}

package rdvmedecins.metier.service;
import javax.ejb.Remote;

@Remote
public interface IMetierRemote extends IMetier{

}

O EJB e o [Metier] implementam estas interfaces da seguinte forma:


package rdvmedecins.metier.service;

import java.io.Serializable;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.ejb.EJB;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;

import rdvmedecins.dao.IDaoLocal;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.entites.CreneauMedecinJour;

@Singleton
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote, Serializable {

  // camada DAO
  @EJB
  private IDaoLocal dao;

  public Metier() {
  }

  @Override
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }

  @Override
  public List<Medecin> getAllMedecins() {
    return dao.getAllMedecins();
  }

  @Override
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    return dao.getAllCreneaux(medecin);
  }

  @Override
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
    return dao.getRvMedecinJour(medecin, jour);
  }

  @Override
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }

  @Override
  public Medecin getMedecinById(Long id) {
    return dao.getMedecinById(id);
  }

  @Override
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }

  @Override
  public Creneau getCreneauById(Long id) {
    return dao.getCreneauById(id);
  }

  @Override
  public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
    return dao.ajouterRv(jour, creneau, client);
  }

  @Override
  public void supprimerRv(Rv rv) {
    dao.supprimerRv(rv);
  }

  @Override
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour) {
    // lista de horários disponíveis do médico
    List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
    // lista de marcações desse mesmo médico para esse mesmo dia
    List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
    // cria-se um dicionário a partir das consultas marcadas
    Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
    for (Rv resa : reservations) {
      hReservations.put(resa.getCreneau().getId(), resa);
    }
    // cria-se a agenda para o dia solicitado
    AgendaMedecinJour agenda = new AgendaMedecinJour();
    // o médico
    agenda.setMedecin(medecin);
    // o dia
    agenda.setJour(jour);
    // os intervalos de reserva
    CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
    agenda.setCreneauxMedecinJour(creneauxMedecinJour);
    // preenchimento dos intervalos de reserva
    for (int i = 0; i < creneauxHoraires.size(); i++) {
      // linha i da agenda
      creneauxMedecinJour[i] = new CreneauMedecinJour();
      // ID do horário
      creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
      // o intervalo está livre ou reservado?
      if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
        // o intervalo está ocupado — regista-se a reserva
        Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
        creneauxMedecinJour[i].setRv(resa);
      }
    }
    // envia-se o resultado
    return agenda;
  }
}
  • na linha 22, a classe [Metier] é um singleton de EJB,
  • linha 23, cada método do EJB decorre no âmbito de uma transação. Isto significa que a transação tem início no início do método, na camada [métier]. Esta irá chamar métodos da camada [DAO]. Estes decorrerão no âmbito da mesma transação,
  • na linha 24, o EJB implementa as suas interfaces local e remota e é, além disso, serializável,
  • linha 27: uma referência à EJB da camada [DAO],
  • linha 29: esta será injetada pelo contentor EJB do servidor Glassfish, graças à anotação @EJB. Assim, quando os métodos da classe [Metier] são executados, a referência à EJB da camada [DAO] já foi inicializada,
  • linhas 33-81: esta referência é utilizada para delegar à camada [DAO] a chamada efetuada à camada [métier],
  • linha 84: o método getAgendaMedecinJour que permite obter a agenda de um médico para um determinado dia. Deixamos que o leitor acompanhe os comentários.

3.5.2. Implementação da camada [métier]

A camada [métier] depende da camada [DAO]. Cada camada foi implementada com um EJB. Para testar o EJB e o [métier], é necessário implementar os dois EJB. Para tal, é necessário um projeto empresarial.

  • [1], criamos um novo projeto,
  • do tipo Maven [2] e Aplicação Empresarial [3],
  • atribuímos-lhe um nome [4]. O sufixo ear será adicionado automaticamente,
  • em [5], escolhe-se o servidor Glassfish e Java EE 6,
  • em [6], uma aplicação empresarial contém módulos, geralmente módulos EJB e módulos web. Aqui, a aplicação empresarial irá conter os módulos dos dois EJB que criámos. Como esses módulos já existem, não assinalamos as caixas de seleção,
  • no [7,8], foram criados dois projetos. O [8] é o projeto empresarial que vamos utilizar. O [7] é um projeto cuja função desconheço. Não tive de o utilizar e, como não aprofundei os meus conhecimentos sobre o Maven, não sei para que pode servir. Por isso, vamos ignorá-lo.

Agora que o projeto da empresa está criado, podemos definir os seus módulos.

  • no [1], criamos uma nova dependência,
  • em [2], selecionamos o projeto EJB [DAO],
  • em [3], declara-se que se trata de um EJB. Não se deve deixar o tipo em branco, pois, nesse caso, será utilizado o tipo jar e, neste contexto, esse tipo não é adequado,
  • no [4], utiliza-se o âmbito [compile],
  • no [5], o projeto com a sua nova dependência,
  • no [6, 7, 8], recomeçamos para adicionar o EJB da camada [métier],
  • em [9], as duas dependências,
  • em [10], compilamos o projeto,
  • em [11], executa-se o projeto,
  • em [12], no separador [Services], verifica-se que o projeto foi implementado no servidor Glassfish. Isto significa que os dois EJB estão agora presentes no servidor.

Nos registos do servidor Glassfish, encontram-se informações sobre a implementação dos dois EJB:

  • e [1], no separador de registos do Glassfish.

Encontram-se aí os seguintes registos:

Infos: rdvmedecins.jpa.Creneau actually got transformed
Infos: rdvmedecins.jpa.Medecin actually got transformed
Infos: rdvmedecins.jpa.Personne actually got transformed
Infos: rdvmedecins.jpa.Client actually got transformed
Infos: rdvmedecins.jpa.Rv actually got transformed
Infos: EclipseLink, version: Eclipse Persistence Services - 2.3.2.v20111125-r10461
Infos: file:/D:/data/istia-1112/netbeans/dvp/jsf2-pf-pfm/maven/netbeans/rdvmedecins-jsf2-ejb/mv-rdvmedecins-metier-dao/mv-rdvmedecins-metier-dao-ear/target/gfdeploy/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT_jar/_dbrdvmedecins2-PU login successful
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: EJB5181:Portable JNDI names for EJB Metier: [java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]
  • linhas 1-5: as entidades JPA foram reconhecidas,
  • linha 7: indica que a criação da unidade de persistência [dbrdvmedecins2-PU] foi bem-sucedida e que a ligação à base de dados associada foi estabelecida,
  • linha 8: os nomes portáveis das interfaces remota e local do EJB, [DaoJpa] e portable significam que foram reconhecidos por todos os servidores de aplicações,
  • linha 9: o mesmo, mas com nomes próprios do GlassFish,
  • linhas 10-11: o mesmo para o EJB e o [Metier].

Vamos reter o nome portátil da interface remota do EJB [Metier]:

java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote

Vamos precisar dele durante os testes da camada [métier].

3.5.3. Teste da camada [métier]

Tal como fizemos com a camada [DAO], vamos testar a camada [métier] no âmbito de uma aplicação cliente/servidor:

O cliente irá testar a interface remota da camada EJB [Metier] implementada no servidor Glassfish.

Começamos por criar um novo projeto Maven. Para tal, seguimos o procedimento utilizado para criar o projeto de teste da camada [dao] (ver parágrafo 3.4.7), excluindo a criação do teste JUnit. O projeto assim criado é o seguinte

  • em [1], o projeto criado com as suas dependências: em relação ao EJB da camada [dao], em relação ao EJB da camada [métier], da biblioteca [gf-client].

Nesta altura, o ficheiro [pom.xml] do projeto é o seguinte:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-client-rdvmedecins-ejb-metier</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-client-rdvmedecins-ejb-metier</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.glassfish.appclient</groupId>
      <artifactId>gf-client</artifactId>
      <version>3.1.1</version>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-metier</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>
</project>

Certifique-se de que possui as dependências descritas nas linhas 17 a 33. O teste será uma simples classe de consola:

O código da classe [ClientRdvMedecinsMetier] é o seguinte:


package istia.st.client;

import java.util.Date;
import java.util.List;

import javax.naming.InitialContext;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;

import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.service.IMetierRemote;

public class ClientRdvMedecinsMetier {

  // o nome da interface remota do EJB [Metier]
  private static String IDaoRemoteName = "java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
  // data de hoje
  private static Date jour = new Date();

  public static void main(String[] args) {
    try {
      // contexto JNDI do servidor Glassfish
      InitialContext initialContext = new InitialContext();
      // referência na camada remota [metier]
      IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
      // visualização de clientes
      List<Client> clients = metier.getAllClients();
      display("Liste des clients :", clients);
      // visualização de médicos
      List<Medecin> medecins = metier.getAllMedecins();
      display("Liste des médecins :", medecins);
      // visualização dos horários de um médico
      Medecin medecin = medecins.get(0);
      List<Creneau> creneaux = metier.getAllCreneaux(medecin);
      display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
      // lista de consultas de um médico, num determinado dia
      display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // visualização da agenda
      AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // adicionar um RV
      Rv rv = null;
      Creneau creneau = creneaux.get(2);
      Client client = clients.get(0);
      System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
      rv = metier.ajouterRv(jour, creneau, client);
      System.out.println("Rv ajouté");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // visualização da agenda
      agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // eliminar um RV
      System.out.println("Suppression du Rv ajouté");
      metier.supprimerRv(rv);
      System.out.println("Rv supprimé");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // visualização da agenda
      agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
    } catch (Throwable ex) {
      System.out.println("Erreur...");
      while (ex != null) {
        System.out.println(String.format("%s : %s", ex.getClass().getName(), ex.getMessage()));
        ex = ex.getCause();
      }
    }
  }

  // método utilitário - apresenta os elementos de uma coleção
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • linha 18: o nome portátil da interface remota do EJB [Metier] foi obtido nos registos do GlassFish,
  • linhas 24-27: obtém-se uma referência à interface remota do EJB [Metier],
  • linhas 29-30: apresentam os clientes,
  • linhas 32-33: apresentam os médicos,
  • linhas 35-37: apresentam os horários disponíveis de um médico,
  • linha 39: exibe as consultas de um médico num determinado dia,
  • linhas 41-42: a agenda desse mesmo médico para o mesmo dia,
  • linhas 44-49: adiciona-se uma consulta,
  • linha 50: exibem-se as consultas do médico. Deve haver mais uma,
  • linhas 52-53: exibe-se a agenda do médico. Deve ser visível a consulta adicionada,
  • linhas 55-57: elimina-se a consulta que acabou de ser adicionada,
  • linha 58: isto deve refletir-se na lista de consultas do médico,
  • linhas 60-61: e na sua agenda.

Executamos o teste:

 

As exibições no ecrã obtidas são as seguintes:


Liste des clients :
Client[1,Mr,Jules,MARTIN]
Client[2,Mme,Christine,GERMAN]
Client[3,Mr,Jules,JACQUARD]
Client[4,Melle,Brigitte,BISTROU]
Liste des médecins :
Médecin[1,Mme,Marie,PELISSIER]
Médecin[2,Mr,Jacques,BROMARD]
Médecin[3,Mr,Philippe,JANDOT]
Médecin[4,Melle,Justine,JACQUEMOT]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER]
Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Ajout d'un Rv le [Wed May 23 16:25:26 CEST 2012] dans le créneau Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] pour le client Client[1,Mr,Jules,MARTIN]
Rv ajouté
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Suppression du Rv ajouté
Rv supprimé
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
  • linha 37: a agenda da Sra. PELISSIER, a 23 de maio de 2012. Não há nenhum horário reservado,
  • linha 39: adição de um compromisso,
  • linha 42: a nova agenda da Sra. PELISSIER. Está agora reservado um horário para o Sr. MARTIN,
  • linha 44: o compromisso foi eliminado,
  • linha 46: a agenda da Sra. PELISSIER mostra que não há nenhum horário reservado.

Consideramos agora que as camadas [DAO] e [métier] estão operacionais. Resta-nos escrever a camada [web] com o framework JSF. Para tal, vamos utilizar os conhecimentos adquiridos no início deste documento.

3.6. A camada [web]

Voltemos à arquitetura que estamos a construir:

Vamos construir a última camada, a camada [web].

3.6.1. O projeto NetBeans

Estamos a criar um projeto Maven:

  • em [1], criamos um novo projeto,
  • em [2, 3], um projeto Maven do tipo [Web Application],
  • em [4], atribuímos-lhe um nome,
  • em [5], escolhe-se o servidor Glassfish e o Java EE 6 Web,
  • em [6], o projeto assim criado,
  • em [7], o projeto após terem sido eliminadas a página [index.jsp] e o pacote presente em [Source Packages],
  • em [8, 9], nas propriedades do projeto, adiciona-se um framework,
  • em [10], seleciona-se Java Server Faces,
  • em [11], a configuração do Java Server Faces. Mantêm-se os valores predefinidos. Note-se que é utilizado o JSF 2,
  • em [12], o projeto é então alterado em dois pontos: é gerado um ficheiro [web.xml], bem como uma página [index.html].

O ficheiro [web.xml] é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>/faces/*</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>

Já nos deparámos com este ficheiro.

  • linhas 7-11: definem o servlet que irá processar todos os pedidos feitos à aplicação. Trata-se do servlet de JSF,
  • linhas 12-15: definem os URL processados por esta servlet. Trata-se dos URL com o formato /faces/*,
  • linhas 21-23: definem a página [index.xhtml] como página inicial.

Esta página é a seguinte:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
  <h:head>
    <title>Facelet Title</title>
  </h:head>
  <h:body>
    Hello from Facelets
  </h:body>
</html>

Já a vimos anteriormente. Podemos executar este projeto:

  • em [1], executamos o projeto e obtemos o resultado [2] no navegador.

Apresentamos agora o projeto completo para, em seguida, detalhar os seus diferentes elementos.

  • em [1], as páginas XHTML do projeto,
  • em [2], os códigos Java,
  • em [3], os ficheiros de mensagens, uma vez que a aplicação está internacionalizada,
  • em [4], as dependências do projeto.

3.6.2. As dependências do projeto

Voltemos à arquitetura do projeto:

A camada JSF assenta nas camadas [métier], [DAO] e [JPA]. Estas três camadas estão encapsuladas nos dois projetos Maven que criámos, o que explica as dependências do projeto [4]. Vamos simplesmente mostrar como estas dependências são adicionadas:

  • no [1], colocaremos «ejb» para indicar que a dependência é de um projeto EJB,
  • no [2], indicaremos [provided]. Com efeito, o projeto web será implementado em simultâneo com os dois projetos EJB. Por isso, não é necessário incluir os ficheiros JAR do EJB.

3.6.3. A configuração do projeto

A configuração do projeto é a mesma dos projetos JSF que analisámos no início deste documento. Enumeramos os ficheiros de configuração sem os voltar a explicar.

 

[web.xml]: configura a aplicação web.


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
  <context-param>
    <param-name>javax.faces.PROJECT_STAGE</param-name>
    <param-value>Production</param-value>
  </context-param>
  <context-param>
    <param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
    <param-value>true</param-value>
  </context-param> 
  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/faces/*</url-pattern>
  </servlet-mapping>
  <session-config>
    <session-timeout>
      30
    </session-timeout>
  </session-config>
  <welcome-file-list>
    <welcome-file>faces/index.xhtml</welcome-file>
  </welcome-file-list>
  <error-page>
    <error-code>500</error-code>
    <location>/faces/exception.xhtml</location>
  </error-page>
  <error-page>
    <exception-type>Exception</exception-type>
    <location>/faces/exception.xhtml</location>
  </error-page>

</web-app>

Note-se, na linha 26, que a página [index.xhtml] é a página inicial da aplicação.

[faces-config.xml]: configura a aplicação JSF


<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
    <message-bundle>messages</message-bundle>
  </application>
</faces-config>

[beans.xml]: vazio, mas necessário para a anotação @Named


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

[styles.css]: a folha de estilo da aplicação


.reservationsHeaders {
   text-align: center;
   font-style: italic;
   color: Snow;
   background: Teal;
}

.creneau {
   height: 25px;
   text-align: center;
   background: MediumTurquoise;
}
.client {
   text-align: left;
   background: PowderBlue;
}

.action {
   width: 6em;
   text-align: left;
   color: Black;
   background: MediumTurquoise;
}
.erreursHeaders {
   background: Teal;
   background-color: #ff6633;
   color: Snow;
   font-style: italic;
   text-align: center

}

.erreurClasse {
   background: MediumTurquoise;
   background-color: #ffcc66;
   height: 25px;
   text-align: center
}

.erreurMessage {
   background: PowderBlue;
   background-color: #ffcc99;
   text-align: left
}

[messages_fr.properties]: o ficheiro de mensagens em francês


# layout
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers
layout.entete.langue1=Fran\u00e7ais
layout.entete.langue2=Anglais
# exceção
exception.header=L'exception suivante s'est produite
exception.httpCode=Code HTTP de l'erreur
exception.message=Message de l'exception
exception.requestUri=Url demand\u00e9e lors de l'erreur
exception.servletName=Nom de la servlet demand\u00e9e lorsque l'erreur s'est produite
# formulário 1
form1.titre=R\u00e9servations
form1.medecin=M\u00e9decin
form1.jour=Jour (jj/mm/aaaa)
form1.button.agenda=Agenda
form1.jour.required=date requise
form1.jour.erreur=date erron\u00e9e
# formulário 2
form2.titre=Agenda de {0} {1} {2} le {3}
form2.titre_detail=Agenda de {0} {1} {2} le {3}
form2.creneauHoraire=Cr\u00e9neau horaire
form2.client=Client
form2.accueil=Accueil
form2.supprimer=Supprimer
form2.reserver=R\u00e9server
# formulário 3
form3.titre=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.titre_detail=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.client=Client
form3.valider=Valider
form3.annuler=Annuler
# erro
erreur.titre=Une erreur s'est produite.
erreur.message=Message d'erreur
erreur.accueil=Page d'accueil
erreur.classe=Cause

[messages_en.properties]: o ficheiro de mensagens em inglês


# layout
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university
layout.entete.langue1=French
layout.entete.langue2=English
# exceção
exception.header=The following exceptions occurred
exception.httpCode=Error HTTP code
exception.message=Exception message
exception.requestUri=Url targeted when error occurred
exception.servletName=Servlet targeted's name when error occurred
# formulário 1
form1.titre=Reservations
form1.medecin=Doctor
form1.jour=Date (dd/mm/yyyy)
form1.button.agenda=Diary
form1.jour.required=The date is required
form1.jour.erreur=The date is invalid
# formulário 2
form2.titre={0} {1} {2}'' diary on {3}
form2.titre_detail={0} {1} {2}'' diary on {3}
form2.creneauHoraire=Time Period
form2.client=Client
form2.accueil=Welcome Page
form2.supprimer=Delete
form2.reserver=Reserve
# formulário 3
form3.titre=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.titre_detail=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.client=Client
form3.valider=Submit
form3.annuler=Cancel
# erro
erreur.titre=An error occurred
erreur.message=Error message
erreur.accueil=Welcome Page
erreur.classe=Cause

3.6.4. As vistas do projeto

Recorde-se o funcionamento da aplicação. A página inicial é a seguinte:

 

A partir desta primeira página, o utilizador (Secretariado, Médico) irá realizar uma série de ações. Apresentamo-las abaixo. A vista à esquerda mostra a página a partir da qual o utilizador efetua um pedido; a vista à direita, a resposta enviada pelo servidor.

Por fim, também é possível obter uma página de erros:

Estas diferentes visualizações são obtidas através das seguintes páginas do projeto web:

  • em [1], as páginas [basdepage, entete, layout] garantem a formatação de todas as visualizações,
  • em [2], a vista produzida por [layout.xhtml].

A tecnologia utilizada aqui é a dos facelets. Esta foi descrita no parágrafo 2.11. Limitamo-nos a apresentar o código das páginas XHTML utilizadas para a formatação:

[entete.xhtml]


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <body>
    <h2><h:outputText value="#{msg['layout.entete']}"/></h2>
    <div align="left">
      <h:commandLink value="#{msg['layout.entete.langue1']}" actionListener="#{changeLocale.setFrenchLocale}"/>
      <h:outputText value=" "/>
      <h:commandLink value="#{msg['layout.entete.langue2']}" actionListener="#{changeLocale.setEnglishLocale}"/>
  </div>
  </body>
</html>

Note-se, nas linhas 10 a 12, os dois links para alterar o idioma da aplicação.

[basdepage.xhtml]


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
  <body>
    <h:outputText value="#{msg['layout.basdepage']}"/>
  </body>
</html>

[layout.xhtml]


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <f:view locale="#{changeLocale.locale}">
    <h:head>
      <title>RdvMedecins</title>
      <h:outputStylesheet library="css" name="styles.css"/>
    </h:head>
    <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
      <h:form id="formulaire">
        <table style="width: 1200px">
          <tr>
            <td colspan="2" bgcolor="#ccccff">
              <ui:include src="entete.xhtml"/>
            </td>
          </tr>
          <tr>
            <td style="width: 100px; height: 200px" bgcolor="#ffcccc">
            </td>
            <td>
              <ui:insert name="contenu" >
                <h2>Contenu</h2>
              </ui:insert>
            </td>
          </tr>
          <tr bgcolor="#ffcc66">
            <td colspan="2">
              <ui:include src="basdepage.xhtml"/>
            </td>
          </tr>         
        </table>
      </h:form>
    </h:body>
  </f:view>
</html>

Esta página é o modelo (template) da página [index.xhtml]:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <ui:composition template="layout.xhtml">
    <ui:define name="contenu">
      <h:panelGroup rendered="#{form.form1Rendered}">
        <ui:include src="form1.xhtml"/>
      </h:panelGroup>
      <h:panelGroup rendered="#{form.form2Rendered}">
        <ui:include src="form2.xhtml"/>
      </h:panelGroup>
      <h:panelGroup rendered="#{form.form3Rendered}">
        <ui:include src="form3.xhtml"/>
      </h:panelGroup>
      <h:panelGroup rendered="#{form.erreurRendered}">
        <ui:include src="erreur.xhtml"/>
      </h:panelGroup>
    </ui:define>
  </ui:composition>
</html>

As linhas 8 a 21 definem a área denominada «conteúdo» (linha 8) em [layout.xhtml] (linha 7). Trata-se da área central das visualizações:

 

A página [index.xhtml] é a única página da aplicação. Não haverá, portanto, qualquer navegação entre páginas. Ela apresenta uma das quatro páginas [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. Esta apresentação é controlada por quatro valores booleanos [form1Rendered, form2Rendered, form3Rendered, erreurRendered] do bean de formulário que iremos descrever em breve.

3.6.5. Os beans do projeto

As classes do pacote [utils] já foram apresentadas:

  • a classe [ChangeLocale] é a classe responsável pela mudança de idioma. Já foi analisada (parágrafo 2.4.4).
  • a classe [Messages] é uma classe que facilita a internacionalização das mensagens de uma aplicação. Foi analisada no parágrafo 2.8.5.7.

3.6.5.1. O bean Application

O bean [Application] é o seguinte:


package beans;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.metier.service.IMetierLocal;

@Named(value = "application")
@ApplicationScoped
public class Application implements Serializable{

  // camada de negócio
  @EJB
  private IMetierLocal metier;
  // cache
  private List<Medecin> medecins;
  private List<Client> clients;
  private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
  private Map<Long, Client> hClients = new HashMap<Long, Client>();
  // erros
  private List<Erreur> erreurs = new ArrayList<Erreur>();
  private Boolean erreur = false;

  public Application() {
  }

  @PostConstruct
  public void init() {
    // os médicos e os clientes são armazenados na cache
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // regista-se o erro
      erreur = true;
      erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
      while (th.getCause() != null) {
        th = th.getCause();
        erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
      }
      return;
    }
    // verificação das listas
    if (medecins.size() == 0) {
      // regista-se o erro
      erreur = true;
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // regista-se o erro
      erreur = true;
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // erro?
    if (erreur) {
      return;
    }

    // os dicionários
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
  }

  // getters e setters
  ...
}
  • linhas 15-16: a classe [Application] é um bean com âmbito de aplicação. É criada uma única vez no início do ciclo de vida da aplicação JSF e está acessível a todos os pedidos de todos os utilizadores. Nela colocam-se, geralmente, dados de leitura única. Neste caso, iremos colocar nela a lista de médicos e a lista de clientes. Partimos, portanto, do princípio de que estas listas não se alteram com frequência. As páginas XHTML acedem a ela através do nome «application»,
  • linhas 20-21: uma referência à interface local do EJB [Metier] será injetada pelo contentor EJB do Glassfish. Recordemos a arquitetura da aplicação:

A aplicação JSF e as aplicações EJB e [Metier] serão executadas na mesma JVM (Java Virtual Machine). Assim, a camada [JSF] irá utilizar a interface local da EJB. Neste caso, o bean da aplicação utiliza a EJB e a [Metier]. Mesmo que não fosse esse o caso, seria normal encontrar aí uma referência à camada [métier]. Trata-se, de facto, de uma informação que pode ser partilhada por todos os pedidos de todos os utilizadores, sendo, portanto, um dado com âmbito Application.

  • linhas 34-35: o método init é executado imediatamente após a instanciação da classe [Application] (presença da anotação @PostConstruct),
  • Nas linhas 36 a 73, o método cria os seguintes elementos: a lista de médicos na linha 23, a lista de clientes na linha 24, um dicionário de médicos indexado pelo seu ID na linha 25 e o mesmo para os clientes na linha 26. Podem ocorrer erros. Estes são registados na lista da linha 28.

A classe [Erreur] é a seguinte:


package beans;

public class Erreur {
  
  public Erreur() {
  }
  
  // campo
  private String classe;
  private String message;

  // construtor
  public Erreur(String classe, String message){
    this.setClasse(classe);
    this.message=message;
  }
  
  // getters e setters
...  
}
  • linha 9, o nome de uma classe de exceção, caso tenha sido lançada uma exceção,
  • linha 10: uma mensagem de erro.

3.6.5.2. O bean [Form]

O seu código é o seguinte:


package beans;

...

@Named(value = "form")
@SessionScoped
public class Form implements Serializable {

  public Form() {
  }

  // bean de aplicação
  @Inject
  private Application application;

  // modelo
  private Long idMedecin;
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private String form2Titre;
  private String form3Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Long idCreneau;
  private Medecin medecin;
  private Client client;
  private Long idClient;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;

  @PostConstruct
  private void init() {
    // A inicialização decorreu corretamente?
    if (application.getErreur()) {
      // recuperar a lista de erros
      erreurs = application.getErreurs();
      // a vista de erros é apresentada
      setForms(false, false, false, true);
    }
  }

  // exibição da vista
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.form3Rendered = form3Rendered;
    this.erreurRendered = erreurRendered;
  }
.................................................
}
  • linhas 5-7: a classe [Form] é um bean com o nome «form» e com âmbito de sessão. Recorde-se que, nesse caso, a classe deve ser serializável.
  • linhas 13-14: o bean «form» tem uma referência ao bean «application». Esta referência será injetada pelo contentor de servlets no qual a aplicação é executada (presença da anotação @Inject).
  • linhas 17-31: o modelo das páginas [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. A exibição destas páginas é controlada pelos valores booleanos das linhas 19-22. Note-se que, por predefinição, é a página [form1.xhtml] que é apresentada,
  • linhas 33-34: o método init é executado logo após a instanciação da classe (presença da anotação @PostConstruct),
  • linhas 35-41: o método init é utilizado para determinar qual a página que deve ser apresentada em primeiro lugar: normalmente a página [form1.xhtml] (linha 19), a menos que a inicialização da aplicação tenha falhado (linha 36), caso em que será apresentada a página [erreur.xhtml] (linha 40).

A página [erreur.xhtml] é a seguinte:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <body>
    <h2><h:outputText value="#{msg['erreur.titre']}"/></h2>
    <p>
      <h:commandButton value="#{msg['erreur.accueil']}" actionListener="#{form.accueil()}"/>
    </p>
    <hr/>
    <h:dataTable value="#{form.erreurs}" var="erreur" headerClass="erreursHeaders" columnClasses="erreurClasse,erreurMessage">
      <h:column>
        <f:facet name="header">
          <h:outputText value="#{msg['erreur.classe']}"/>
        </f:facet>
        <h:outputText value="#{erreur.classe}"/>
      </h:column>
      <h:column>
        <f:facet name="header">
          <h:outputText value="#{msg['erreur.message']}"/>
        </f:facet>
        <h:outputText value="#{erreur.message}"/>
      </h:column>
    </h:dataTable>
  </body>
</html>

Utiliza uma baliza <h:dataTable> (linhas 14-27) para apresentar a lista de erros. Isto resulta numa página semelhante à seguinte:

Image

Vamos agora definir as diferentes fases do ciclo de vida da aplicação.

3.6.6. Interações entre páginas e modelo

3.6.6.1. A exibição da página inicial

Se tudo correr bem, a primeira página apresentada é a [form1.xhtml]. O resultado é a seguinte visualização:

 

A página [form1.xhtml] é a seguinte:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <body>
    <h2><h:outputText value="#{msg['form1.titre']}"/></h2>
    <h:panelGrid columns="3">
      <h:panelGroup>
      <div align="center"><h3><h:outputText value="#{msg['form1.medecin']}"/></h3></div>
      </h:panelGroup>
      <h:panelGroup>
      <div align="center"><h3><h:outputText value="#{msg['form1.jour']}"/></h3></div>
      </h:panelGroup>
      <h:panelGroup/>
      <h:selectOneMenu value="#{form.idMedecin}">  
        <f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>  
      </h:selectOneMenu>              
      <h:inputText id="jour" value="#{form.jour}"  required="true" requiredMessage="#{msg['form1.jour.required']}" converterMessage="#{msg['form1.jour.erreur']}">
        <f:convertDateTime pattern="dd/MM/yyyy"/>
      </h:inputText>
      <h:message for="jour" styleClass="error"/>
    </h:panelGrid>
    <h:commandButton value="#{msg['form1.button.agenda']}" actionListener="#{form.getAgenda}"/>
  </body>
</html>

Esta página é alimentada pelo seguinte modelo:


@Named(value = "form")
@SessionScoped
public class Form implements Serializable {

  // Bean Application
  @Inject
  private Application application;
  // modelo
  private Long idMedecin;
  private Date jour = new Date();
  
// lista de médicos
  public List<Medecin> getMedecins() {
    return application.getMedecins();
  }
  // agenda
  public void getAgenda() {
    ...
}
  • O campo da linha 9 alimenta, em leitura e escrita, o valor da lista da linha 18 da página. Na exibição inicial da página, esta define o valor selecionado na lista suspensa. Na exibição inicial, idMedecin é igual a null, pelo que será selecionado o primeiro médico;
  • o método das linhas 13-15 gera os elementos da lista suspensa de médicos (linha 19 da página). Cada opção gerada terá como rótulo (itemLabel) o título, apelido e nome próprio do médico e, como valor (itemValue), o ID do médico,
  • o campo da linha 10 alimenta, em leitura/gravação, o campo de introdução de dados da linha 21 da página. Na exibição inicial, é, portanto, a data de hoje que é apresentada,
  • linhas 17-19: o método getAgenda gere o clique no botão [Agenda] da linha 26 da página. Como não há navegação (é sempre a página [index.html] que é solicitada), utilizar-se-á frequentemente o atributo actionListener em vez do atributo action. Neste caso, o método chamado no modelo não devolve qualquer resultado.

Quando se clica no botão [Agenda],

  • são enviados valores: o valor selecionado na lista suspensa de médicos é registado no campo idMedecin do modelo e o dia escolhido no campo «dia»,
  • é chamado o método getAgenda do modelo.

O método getAgenda é o seguinte:


  // Bean de aplicação
  @Inject
  private Application application;

  // modelo
  private Long idMedecin;
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private String form2Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Medecin medecin;
  private List<Erreur> erreurs;

  // agenda
  public void getAgenda() {
    try {
      // procurar o médico
      medecin = application.gethMedecins().get(idMedecin);
      // título do formulário 2
      form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
      // Agenda do médico para um determinado dia
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // exibir o formulário 2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // visualização dos erros
      prepareVueErreur(th);
    }
  }

  // preparação vueErreur
  private void prepareVueErreur(Throwable th) {
    // cria-se a lista de erros
    erreurs = new ArrayList<Erreur>();
    erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
    while (th.getCause() != null) {
      th = th.getCause();
      erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
    }
// a visualização dos erros é apresentada
    setForms(false, false, false, true);
}

Recorde-se o que o método getAgenda deve apresentar:

  • linha 21: recupera-se o médico selecionado no dicionário de médicos, que foi armazenado no bean application. Para tal, utiliza-se o seu ID, que foi enviado para idMedecin,
  • linha 23: prepara-se o título da página [form2.xhtml] que vai ser apresentada. Esta mensagem é retirada do ficheiro de mensagens para que possa ser internacionalizada. Esta técnica foi descrita no parágrafo 2.8.5.7, página 135.
  • linha 25: recorre-se à camada [métier] para calcular a agenda do médico selecionado para o dia selecionado,
  • linha 27: é exibido o [form2.xhtml],
  • linha 28: se ocorrer uma exceção, é então criada uma lista de erros (linhas 37-42) e a página [erreur.xhtml] é apresentada (linha 44).

3.6.6.2. Exibir a agenda de um médico

A página [form2.xhtml] corresponde à seguinte visualização:

O código da página [form2.xhtml] é o seguinte:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:c="http://java.sun.com/jsp/jstl/core">

  <body>
    <h2><h:outputText value="#{form.form2Titre}"/></h2>
    <h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
    <h:dataTable value="#{form.agendaMedecinJour.creneauxMedecinJour}" var="creneauMedecinJour" headerClass="reservationsHeaders" columnClasses="creneau,client,action">
      <h:column>  
        <f:facet name="header">  
          <h:outputText value="#{msg['form2.creneauHoraire']}"/> 
        </f:facet>  
        <h:outputText value="#{creneauMedecinJour.creneau.hdebut}:#{creneauMedecinJour.creneau.mdebut} - #{creneauMedecinJour.creneau.hfin}:#{creneauMedecinJour.creneau.mfin}" />  
      </h:column>  
      <h:column>  
        <f:facet name="header">  
          <h:outputText value="#{msg['form2.client']}"/>  
        </f:facet>  
        <c:if test="#{creneauMedecinJour.rv==null}">
          <h:outputText value=""/>
          <c:otherwise>
            <h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}"/>
          </c:otherwise>
        </c:if>
      </h:column>  
      <h:column>  
        <f:facet name="header"/>
        <h:commandLink action="#{form.action()}" value="#{creneauMedecinJour.rv==null ? msg['form2.reserver'] : msg['form2.supprimer']}">
          <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneau}"/>
        </h:commandLink>
      </h:column>  
    </h:dataTable>
  </body>
</html>

Recorde-se que o método getAgenda inicializou dois campos no modelo:


// modelo
  private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;

Estes dois campos alimentam a página [form2.xhtml]:

  • linha 10, o título da página,
  • linha 12: a agenda do médico é apresentada por uma baliza <h:dataTable> com três colunas,
  • linhas 13-18: a primeira coluna apresenta os horários disponíveis,
  • linhas 19-30: a segunda coluna apresenta o nome do cliente que, eventualmente, reservou o horário ou nada, caso contrário. Para efetuar esta escolha, utilizam-se as balizas da biblioteca JSTL Core referenciada na linha 7,
  • linhas 30-35: a terceira coluna apresenta o link [Réserver] se o intervalo de tempo estiver livre, e o link [Supprimer] se o intervalo de tempo estiver ocupado.

Os links da terceira coluna estão associados ao seguinte modelo:


// modelo
  private Long idCreneau;

  // ação em RV
  public void action() {
    ...
}
  • o método action é chamado quando o utilizador clica no link Reservar / Eliminar (linha 32). Note-se que aqui foi utilizado o atributo action. O método a que este atributo aponta deve ter a assinatura String action(), uma vez que o método tem de devolver uma chave de navegação. No entanto, neste caso, a assinatura é `void action()`. Isto não provocou qualquer erro e pode-se supor que, neste caso, não há navegação. Era isso que se pretendia. Substituir `actionListener` por `action` provocava um mau funcionamento,
  • o campo idCreneau da linha 2 irá recuperar o ID do intervalo horário do link em que se clicou (linha 33 da página).

3.6.6.3. Eliminação de um compromisso

Vamos analisar o código que gere a eliminação de um compromisso. Isso corresponde à seguinte sequência de vistas:

O código relacionado com esta operação é o seguinte:


// bean Application
  @Inject
  private Application application;

  // modelo
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private AgendaMedecinJour agendaMedecinJour;
  private Long idCreneau;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;

  // ação sobre RV
  public void action() {
    // procura-se um horário disponível na agenda
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // Encontrou-se alguma?
    if (!trouvé) {
      // É estranho — volta a apresentar o form2
      setForms(false, true, false, false);
      return;
    }
    // Encontrámos
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // de acordo com a ação pretendida
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // reserva

  public void reserver() {
    ...
  }

  public void supprimer() {
    try {
      // eliminação de um compromisso
      application.getMetier().supprimerRv(creneauChoisi.getRv());
      // atualiza-se a agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // exibe o form2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // visualização de erros
      prepareVueErreur(th);
    }
  }
  • linha 16: quando o método action é iniciado, o ID do intervalo horário selecionado foi gravado em idCreneau (linha 11),
  • linhas 18-26: procura-se recuperar o intervalo horário a partir do seu id (linha 21). Procura-se no calendário atual, agendaMedecinJour da linha 10. Normalmente, deve ser encontrado. Se não for o caso, não se faz nada (linhas 28-32),
  • linha 34: se tivermos encontrado o intervalo de tempo procurado, recuperamos uma referência que armazenamos na linha 12,
  • linha 36: verifica-se se o intervalo de tempo selecionado tinha um compromisso. Se sim, este é eliminado (linha 39); caso contrário, reserva-se um (linha 37),
  • linha 51: a consulta do intervalo escolhido é eliminada. É a camada [métier] que realiza esta tarefa,
  • linha 53: solicita-se à camada [métier] a nova agenda do médico. É claro que aí se verá menos uma consulta. Mas, como a aplicação é multiutilizador, é possível ver as alterações feitas por outros utilizadores,
  • linha 55: volta a ser apresentada a página [form2.xhtml],
  • linha 58: uma vez que a camada [métier] foi solicitada, podem ocorrer exceções. Neste caso, a pilha de exceções é registada na lista de erros da linha 13 e exibida através da vista [erreur.xhtml].

3.6.6.4. Marcação de consultas

A marcação de consultas segue a seguinte sequência:

O modelo envolvido nesta ação é o seguinte:


// modelo
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private String form3Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Medecin medecin;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;

  // ação no RV
  public void action() {
...
    // foi encontrado
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // de acordo com a ação pretendida
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // reserva

    public void reserver() {
    try {
      // título do formulário 3
      form3Titre = Messages.getMessage(null, "form3.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour),
                creneauChoisi.getCreneau().getHdebut(), creneauChoisi.getCreneau().getMdebut(), creneauChoisi.getCreneau().getHfin(), creneauChoisi.getCreneau().getMfin()}).getSummary();
      // cliente selecionado na lista suspensa
      idClient=null;
      // é apresentado o formulário 3
      setForms(false, false, true, false);
    } catch (Throwable th) {
      // visualização de erros
      prepareVueErreur(th);
    }
  }
  • linha 14: se o intervalo de tempo escolhido não tiver qualquer compromisso, trata-se de uma reserva,
  • linha 30: prepara-se o título da página [form3.xhtml] com a mesma técnica utilizada para o título da página [form2.xhtml],
  • linha 34: neste formulário, existe um menu suspenso cujo valor é fornecido por idClient. Define-se o valor deste campo para null para não selecionar ninguém,
  • linha 36: exibe-se a página [form3.xhtml],
  • linha 39: ou a página de erros, caso tenha ocorrido uma exceção.

A página [form3.xhtml] é a seguinte:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <body>
    <h2><h:outputText value="#{form.form3Titre}"/></h2>
    <h:panelGrid columns="2">
      <h:outputText value="#{msg['form3.client']}"/>
      <h:selectOneMenu value="#{form.idClient}">
        <f:selectItems value="#{form.clients}" var="client" itemLabel="#{client.titre} #{client.prenom} #{client.nom}" itemValue="#{client.id}"/>
      </h:selectOneMenu>
      <h:panelGroup>
        <h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
        <h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
      </h:panelGroup>
    </h:panelGrid>
  </body>
</html>

Esta página é alimentada pelo seguinte modelo:


// bean Application
  @Inject
  private Application application;

  // modelo
  private Long idClient;

  // lista de clientes
  public List<Client> getClients() {
    return application.getClients();
  }
  • linha 6: o n.º do cliente preenche o atributo value da lista suspensa de clientes na linha 12 da página. Este atributo define o elemento selecionado na lista suspensa,
  • linhas 9-11: o método getClients preenche o conteúdo do menu suspenso (linha 13). O texto (itemLabel) de cada opção é [Titre Prénom Nom] do cliente, e o valor associado (itemValue) é o ID do cliente. É, portanto, este valor que será enviado.

3.6.6.5. Validação de um compromisso

A validação de uma marcação corresponde à seguinte sequência:

e corresponde ao clique no botão [Valider]:


        <h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />

É, portanto, o método [Form].validerRv que irá gerir este evento. O seu código é o seguinte:


  // bean da aplicação
  @Inject
  private Application application;
  
  // modelo
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private Long idCreneau;
  private Long idClient;
  private List<Erreur> erreurs;

  // validação de compromisso
  public void validerRv() {
    try {
      // recupera-se uma instância do intervalo horário selecionado
      Creneau creneau = application.getMetier().getCreneauById(idCreneau);
      // adiciona-se o compromisso
      application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
      // atualiza-se a agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // exibe-se o form2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // visualização de erros
      prepareVueErreur(th);
    }
}
  • linha 12: antes de o método validerRv ser executado, o campo idClient recebeu o ID do cliente selecionado pelo utilizador,
  • linha 19: a partir do ID do intervalo horário memorizado numa etapa anterior (o bean tem alcance de sessão), solicita-se à camada [métier] uma referência ao próprio intervalo horário,
  • linha 21: solicita-se à camada [métier] que adicione uma consulta para o dia escolhido (dia), o intervalo horário escolhido (intervalo) e o cliente escolhido (idClient),
  • linha 23: solicita-se à camada [métier] que atualize a agenda do médico. Ver-se-á a consulta adicionada, bem como todas as alterações que outros utilizadores da aplicação possam ter feito,
  • linha 25: volta a apresentar-se a agenda [form2.xhtml],
  • linha 28: exibe-se a página de erro caso ocorra algum erro.

3.6.6.6. Cancelamento de uma marcação

Isto corresponde à seguinte sequência:

O botão [Annuler] na página [form3.xhtml] é o seguinte:


        <h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>

O método [Form].annulerRv é, portanto, chamado:


  // cancelamento da marcação
  public void annulerRv() {
    // exibe o formulário 2
    setForms(false, true, false, false);
}

3.6.6.7. Voltar à página inicial

Resta ainda uma ação a analisar, a da sequência seguinte:

O código do botão [Accueil] na página [form2.xhtml] é o seguinte:


    <h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />

O método [Form].accueil é o seguinte:


  public void accueil() {
    // exibe a página inicial
    setForms(true, false, false, false);
}

3.7. Conclusion

Criámos a seguinte aplicação:

Concentrámo-nos mais nas funcionalidades da aplicação do que na sua apresentação ao utilizador. Esta será melhorada com a utilização da biblioteca de componentes PrimeFaces. Criámos uma aplicação básica, mas que, mesmo assim, é representativa de uma arquitetura Java EE em camadas, utilizando EJB. A aplicação pode ser melhorada de várias formas:

  • é necessária uma autenticação. Nem todos têm autorização para adicionar/eliminar compromissos,
  • deveria ser possível percorrer a agenda para a frente e para trás ao procurar um dia com horários disponíveis,
  • deveria ser possível solicitar a lista dos dias em que existem horários livres para um médico. Com efeito, se este for oftalmologista, as suas consultas são geralmente marcadas com seis meses de antecedência,
  • ...

3.8. Testes com o Eclipse

3.8.1. A camada [DAO]

  • em [1], importa-se o projeto EJB da camada [DAO] e o seu cliente,
  • em [2], seleciona-se o projeto EJB da camada [DAO] e executa-se [3],
  • em [4], executa-se num servidor,
  • em [5], apenas o servidor Glassfish é proposto, pois é o único que possui um contentor EJB,
  • no [6], o módulo EJB foi implementado,
  • no [7], são apresentados os registos:
1
2
3
4
5
6
7
8
Infos: Hibernate Validator 4.2.0.Final

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

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

São os mesmos que tínhamos com o NetBeans.

  • em [7A] [7B] executa-se o teste JUnit do cliente,
  • em [8], o teste é bem-sucedido,
  • em [9], os registos da consola.

Em [10], descarrega-se a aplicação EJB.

3.8.2. A fralda [métier]

  • em [1], importam-se os quatro projetos Maven da camada [métier],
  • em [2], seleciona-se o projeto da empresa e executa-se-o em [3], num servidor Glassfish [4] [5],
  • em [6], o projeto empresarial foi implementado no Glassfish,
  • em [7], analisamos os registos do Glassfish,
1
2
3
4
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: EJB5181:Portable JNDI names for EJB Metier: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]

Na linha 3, anotamos o nome portátil do EJB [Metier] e colamo-lo na consola do cliente deste EJB:


public class ClientRdvMedecinsMetier {

  // o nome da interface remota do EJB [Metier]
  private static String IDaoRemoteName = "java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
  // data de hoje
private static Date jour = new Date();
  • no [8], executamos o cliente de consola,
  • no [9], os seus registos.
  • em [10], descarrega-se a aplicação empresarial;

3.8.3. A camada [web]

  • em [1], importam-se os três projetos Maven da camada [web]. O projeto com a extensão «ear» é o projeto empresarial que deve ser implementado no Glassfish,
  • em [2], executa-se o projeto,
  • no servidor Glassfish [3],
  • em [4], a aplicação empresarial foi corretamente implementada,
  • em [5], solicitamos o URL da aplicação no navegador interno do Eclipse.