Skip to content

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

O texto a seguir refere-se aos seguintes documentos:

A aplicação de exemplo a ser estudada provém de [ref9].

3.1. A Aplicação

Uma empresa de serviços de TI [ISTIA-AGI] pretende oferecer um serviço de agendamento de consultas. O principal mercado-alvo são os médicos que exercem em regime individual. Estes médicos geralmente não dispõem de pessoal administrativo. Os clientes que desejam marcar uma consulta ligam, portanto, diretamente para o médico. Isto perturba frequentemente o trabalho do médico ao longo do dia, reduzindo a sua disponibilidade para os pacientes. A empresa [ISTIA-AGI] pretende oferecer-lhes um serviço de agendamento de consultas baseado no seguinte princípio:

  • uma recepcionista gere a marcação de consultas para um grande número de médicos. Esta recepcionista pode ser apenas uma pessoa. O seu salário é partilhado entre todos os médicos que utilizam o serviço de marcação de consultas.
  • O escritório administrativo e todos os médicos estão ligados à Internet
  • As consultas são registadas numa base de dados centralizada, acessível através da Internet pelo escritório administrativo e pelos médicos
  • As consultas são normalmente agendadas pelo escritório administrativo. Também podem ser agendadas pelos próprios médicos. Este é particularmente o caso quando, no final de uma consulta, o médico marca uma nova consulta para o paciente.

A arquitetura do serviço de agendamento de consultas é a seguinte:

Os médicos tornam-se mais eficientes se deixarem de ter de gerir as consultas. Se houver um número suficiente deles, a sua contribuição para os custos operacionais do serviço administrativo será mínima.

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

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

3.2. Como funciona a aplicação

Vamos chamar à aplicação [RdvMedecins]. Abaixo estão capturas de ecrã que mostram como funciona.

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

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

Por fim, também pode aparecer uma página de erro:

3.3. A base de dados

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

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

  

3.3.1. A tabela [MEDECINS]

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

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

3.3.2. A tabela [CLIENTS]

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

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

3.3.3. A tabela [SLOTS]

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

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

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

3.3.4. A tabela [RV]

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

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

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

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

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

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

3.3.5. Gerar a base de dados

Para criar as tabelas e preenchê-las, pode utilizar o script [dbrdvmedecins2.sql], que se encontra no site de exemplos. Utilizando o [WampServer] (ver secção 1.3.3), proceda da seguinte forma:

  • Em [1], clique no ícone [WampServer] e selecione a opção [PhpMyAdmin] [2],
  • em [3], na janela que se abre, selecione o link [Bases de dados],
  • em [2], crie uma base de dados com o nome [4] e a codificação [5],
  • em [7], a base de dados foi criada. Clique no seu link,
  • em [8], importe um ficheiro SQL,
  • que seleciona a partir do sistema de ficheiros utilizando o botão [9],
  • em [11], selecione o script SQL e, em [12], execute-o,
  • em [13], as quatro tabelas na base de dados foram criadas. Siga uma das ligações,
  • em [14], o conteúdo da tabela.

Não voltaremos a esta base de dados. No entanto, convidamos o leitor a acompanhar a sua evolução ao longo dos programas, especialmente quando as coisas não funcionam.

3.4. As camadas [DAO] e [JPA]

Voltemos à arquitetura que precisamos de construir:

Vamos criar quatro projetos Maven:

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

Vamos agora criar o projeto Maven para as camadas [DAO] e [JPA].

Nota: Compreender as camadas [negócio], [DAO] e [JPA] requer conhecimentos de Java EE. Para tal, pode consultar [ref7] (ver parágrafo 3).

3.4.1. O projeto NetBeans

Eis o que deve fazer:

  • Em [1], criamos um projeto Maven do tipo [Módulo EJB] [2],
  • em [3], nomeamos o projeto,
  • em [4], selecionamos o servidor GlassFish,
  • em [5], o projeto gerado.

3.4.2. Gerar a camada [JPA]

Voltemos à arquitetura que precisamos de construir:

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

Iremos agora descrever algumas destas ferramentas de geração automática. Para compreender o código gerado, é necessário ter um conhecimento sólido das entidades JPA [ref8] e dos EJBs [ref7] (ver Secção 3).

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

  • Inicie o SGBD MySQL 5 para que a base de dados fique disponível,
  • crie uma ligação do NetBeans à base de dados [dbrdvmedecins2],
  • no separador [Serviços] [1], na secção [Bases de dados] [2], selecione o controlador JDBC do MySQL [3],
  • depois selecione a opção [4] «Ligar utilizando» para criar uma ligação a uma base de dados MySQL,
  • em [5], introduza as informações solicitadas. Em [6], o nome da base de dados; em [7], o utilizador e a palavra-passe da base de dados;
  • em [8], pode testar as informações que forneceu,
  • em [9], a mensagem esperada se as informações estiverem corretas,
  • em [10], a ligação é estabelecida. Pode ver as quatro tabelas na base de dados conectada.

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

Voltemos à arquitetura que estamos a construir:

Estamos atualmente a construir a camada [JPA]. A sua configuração é feita num ficheiro [persistence.xml], onde são definidas as unidades de persistência. Cada uma delas requer as seguintes informações:

  • os detalhes da ligação JDBC à base de dados (URL, nome de utilizador, palavra-passe),
  • as classes que representarão as tabelas da base de dados,
  • a implementação JPA utilizada. Na verdade, o JPA é uma especificação implementada por vários produtos. Aqui, utilizaremos o EclipseLink, que é a implementação padrão utilizada pelo servidor GlassFish. Isto evita-nos ter de adicionar bibliotecas de outra implementação ao GlassFish.

O NetBeans pode gerar este ficheiro de persistência utilizando um assistente.

  • Clique com o botão direito do rato no projeto e selecione «Criar Unidade de Persistência» [1],
  • em [2], atribua um nome à unidade de persistência que está a criar,
  • em [3], selecione a implementação EclipseLink JPA (JPA 2.0),
  • em [4], especifique que as transações da base de dados serão geridas pelo contentor EJB do servidor GlassFish,
  • em [5], especifique que as tabelas da base de dados já foram criadas e, por isso, não serão criadas,
  • em [6], crie uma nova fonte de dados para o servidor GlassFish,
  • em [7], forneça um nome JNDI (Java Naming Directory Interface),
  • em [8], associe este nome à ligação MySQL criada no passo anterior,
  • em [9], conclua o assistente,
  • em [10], o novo projeto,
  • em [11], o ficheiro [persistence.xml] foi gerado na pasta [META-INF],
  • em [12], foi gerada uma pasta [setup],
  • em [13], foram adicionadas novas dependências ao projeto Maven.

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


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

Inclui as informações fornecidas no assistente:

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

Normalmente, este ficheiro especifica o tipo de implementação JPA que está a ser utilizada. No assistente, selecionámos o EclipseLink. Uma vez que esta é a implementação JPA predefinida utilizada pelo servidor GlassFish, não é mencionada no ficheiro [persistence.xml].

No separador [Design], pode ver uma visão geral do ficheiro [persistence.xml]:

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


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/> 
    </properties>
  </persistence-unit>
</persistence>
  • linha 4: especifica que está a ser utilizada a implementação JPA do EclipseLink,
  • linhas 7–9: contêm as propriedades de configuração para o provedor JPA, neste caso o EclipseLink,
  • linha 8: esta propriedade permite o registo das instruções SQL que o EclipseLink irá executar.

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


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

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

  • linhas 5–11: as propriedades JDBC da base de dados MySQL5 [dbrdvmedecins2],
  • linha 13: o nome JNDI da fonte de dados.

Este ficheiro será utilizado para criar a fonte de dados JNDI [jdbc/dbrdvmedecins2] para o servidor GlassFish. Esta configuração é específica para este servidor. Para outro servidor, seria necessária uma abordagem diferente, normalmente utilizando uma ferramenta de administração. Essa ferramenta também está disponível para o GlassFish.

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


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>istia.st</groupId>
    <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>ejb</packaging>
 
    <name>mv-rdvmedecins-ejb-dao-jpa</name>
 
    ...
    <dependencies>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>eclipselink</artifactId>
            <version>2.3.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>javax.persistence</artifactId>
            <version>2.0.3</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
            <version>2.3.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
 
...
    <repositories>
        <repository>
            <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
            <id>eclipselink</id>
            <layout>default</layout>
            <name>Repository for library Library[eclipselink]</name>
        </repository>
    </repositories>
</project>
  • Linhas 32–37: Uma camada [JPA] requer o artefacto [javaee-api];
  • linhas 16, 22, 28: os artefactos necessários para a implementação JPA/EclipseLink aqui utilizada.
  • linhas 18, 24, 30, 36: todos os artefactos têm o atributo provided. Note que isto significa que são necessários para a compilação, mas não para o tempo de execução. Na verdade, em tempo de execução, são fornecidos pelo servidor GlassFish,
  • Linhas 41–48: definem um novo repositório de artefactos Maven, onde os artefactos EclipseLink podem ser encontrados.

3.4.2.3. Gerando entidades JPA

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

  • em [1], crie entidades JPA a partir de uma base de dados,
  • em [2], selecione a fonte de dados criada anteriormente [jdbc / dbrdvmedecins2],
  • em [3], a lista de tabelas para esta fonte de dados,
  • em [4], selecione todas elas,
  • em [5], as tabelas selecionadas,
  • em [6], nomeamos as classes Java associadas às quatro tabelas,
  • bem como um nome de pacote [7],
  • Em [8], o JPA agrupa as linhas das tabelas da base de dados em coleções. Escolhemos uma lista como coleção,
  • em [9], as classes Java criadas pelo assistente.

3.4.2.4. As entidades JPA geradas

A entidade [Medecin] representa a tabela [medecins]. A classe Java está repleta de anotações que tornam o código difícil de ler à primeira vista. Se mantivermos apenas o que é essencial para compreender o papel 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;
 
// manufacturers
....
 
  // getters and setters
....
 
  @Override
  public int hashCode() {
  ...
  }
 
  @Override
  public boolean equals(Object object) {
  ...
  }
 
  @Override
  public String toString() {
    ...
  }
  
}
  • Linha 4: A anotação @Entity torna a classe [Medecin] uma entidade JPA, ou seja, uma classe ligada a uma tabela de base de dados através da API JPA.
  • linha 5: o nome da tabela da base de dados associada à entidade JPA. Cada campo da tabela corresponde a um campo na classe Java,
  • linha 6: a classe implementa a interface Serializable. Isto é necessário em aplicações cliente/servidor, onde as entidades são serializadas entre o cliente e o servidor.
  • linhas 10–11: o campo id da classe [Medecin] corresponde ao campo [ID] (linha 10) da tabela [medecins],
  • linhas 13–14: o campo *title* na classe [Doctor] corresponde ao campo [TITLE] (linha 13) na tabela [doctors],
  • linhas 16–17: o campo name da classe [Doctor] corresponde ao campo [NAME] (linha 16) da tabela [doctors],
  • linhas 19-20: o campo version da classe [Medecin] corresponde ao campo [VERSION] (linha 19) da tabela [medecins]. Aqui, o assistente não reconhece que a coluna é, na verdade, uma coluna de versão que deve ser incrementada sempre que a linha a que pertence for modificada. Para atribuir-lhe esta função, deve adicionar a anotação @Version. Faremos isso numa etapa posterior,
  • linhas 22–23: o campo first_name da classe [Doctor] corresponde ao campo [FIRST_NAME] da tabela [doctors],
  • linhas 10–11: o campo id corresponde à chave primária [ID] da tabela. As anotações nas linhas 8–9 esclarecem este ponto,
  • linha 8: a anotação @Id indica que o campo anotado está associado à chave primária da tabela,
  • linha 9: a camada [JPA] irá gerar a chave primária para as linhas que inserir na tabela [Doctors]. Existem várias estratégias possíveis. Aqui, a estratégia GenerationType.IDENTITY indica que a camada JPA irá utilizar o modo auto_increment da tabela MySQL,
  • linhas 25–26: a tabela [slots] possui uma chave estrangeira na tabela [doctors]. Um horário pertence a um médico. Por outro lado, um médico tem vários horários associados a si. Temos, portanto, uma relação um-para-muitos (um médico para muitos slots), uma relação qualificada pela anotação @OneToMany no JPA (linha 25). O campo na linha 26 conterá todos os slots do médico. Isto é conseguido sem qualquer programação. Para compreender totalmente a linha 25, precisamos de introduzir a classe [Creneau].

É 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;
 
// manufacturers
...
// getters and setters
...
 
  @Override
  public int hashCode() {
    ...
  }
 
  @Override
  public boolean equals(Object object) {
    ...
  }
 
  @Override
  public String toString() {
    ...
  }
  
}

Apenas comentamos as novas anotações:

  • especificámos que a tabela [slots] tem uma chave estrangeira para a tabela [doctors]: um slot está associado a um médico. Vários slots podem estar associados ao mesmo médico. Temos uma relação da tabela [slots] para a tabela [doctors] que é definida como muitos-para-um (slots para médico). A anotação @ManyToOne na linha 32 é utilizada para definir a chave estrangeira,
  • a linha 31, com a anotação @JoinColumn, especifica a relação de chave estrangeira: a coluna [ID_MEDECIN] na tabela [slots] é uma chave estrangeira na coluna [ID] da tabela [doctors],
  • Linha 33: uma referência ao médico proprietário do horário. Isto é conseguido aqui também sem qualquer codificação.

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

  • na entidade [Creneau]:

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

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

Ambas as anotações refletem a mesma relação: a da chave estrangeira da tabela [Appointments] para a tabela [Doctors]. Diz-se que são inversas uma da outra. Apenas a relação @ManyToOne é essencial. Ela define de forma inequívoca a relação de chave estrangeira. A relação @OneToMany é opcional. Se estiver presente, ela simplesmente faz referência à relação @ManyToOne à qual está associada. Este é o significado do atributo mappedBy na linha 1 da entidade [Doctor]. O valor deste atributo é o nome do campo na entidade [Slot] que possui a anotação @ManyToOne especificando a chave estrangeira. Também na linha 1 da entidade [Medecin], o atributo cascade=CascadeType.ALL define o comportamento da entidade [Medecin] em relação à entidade [Creneau]:

  • se uma nova entidade [Doctor] for inserida na base de dados, então as entidades [TimeSlot] no campo da linha 2 também devem ser inseridas,
  • se uma entidade [Doctor] for modificada na base de dados, então as entidades [Slot] no campo da linha 2 também devem ser modificadas,
  • se uma entidade [Doctor] for eliminada da base de dados, então as entidades [Slot] no campo da linha 2 também devem ser eliminadas.

Fornecemos o código para as outras duas entidades sem comentários específicos, uma vez que não introduzem qualquer notação nova.

A entidade [Cliente]


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;
 
// manufacturers
...
// getters and 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;
 
  // manufacturers
...
 
  // getters and setters
...
 
  @Override
  public int hashCode() {
    ...
  }
 
  @Override
  public boolean equals(Object object) {
    ...
  }
 
  @Override
  public String toString() {
    ...
  }
  
}
  • A linha 13 especifica que o campo `jour` é do tipo Java Date. Indica 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] para a tabela [slots],
  • Linhas 20–22: definem a relação de chave estrangeira da tabela [rv] para a tabela [clients].

A geração automática de entidades JPA fornece-nos uma base funcional. Por vezes, isto é suficiente; outras vezes, não. É o caso aqui:

  • precisamos de adicionar a anotação @Version aos vários campos de versão das entidades,
  • precisamos de escrever métodos toString que sejam mais explícitos do que os gerados,
  • as entidades [Medecin] e [Client] são análogas. Vamos fazê-las derivar de uma classe [Person],
  • Vamos remover as relações @OneToMany inversas das relações @ManyToOne. Estas não são essenciais e introduzem complicações na programação,
  • removemos a validação @NotNull nas chaves primárias. Ao persistir uma entidade JPA no MySQL, a entidade tem inicialmente uma chave primária nula. Só após a persistência na base de dados é que a chave primária da entidade persistida tem um valor.

Com estas especificações, as várias classes ficam da seguinte forma:

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


package rdvmedecins.jpa;
 
import java.io.Serializable;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
 
@MappedSuperclass
public class Personne implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;
 
  @Basic(optional = false)
  @Size(min = 1, max = 5)
  @Column(name = "TITRE")
  private String titre;
 
  @Basic(optional = false)
  @NotNull
  @Size(min = 1, max = 30)
  @Column(name = "NOM")
  private String nom;
 
  @Basic(optional = false)
  @NotNull
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @Basic(optional = false)
  @NotNull
  @Size(min = 1, max = 30)
  @Column(name = "PRENOM")
  private String prenom;
// manufacturers
...
 
// getters and setters
  ...
 
  @Override
  public String toString() {
    return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
  }
  
}
  • Linha 8: Note que a classe [Person] não é, por si só, uma entidade (@Entity). Servirá como classe pai para as entidades. A anotação @MappedSuperClass indica isso.

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


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;
 
// manufacturers
...
 
  @Override
  public int hashCode() {
...
  }
 
  @Override
  public boolean equals(Object object) {
  ...
  }
 
  @Override
  public String toString() {
    return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
  }
  
}
  • Linha 6: A classe [Client] é uma entidade JPA,
  • linha 7: está associada à tabela [clients],
  • linha 8: deriva da classe [Person].

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


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;
 
  // manufacturers
...
 
  @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;
 
  // manufacturers
  ...
 
  // getters and setters
  ...
 
  @Override
  public int hashCode() {
    ...
  }
 
  @Override
  public boolean equals(Object object) {
    // TODO: Warning - this method won't work in the case the id fields are not set
    ...
  }
 
  @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 representam a relação «muitos-para-um» entre a tabela [slots] e a tabela [doctors] na base 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;
 
  // manufacturers
...
 
  // getters and 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» entre a tabela [rv] e a tabela [clients] (um cliente pode aparecer em várias entradas Rv) na base de dados, e as linhas 25–27 modelam a relação «muitos-para-um» entre a tabela [rv] e a tabela [slots] (um slot pode aparecer em vários Rv).

3.4.3. A classe de exceção

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


package rdvmedecins.exceptions;
 
import java.io.Serializable;
import javax.ejb.ApplicationException;
 
@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException implements Serializable{
 
  // private fields
  private int code = 0;
 
  // manufacturers
  public RdvMedecinsException() {
    super();
  }
 
  public RdvMedecinsException(String message) {
    super(message);
  }
 
  public RdvMedecinsException(String message, Throwable cause) {
    super(message, cause);
  }
 
  public RdvMedecinsException(Throwable cause) {
    super(cause);
  }
 
  public RdvMedecinsException(String message, int code) {
    super(message);
    setCode(code);
  }
 
  public RdvMedecinsException(Throwable cause, int code) {
    super(cause);
    setCode(code);
  }
 
  public RdvMedecinsException(String message, Throwable cause, int code) {
    super(message, cause);
    setCode(code);
  }
 
  // getters - setters
  public int getCode() {
    return code;
  }
 
  public void setCode(int code) {
    this.code = code;
  }
}
  • Linha 7: A classe estende a classe [RuntimeException]. Por isso, o compilador não exige que seja tratada com blocos try/catch.
  • linha 6: A anotação @ApplicationException garante que a exceção não será «engolida» por uma [EjbException].

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

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

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

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

  • linha 14: a lista de clientes. Precisaremos disso 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 disso 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. Combinado com o método anterior, isto permitir-nos-á exibir a agenda do médico para um determinado dia com os horários já marcados,
  • linha 22: permite-nos encontrar um cliente pelo seu número de identificação. Este método permitirá-nos encontrar um cliente selecionando-o na lista suspensa de clientes,
  • linha 24: o mesmo que acima para médicos,
  • linha 26: recupera uma consulta pelo seu número. Pode ser usado ao eliminar uma consulta para verificar previamente se esta existe realmente,
  • linha 28: recupera um horário pelo seu número. Permite identificar o horário que um utilizador pretende adicionar ou eliminar,
  • linha 30: para adicionar um compromisso,
  • linha 32: para eliminar uma consulta.

A interface local do EJB [IDaoLocal] deriva simplesmente da interface anterior [IDao]:


package rdvmedecins.dao;
 
import javax.ejb.Local;
 
@Local
public interface IDaoLocal extends IDao{
 
}

O mesmo se aplica à interface remota [IDaoRemote]:


package rdvmedecins.dao;
 
import javax.ejb.Remote;
 
@Remote
public interface IDaoRemote extends IDao{
 
}

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


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

O código EJB completo é 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;
 
  // customer list
  public List<Client> getAllClients() {
    try {
      return em.createQuery("select rc from Client rc").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 1);
    }
  }
 
  // list of doctors
  public List<Medecin> getAllMedecins() {
    try {
      return em.createQuery("select rm from Medecin rm").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 2);
    }
  }
 
  // list of time slots for a given doctor
  // doctor: the doctor
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    try {
      return em.createQuery("select 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);
    }
  }
 
  // list of appointments for a given doctor on a given day
  // doctor: the doctor
  // day: the day
  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);
    }
  }
 
  // add Rv
  // day : day of appointment
  // creneau: Rv time slot
  // customer: customer for whom the appointment is taken
  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);
    }
  }

  // deleting an appointment
  // rv: Rv deleted
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }
 
  // retrieve a specific customer
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
 
  // retrieve a specific doctor
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
 
  // retrieve a given Rv
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
 
  // retrieve a given slot
  public Creneau getCreneauById(Long id) {
    try {
      return (Creneau) em.find(Creneau.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
}
  • linha 22: o objeto EntityManager que gere o acesso ao contexto de persistência. Quando a classe for instanciada, este campo será inicializado pelo contentor EJB utilizando a anotação @PersistenceContext na linha 21,
  • linha 27: consulta JPQL (Java Persistence Query Language) que devolve todas as linhas da tabela [clients] como uma lista de objetos [Client],
  • linha 36: uma consulta semelhante para médicos,
  • linha 46: uma consulta JPQL que realiza uma junção entre as tabelas [slots] e [doctors]. É parametrizada pelo ID do médico,
  • linha 57: uma consulta JPQL que realiza uma junção entre as tabelas [appointments], [slots] e [doctors] e que tem dois parâmetros: o ID do médico e a data da consulta,
  • linhas 69–73: criação de uma consulta seguida da sua persistência na base de dados,
  • linha 83: eliminação de uma consulta da base de dados,
  • linha 92: executa uma consulta SELECT na base de dados para encontrar um determinado cliente,
  • linha 101: o mesmo para um médico,
  • linha 110: o mesmo para uma consulta,
  • linha 119: o mesmo para um intervalo de tempo,
  • Todas as operações que utilizam o contexto de persistência da linha 22 são suscetíveis de encontrar um problema com a base de dados. Por conseguinte, estão todas incluídas num bloco try/catch. Qualquer exceção é encapsulada na exceção personalizada RdvMedecinsException.

3.4.5. Implementação do controlador JDBC do MySQL

Na arquitetura abaixo:

O EclipseLink requer o controlador JDBC do MySQL. Este deve ser instalado nas bibliotecas do servidor GlassFish, na pasta <glassfish>/domains/domain1/lib/ext, onde <glassfish> é o diretório de instalação do servidor GlassFish. Pode ser obtido da seguinte forma:

A pasta onde o controlador JDBC do MySQL deve ser colocado é <pasta Domains>[1]/domain1/lib/ext [2]. Este controlador 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 este reconheça esta nova biblioteca.

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

Voltemos à arquitetura que construímos até agora:

Toda a pilha [web, lógica de negócio, DAO, JPA] deve ser implementada no servidor GlassFish. Eis como o fazemos:

  • Em [1], compilamos o projeto Maven,
  • Em [2], executamo-lo,
  • em [3], ele foi implementado no servidor GlassFish (separador [Serviços])

Talvez esteja curioso para verificar os registos do GlassFish:

Em [1], os registos do GlassFish estão disponíveis no separador [Saída / 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 marcadas com [Config] e [Details] são registos do EclipseLink; as marcadas com [Info] provêm do GlassFish.

  • Linhas 1–12: o EclipseLink processa as entidades JPA que detectou,
  • linhas 13–17: informação indicando que o processamento das entidades JPA decorreu normalmente,
  • Linha 18: o EclipseLink reporta a sua presença,
  • linha 19: o EclipseLink reconhece que está a lidar com o SGBD MySQL,
  • linhas 20–24: o EclipseLink tenta ligar-se à base de dados,
  • linhas 25–28: conseguiu,
  • linhas 29–33: tenta reconectar-se, desta vez utilizando especificamente uma plataforma MySQL (linha 30),
  • linhas 34–37: bem-sucedido novamente,
  • linha 38: confirmação de que a unidade de persistência [dbrdvmedecins-PU] pôde ser instanciada,
  • linha 39: os nomes portáteis das interfaces remotas e locais do EJB [DaoJpa], onde «portátil» significa reconhecido por todos os servidores de aplicações Java EE 6,
  • linha 40: os nomes das interfaces remotas e locais do EJB [DaoJpa], específicos do GlassFish. No próximo teste, utilizaremos o nome «rdvmedecins.dao».

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

3.4.7. Testar a camada [DAO] do EJB

Agora que o EJB da camada [DAO] da nossa aplicação foi implementado, podemos testá-lo. Faremos isso no contexto de uma aplicação cliente/servidor:

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

Começamos por criar um novo projeto Maven :

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

O projeto evolui da seguinte forma:

  • em [1], o projeto em que as duas classes geradas foram removidas, juntamente com a dependência do JUnit.

Voltemos à arquitetura cliente/servidor que será utilizada para os testes:

O cliente precisa de conhecer a interface remota fornecida pelo EJB [DAO]. Além disso, irá trocar entidades JPA com o EJB. Por isso, necessita das definições dessas entidades. Para garantir que o projeto de teste do EJB tem acesso a esta informação, iremos adicionar o projeto EJB [DAO] como uma dependência ao projeto:

  • Em [1], adicione uma dependência ao ramo [Test Dependencies],
  • em [2], selecione o separador [Abrir projetos],
  • em [3], selecione o projeto EJB [DAO] do Maven,
  • em [4], a dependência é adicionada.

Voltemos à arquitetura cliente/servidor do teste:

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

  • em [1], adicionamos uma dependência,
  • em [2], especificamos as características do artefacto pretendido,
  • em [3], é adicionado um grande número de dependências. O Maven irá descarregá-las. Isto pode demorar vários minutos. Em seguida, são armazenadas no repositório local do Maven.

Agora podemos criar o teste JUnit:

  • em [2], clique com o botão direito do rato em [Test Packages] para criar um novo teste JUnit,
  • em [3], nomeamos a classe de teste e especificamos um pacote para ela [4],
  • em [5], selecione a estrutura JUnit 4.x,
  • em [6], a classe de teste gerada,
  • em [7], as novas dependências do projeto Maven.

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


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>istia.st</groupId>
  <artifactId>mv-client-rdvmedecins-ejb-dao</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
 
  <name>mv-client-rdvmedecins-ejb-dao</name>
  <url>http://maven.apache.org</url>
 
  <repositories>
    <repository>
      <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
      <id>eclipselink</id>
      <layout>default</layout>
      <name>Repository for library Library[eclipselink]</name>
    </repository>
    <repository>
      <url>http://repo1.maven.org/maven2/</url>
      <id>junit_4</id>
      <layout>default</layout>
      <name>Repository for library Library[junit_4]</name>
    </repository>
  </repositories>
 
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
 
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish.appclient</groupId>
      <artifactId>gf-client</artifactId>
      <version>3.1.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Nota:

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

A classe de teste será a seguinte:


package rdvmedecins.tests.dao;
 
import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import junit.framework.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import rdvmedecins.dao.IDaoRemote;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
 
public class JUnitTestDao {
 
  // layer [dao] tested
  private static IDaoRemote dao;
  // today's date
  Date jour = new Date();
 
  @BeforeClass
  public static void init() throws NamingException {
    // environment initialization JNDI
    InitialContext initialContext = new InitialContext();
    // dao layer instantiation
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }
 
  @Test
  public void test1() {
    // customer display
    List<Client> clients =dao.getAllClients();
    display("Liste des clients :", clients);
    // physician display
    List<Medecin> medecins =dao.getAllMedecins();
    display("Liste des médecins :", medecins);
    // display doctor's slots
    Medecin medecin = medecins.get(0);
    List<Creneau> creneaux = dao.getAllCreneaux(medecin);
    display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
    // list of doctor's appointments on a given day
    display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // add a 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));
    // add a RV in the same slot on the same day
    // must trigger an exception
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    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();
      }
      // we note the error
      erreur=true;
    }
    // check for errors
    Assert.assertTrue(erreur);
    // RV list
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // delete a 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));
  }
 
  // utility method - displays items in a collection
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • linhas 23–29: O método anotado com @BeforeClass é executado antes de todos os outros. Aqui, criamos uma referência à interface remota do EJB [DaoJpa]. Recorde-se que lhe atribuímos o nome JNDI "rdvmedecins.dao",
  • linhas 34–35: exibem a lista de clientes,
  • linhas 37-38: exibem a lista de médicos,
  • linhas 40–42: exibem os horários disponíveis para o primeiro médico,
  • linha 44: exibe as consultas do primeiro médico no dia especificado na linha 21,
  • linhas 46–51: adicionar uma consulta para o primeiro médico, para o horário n.º 2 e o dia especificado na linha 21,
  • linha 52: exibe, para verificação, as consultas do primeiro médico para o dia especificado na linha 21. Deve haver pelo menos uma — a que acabou de ser adicionada,
  • linhas 55-70: adicionam a mesma consulta. Uma vez que a tabela [RV] tem uma restrição de unicidade, esta adição deve causar uma exceção. Verificamos isto na linha 70,
  • linha 72: exibe as consultas do primeiro médico para o dia da linha 21 para verificação. A que queríamos adicionar não deveria estar lá,
  • linhas 74–76: eliminamos a única consulta que foi adicionada,
  • linha 77: exibimos as consultas do primeiro médico no dia especificado na linha 21 para verificação. A que acabámos de eliminar não deve estar lá.

Este é um teste JUnit fictício. Contém apenas uma asserção (linha 70). É um teste visual com as falhas que isso acarreta.

Se tudo correr bem, os testes devem ser aprovados:

  • em [1], compilamos o projeto de teste,
  • em [2], o teste é executado,
  • em [3], o teste foi aprovado.

Vamos analisar mais detalhadamente o resultado 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]

Convidamos o leitor a ler estes registos juntamente com o código que os gerou. Vamos concentrar-nos na exceção que ocorreu ao adicionar uma consulta existente, nas linhas 41–49. O rastreio da pilha de exceções é apresentado nas linhas 42–48. É inesperado. Voltemos ao código do método que adiciona uma consulta:


  // add Rv
  // day : day of appointment
  // creneau: Rv time slot
  // customer: customer for whom the appointment is taken
  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 analisar os registos do GlassFish ao adicionar as duas consultas:

...
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: a instrução INSERT que será executada. Note que ela não ocorre ao mesmo tempo que a operação persist. Se isso acontecesse, este registo teria aparecido antes da linha 2. A operação INSERT ocorre normalmente no final da transação em que o método é executado,
  • Linha 6: O EclipseLink solicita ao MySQL a última chave primária utilizada. Ele irá recuperar a chave primária do compromisso recém-adicionado. Este valor irá preencher o campo id da entidade [Rv] persistida,
  • Linhas 7–8: A consulta SELECT que irá apresentar os compromissos do médico,
  • Linhas 9–10: O ecrã exibe a segunda persistência,
  • Linhas 11–12: A instrução INSERT que será executada. Deverá lançar uma exceção. Esta exceção aparece nas linhas 15–16 e é clara. É inicialmente lançada pelo controlador JDBC do MySQL devido a uma violação da restrição de unicidade nos compromissos. Podemos inferir que deveríamos ver estas exceções nos registos de teste do JUnit. No entanto, não é esse o caso:
1
2
3
4
5
6
7
8
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe

Vamos rever a arquitetura cliente/servidor do teste:

Quando o EJB [DAO] lança uma exceção, esta deve ser serializada para chegar ao cliente. É provável que esta operação tenha falhado por uma razão que não compreendi. Uma vez que a nossa aplicação completa não será executada num ambiente cliente/servidor, podemos ignorar esta questão.

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

3.5. A camada [business]

Voltemos à arquitetura da aplicação que estamos a construir:

Vamos criar um novo projeto Maven para o EJB [de negócios]. Conforme mostrado acima, ele dependerá do projeto Maven que foi criado para as camadas [DAO] e [JPA].

3.5.1. O projeto NetBeans

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

  • Em [1], o projeto Maven para a camada [business],
  • em [2], adicione uma dependência,
  • em [3], selecione o projeto Maven para as camadas [DAO] e [JPA],
  • em [4], selecione o âmbito [provided]. Note que isto significa que é necessário para a compilação, mas não para a execução do projeto. Na verdade, o EJB da camada [business] será implementado no servidor GlassFish juntamente com os EJBs das camadas [DAO] e [JPA]. Assim, quando for executado, os EJBs das camadas [DAO] e [JPA] já estarão presentes,
  • em [6], o novo projeto com a sua dependência.

Vamos agora analisar o código-fonte da camada [de negócios]:

O EJB [Business] 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 {
 
    // dao layer
    // customer list
    public List<Client> getAllClients();
 
    // list of doctors
    public List<Medecin> getAllMedecins();
 
    // list of physician slots
    public List<Creneau> getAllCreneaux(Medecin medecin);
 
    // list of doctor's appointments on a given day
    public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
 
    // find a customer identified by its id
    public Client getClientById(Long id);
 
    // find a customer identified by its id
    public Medecin getMedecinById(Long id);
 
    // find an Rv identified by its id
    public Rv getRvById(Long id);
 
    // find a time slot identified by its id
    public Creneau getCreneauById(Long id);
 
    // add a RV to the list
    public Rv ajouterRv(Date jour, Creneau creneau, Client client);
 
    // delete a RV
    public void supprimerRv(Rv rv);
    
    // job
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
 
}

Para compreender esta interface, devemos recordar a arquitetura do projeto:

Definimos a interface da camada [DAO] (Secção 3.4.4) e especificámos que ela responde às necessidades da camada [web], ou seja, aos requisitos do utilizador. A camada [web] comunica com a camada [DAO] apenas através da camada [business]. Isto explica por que razão todos os métodos da camada [DAO] se encontram na camada [business]. Estes métodos irão simplesmente delegar o pedido da camada [web] à camada [DAO]. Nada mais.

Durante a análise da aplicação, surgiu um requisito: a capacidade de apresentar a agenda de um médico para um determinado dia numa página web, de modo a mostrar quais os horários que estão marcados e quais os que estão disponíveis. Este é tipicamente o caso quando uma secretária recebe um pedido por telefone. A pessoa que liga solicita uma consulta num dia específico com um médico específico. Para satisfazer esta necessidade, a camada [business] fornece o método na linha 46.


    // job
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);

Pode-se perguntar onde colocar este método:

  • poderia ser colocado na camada [DAO]. No entanto, este método não responde realmente a uma necessidade de acesso a dados, mas sim a uma necessidade de negócio;
  • poderíamos colocá-lo na camada [web]. Isso seria uma má ideia. Porque se mudarmos a camada [web] para uma camada [Swing], perderemos o método, mesmo que a necessidade ainda exista.

O método recebe como parâmetros o médico e o dia para o qual queremos a agenda de consultas. Ele retorna um objeto [AgendaMedecinJour] que representa a agenda do médico para esse dia:


package rdvmedecins.metier.entites;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.jpa.Medecin;
 
public class AgendaMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;
 
    // manufacturers
    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 and setters
...
  
}
  • linha 12: o médico a quem pertence este horário,
  • linha 13: o dia da agenda,
  • linha 14: os horários do médico para esse dia.
  • A classe inclui construtores (linhas 17, 21), bem como um método toString personalizado (linha 27).

A classe [DoctorTimeSlotDay] (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;
    // fields
    private Creneau creneau;
    private Rv rv;
 
    // manufacturers
    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 and setters
 
  ...
}
  • linha 12: um horário de atendimento médico,
  • linha 13: a consulta associada, nulo se o horário estiver livre.

Podemos ver que o campo creneauxMedecinJour na linha 14 da classe [AgendaMedecinJour] permite-nos recuperar todos os horários do médico com o estado «ocupado» ou «livre» para cada um. Este era o objetivo do novo método [getAgendaMedecinJour] da classe [IMetier].

O nosso EJB [Metier] terá uma interface local e uma interface remota que simplesmente estendem a 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 [Metier] implementa estas interfaces da seguinte forma:


package rdvmedecins.metier.service;
 
import java.io.Serializable;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
 
import javax.ejb.EJB;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
 
import rdvmedecins.dao.IDaoLocal;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.entites.CreneauMedecinJour;
 
@Singleton
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote, Serializable {
 
  // dao layer
  @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) {
    // list of doctor's time slots
    List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
    // list of bookings for the same doctor on the same day
    List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
    // a dictionary is created from the Rvs taken
    Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
    for (Rv resa : reservations) {
      hReservations.put(resa.getCreneau().getId(), resa);
    }
    // create the agenda for the requested day
    AgendaMedecinJour agenda = new AgendaMedecinJour();
    // the doctor
    agenda.setMedecin(medecin);
    // the day
    agenda.setJour(jour);
    // reservation slots
    CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
    agenda.setCreneauxMedecinJour(creneauxMedecinJour);
    // filling reservation slots
    for (int i = 0; i < creneauxHoraires.size(); i++) {
      // line i agenda
      creneauxMedecinJour[i] = new CreneauMedecinJour();
      // slot id
      creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
      // is the slot free or reserved?
      if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
        // the slot is occupied - we note the resa
        Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
        creneauxMedecinJour[i].setRv(resa);
      }
    }
    // we return the result
    return agenda;
  }
}
  • linha 22, a classe [Metier] é um EJB singleton,
  • Linha 23: Cada método EJB é executado dentro de uma transação. Isto significa que a transação começa no início do método, na camada [business]. Esta camada irá chamar métodos na camada [DAO]. Estes métodos serão executados dentro da mesma transação,
  • linha 24: o EJB implementa as suas interfaces locais e remotas e é também serializável,
  • linha 27: uma referência ao EJB na camada [DAO],
  • linha 29: esta será injetada pelo contentor EJB do servidor GlassFish, graças à anotação @EJB. Assim, quando os métodos da classe [Business] são executados, a referência ao EJB da camada [DAO] já foi inicializada,
  • linhas 33–81: esta referência é utilizada para delegar a chamada feita à camada [Business] à camada [DAO],
  • linha 84: o método getAgendaMedecinJour, que recupera a agenda de um médico para um determinado dia. Deixaremos que o leitor acompanhe os comentários.

3.5.2. Implantação da camada [business]

A camada [business] depende da camada [DAO]. Cada camada foi implementada com um EJB. Para testar o EJB [business], precisamos de implementar ambos os EJBs. Para tal, precisamos de um projeto empresarial.

  • [1], crie um novo projeto,
  • do tipo Maven [2] e Aplicação Empresarial [3],
  • e atribua-lhe um nome [4]. O sufixo «ear» será adicionado automaticamente,
  • em [5], selecionamos o servidor GlassFish e Java EE 6,
  • em [6], uma aplicação empresarial contém módulos, normalmente módulos EJB e módulos web. Aqui, a aplicação empresarial irá conter os módulos dos dois EJBs que criámos. Uma vez que estes módulos já existem, não marcamos as caixas,
  • em [7,8], foram criados dois projetos. [8] é o projeto empresarial que iremos utilizar. [7] é um projeto cuja finalidade desconheço. Ainda não tive de o utilizar e, como não me aprofundei no Maven, não sei para que serve. Por isso, vamos ignorá-lo.

Agora que o projeto empresarial foi criado, podemos definir os seus módulos.

  • Em [1], criamos uma nova dependência,
  • em [2], selecionamos o projeto EJB [DAO],
  • em [3], declaramos que se trata de um EJB. Não deixe o tipo em branco, pois, nesse caso, será utilizado o tipo jar, e esse tipo não é adequado aqui,
  • em [4], usamos o âmbito [compile],
  • Em [5], o projeto com a sua nova dependência,
  • em [6, 7, 8], repita o processo para adicionar o EJB da camada [business],
  • em [9], as duas dependências,
  • em [10], compilamos o projeto,
  • em [11], executamo-lo,
  • em [12], no separador [Serviços], vemos que o projeto foi implementado no servidor GlassFish. Isto significa que ambos os EJBs estão agora presentes no servidor.

Nos registos do servidor GlassFish, pode encontrar informações sobre a implementação dos dois EJBs:

  • em [1], no separador de registos do GlassFish.

Encontram-se aí os seguintes registos:

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

Iremos utilizar o nome portátil da interface remota do EJB [Metier]:

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

Iremos precisar disto ao testar a camada [de negócio].

3.5.3. Testar a camada [de negócios]

Tal como fizemos para a camada [DAO], iremos testar a camada [business] como parte de uma aplicação cliente/servidor:

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

Começamos por criar um novo projeto Maven. Para tal, seguimos o mesmo procedimento utilizado para criar o projeto de teste da camada [DAO] (ver secção 3.4.7), excluindo a criação do teste JUnit. O projeto resultante é o seguinte

  • [1] mostra o projeto criado com as suas dependências: no que diz respeito ao EJB da camada [DAO], ao EJB da camada [Business] e à biblioteca [gf-client].

Neste momento, o ficheiro [pom.xml] do projeto tem o seguinte conteúdo:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>istia.st</groupId>
  <artifactId>mv-client-rdvmedecins-ejb-metier</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
 
  <name>mv-client-rdvmedecins-ejb-metier</name>
  <url>http://maven.apache.org</url>
 
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
 
  <dependencies>
    <dependency>
      <groupId>org.glassfish.appclient</groupId>
      <artifactId>gf-client</artifactId>
      <version>3.1.1</version>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-metier</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>
</project>

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

O código para a 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 {
 
  // the remote interface name of the 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";
  // today's date
  private static Date jour = new Date();
 
  public static void main(String[] args) {
    try {
      // context JNDI of Glassfish server
      InitialContext initialContext = new InitialContext();
      // reference on remote [metier] layer
      IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
      // customer display
      List<Client> clients = metier.getAllClients();
      display("Liste des clients :", clients);
      // physician display
      List<Medecin> medecins = metier.getAllMedecins();
      display("Liste des médecins :", medecins);
      // display doctor's slots
      Medecin medecin = medecins.get(0);
      List<Creneau> creneaux = metier.getAllCreneaux(medecin);
      display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
      // list of doctor's appointments on a given day
      display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // calendar display
      AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // add a RV to the list
      Rv rv = null;
      Creneau creneau = creneaux.get(2);
      Client client = clients.get(0);
      System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
      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));
      // calendar display
      agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // delete a 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));
      // calendar display
      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();
      }
    }
  }
 
  // utility method - displays items in a collection
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • linha 18: o nome portátil da interface remota do EJB [Metier] foi obtido dos registos do GlassFish,
  • linhas 24–27: é obtida 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 num determinado dia,
  • linhas 41-42: a agenda deste médico para o mesmo dia,
  • linhas 44-49: adicionar uma consulta,
  • linha 50: exibe as consultas do médico. Deve haver mais uma,
  • linhas 52-53: exibe a agenda do médico. A consulta adicionada deve estar visível,
  • linhas 55-57: apaga a consulta que acabou de adicionar,
  • linha 58: isto deve ser refletido na lista de consultas do médico,
  • linhas 60–61: e no seu calendário.

Executamos o teste:

 

Os ecrãs resultantes são os 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: agenda da Sra. PELISSIER, 23 de maio de 2012. Não há horários reservados,
  • linha 39: adição de uma consulta,
  • linha 42: nova agenda da Sra. PELISSIER. Está agora reservado um horário para o Sr. MARTIN,
  • linha 44: o compromisso foi eliminado,
  • linha 46: o calendário da Sra. PELISSIER mostra que não há nenhum horário reservado.

Consideramos agora que as camadas [CAD] e [negócios] estão operacionais. Ainda precisamos de escrever a camada [web] utilizando a estrutura JSF. Para tal, utilizaremos os conhecimentos adquiridos no início deste documento.

3.6. A camada [web]

Voltemos à arquitetura atualmente em construção:

Vamos construir a camada final, a camada [web].

3.6.1. O projeto NetBeans

Estamos a criar um projeto Maven:

  • Em [1], criamos um novo projeto,
  • em [2, 3], um projeto Maven do tipo [Aplicação Web],
  • em [4], atribuímos-lhe um nome,
  • em [5], selecione o servidor GlassFish e Java EE 6 Web,
  • em [6], o projeto assim criado,
  • em [7], o projeto após remover a página [index.jsp] e o pacote em [Source Packages],
  • em [8, 9], nas propriedades do projeto, adicione uma estrutura,
  • em [10], selecione Java Server Faces,
  • em [11], a configuração do Java Server Faces. Deixe os valores predefinidos. Note que está a ser utilizado o JSF 2,
  • em [12], o projeto é então modificado de duas formas: é gerado um ficheiro [web.xml], bem como uma página [index.html].

O ficheiro [web.xml] é o seguinte:


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

Já nos deparámos com este ficheiro.

  • linhas 7–11: definem o servlet que irá tratar de todos os pedidos feitos à aplicação. Este é o servlet JSF,
  • linhas 12–15: definem os URLs tratados por este servlet. Estes são URLs do formato /faces/*,
  • linhas 21–23: definem a página [index.xhtml] como a página inicial.

Esta página é a seguinte:


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

Já vimos isto antes. Podemos executar este projeto:

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

Vamos agora apresentar o projeto completo e, em seguida, abordar em detalhe os seus vários componentes.

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

3.6.2. Dependências do projeto

Voltemos à arquitetura do projeto:

A camada JSF depende das camadas [business], [DAO] e [JPA]. Estas três camadas estão encapsuladas nos dois projetos Maven que criámos, o que explica as dependências do projeto [4]. Vamos mostrar de forma simples como estas dependências são adicionadas:

  • em [1], usaremos ejb para indicar que a dependência é de um projeto EJB,
  • em [2], introduziremos [provided]. Isto porque o projeto web será implementado juntamente com os dois projetos EJB. Por conseguinte, não precisa de incluir os JARs EJB.

3.6.3. Configuração do projeto

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

 

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


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

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

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


<?xml version='1.0' encoding='UTF-8'?>
 
<!-- =========== FULL CONFIGURATION FILE ================================== -->
 
<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
 
  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
    <message-bundle>messages</message-bundle>
  </application>
</faces-config>

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


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

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


.reservationsHeaders {
   text-align: center;
   font-style: italic;
   color: Snow;
   background: Teal;
}
 
.creneau {
   height: 25px;
   text-align: center;
   background: MediumTurquoise;
}
.client {
   text-align: left;
   background: PowderBlue;
}
 
.action {
   width: 6em;
   text-align: left;
   color: Black;
   background: MediumTurquoise;
}
.erreursHeaders {
   background: Teal;
   background-color: #ff6633;
   color: Snow;
   font-style: italic;
   text-align: center
 
}
 
.erreurClasse {
   background: MediumTurquoise;
   background-color: #ffcc66;
   height: 25px;
   text-align: center
}
 
.erreurMessage {
   background: PowderBlue;
   background-color: #ffcc99;
   text-align: left
}

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


# layout
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers
layout.entete.langue1=Fran\u00e7ais
layout.entete.langue2=Anglais
# exception
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
# formulaire 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
# formulaire 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
# formulaire 3
form3.titre=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.titre_detail=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.client=Client
form3.valider=Valider
form3.annuler=Annuler
# erreur
erreur.titre=Une erreur s'est produite.
erreur.message=Message d'erreur
erreur.accueil=Page d'accueil
erreur.classe=Cause

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


# layout
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university
layout.entete.langue1=French
layout.entete.langue2=English
# exception
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
# formulaire 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
# formulaire 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
# formulaire 3
form3.titre=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.titre_detail=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.client=Client
form3.valider=Submit
form3.annuler=Cancel
# erreur
erreur.titre=An error occurred
erreur.message=Error message
erreur.accueil=Welcome Page
erreur.classe=Cause

3.6.4. Visualizações do projeto

Vamos rever como funciona a aplicação. A página inicial é a seguinte:

 

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

Por fim, também pode ser apresentada uma página de erro:

Estas diferentes visualizações são geradas pelas seguintes páginas do projeto web:

  • em [1], as páginas [basdepage, entete, layout] tratam da formatação de todas as vistas,
  • em [2], a visualização gerada por [layout.xhtml].

Aqui foi utilizada a tecnologia Facelets. Esta foi descrita na Secção 2.11. Iremos simplesmente fornecer o código das páginas XHTML utilizadas para o layout:

[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 as linhas 10–12, os dois links para alterar o idioma da aplicação.

[basdepage.xhtml]


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

[layout.xhtml]


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

Esta página é o modelo para a 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–21 definem a área denominada «content» (linha 8) em [layout.xhtml] (linha 7). Esta é a área central das visualizações:

 

A página [index.xhtml] é a única página da aplicação. Por conseguinte, não haverá navegação entre páginas. Apresenta uma das quatro páginas [form1.xhtml, form2.xhtml, form3.xhtml, error.xhtml]. Esta apresentação é controlada por quatro variáveis booleanas [form1Rendered, form2Rendered, form3Rendered, errorRendered] do bean do formulário, que iremos descrever em breve.

3.6.5. Os beans do projeto

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

  • a classe [ChangeLocale] é a classe que lida com a mudança de idioma. Já foi discutida (Secção 2.4.4).
  • A classe [Messages] é uma classe que facilita a internacionalização das mensagens de uma aplicação. Foi discutida na secção 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{
 
  // business layer
  @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>();
  // errors
  private List<Erreur> erreurs = new ArrayList<Erreur>();
  private Boolean erreur = false;
 
  public Application() {
  }
 
  @PostConstruct
  public void init() {
    // caching doctors and customers
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // we note the error
      erreur = true;
      erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
      while (th.getCause() != null) {
        th = th.getCause();
        erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
      }
      return;
    }
    // list checking
    if (medecins.size() == 0) {
      // we note the error
      erreur = true;
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // we note the error
      erreur = true;
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // mistake?
    if (erreur) {
      return;
    }
 
    // dictionaries
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
  }

  // getters and setters
  ...
}
  • linhas 15-16: a classe [Application] é um bean de âmbito da aplicação. É criada uma vez no início do ciclo de vida da aplicação JSF e é acessível a todos os pedidos de todos os utilizadores. Geralmente, armazenamos dados de leitura apenas na aplicação. Aqui, iremos armazenar a lista de médicos e a lista de clientes. Partimos, portanto, do princípio de que estes não se alteram frequentemente. As páginas XHTML acedem-lhes através do nome da aplicação,
  • linhas 20–21: uma referência à interface local do EJB [Business] será injetada pelo contentor EJB do GlassFish. Vamos rever a arquitetura da aplicação:

A aplicação JSF e o EJB [Metier] serão executados na mesma JVM (Java Virtual Machine). Portanto, a camada [JSF] utilizará a interface local do EJB. Aqui, o bean da aplicação utiliza o EJB [Business]. Mesmo que não fosse esse o caso, seria normal encontrar uma referência a ele na camada [business]. Trata-se, de facto, de informação que pode ser partilhada por todos os pedidos de todos os utilizadores e, portanto, dados com âmbito Application.

  • Linhas 34–35: O método init é executado imediatamente após a instância da classe [Application] (presença da anotação @PostConstruct).
  • Linhas 36–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 indexados pelo seu ID na linha 25 e o mesmo para clientes na linha 26. Podem ocorrer erros. Estes são registados na lista na linha 28.

A classe [Error] é a seguinte:


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

3.6.5.2. O bean [Form]

O seu código é o seguinte:


package beans;
 
...
 
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
 
  public Form() {
  }
 
  // bean Application
  @Inject
  private Application application;
 
  // model
  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() {
    // was the initialization successful?
    if (application.getErreur()) {
      // retrieve the list of errors
      erreurs = application.getErreurs();
      // the error view is displayed
      setForms(false, false, false, true);
    }
  }
 
  // view display
  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 denominado «form» com âmbito de sessão. Note-se que a classe deve, por conseguinte, ser serializável.
  • linhas 13-14: O bean do formulário possui uma referência ao bean da aplicação. Esta referência será injetada pelo contentor de servlets no qual a aplicação é executada (presença da anotação @Inject).
  • linhas 17-31: os modelos de página [form1.xhtml, form2.xhtml, form3.xhtml, error.xhtml]. A exibição destas páginas é controlada pelos valores booleanos nas linhas 19-22. Note-se que, por predefinição, a página [form1.xhtml] é renderizada,
  • linhas 33–34: o método init é executado imediatamente após a instância da classe (presença da anotação @PostConstruct),
  • linhas 35–41: o método init é utilizado para determinar qual a página que deve ser apresentada em primeiro lugar: normalmente a página [form1.xhtml] (linha 19), a menos que a inicialização da aplicação tenha falhado (linha 36), caso em que será apresentada a página [error.xhtml] (linha 40).

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


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
 
  <body>
    <h2><h:outputText value="#{msg['erreur.titre']}"/></h2>
    <p>
      <h:commandButton value="#{msg['erreur.accueil']}" actionListener="#{form.accueil()}"/>
    </p>
    <hr/>
    <h:dataTable value="#{form.erreurs}" var="erreur" headerClass="erreursHeaders" columnClasses="erreurClasse,erreurMessage">
      <h:column>
        <f:facet name="header">
          <h:outputText value="#{msg['erreur.classe']}"/>
        </f:facet>
        <h:outputText value="#{erreur.classe}"/>
      </h:column>
      <h:column>
        <f:facet name="header">
          <h:outputText value="#{msg['erreur.message']}"/>
        </f:facet>
        <h:outputText value="#{erreur.message}"/>
      </h:column>
    </h:dataTable>
  </body>
</html>

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

Image

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

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

3.6.6.1. Exibição da página inicial

Se tudo correr bem, a primeira página apresentada é [form1.xhtml]. Isto 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>

Esta página é alimentada pelo seguinte modelo:


@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
 
  // bean Application
  @Inject
  private Application application;
  // model
  private Long idMedecin;
  private Date jour = new Date();
  
// list of doctors
  public List<Medecin> getMedecins() {
    return application.getMedecins();
  }
  // agenda
  public void getAgenda() {
    ...
}
  • O campo na linha 9 lê e grava o valor da lista na linha 18 da página. Quando a página é carregada pela primeira vez, define o valor selecionado na caixa combinada. No carregamento inicial, idMedecin é igual a null, pelo que o primeiro médico será selecionado.
  • O método nas linhas 13–15 gera os itens para o menu suspenso dos médicos (linha 19 da página). Cada opção gerada terá como rótulo (itemLabel) o título, o apelido e o nome do médico e, como valor (itemValue), o ID do médico;
  • o campo na linha 10 fornece acesso de leitura/gravação ao campo de entrada na linha 21 da página. Na exibição inicial, é apresentada a data atual,
  • Linhas 17–19: O método getAgenda trata do clique no botão [Agenda] na linha 26 da página. Uma vez que não há navegação (é sempre a página [index.html] que é solicitada), utilizaremos frequentemente o atributo actionListener em vez do atributo action. Neste caso, o método chamado no modelo não devolve qualquer resultado.

Quando o botão [Agenda] é clicado,

  • são enviados valores: o valor selecionado no menu suspenso dos médicos é armazenado no campo idMedecin do modelo, e o dia selecionado no campo day;
  • o método getAgenda do modelo é chamado.

O método getAgenda é o seguinte:


  // bean Application
  @Inject
  private Application application;
 
  // model
  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 {
      // we get the doctor back
      medecin = application.gethMedecins().get(idMedecin);
      // title form 2
      form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
      // the doctor's diary for a given day
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // form 2 is displayed
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
    }
  }
 
  // preparation vueErreur
  private void prepareVueErreur(Throwable th) {
    // create an error list
    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()));
    }
// the error view is displayed
    setForms(false, false, false, true);
}

Vamos rever o que o método getAgenda deve exibir:

  • linha 21: recuperamos o médico selecionado do dicionário de médicos armazenado no bean da aplicação. Para isso, usamos o seu ID, que foi enviado para idMedecin,
  • linha 23: preparamos o título da página [form2.xhtml] que será exibida. Esta mensagem é recuperada do ficheiro de mensagens para que possa ser internacionalizada. Esta técnica foi descrita na secção 2.8.5.7, página 135.
  • linha 25: chamamos a camada [business] para calcular a agenda do médico selecionado para o dia selecionado,
  • linha 27: [form2.xhtml] é exibida,
  • linha 28: se ocorrer uma exceção, é criada uma lista de erros (linhas 37–42) e a página [error.xhtml] é exibida (linha 44).

3.6.6.2. Exibir a agenda de um médico

A página [form2.xhtml] corresponde à seguinte vista:

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>

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


// modèle
  private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;

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

  • linha 10, o título da página,
  • linha 12: a agenda do médico é apresentada utilizando uma tag <h:dataTable> de 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 pode ter reservado o horário, ou nada se não for o caso. Para fazer esta seleção, usamos tags da biblioteca JSTL Core referenciada na linha 7,
  • linhas 30–35: a terceira coluna exibe o link [Reservar] se o horário estiver disponível e o link [Eliminar] se o horário estiver reservado.

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


// modèle
  private Long idCreneau;
 
  // action sur RV
  public void action() {
    ...
}
  • O método action é chamado quando o utilizador clica na ligação Reservar / Eliminar (linha 32). Note-se que o atributo action é utilizado aqui. O método apontado por este atributo deve ter a assinatura String action(), pois o método deve então devolver uma chave de navegação. No entanto, aqui é void action(). Isto não causou um erro, e podemos assumir que, neste caso, não há navegação. Era isto que se pretendia. A utilização de actionListener em vez de action causou um mau funcionamento,
  • O campo `idCreneau` na linha 2 irá recuperar o ID do intervalo de tempo associado ao link que foi clicado (linha 33 da página).

3.6.6.3. Eliminar um compromisso

Vamos examinar o código que lida com a eliminação de um compromisso. Isto corresponde à seguinte sequência de vistas:

O código envolvido nesta operação é o seguinte:


// bean Application
  @Inject
  private Application application;
 
  // model
  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;
 
  // action on RV
  public void action() {
    // search for the time slot in the calendar
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // have we found?
    if (!trouvé) {
      // it's weird - form2 is redisplayed
      setForms(false, true, false, false);
      return;
    }
    // we found
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // according to desired action
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // reservation
 
  public void reserver() {
    ...
  }
 
  public void supprimer() {
    try {
      // deleting an appointment
      application.getMetier().supprimerRv(creneauChoisi.getRv());
      // updating the agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // form2 is displayed
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
    }
  }
  • linha 16: quando o método de ação é iniciado, o ID do intervalo de tempo selecionado já foi armazenado em idCreneau (linha 11),
  • linhas 18–26: tentamos recuperar o intervalo de tempo com base no seu ID (linha 21). Procuramo-lo no calendário atual, agendaMedecinJour, a partir da linha 10. Normalmente, devemos encontrá-lo. Caso contrário, não fazemos nada (linhas 28–32),
  • linha 34: se o intervalo foi encontrado, recuperamos uma referência para ele e armazenamo-la na linha 12,
  • linha 36: verificamos se o intervalo de tempo selecionado tinha um compromisso. Se sim, eliminamo-lo (linha 39); caso contrário, reservamos um (linha 37),
  • linha 51: o compromisso no intervalo selecionado é eliminado. A camada [de negócios] trata disso,
  • linha 53: solicitamos a agenda atualizada do médico à camada [business]. Veremos, naturalmente, menos um compromisso lá. Mas, como a aplicação é multiutilizador, podemos ver alterações feitas por outros utilizadores,
  • linha 55: a página [form2.xhtml] é exibida novamente,
  • linha 58: uma vez que a camada [business] foi chamada, podem ocorrer exceções. Neste caso, armazenamos a pilha de exceções na lista de erros na linha 13 e exibimo-las utilizando a vista [error.xhtml].

3.6.6.4. Agendamento de compromissos

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

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


// model
  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;
 
  // action on RV
  public void action() {
...
    // we found
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // according to desired action
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // reservation
 
    public void reserver() {
    try {
      // title form 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();
      // customer selected in combo
      idClient=null;
      // form 3 is displayed
      setForms(false, false, true, false);
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
    }
  }
  • linha 14: se o intervalo de tempo selecionado não tiver nenhum compromisso, então trata-se de uma reserva,
  • linha 30: preparamos o título da página [form3.xhtml] utilizando a mesma técnica que para o título da página [form2.xhtml],
  • linha 34: neste formulário, existe uma caixa combinada cujo valor é preenchido por idClient. Definimos o valor deste campo como nulo para que nada seja selecionado,
  • linha 36: exibimos a página [form3.xhtml],
  • linha 39: ou a página de erro, 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;
 
  // model
  private Long idClient;
 
  // customer list
  public List<Client> getClients() {
    return application.getClients();
  }
  • linha 6: o ID do cliente preenche o atributo value da caixa de combinação do cliente na linha 12 da página. Define o item selecionado da caixa de combinação,
  • linhas 9–11: o método getClients preenche a lista suspensa (linha 13). O rótulo (itemLabel) para cada opção é [Título Nome Apelido] para o cliente, e o valor associado (itemValue) é o ID do cliente. É, portanto, este valor que será enviado.

3.6.6.5. Confirmar uma marcação

A confirmação de uma marcação segue esta sequência:

e corresponde a clicar no botão [Confirmar]:


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

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


  // bean Application
  @Inject
  private Application application;
  
  // model
  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;
 
  // rv validation
  public void validerRv() {
    try {
      // retrieve an instance of the selected time slot
      Creneau creneau = application.getMetier().getCreneauById(idCreneau);
      // we add the Rv
      application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
      // updating the agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // form2 is displayed
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
    }
}
  • linha 12: Antes da execução do método `validerRv`, o campo `idClient` recebeu o ID do cliente selecionado pelo utilizador,
  • linha 19: utilizando o ID do intervalo de tempo armazenado numa etapa anterior (o bean tem âmbito de sessão), solicitamos uma referência ao próprio intervalo de tempo à camada [business],
  • linha 21: a camada [business] é solicitada a adicionar uma consulta para o dia selecionado (day), o intervalo de tempo selecionado (slot) e o cliente selecionado (idClient),
  • linha 23: solicitamos à camada [business] que atualize o calendário do médico. Veremos o compromisso adicionado juntamente com quaisquer alterações que outros utilizadores da aplicação possam ter feito,
  • linha 25: o calendário [form2.xhtml] é exibido novamente,
  • linha 28: exibe a página de erro se ocorrer um erro.

3.6.6.6. Cancelar um compromisso

Isto corresponde à seguinte sequência:

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


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

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


  // annulation prise de Rdv
  public void annulerRv() {
    // on affiche form2
    setForms(false, true, false, false);
}

3.6.6.7. Voltar à página inicial

Há mais uma ação a analisar, na sequência seguinte:

O código para o botão [Home] 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() {
    // on affiche la page d'accueil
    setForms(true, false, false, false);
}

3.7. Conclusão

Criámos a seguinte aplicação:

Concentramo-nos na funcionalidade da aplicação, em vez de na sua interface de utilizador. Esta última será melhorada utilizando a biblioteca de componentes PrimeFaces. Criámos uma aplicação básica que, no entanto, representa uma arquitetura Java EE em camadas utilizando EJBs. A aplicação pode ser melhorada de várias formas:

  • é necessária autenticação. Nem todos estão autorizados a adicionar ou eliminar compromissos,
  • devemos poder percorrer o calendário para a frente e para trás ao procurar um dia com horários disponíveis,
  • deveria ser possível solicitar uma lista de dias em que um médico tem horários disponíveis. De facto, se o médico for um oftalmologista, as consultas são normalmente marcadas com seis meses de antecedência,
  • ...

3.8. Testes com o Eclipse

3.8.1. A camada [DAO]

  • Em [1], importe o projeto EJB para a camada [DAO] e o seu cliente,
  • em [2], selecionamos o projeto EJB da camada [DAO] e executamo-lo [3],
  • em [4], execute-o num servidor,
  • Em [5], apenas o servidor Glassfish é apresentado, pois é o único com um contentor EJB,
  • Em [6], o módulo EJB foi implementado,
  • Em [7], os registos são apresentados:
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.

Estes são os que tínhamos com o NetBeans.

  • Em [7A] [7B] executamos o teste JUnit do cliente,
  • em [8], o teste é aprovado,
  • em [9], o console regista.

Em [10], a aplicação EJB é descarregada.

3.8.2. A camada [de negócios]

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

Na linha 3, anotamos o nome portátil do EJB [Metier] e colamo-lo no cliente da consola para este EJB:


public class ClientRdvMedecinsMetier {
 
  // the remote interface name of the 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";
  // today's date
private static Date jour = new Date();
  • Em [8], executamos o cliente de consola,
  • em [9], os seus registos.
  • em [10], a aplicação empresarial é descarregada;

3.8.3. A camada [web]

  • Em [1], importamos os três projetos Maven da camada [web]. Aquele com a extensão .ear é o projeto empresarial que precisa de ser implementado no GlassFish,
  • em [2], executamo-lo,
  • no servidor GlassFish [3],
  • em [4], a aplicação empresarial foi implementada com sucesso,
  • em [5], introduzimos o URL da aplicação no navegador interno do Eclipse.