Skip to content

3. Aplicativo de exemplo – 01: rdvmedecins-jsf2-ejb

O texto a seguir faz referência aos seguintes documentos:

  • [ref7]: Introdução ao Java EE 5. Este documento permite conhecer o JSF 1 e os EJB3.
  • [ref8]: Persistência em Java na prática. Este documento permite conhecer a persistência de dados com JPA (Java Persistence API).
  • [ref9]: Como criar um serviço web em Java EE com o NetBeans e o servidor GlassFish. Este documento aborda a criação de um serviço web.

O aplicativo de exemplo que será analisado é proveniente de [ref9].

3.1. L'application

Uma empresa de serviços de informática, [ISTIA-AGI], deseja oferecer um serviço de agendamento de consultas. O primeiro mercado-alvo é o dos médicos que atuam por conta própria. Esses profissionais geralmente não contam com secretariado. Os clientes que desejam marcar uma consulta ligam diretamente para o médico. Assim, ele é frequentemente interrompido ao longo do dia, o que reduz sua disponibilidade para os pacientes. A empresa [ISTIA-AGI] deseja oferecer a eles um serviço de agendamento de consultas que funcione com base no seguinte princípio:

  • um secretariado se encarrega de agendar consultas para um grande número de médicos. Esse secretariado pode ser composto por apenas uma pessoa. O salário dessa pessoa é dividido entre todos os médicos que utilizam o serviço.
  • O secretariado e todos os médicos estão conectados à Internet
  • as consultas são registradas em um banco de dados centralizado, acessível pela Internet, tanto pela secretaria quanto pelos médicos
  • O registro do RV é normalmente feito pela secretaria. Ele também pode ser feito pelos próprios médicos. Esse é o caso, notadamente, quando, ao final de uma consulta, o próprio médico atribui um novo RV ao seu paciente.

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

Os médicos ganham em eficiência ao não precisarem mais gerenciar os RV. Se forem em número suficiente, sua contribuição para as despesas operacionais da secretaria será baixa.

A empresa [ISTIA-AGI] decide desenvolver o aplicativo 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 do aplicativo

Chamaremos a aplicação de [RdvMedecins]. Apresentamos abaixo capturas de tela de seu funcionamento.

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

A partir dessa primeira página, o usuário (Secretaria, Médico) realizará uma série de ações. Apresentamos essas ações a seguir. A imagem à esquerda mostra a tela a partir da qual o usuário faz uma solicitação; a imagem à direita, a resposta enviada pelo servidor.

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

3.3. O banco de dados

Voltemos à arquitetura do aplicativo a ser desenvolvido:

O banco de dados, que chamaremos de [dbrdvmedecins2] , é um banco de dados MySQL5 com quatro tabelas:

  

3.3.1. A tabela [MEDECINS]

Ela contém informações sobre os médicos gerenciados pelo aplicativo [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. Esse número é incrementado em 1 sempre que uma alteração é feita na linha.
  • NOM: o sobrenome do médico
  • PRENOM: seu nome
  • TITRE: seu título (Srta., Sra., Sr.)

3.3.2. A tabela [CLIENTS]

Os clientes dos diferentes médicos estão registrados 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. Esse número é incrementado em 1 sempre que uma modificação é feita na linha.
  • NOM: o nome do cliente
  • PRENOM: seu nome
  • TITRE: seu título (Srta., Sra., Sr.)

3.3.3. A tabela [CRENEAUX]

Ela lista os 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. Esse número é incrementado em 1 sempre que uma alteração é feita na linha.
  • ID_MEDECIN: número que identifica o médico ao qual esse horário pertence – chave estrangeira na coluna MEDECINS (ID).
  • HDEBUT: hora de início do horário
  • MDEBUT: minutos de início do horário
  • HFIN: hora de término do horário
  • MFIN: minutos de término do intervalo

A segunda linha da tabela [CRENEAUX] (ver [1] acima) indica, por exemplo, que o horário 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]

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

  • ID: número que identifica o RV de forma exclusiva – chave primária
  • JOUR: dia do RV
  • ID_CRENEAU: horário do RV – chave estrangeira no campo [ID] da tabela [CRENEAUX] – define tanto o horário quanto o médico em questão.
  • ID_CLIENT: número do cliente para o qual a reserva foi feita – 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 nenhum outro lugar. Caso contrário, isso significaria que dois RV foram registrados ao mesmo tempo para o mesmo médico. Do ponto de vista da programação em Java, o driver JDBC do banco de dados aciona um SQLException quando esse caso ocorre.

A linha de id igual a 3 (ver [1] acima) significa que um RV foi agendado para o horário nº 20 e o cliente nº 4 em 23/08/2006. A tabela [CRENEAUX] nos informa que o horário n.º 20 corresponde ao intervalo das 16h20 às 16h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER). A tabela [CLIENTS] nos informa que o cliente nº 4 é a Srta. Brigitte BISTROU.

3.3.5. Geração do banco de dados

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

  • no [1], clicar no ícone do [WampServer] e selecionar a opção [PhpMyAdmin] [2],
  • em [3], na janela que se abriu, selecione o link [Bases de données],
  • em [2], cria-se um banco de dados ao qual foi atribuído o nome [4] e a codificação [5],
  • em [7], o banco de dados foi criado. Clique no link correspondente,
  • em [8], importamos um arquivo SQL,
  • que é selecionado no sistema de arquivos com o botão [9],
  • em [11], seleciona-se o script SQL e, em [12], ele é executado,
  • em [13], as quatro tabelas do banco de dados foram criadas. Seguimos um dos links,
  • em [14], o conteúdo da tabela.

A partir daqui, não voltaremos mais a abordar esse banco de dados. Mas o leitor está convidado a acompanhar sua evolução ao longo dos programas, principalmente quando algo não funcionar.

3.4. As camadas [DAO] e [JPA]

Voltemos à arquitetura que precisamos 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 corporativo que reunirá os três projetos anteriores.

Agora, vamos construir o projeto Maven das camadas [DAO] e [JPA].

Observação: a compreensão das camadas [métier], [DAO] e [JPA] requer conhecimentos em Java EE. Para isso, pode-se consultar [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 precisamos 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 esses 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.

Descreveremos agora 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 conexão do NetBeans com o banco de dados

  • execute o SGBD e o MySQL 5 para que o BD fique disponível,
  • criar uma conexão do NetBeans com o banco de dados [dbrdvmedecins2],
  • na guia [Services] [1], no ramo [Databases] [2], selecione o driver JDBC MySQL [3],
  • em seguida, selecione a opção [4] “Connect Using”, que permite criar uma conexão com um banco de dados MySQL,
  • em [5], forneça as informações solicitadas. Em [6], o nome do banco de dados; em [7], o nome de usuário do banco de dados e sua senha;
  • em [8], é possível testar as informações fornecidas,
  • em [9], a mensagem esperada quando as informações estiverem corretas,
  • em [10], a conexão é estabelecida. Nela, podem-se ver as quatro tabelas do banco de dados conectado.

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

Voltemos à arquitetura em construção:

Estamos construindo a camada [JPA]. Sua configuração é feita em um arquivo [persistence.xml], no qual definimos as unidades de persistência. Cada uma delas requer as seguintes informações:

  • as características JDBC de acesso ao banco de dados (URL, usuário, senha),
  • as classes que servirão de representação das tabelas do banco de dados,
  • a implementação JPA utilizada. De fato, JPA é uma especificação implementada por diversos produtos. Aqui, utilizaremos EclipseLink, que é a implementação padrão utilizada pelo servidor GlassFish. Isso nos poupa de ter que adicionar ao GlassFish as bibliotecas de outra implementação.

O NetBeans pode gerar esse arquivo de persistência por meio de um assistente.

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

O arquivo [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>

Ele contém as informações fornecidas no assistente:

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

Normalmente, nesse arquivo está indicado o tipo de implementação JPA utilizada. No assistente, indicamos EclipseLink. Como essa é a implementação JPA usada por padrão pelo servidor Glassfish, ela não é mencionada no arquivo [persistence.xml].

Na guia [Design], é possível ter uma visão geral do arquivo [persistence.xml]:

Para obter os logs do EclipseLink, utilizaremos o seguinte arquivo [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 estamos utilizando a implementação JPA do EclipseLink,
  • linhas 7-9: reúnem as propriedades de configuração do provedor JPA, neste caso EclipseLink,
  • linha 8: essa propriedade permite registrar as ordens SQL que serão emitidas pelo EclipseLink.

O arquivo [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 arquivo contém as informações que fornecemos nos dois assistentes utilizados anteriormente:

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

Esse arquivo será usado para criar a fonte de dados JNDI [jdbc/dbrdvmedecins2] do servidor Glassfish. Isso é específico desse servidor. Para outro servidor, seria necessário proceder de outra forma, geralmente por meio de uma ferramenta de administração. Essa ferramenta também existe para o Glassfish.

Por fim, foram adicionadas dependências ao projeto. O arquivo [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>
  • linhas 32-37, uma camada [JPA] requer o artefato [javaee-api],
  • linhas 16, 22 e 28: os artefatos necessários para a implementação JPA / EclipseLink utilizada aqui.
  • linhas 18, 24, 30, 36: todos os artefatos possuem o atributo provided. Vale lembrar que isso significa que eles são necessários para a compilação, mas não para a execução. De fato, durante a execução, eles são fornecidos (provided) pelo servidor Glassfish,
  • linhas 41-48: definem um novo repositório de artefatos Maven, onde os artefatos 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 um banco de dados,
  • em [2], seleciona-se a fonte de dados [jdbc / dbrdvmedecins2] criada anteriormente,
  • em [3], a lista de tabelas dessa fonte de dados,
  • em [4], seleciona-se todas elas,
  • 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 a compreensão da 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 define a classe [Medecin] como uma entidade JPA, c.a.d. uma classe vinculada a uma tabela de BD por meio 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. Isso é necessário em aplicações cliente/servidor, nas quais 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 a cada modificação da linha à qual pertence. Para atribuir essa função a ela, é preciso adicionar a anotação @Version. Faremos isso em uma etapa posterior,
  • 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 nas linhas 8-9 esclarecem esse 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 ela inserirá na tabela [Medecins]. Existem várias estratégias possíveis. Aqui, a estratégia GenerationType.IDENTITY indica que a camada JPA 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 associados a ele. Portanto, temos uma relação de um (médico) para vários (horários), 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. Isso sem necessidade de programação. Para compreender totalmente a linha 25, precisamos apresentar a classe [Creneau].

Ela é 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() {
    ...
  }
  
}

Comentaremos apenas as novas anotações:

  • já mencionamos que a tabela [creneaux] possui uma chave estrangeira para a tabela [medecins]: um horário está associado a um médico. Vários horários podem estar associados ao mesmo médico. Temos 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 de 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 responsável pelo horário. Isso também é obtido sem programação.

A relação de chave estrangeira entre a entidade [Creneau] e a entidade [Medecin] é, portanto, representada 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;

As duas anotações refletem a mesma relação: a da chave estrangeira da tabela [creneaux] para a tabela [medecins]. Diz-se que elas são inversas uma da outra. Apenas a relação @ManyToOne é indispensável. Ela define, sem ambiguidade, a relação de chave estrangeira. A relação @OneToMany é opcional. Se estiver presente, ela se limita a referenciar a relação @ManyToOne à qual está associada. Esse é o significado do atributo mappedBy da linha 1 da entidade [Medecin]. O valor desse atributo é o nome do campo da entidade [Creneau] que possui a anotação @ManyToOne, a qual especifica a chave estrangeira. Ainda nessa 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] no banco de dados, então as entidades [Creneau] do campo da linha 2 também devem ser inseridas;
  • se for modificada uma entidade [Medecin] no banco de dados, então as entidades [Creneau] do campo da linha 2 também devem ser modificadas,
  • se for excluída uma entidade [Medecin] do banco de dados, então as entidades [Creneau] do campo da linha 2 também devem ser excluídas.

Apresentamos o código das outras duas entidades sem comentários específicos, uma vez que elas 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” do 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 nos permite obter uma base de trabalho. Às vezes ela é suficiente, outras vezes não. É o caso aqui:

  • é preciso adicionar a anotação @Version aos diversos campos de versão das entidades,
  • é preciso 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 remover as relações @OneToMany inversas às relações @ManyToOne. Elas não são indispensáveis e trazem complicações à programação,
  • eliminamos a validação @NotNull nas chaves primárias. Quando persistimos uma entidade JPA com MySQL, a entidade inicial possui uma chave primária null. Somente após a persistência no banco de dados é que a chave primária do elemento persistido passa a ter um valor.

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

A classe Pessoa é usada 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: observe-se que a classe [Personne] não é, por si só, uma entidade (@Entity). Ela será a classe pai de entidades. A anotação @MappedSuperClass indica essa situação.

A entidade [Client] encapsula as linhas da tabela [clients]. Ela 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: ela está associada à tabela [clients],
  • linha 8: ela 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] do banco de dados: um médico tem vários horários, e 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) do banco de dados, e as linhas 25 a 27 modelam a relação “muitos para um” que existe entre a tabela [rv] e a tabela [creneaux] (um horário pode aparecer em vários Rv).

3.4.3. A classe de exceção

A classe de exceção [RdvMedecinsException] do aplicativo é 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]. Portanto, o compilador não obriga 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 entender 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] dentro do contêiner EJB3 e interceptada por este. Sem a anotação @ApplicationException, o contêiner EJB3 encapsula a exceção ocorrida em uma exceção do tipo [EjbException] e a relança. Pode-se optar por não realizar esse encapsulamento e permitir que uma exceção do tipo [RdvMedecinsException] seja lançada pelo contêiner EJB3. É isso que a anotação @ApplicationException permite. Além disso, o atributo (rollback=true) dessa anotação indica ao contêiner EJB3 que, se a exceção do tipo [RdvMedecinsException] ocorrer dentro de um método executado em uma transação com um SGBD, esta deve ser revertida. Em termos técnicos, isso é chamado de realizar 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 em um determinado dia
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
  // localizar um cliente identificado por seu ID
  public Client getClientById(Long id);
  // localizar um cliente identificado por seu ID
  public Medecin getMedecinById(Long id);
  // encontrar uma consulta identificada pelo seu ID
  public Rv getRvById(Long id);
  // encontrar um horário identificado pelo seu ID
  public Creneau getCreneauById(Long id);
  // adicionar um RV
  public Rv ajouterRv(Date jour, Creneau creneau, Client client);
  // excluir um RV
  public void supprimerRv(Rv rv);
}

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

  • linha 14: a lista de clientes. Precisaremos 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 de horários disponíveis de um médico. Precisaremos dela para exibir 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, ela nos permitirá exibir a agenda do médico para um determinado dia com seus horários já reservados,
  • linha 22: permite localizar um cliente a partir de seu número. O método nos permitirá localizar um cliente a partir de uma seleção na lista suspensa de clientes,
  • linha 24: o mesmo vale para os médicos,
  • linha 26: localiza um agendamento pelo seu número. Pode ser usado ao excluir um agendamento para verificar previamente se ele realmente existe,
  • linha 28: localiza um horário a partir de seu número. Permite identificar o horário que um usuário deseja adicionar ou excluir,
  • linha 30: para adicionar um compromisso,
  • linha 32: para excluir um compromisso.

A interface local [IDaoLocal] do EJB limita-se a derivar a 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 implementa as duas interfaces, local e 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 será criada apenas 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 pool de EJB,
  • a linha 6 indica que todos os métodos do EJB são executados dentro de uma transação gerenciada pelo contêiner 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 de 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, em um 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
  // horário: horário da consulta
  // cliente: cliente para o qual a consulta foi marcada
  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);
    }
  }

  // cancelamento de uma consulta
  // consulta: a consulta excluída
  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 gerencia o acesso ao contexto de persistência. Ao instanciar a classe, esse campo será inicializado pelo contêiner EJB por meio da anotação @PersistenceContext da linha 21,
  • linha 27: consulta JPQL (Java Persistence Query Language) que retorna todas as linhas da tabela [clients] na 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]. Ela é parametrizada pelo ID do médico,
  • linha 57: uma consulta JPQL que realiza uma junção entre as tabelas [rv], [creneaux] e [medecins] e possui dois parâmetros: o ID do médico e o dia da consulta,
  • linhas 69-73: criação de uma consulta e, em seguida, seu armazenamento no banco de dados,
  • linha 83: exclusão de uma consulta do banco de dados,
  • linha 92: executa um select no banco de dados para localizar um determinado cliente,
  • linha 101: o mesmo para um médico,
  • linha 110: o mesmo para uma consulta,
  • linha 119: o mesmo para um horário,
  • todas as operações com o contexto de persistência em da linha 22 podem apresentar um problema com o banco de dados. Por isso, todas elas estão envoltas por um try/catch. A eventual exceção é encapsulada na exceção “própria” RdvMedecinsException.

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

Na arquitetura abaixo:

O EclipseLink depende do driver JDBC do MySQL. É necessário instalar esse driver nas bibliotecas do servidor GlassFish, na pasta <glassfish>/domains/domain1/lib/ext, onde <glassfish> é a pasta de instalação do servidor GlassFish. É possível obtê-lo da seguinte maneira:

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

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

Voltemos à arquitetura construída até o momento:

O conjunto [web, métier, DAO, JPA] deve ser implantado no servidor Glassfish. Vamos fazer isso:

  • em [1], compilamos o projeto Maven,
  • em [2], executamos o projeto,
  • em [3], ele foi implantado no servidor Glassfish (aba [Services])

Pode-se ter a curiosidade de verificar os logs do Glassfish:

No [1], os logs do Glassfish estão disponíveis na aba [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 logs de EclipseLink; as identificadas por [Infos] provêm do Glassfish.

  • linhas 1-12: EclipseLink processa as entidades JPA que encontrou,
  • linhas 13-17: informações que indicam que o processamento das entidades JPA ocorreu normalmente,
  • linha 18: EclipseLink se identifica,
  • linha 19: EclipseLink reconhece que está lidando com SGBD MySQL,
  • linhas 20-24: EclipseLink tenta se conectar ao BD,
  • linhas 25-28: ele conseguiu,
  • linhas 29-33: ele tenta se reconectar, desta vez usando 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 reconhecidos por todos os servidores de aplicativos Java EE 6,
  • linha 40: os nomes das interfaces remota e local do EJB [DaoJpa], específicos do Glassfish. Usaremos, no teste a seguir, 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] de nossa aplicação foi implantado, podemos testá-lo. Faremos isso no contexto de uma aplicação cliente/servidor:

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

Começamos criando um novo projeto no Maven:

  • No [1], criamos um novo projeto,
  • em [2,3], criamos um projeto Maven do tipo [Java Application],
  • em [4], atribuímos um nome a ele e o colocamos na mesma pasta que o EJB e o [DAO],
  • em [5], o projeto gerado,
  • em [6], foi gerada uma classe [App.java]. Vamos excluí-la,
  • em [7], foi gerado um ramo [Source Packages]. Ainda não tínhamos encontrado esse ramo. Podemos colocar testes JUnit nesse ramo. Faremos isso. Não manteremos a classe de teste [AppTest] gerada,
  • em [8], as dependências do projeto Maven. O branch [Dependencies] está vazio. Teremos que adicionar novas dependências a ele. 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 que alterá-la.

O projeto evolui da seguinte forma:

  • para [1], o projeto em que as duas classes geradas foram removidas, assim como a dependência JUnit.

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

O cliente precisa conhecer a interface remota oferecida pelo EJB e pelo [DAO]. Além disso, ele irá trocar entidades do JPA com o EJB. Portanto, ele precisa da definição dessas entidades. Para que o projeto de teste do EJB tenha acesso a essas 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 a aba [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 se comunicam pela rede TCP-IP. Não vamos programar essas trocas de dados. Para cada servidor de aplicativos, existe uma biblioteca a ser integrada às dependências do cliente. A do Glassfish se chama [gf-client]. Vamos adicioná-la:

  • no [1], adicionamos uma dependência,
  • em [2], definimos as características do artefato desejado,
  • em [3], são adicionadas inúmeras dependências. O Maven irá baixá-las. Isso pode levar vários minutos. Em seguida, elas são armazenadas no repositório local do Maven.

Agora podemos criar o teste JUnit:

  • em [2], clica-se com o botão direito do mouse em [Test Packages] para criar um novo teste JUnit,
  • em [3], atribua um nome à classe de teste e um pacote para ela, [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 arquivo [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>

Observe:

  • nas linhas 32 a 51, as dependências do projeto,
  • linhas 13-26: foram definidos dois repositórios Maven, um para EclipseLink (linhas 14-19) e outro para 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 em um 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 gerar 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();
      }
      // registra-se o erro
      erreur=true;
    }
    // verifica-se se houve 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));
    // excluir 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 — exibe 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 de EJB [DaoJpa]. Lembramos que lhe havíamos dado o nome JNDI “rdvmedecins.dao”,
  • linhas 34-35: exibem a lista de clientes,
  • linhas 37-38: exibem a lista de pacientes,
  • linhas 40-42: exibem 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 seu horário nº 2 e o dia da linha 21,
  • linha 52: exibe, para verificação, os agendamentos do primeiro médico para o dia da linha 21. Deve haver pelo menos um, aquele que acabamos de adicionar,
  • linhas 55-70: adiciona-se a mesma consulta. Como a tabela [RV] possui uma restrição de unicidade, essa adição deve gerar uma exceção. Verifica-se isso na linha 70,
  • linha 72: exibimos, para verificação, as consultas do primeiro médico para o dia da linha 21. A consulta que queríamos adicionar não deve estar lá,
  • linhas 74-76: exclui-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 acabamos de excluir não deve constar ali.

Este teste é um teste falso JUnit. Nele há apenas uma asserção (linha 70). Trata-se de um teste visual com as falhas inerentes a ele.

Se tudo estiver correto, os testes devem ser aprovados:

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

Vamos examinar mais de perto 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]

Recomenda-se ao leitor que leia esses logs juntamente com o código que os gerou. Vamos nos concentrar na exceção que ocorreu ao adicionar um compromisso já existente, nas linhas 41 a 49. A pilha de exceções é reproduzida nas linhas 42 a 48. Ela é inesperada. Voltemos ao código do método de adição de um compromisso:


  // adição de um compromisso
  // dia: dia do compromisso
  // intervalo: intervalo horário da consulta
  // cliente: cliente para o qual foi agendada 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);
    }
}

Vamos examinar os logs 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 será executado. Observe-se que ela não ocorre ao mesmo tempo que a operação persist. Se fosse esse o caso, esse registro teria aparecido antes da linha 2. A operação INSERT ocorre, então, normalmente no final da transação na qual o método é executado,
  • linha 6: EclipseLink solicita a MySQL qual é a última chave primária utilizada. Ele obterá a chave primária da consulta adicionada. Esse valor preencherá o campo id da entidade [Rv] persistida,
  • linhas 7-8: a consulta SELECT, que exibirá os compromissos do médico,
  • linhas 9-10: as exibições na tela da segunda consulta persist,
  • linhas 11-12: a ordem INSERT que será executada. Ela deve provocar uma exceção. Essa exceção aparece nas linhas 15-16 e é clara. Ela é acionada inicialmente pelo driver JDBC do MySQL por violação da restrição de exclusividade das consultas. Deduz-se que essas exceções deveriam aparecer nos logs do teste JUnit. No entanto, esse não é 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

Vale lembrar a arquitetura cliente/servidor do teste:

Quando o EJB [DAO] lança uma exceção, ela deve ser serializada para chegar ao cliente. Provavelmente foi essa operação que falhou por um motivo que não consegui entender. Como nossa aplicação completa não funcionará no modelo cliente/servidor, podemos ignorar esse 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 do aplicativo em desenvolvimento:

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

3.5.1. O projeto NetBeans

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

  • no [1], o projeto Maven da camada [métier],
  • no [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]. Vale lembrar que isso significa que ele é necessário para a compilação, mas não para a execução do projeto. De fato, oEJB da camada [métier] será implantado no servidor Glassfish junto com o EJB das camadas [DAO] e [JPA]. Portanto, quando for executado, o EJB das camadas [DAO] e [JPA] já estará presente,
  • no [6], o novo projeto com 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 de consultas de um médico em um determinado dia
    public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);

    // localizar um cliente identificado por seu ID
    public Client getClientById(Long id);

    // localizar um cliente identificado por seu ID
    public Medecin getMedecinById(Long id);

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

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

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

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

}

Para entender essa interface, é preciso lembrar a arquitetura do projeto:

Definimos a interface da camada [DAO] (parágrafo 3.4.4) e indicamos que ela atendia às necessidades da camada [web], ou seja, às necessidades do usuário. A camada [web] se comunica com a camada [DAO] apenas por meio da camada [métier]. Isso explica por que encontramos na camada [métier] todos os métodos da camada [DAO]. Esses métodos se limitarão a delegar a solicitação da camada [web] à camada [DAO]. Nada mais.

Durante o estudo da aplicação, surge uma necessidade: ser capaz de exibir em uma página da web a agenda de um médico para um determinado dia, a fim de saber os horários ocupados e disponíveis naquele dia. Esse é tipicamente o caso quando a secretária atende a uma solicitação por telefone. Pede-se a ela uma consulta para tal dia com tal médico. Para atender a essa necessidade, a camada [métier] oferece o método da linha 46.


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

Pode-se questionar onde colocar esse método:

  • poderia ser colocado na camada [DAO]. No entanto, esse método não atende realmente a uma necessidade de acesso aos dados, mas sim a uma necessidade de negócio;
  • poderíamos colocá-lo 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 ainda esteja presente.

O método recebe como parâmetros o médico e o dia para o qual se deseja a agenda de consultas. Ele retorna um objeto [AgendaMedecinJour] que representa a agenda do médico para aquele 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;

    // fabricantes
    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, caso o horário esteja disponível.

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

Nosso EJB [Metier] terá uma interface local e uma interface remota, que se limitarão a herdar 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 essas interfaces da seguinte maneira:


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 agendamentos desse mesmo médico para o mesmo dia
    List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
    // cria-se um dicionário a partir das consultas agendadas
    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 horários disponíveis para agendamento
    CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
    agenda.setCreneauxMedecinJour(creneauxMedecinJour);
    // preenchimento dos horários de agendamento
    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 horário está livre ou reservado?
      if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
        // o horário está ocupado — registra-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 é executado dentro de uma transação. Isso significa que a transação é iniciada no início do método, na camada [métier]. Esta chamará métodos da camada [DAO]. Esses métodos serão executados dentro da mesma transação,
  • na linha 24, o EJB implementa 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 contêiner EJB do servidor Glassfish, graças à anotação @EJB. Portanto, quando os métodos da classe [Metier] são executados, a referência à EJB da camada [DAO] já foi inicializada,
  • linhas 33-81: essa referência é usada para delegar à camada [DAO] a chamada feita à 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. Implantaçã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], precisamos implantar os dois EJB. Para isso, precisamos de um projeto corporativo.

  • [1], criamos um novo projeto,
  • do tipo Maven [2] e Aplicativo corporativo [3],
  • e atribuímos a ele o nome [4]. O sufixo ear será adicionado automaticamente,
  • em [5], escolhe-se o servidor Glassfish e Java EE 6,
  • em [6], uma aplicação corporativa contém módulos, geralmente módulos EJB e módulos web. Aqui, a aplicação corporativa conterá os módulos dos dois EJB que criamos. Como esses módulos já existem, não marcamos as caixas de seleção,
  • no [7,8], foram criados dois projetos. O [8] é o projeto corporativo que vamos usar. O [7] é um projeto cuja função desconheço. Não precisei usá-lo e, como não me aprofundei no Maven, não sei para que ele pode servir. Portanto, vamos ignorá-lo.

Agora que o projeto corporativo está criado, podemos definir seus módulos.

  • no [1], criamos uma nova dependência,
  • em [2], escolhemos o projeto EJB [DAO],
  • em [3], declara-se que se trata de um EJB. Não deixe o tipo em branco, pois, nesse caso, será utilizado o tipo jar, e aqui esse tipo não é adequado,
  • no [4], utiliza-se o escopo [compile],
  • no [5], o projeto com 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], ele é executado,
  • em [12], na aba [Services], vemos que o projeto foi implantado no servidor Glassfish. Isso significa que os dois EJBs agora estão presentes no servidor.

Nos logs do servidor Glassfish, encontramos informações sobre a implantação dos dois EJB:

  • e [1], na aba de logs do Glassfish.

Lá estão os seguintes logs:

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 construção da unidade de persistência [dbrdvmedecins2-PU] foi bem-sucedida e que a conexão com o banco de dados associado 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 aplicativos,
  • linha 9: o mesmo, mas com nomes proprietários do GlassFish,
  • linhas 10-11: o mesmo para o EJB e o [Metier].

Vamos considerar o nome portátil da interface remota de EJB e [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

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

3.5.3. Teste da camada [métier]

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

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

Começamos criando um novo projeto Maven. Para isso, 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 suas dependências: em relação ao EJB da camada [dao], em relação ao EJB da camada [métier], da biblioteca [gf-client].

Nesse ponto, o arquivo [pom.xml] do projeto fica 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-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 as dependências descritas nas linhas 17 a 33 estejam presentes. O teste será uma simples classe de console:

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);
      // exibiçã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);
      // exibição dos horários de atendimento 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 em um 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));
      // exibição da agenda
      agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // excluir 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));
      // exibiçã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 — exibe 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 logs do GlassFish,
  • linhas 24-27: obtém-se uma referência à interface remota do EJB [Metier],
  • linhas 29-30: exibem os clientes,
  • linhas 32-33: exibem os médicos,
  • linhas 35-37: exibem os horários disponíveis de um médico,
  • linha 39: exibe as consultas de um médico em um determinado dia,
  • linhas 41-42: a agenda desse mesmo médico para o mesmo dia,
  • linhas 44-49: adiciona-se uma consulta,
  • linha 50: exibe-se a lista de consultas do médico. Deve haver mais uma,
  • linhas 52-53: exibe-se a agenda do médico. Deve-se ver a consulta adicionada,
  • linhas 55-57: exclui-se a consulta que acabou de ser adicionada,
  • linha 58: isso deve aparecer na lista de consultas do médico,
  • linhas 60-61: e na agenda dele.

Executamos o teste:

 

As exibições na tela 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, em 23 de maio de 2012. Nenhum horário está reservado,
  • linha 39: adição de um compromisso,
  • linha 42: a nova agenda da Sra. PELISSIER. Um horário está agora reservado para o Sr. MARTIN,
  • linha 44: o compromisso foi excluído,
  • linha 46: a agenda da Sra. PELISSIER mostra que nenhum horário está reservado.

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

3.6. A camada [web]

Voltemos à arquitetura que estamos construindo:

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

3.6.1. O projeto NetBeans

Estamos criando um projeto Maven:

  • em [1], criamos um novo projeto,
  • em [2, 3], um projeto Maven do tipo [Web Application],
  • em [4], atribuímos um nome a ele,
  • 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 a remoção da página [index.jsp] e do 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 padrão. Observa-se que é o JSF 2 que é utilizado,
  • em [12], o projeto é então modificado em dois pontos: é gerado um arquivo [web.xml], bem como uma página [index.html].

O arquivo [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 deparamos com esse arquivo.

  • linhas 7-11: definem o servlet que processará todas as solicitações feitas à aplicação. Trata-se do servlet de JSF,
  • linhas 12-15: definem os URL processados por essa servlet. São os URL no formato /faces/*,
  • linhas 21-23: definem a página [index.xhtml] como página inicial.

Essa 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 antes. 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 seus diferentes elementos.

  • em [1], as páginas XHTML do projeto,
  • em [2], os códigos Java,
  • em [3], os arquivos de mensagens, já que o aplicativo é internacionalizado,
  • em [4], as dependências do projeto.

3.6.2. As dependências do projeto

Voltemos à arquitetura do projeto:

A camada JSF depende das camadas [métier], [DAO] e [JPA]. Essas três camadas estão encapsuladas nos dois projetos Maven que criamos, o que explica as dependências do projeto [4]. Vamos mostrar, de forma simples, como essas dependências são adicionadas:

  • no [1], colocaremos ejb para indicar que a dependência é de um projeto EJB,
  • no [2], colocaremos [provided]. De fato, o projeto web será implantado ao mesmo tempo que os dois projetos EJB. Portanto, não é necessário incluir os arquivos JAR do EJB.

3.6.3. A configuração do projeto

A configuração do projeto é a mesma dos projetos JSF que analisamos no início deste documento. Listamos os arquivos de configuração sem explicá-los novamente.

 

[web.xml]: configura o aplicativo 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>

Observe, na linha 26, que a página [index.xhtml] é a página inicial do aplicativo.

[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 do aplicativo


.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 arquivo 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 arquivo 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 visualizações do projeto

Vamos relembrar como o aplicativo funciona. A página inicial é a seguinte:

 

A partir dessa primeira página, o usuário (Secretaria, Médico) realizará uma série de ações. Apresentamos essas ações a seguir. A visualização à esquerda mostra a tela a partir da qual o usuário faz uma solicitação; a visualização à direita, a resposta enviada pelo servidor.

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

Essas diferentes visualizações são obtidas nas 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 visualização produzida por [layout.xhtml].

A tecnologia utilizada aqui é a dos facelets. Ela foi descrita no parágrafo 2.11. Limitar-nos-emos 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>

Observe-se, nas linhas 10 a 12, os dois links para alterar o idioma do aplicativo.

[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) na [layout.xhtml] (linha 7). Essa é a área central das visualizações:

 

A página [index.xhtml] é a única página do aplicativo. Portanto, não haverá navegação entre páginas. Ela exibe uma das quatro páginas [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. Essa exibição é controlada por quatro variáveis booleanas [form1Rendered, form2Rendered, form3Rendered, erreurRendered] do bean de formulário, que descreveremos a seguir.

3.6.5. Os beans do projeto

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

  • a classe [ChangeLocale] é a responsável pela mudança de idioma. Ela já foi analisada (parágrafo 2.4.4).
  • a classe [Messages] é uma classe que facilita a internacionalização das mensagens de um aplicativo. Ela 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ócios
  @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() {
    // armazenamos médicos e clientes no cache
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // registramos 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) {
      // registra-se o erro
      erreur = true;
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // registra-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 de escopo Application. Ela é criada uma única vez no início do ciclo de vida da aplicação JSF e fica acessível a todas as solicitações de todos os usuários. Nela, geralmente são colocados dados somente para leitura. Aqui, colocaremos a lista de médicos e a lista de clientes. Partimos, portanto, do pressuposto de que essas listas não mudam com frequência. As páginas XHTML têm acesso a ela por meio do nome da aplicação,
  • linhas 20-21: uma referência à interface local do EJB [Metier] será injetada pelo contêiner EJB do GlassFish. Vamos relembrar 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). Portanto, a camada [JSF] utilizará a interface local da EJB. Aqui, o bean da aplicação utiliza a EJB e a [Metier]. Mesmo que não fosse esse o caso, seria normal encontrar uma referência na camada [métier]. Trata-se, de fato, de uma informação que pode ser compartilhada por todas as solicitações de todos os usuários; portanto, um dado com escopo Application.

  • linhas 34-35: o método init é executado logo 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 por seu ID na linha 25 e o mesmo para os clientes na linha 26. Podem ocorrer erros. Estes são registrados 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 uma exceção tenha sido lançada,
  • linha 10: uma mensagem de erro.

3.6.5.2. O bean [Form]

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 ocorreu corretamente?
    if (application.getErreur()) {
      // recuperando a lista de erros
      erreurs = application.getErreurs();
      // a visualização dos erros é exibida
      setForms(false, false, false, true);
    }
  }

  // exibição da visualização
  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 de nome “form” e de escopo de sessão. Vale lembrar que, nesse caso, a classe deve ser serializável.
  • linhas 13-14: o bean “form” possui uma referência ao bean “application”. Essa referência será injetada pelo contêiner 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 dessas páginas é controlada pelos valores booleanos nas linhas 19-22. Observe-se que, por padrão, é a página [form1.xhtml] que é exibida,
  • 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 é usado para determinar qual página deve ser exibida primeiro: normalmente a página [form1.xhtml] (linha 19), a menos que a inicialização do aplicativo tenha ocorrido com erro (linha 36); nesse caso, será exibida 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>

Ela utiliza uma tag <h:dataTable> (linhas 14-27) para exibir a lista de erros. Isso resulta em uma página semelhante à seguinte:

Image

Vamos agora definir as diferentes fases do ciclo de vida do aplicativo.

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

3.6.6.1. Exibição da página inicial

Se tudo correr bem, a primeira página exibida será [form1.xhtml]. Isso resulta na 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>

Essa 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 gravação, o valor da lista da linha 18 da página. Na exibição inicial da página, ela define o valor selecionado no menu suspenso. Na exibição inicial, idMedecin é igual a null; portanto, o primeiro médico será selecionado;
  • o método das linhas 13-15 gera os itens da lista suspensa de médicos (linha 19 da página). Cada opção gerada terá como rótulo (itemLabel) o título, sobrenome e nome 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 entrada da linha 21 da página. Na exibição inicial, é exibida a data de hoje,
  • linhas 17-19: o método getAgenda gerencia o clique no botão [Agenda] da linha 26 da página. Como não há navegação (sempre é solicitada a página [index.html]), costuma-se usar o atributo actionListener em vez do atributo action. Nesse caso, o método chamado no modelo não retorna nenhum resultado.

Quando o usuário clica no botão [Agenda],

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

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 {
      // busca do 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);
      // exibe-se 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 de erros é exibida
    setForms(false, false, false, true);
}

Vale lembrar o que o método getAgenda deve exibir:

  • linha 21: recupera-se o médico selecionado no dicionário de médicos, que foi armazenado no bean application. Para isso, utiliza-se seu ID, que foi enviado em idMedecin,
  • linha 23: prepara-se o título da página [form2.xhtml] que será exibida. Essa mensagem é extraída do arquivo de mensagens para que possa ser internacionalizada. Essa técnica foi descrita no parágrafo 2.8.5.7, página 135.
  • linha 25: é chamada a camada [métier] para calcular a agenda do médico selecionado para o dia escolhido,
  • linha 27: exibe-se [form2.xhtml],
  • linha 28: se ocorrer uma exceção, é gerada uma lista de erros (linhas 37-42) e a página [erreur.xhtml] é exibida (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>

Lembramos que o método getAgenda inicializou dois campos no modelo:


// modelo
  private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;

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

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

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


// modelo
  private Long idCreneau;

  // ação em RV
  public void action() {
    ...
}
  • o método action é chamado quando o usuário clica no link Reservar / Excluir (linha 32). Observe-se que aqui foi utilizado o atributo action. O método apontado por esse atributo deveria ter a assinatura String action(), pois o método deve, nesse caso, retornar uma chave de navegação. No entanto, aqui ela é `void action()`. Isso não causou nenhum erro e pode-se supor que, nesse caso, não haja navegação. Era isso que se desejava. Colocar `actionListener` em vez de `action` causava um mau funcionamento,
  • o campo idCreneau da linha 2 irá recuperar o ID do horário do link que foi clicado (linha 33 da página).

3.6.6.3. Exclusão de um compromisso

Vamos examinar o código que gerencia a exclusão de um compromisso. Isso corresponde à seguinte sequência de visualizações:

O código relacionado a essa 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 em RV
  public void action() {
    // procurando 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?
    if (!trouvé) {
      // isso é estranho — exibimos novamente o form2
      setForms(false, true, false, false);
      return;
    }
    // encontramos
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // de acordo com a ação desejada
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // reserva

  public void reserver() {
    ...
  }

  public void supprimer() {
    try {
      // exclusã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 horário selecionado foi gravado em idCreneau (linha 11),
  • linhas 18-26: procura-se recuperar o intervalo de horário a partir do seu id (linha 21). Procura-se no calendário atual, agendaMedecinJour da linha 10. Normalmente, deve ser encontrado. Caso contrário, não se faz nada (linhas 28-32),
  • linha 34: se o horário procurado foi encontrado, recupera-se uma referência que é armazenada na linha 12,
  • linha 36: verifica-se se o intervalo escolhido já tinha um compromisso. Se sim, ele é excluído (linha 39); caso contrário, reserva-se um (linha 37),
  • linha 51: a consulta do horário escolhido é excluída. É a camada [métier] que realiza essa tarefa,
  • linha 53: solicita-se à camada [métier] a nova agenda do médico. É claro que veremos um compromisso a menos. Mas, como o aplicativo é multiusuário, podemos ver as alterações feitas por outros usuários,
  • linha 55: exibimos novamente a página [form2.xhtml],
  • linha 58: como a camada [métier] foi acionada, podem ocorrer exceções. Nesse caso, a pilha de exceções é armazenada na lista de erros da linha 13 e exibida por meio da visualização [erreur.xhtml].

3.6.6.4. Agendamento de consultas

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

O modelo envolvido nessa 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 em RV
  public void action() {
...
    // foi encontrado
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // de acordo com a ação desejada
    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;
      // exibe-se o formulário 3
      setForms(false, false, true, false);
    } catch (Throwable th) {
      // visualização de erros
      prepareVueErreur(th);
    }
  }
  • linha 14: se o horário escolhido não tiver nenhuma consulta agendada, 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, há um campo de lista suspensa cujo valor é fornecido por idClient. Definimos o valor desse campo como 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úmero do cliente preenche o atributo value da lista suspensa de clientes na linha 12 da página. Ele define o item 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, é esse valor que será enviado.

3.6.6.5. Validação de um agendamento

A validação de um agendamento segue a 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á gerenciar esse evento. Seu código é o seguinte:


  // bean de 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 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 o form2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // visualização de erros
      prepareVueErreur(th);
    }
}
  • linha 12: antes que o método validerRv seja executado, o campo idClient recebeu o ID do cliente selecionado pelo usuário,
  • linha 19: a partir do ID do intervalo de horário memorizado em uma etapa anterior (o bean tem escopo de sessão), solicita-se à camada [métier] uma referência ao próprio intervalo de horário,
  • linha 21: solicita-se à camada [métier] que adicione um compromisso para o dia escolhido (dia), o horário escolhido (horário) e o cliente escolhido (idClient),
  • linha 23: solicita-se à camada [métier] que atualize a agenda do médico. Veremos a consulta adicionada, além de todas as alterações que outros usuários do aplicativo possam ter feito,
  • linha 25: exibe-se novamente a agenda [form2.xhtml],
  • linha 28: exibe-se a página de erro caso ocorra algum erro.

3.6.6.6. Cancelamento de uma consulta

Isso 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 de consulta
  public void annulerRv() {
    // exibe o formulário 2
    setForms(false, true, false, false);
}

3.6.6.7. Voltar à página inicial

Resta verificar mais uma ação, a da sequência a seguir:

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

Criamos o seguinte aplicativo:

Nos concentramos mais nas funcionalidades do aplicativo do que em sua aparência para o usuário. Esta será aprimorada com o uso da biblioteca de componentes PrimeFaces. Criamos um aplicativo básico, mas que, mesmo assim, é representativo de uma arquitetura Java EE em camadas, utilizando EJB. O aplicativo pode ser aprimorado de várias maneiras:

  • é necessária uma autenticação. Nem todos têm permissão para adicionar ou excluir compromissos;
  • deveria ser possível percorrer a agenda para frente e para trás ao procurar um dia com horários disponíveis,
  • deveria ser possível solicitar a lista de dias em que há horários disponíveis para um médico. De fato, se ele for oftalmologista, suas consultas geralmente são agendadas 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 seu cliente,
  • em [2], seleciona-se o projeto EJB da camada [DAO] e o executa-se em [3],
  • em [4], executa-se em um servidor,
  • em [5], apenas o servidor Glassfish é sugerido, pois é o único que possui um contêiner EJB,
  • no [6], o módulo EJB foi implantado,
  • no [7], exibimos os logs:
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] e [7B], executa-se o teste JUnit do cliente,
  • em [8], o teste é aprovado,
  • em [9], os logs do console.

Em [10], descarregamos o aplicativo 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 corporativo e o executa-se em [3], em um servidor Glassfish [4] [5],
  • em [6], o projeto empresarial foi implantado no Glassfish,
  • em [7], verificamos os logs 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 o colamos no console 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 console,
  • no [9], seus logs.
  • em [10], faz-se o download do aplicativo corporativo;

3.8.3. A camada [web]

  • em [1], importam-se os três projetos Maven da camada [web]. Aquele com a extensão “ear” é o projeto corporativo que deve ser implantado no GlassFish,
  • em [2], ele é executado,
  • no servidor Glassfish [3],
  • em [4], a aplicação corporativa foi implantada com sucesso,
  • em [5], solicitamos o URL do aplicativo no navegador interno do Eclipse.