3. Aplicação de exemplo – 01: rdvmedecins-jsf2-ejb
O texto a seguir refere-se aos seguintes documentos:
- [ref7]: Introdução ao Java EE 5 (junho de 2010) [http://tahe.developpez.com/java/javaee]. Este documento fornece uma introdução ao JSF 1 e ao EJB 3.
- [ref8]: Java Persistence in Practice (junho de 2007) [http://tahe.developpez.com/java/jpa]. Este documento apresenta a persistência de dados com JPA (Java Persistence API).
- [ref9]: Construir um Serviço Web Java EE com o NetBeans e o Servidor GlassFish (janeiro de 2009) [http://tahe.developpez.com/java/webservice-jee]. Este documento explora o processo de construção de um serviço web.
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):
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:
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:
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:
- 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:
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:
- 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:

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:
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,
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.


























































































































