3. Aplicação de exemplo – 01: rdvmedecins-jsf2-ejb
O texto que se segue faz referência aos seguintes documentos:
- [ref7]: Introdução ao Java EE 5 (junho de 2010) [http://tahe.developpez.com/java/javaee]. Este documento permite conhecer o JSF 1 e os EJB3.
- [ref8]: Persistência em Java na prática (junho de 2007) [http://tahe.developpez.com/java/jpa]. Este documento permite conhecer a persistência de dados com JPA (Java Persistence API).
- [ref9]: Criar um serviço web Java EE com o NetBeans e o servidor GlassFish (janeiro de 2009) [http://tahe.developpez.com/java/webservice-jee]. Este documento aborda a criação de um serviço web.
A aplicação de exemplo que será analisada provém de [ref9].
3.1. L'application
Uma empresa de serviços informáticos, a [ISTIA-AGI], pretende oferecer um serviço de marcação de consultas. O primeiro mercado-alvo é o dos médicos que exercem a profissão de forma independente. Estes, em geral, não dispõem de secretariado. Os clientes que pretendem marcar uma consulta ligam, então, diretamente para o médico. Este é, assim, frequentemente incomodado ao longo do dia, o que diminui a sua disponibilidade para com os doentes. A empresa [ISTIA-AGI] pretende propor-lhes um serviço de marcação de consultas que funcione segundo o seguinte princípio:
- um secretariado assegura a marcação de consultas para um grande número de médicos. Este secretariado pode ser reduzido a uma única pessoa. O salário desta pessoa é partilhado entre todos os médicos que utilizam o serviço.
- O secretariado e todos os médicos estão ligados à Internet
- as marcações são registadas numa base de dados centralizada, acessível pela Internet, tanto pela secretaria como pelos médicos
- A emissão de RV é normalmente efetuada pela secretaria. Pode também ser efetuada pelos próprios médicos. É o caso, nomeadamente, quando, no final de uma consulta, o próprio médico atribui um novo RV ao seu doente.
A arquitetura do serviço de atribuição do RV é a seguinte:
![]() |
Os médicos ganham em eficiência se deixarem de ter de gerir os RV. Se forem em número suficiente, a sua contribuição para as despesas de funcionamento do secretariado será reduzida.
A empresa [ISTIA-AGI] decide desenvolver a aplicação em duas versões:
- uma versão JSF / EJB3 / JPA EclipseLink / servidor Glassfish:
![]() |
- e uma versão JSF / Spring / JPA Hibernate / servidor Tomcat:
![]() |
3.2. Funcionamento da aplicação
Chamaremos à aplicação [RdvMedecins]. Apresentamos abaixo capturas de ecrã do seu funcionamento.
A página inicial da aplicação é a seguinte:
![]() |
A partir desta primeira página, o utilizador (Secretariado, Médico) irá realizar uma série de ações. Apresentamo-las abaixo. A imagem à esquerda mostra a vista a partir da qual o utilizador efetua um pedido; a imagem à direita mostra a resposta enviada pelo servidor.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Por fim, também é possível obter uma página de erros:
![]() |
3.3. A base de dados
Voltemos à arquitetura da aplicação a construir:
![]() |
A base de dados, a que chamaremos [dbrdvmedecins2] , é uma base de dados MySQL5 com quatro tabelas:
![]() |
3.3.1. A tabela [MEDECINS]
Contém informações sobre os médicos geridos pela aplicação [RdvMedecins].
![]() | ![]() |
- ID: número de identificação do médico — chave primária da tabela
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
- NOM: o nome do médico
- PRENOM: o seu nome próprio
- TITRE: o seu título (Menina, Sra., Sr.)
3.3.2. A tabela [CLIENTS]
Os clientes dos diferentes médicos estão registados na tabela [CLIENTS]:
![]() | ![]() |
- ID: número de identificação do cliente — chave primária da tabela
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
- NOM: o nome do cliente
- PRENOM: o seu nome próprio
- TITRE: o seu título (Menina, Sra., Sr.)
3.3.3. A tabela [CRENEAUX]
Esta tabela lista os intervalos horários em que os RV são possíveis:
![]() |
![]() |
- ID: número que identifica o intervalo horário — chave primária da tabela (linha 8)
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
- ID_MEDECIN: número que identifica o médico a quem pertence este intervalo horário – chave estrangeira na coluna MEDECINS (ID).
- HDEBUT: hora de início do intervalo
- MDEBUT: minutos de início do intervalo
- HFIN: hora de fim do intervalo
- MFIN: minutos de fim do intervalo
A segunda linha da tabela [CRENEAUX] (ver [1] acima) indica, por exemplo, que o intervalo n.º 2 começa às 8h20 e termina às 8h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER).
3.3.4. A tabela [RV]
Esta tabela lista os RV atribuídos a cada médico:
![]() |
- ID: número que identifica o RV de forma única – chave primária
- JOUR: dia do RV
- ID_CRENEAU: intervalo horário do RV – chave estrangeira no campo [ID] da tabela [CRENEAUX] – define simultaneamente o intervalo horário e o médico em questão.
- ID_CLIENT: número do cliente para quem é feita a reserva – chave estrangeira no campo [ID] da tabela [CLIENTS]
Esta tabela possui uma restrição de unicidade na sobre os valores das colunas associadas (JOUR, ID_CRENEAU):
Se uma linha da tabela [RV] tiver o valor (JOUR1, ID_CRENEAU1) para as colunas (JOUR, ID_CRENEAU), esse valor não pode aparecer em mais nenhum outro local. Caso contrário, isso significaria que dois RV foram registados ao mesmo tempo para o mesmo médico. Do ponto de vista da programação Java, o controlador JDBC da base de dados lança um SQLException quando esta situação ocorre.
A linha de id igual a 3 (ver [1] acima) significa que um RV foi marcado para o intervalo n.º 20 e o cliente n.º 4 em 23/08/2006. A tabela [CRENEAUX] indica-nos que o horário n.º 20 corresponde ao intervalo horário das 16h20 às 16h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER). A tabela [CLIENTS] indica-nos que o cliente n.º 4 é a Srta. Brigitte BISTROU.
3.3.5. Criação da base de dados
Para criar as tabelas e preenchê-las, pode utilizar-se o script [dbrdvmedecins2.sql], disponível no site de exemplos. Com o [WampServer] (ver parágrafo 1.3.3), pode proceder-se da seguinte forma:
![]() |
- no [1], clica-se no ícone do [WampServer] e seleciona-se a opção [PhpMyAdmin] [2],
- em [3], na janela que se abriu, selecione o link [Bases de données],
![]() |
- em [2], cria-se uma base de dados à qual se atribuiu o nome [4] e a codificação [5],
- em [7], a base de dados foi criada. Clica-se no respetivo link,
![]() |
- em [8], importa-se um ficheiro SQL,
- que se seleciona no sistema de ficheiros com o botão [9],
![]() |
- em [11], seleciona-se o script SQL e, em [12], executa-se o mesmo,
- em [13], as quatro tabelas da base de dados foram criadas. Seguimos uma das ligações,
![]() |
- em [14], o conteúdo da tabela.
A partir daqui, não voltaremos a abordar esta base de dados. No entanto, convidamos o leitor a acompanhar a sua evolução ao longo dos programas, sobretudo quando algo não funcionar.
3.4. As camadas [DAO] e [JPA]
Voltemos à arquitetura que temos de construir:
![]() |
Vamos criar quatro projetos Maven:
- um projeto para as camadas [DAO] e [JPA],
- um projeto para a camada [métier],
- um projeto para a camada [web],
- um projeto empresarial que irá reunir os três projetos anteriores.
Vamos agora compilar o projeto Maven das camadas [DAO] e [JPA].
Nota: a compreensão das camadas [métier], [DAO] e [JPA] requer conhecimentos de Java EE. Para tal, pode consultar-se o [ref7] (ver parágrafo 3).
3.4.1. O projeto NetBeans
É o seguinte:
![]() |
- em [1], cria-se um projeto Maven do tipo [EJB Module] [2],
- em [3], atribui-se um nome ao projeto,
![]() |
- em [4], escolhe-se o servidor Glassfish como servidor,
- em [5], o projeto gerado.
3.4.2. Geração da camada [JPA]
Voltemos à arquitetura que temos de construir:
![]() |
Com o NetBeans, é possível gerar automaticamente a camada [JPA] e a camada [EJB], que controla o acesso às entidades JPA geradas. É interessante conhecer estes métodos de geração automática, pois o código gerado fornece indicações valiosas sobre como escrever entidades JPA ou o código EJB que as utiliza.
Passamos agora a descrever algumas dessas ferramentas de geração automática. Para compreender o código gerado, é necessário ter bons conhecimentos sobre as entidades JPA, [ref8] e as EJB, [ref7] (ver parágrafo 3).
3.4.2.1. Criação de uma ligação do NetBeans à base de dados
- executar o SGBD e o MySQL 5 para que o BD fique disponível,
- criar uma ligação do NetBeans à base de dados [dbrdvmedecins2],
![]() |
- no separador [Services] [1], no ramo [Databases] [2], selecione o controlador JDBC MySQL [3],
- depois selecione a opção [4] «Connect Using», que permite criar uma ligação a uma base de dados MySQL,
- em [5], introduza as informações solicitadas. Em [6], o nome da base de dados; em [7], o utilizador da base de dados e a sua palavra-passe;
- em [8], é possível testar as informações fornecidas,
- em [9], a mensagem esperada quando estas estão corretas,
![]() |
- em [10], a ligação é estabelecida. Aqui podem ver-se as quatro tabelas da base de dados à qual se ligou.
3.4.2.2. Criação de uma unidade de persistência
Voltemos à arquitetura em fase de construção:
![]() |
Estamos a construir a camada [JPA]. A sua configuração é feita num ficheiro [persistence.xml], no qual se definem as unidades de persistência. Cada uma delas necessita das seguintes informações:
- as características JDBC de acesso à base de dados (URL, utilizador, palavra-passe),
- as classes que servirão de representação das tabelas da base de dados,
- a implementação JPA utilizada. Com efeito, JPA é uma especificação implementada por vários produtos. Aqui, utilizaremos o EclipseLink, que é a implementação predefinida utilizada pelo servidor Glassfish. Isto evita-nos ter de adicionar ao Glassfish as bibliotecas de outra implementação.
O NetBeans pode gerar este ficheiro de persistência através de um assistente.
![]() |
- clique com o botão direito do rato no projeto e selecione a criação de uma unidade de persistência [1],
- em [2], atribuir um nome à unidade de persistência que se está a criar,
- em [3], selecione a implementação JPA EclipseLink (JPA 2.0),
- em [4], indicar que as transações com a base de dados serão geridas pelo contentor EJB do servidor Glassfish,
- em [5], indicar que as tabelas do BD já estão criadas e que, por isso, não serão criadas,
![]() |
- no [6], criar uma nova fonte de dados para o servidor Glassfish,
- em [7], atribuir um nome JNDI (Java Naming Directory Interface),
- em [8], associar esse nome à ligação MySQL criada na etapa anterior,
![]() |
- em [9], concluir o assistente,
- em [10], o novo projeto,
- em [11], o ficheiro [persistence.xml] foi gerado na pasta [META-INF],
- em [12], foi gerada uma pasta [setup],
- no [13], foram adicionadas novas dependências ao projeto Maven.
O ficheiro [META-INF/persistence.xml] gerado é o seguinte:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
<jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties/>
</persistence-unit>
</persistence>
Este ficheiro retoma as informações fornecidas no assistente:
- linha 3: o nome da unidade de persistência,
- linha 3: o tipo de transações com a base de dados, neste caso transações JTA (Java Transaction API) geridas pelo contentor EJB3 do servidor GlassFish,
- linha 4: o nome JNDI da fonte de dados.
Normalmente, neste ficheiro encontra-se o tipo de implementação JPA utilizado. No assistente, indicámos EclipseLink. Como se trata da implementação JPA utilizada por predefinição pelo servidor Glassfish, esta não é mencionada no ficheiro [persistence.xml].
No separador [Design], é possível ter uma visão geral do ficheiro [persistence.xml]:
![]() |
Para obter os registos do EclipseLink, utilizaremos o seguinte ficheiro [persistence.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="eclipselink.logging.level" value="FINE"/>
</properties>
</persistence-unit>
</persistence>
- linha 4: indica-se que se utiliza a implementação JPA do EclipseLink,
- linhas 7-9: reúnem as propriedades de configuração do provedor JPA, neste caso EclipseLink,
- linha 8: esta propriedade permite registar as ordens SQL que serão emitidas pelo EclipseLink.
O ficheiro [glassfish-resources.xml] que foi criado é o seguinte:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
<jdbc-connection-pool allow-non-component-callers="false" ... steady-pool-size="8" validate-atmost-once-period-in-seconds="0" wrap-jdbc-objects="false">
<property name="serverName" value="localhost"/>
<property name="portNumber" value="3306"/>
<property name="databaseName" value="dbrdvmedecins2"/>
<property name="User" value="root"/>
<property name="Password" value=""/>
<property name="URL" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
</jdbc-connection-pool>
<jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins2" object-type="user" pool-name="mysql_dbrdvmedecins2_rootPool"/>
</resources>
Este ficheiro contém as informações que introduzimos nos dois assistentes utilizados anteriormente:
- linhas 5-11: as características JDBC da base de dados MySQL5 [dbrdvmedecins2],
- linha 13: o nome JNDI da fonte de dados.
Este ficheiro será utilizado para criar a fonte de dados JNDI [jdbc/dbrdvmedecins2] do servidor Glassfish. Trata-se de um processo exclusivo deste servidor. Para outro servidor, seria necessário proceder de forma diferente, geralmente através de uma ferramenta de administração. Esta ferramenta também existe para o Glassfish.
Por fim, foram adicionadas dependências ao projeto. O ficheiro [pom.xml] é o seguinte:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>ejb</packaging>
<name>mv-rdvmedecins-ejb-dao-jpa</name>
...
<dependencies>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>javax.persistence</artifactId>
<version>2.0.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>6.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
...
<repositories>
<repository>
<url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
<id>eclipselink</id>
<layout>default</layout>
<name>Repository for library Library[eclipselink]</name>
</repository>
</repositories>
</project>
- nas linhas 32-37, uma camada [JPA] requer o artefacto [javaee-api],
- linhas 16, 22 e 28: os artefactos necessários para a implementação JPA / EclipseLink aqui utilizada.
- linhas 18, 24, 30, 36: todos os artefactos têm o atributo provided. Recorde-se que isto significa que são necessários para a compilação, mas não para a execução. Com efeito, durante a execução, são fornecidos (provided) pelo servidor Glassfish,
- linhas 41-48: definem um novo repositório de artefactos Maven, onde os artefactos EclipseLink podem ser encontrados.
3.4.2.3. Geração das entidades JPA
As entidades JPA podem ser geradas por um assistente do NetBeans:
![]() |
- em [1], criam-se entidades JPA a partir de uma base de dados,
- no [2], seleciona-se a fonte de dados [jdbc / dbrdvmedecins2] criada anteriormente,
- em [3], a lista de tabelas dessa fonte de dados,
- em [4], selecionam-se todas,
![]() |
- em [5], as tabelas selecionadas,
- em [6], atribuímos um nome às classes Java associadas às quatro tabelas,
- bem como um nome de pacote [7],
- em [8], JPA reúne as linhas das tabelas de BD em coleções. Escolhemos a lista como coleção,
![]() |
- em [9], as classes Java criadas pelo assistente.
3.4.2.4. As entidades JPA geradas
A entidade [Medecin] é a representação da tabela [medecins]. A classe Java está repleta de anotações que tornam o código pouco legível à primeira vista. Se mantivermos apenas o que é essencial para compreender a função da entidade, obtemos o seguinte código:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "medecins")
public class Medecin implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
// construtores
....
// getters e setters
....
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- na linha 4, a anotação @Entity transforma a classe [Medecin] numa entidade JPA, c.a.d. uma classe associada a uma tabela de BD através de API e JPA,
- na linha 5, o nome da tabela BD associada à entidade JPA. Cada campo da tabela corresponde a um campo na classe Java,
- linha 6, a classe implementa a interface Serializable. Isto é necessário em aplicações cliente/servidor, onde as entidades são serializadas entre o cliente e o servidor.
- linhas 10-11: o campo id da classe [Medecin] corresponde ao campo [ID] (linha 10) da tabela [medecins],
- linhas 13-14: o campo «título» da classe [Medecin] corresponde ao campo [TITRE] (linha 13) da tabela [medecins],
- linhas 16-17: o campo «nome» da classe [Medecin] corresponde ao campo [NOM] (linha 16) da tabela [medecins],
- linhas 19-20: o campo «versão» da classe [Medecin] corresponde ao campo [VERSION] (linha 19) da tabela [medecins]. Aqui, o assistente não reconhece que a coluna é, na verdade, uma coluna de versão que deve ser incrementada sempre que a linha a que pertence for alterada. Para lhe atribuir essa função, é necessário adicionar a anotação @Version. Faremos isso numa etapa seguinte,
- linhas 22-23: o campo prenom da classe [Medecin] corresponde ao campo [PRENOM] da tabela [medecins],
- linhas 10-11: o campo id corresponde à chave primária [ID] da tabela. As anotações das linhas 8-9 esclarecem este ponto,
- linha 8: a anotação @Id indica que o campo anotado está associado à chave primária da tabela,
- linha 9: a camada [JPA] irá gerar a chave primária das linhas que irá inserir na tabela [Medecins]. Existem várias estratégias possíveis. Aqui, a estratégia GenerationType.IDENTITY indica que a camada JPA irá utilizar o modo auto_increment da tabela MySQL,
- linhas 25-26: a tabela [creneaux] possui uma chave estrangeira na tabela [medecins]. Um horário pertence a um médico. Por outro lado, um médico tem vários horários que lhe estão associados. Temos, portanto, uma relação de um (médico) para vários (intervalos), uma relação qualificada pela anotação @OneToMany por JPA (linha 25). O campo da linha 26 conterá todos os horários do médico. Isto sem qualquer programação. Para compreender totalmente a linha 25, é necessário apresentar a classe [Creneau].
Esta é a seguinte:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "MDEBUT")
private int mdebut;
@Column(name = "HFIN")
private int hfin;
@Column(name = "HDEBUT")
private int hdebut;
@Column(name = "MFIN")
private int mfin;
@Column(name = "VERSION")
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
private List<Rv> rvList;
// construtores
...
// getters e setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
Apenas comentamos as novas anotações:
- já referimos que a tabela [creneaux] possui uma chave estrangeira para a tabela [medecins]: um horário está associado a um médico. Podem estar associados vários horários ao mesmo médico. Existe uma relação da tabela [creneaux] para a tabela [medecins], que é qualificada como «muitos (horários) para um (médico)». É a anotação @ManyToOne da linha 32 que serve para definir a chave estrangeira,
- a linha 31, com a anotação @JoinColumn, especifica a relação da chave estrangeira: a coluna [ID_MEDECIN] da tabela [creneaux] é uma chave estrangeira na coluna [ID] da tabela [medecins],
- linha 33: uma referência ao médico titular do horário. Mais uma vez, obtém-se esta informação sem necessidade de programação.
A ligação de chave estrangeira entre a entidade [Creneau] e a entidade [Medecin] é, portanto, concretizada por duas anotações:
- na entidade [Creneau]:
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
- na entidade [Medecin]:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
Ambas as anotações refletem a mesma relação: a da chave estrangeira da tabela [creneaux] para a tabela [medecins]. Diz-se que são inversas uma da outra. Apenas a relação @ManyToOne é indispensável. Esta qualifica, sem ambiguidade, a relação de chave estrangeira. A relação @OneToMany é opcional. Se estiver presente, limita-se a referenciar a relação @ManyToOne à qual está associada. É este o significado do atributo mappedBy da linha 1 da entidade [Medecin]. O valor deste atributo é o nome do campo da entidade [Creneau] que possui a anotação @ManyToOne, a qual especifica a chave estrangeira. Ainda nesta mesma linha 1 da entidade [Medecin], o atributo cascade=CascadeType.ALL define o comportamento da entidade [Medecin] em relação à entidade [Creneau]:
- se for inserida uma nova entidade [Medecin] na base de dados, então as entidades [Creneau] do campo da linha 2 também devem ser inseridas,
- se se alterar uma entidade [Medecin] na base de dados, então as entidades [Creneau] do campo da linha 2 também devem ser alteradas,
- se se eliminar uma entidade [Medecin] da base de dados, então as entidades [Creneau] do campo da linha 2 também devem ser eliminadas.
Apresentamos o código das outras duas entidades sem comentários específicos, uma vez que não introduzem novas notações.
A entidade [Client]
package rdvmedecins.jpa;
...
@Entity
@Table(name = "clients")
public class Client implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idClient")
private List<Rv> rvList;
// construtores
...
// getters e setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- as linhas 24-25 refletem a relação de chave estrangeira entre a tabela [rv] e a tabela [clients].
A entidade [Rv]:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau idCreneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client idClient;
// construtores
...
// getters e setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- a linha 13 define o campo «dia» de tipo Java Date. Indica-se que, na tabela [rv], a coluna [JOUR] (linha 12) é do tipo data (sem hora),
- linhas 16-18: definem a relação de chave estrangeira da tabela [rv] com a tabela [creneaux],
- linhas 20-22: definem a relação de chave estrangeira da tabela [rv] com a tabela [clients].
A geração automática das entidades JPA permite-nos obter uma base de trabalho. Por vezes é suficiente, outras vezes não. É o caso aqui:
- é necessário adicionar a anotação @Version aos diferentes campos de versão das entidades,
- é necessário escrever métodos toString mais explícitos do que os gerados,
- as entidades [Medecin] e [Client] são análogas. Vamos fazê-las derivar de uma classe [Personne],
- vamos eliminar as relações @OneToMany inversas às relações @ManyToOne. Estas não são indispensáveis e causam complicações na programação,
- eliminamos a validação @NotNull nas chaves primárias. Quando se persiste uma entidade JPA com MySQL, a entidade inicial tem uma chave primária null. Só após a persistência na base de dados é que a chave primária do elemento persistido assume um valor.
Com estas especificações, as diferentes classes passam a ser as seguintes:
A classe Pessoa é utilizada para representar médicos e clientes:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@MappedSuperclass
public class Personne implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@Size(min = 1, max = 5)
@Column(name = "TITRE")
private String titre;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 30)
@Column(name = "NOM")
private String nom;
@Basic(optional = false)
@NotNull
@Column(name = "VERSION")
@Version
private int version;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 30)
@Column(name = "PRENOM")
private String prenom;
// construtores
...
// getters e setters
...
@Override
public String toString() {
return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
}
}
- linha 8: note-se que a classe [Personne] não é, por si só, uma entidade (@Entity). Será a classe pai de entidades. A anotação @MappedSuperClass indica esta situação.
A entidade [Client] encapsula as linhas da tabela [clients]. Deriva da classe anterior [Personne]:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// construtores
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
- linha 6: a classe [Client] é uma entidade JPA,
- linha 7: está associada à tabela [clients],
- linha 8: deriva da classe [Personne].
A entidade [Medecin], que encapsula as linhas da tabela [medecins], segue o mesmo modelo:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// construtores
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Médecin[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
A entidade [Creneau] encapsula as linhas da tabela [creneaux]:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@NotNull
@Column(name = "MDEBUT")
private int mdebut;
@Basic(optional = false)
@NotNull
@Column(name = "HFIN")
private int hfin;
@Basic(optional = false)
@NotNull
@Column(name = "HDEBUT")
private int hdebut;
@Basic(optional = false)
@NotNull
@Column(name = "MFIN")
private int mfin;
@Basic(optional = false)
@NotNull
@Column(name = "VERSION")
@Version
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin medecin;
// construtores
...
// getters e setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
// TODO: Aviso - este método não funcionará caso os campos de ID não estejam definidos
...
}
@Override
public String toString() {
return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
}
}
- as linhas 45-47 modelam a relação «muitos para um» que existe entre a tabela [creneaux] e a tabela [medecins] da base de dados: um médico tem vários horários, um horário pertence a um único médico.
A entidade [Rv] encapsula as linhas da tabela [rv]:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@NotNull
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau creneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client client;
// construtores
...
// getters e setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Rv[%s, %s, %s]", id, creneau, client);
}
}
- as linhas 29-31 modelam a relação «muitos para um» que existe entre a tabela [rv] e a tabela [clients] (um cliente pode aparecer em vários Rv) da base de dados e as linhas 25-27 a relação «muitos para um» que existe entre a tabela [rv] e a tabela [creneaux] (um intervalo de tempo pode aparecer em vários Rv).
3.4.3. A classe de exceção
![]() |
A classe de exceção [RdvMedecinsException] da aplicação é a seguinte:
package rdvmedecins.exceptions;
import java.io.Serializable;
import javax.ejb.ApplicationException;
@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException implements Serializable{
// campos privados
private int code = 0;
// construtores
public RdvMedecinsException() {
super();
}
public RdvMedecinsException(String message) {
super(message);
}
public RdvMedecinsException(String message, Throwable cause) {
super(message, cause);
}
public RdvMedecinsException(Throwable cause) {
super(cause);
}
public RdvMedecinsException(String message, int code) {
super(message);
setCode(code);
}
public RdvMedecinsException(Throwable cause, int code) {
super(cause);
setCode(code);
}
public RdvMedecinsException(String message, Throwable cause, int code) {
super(message, cause);
setCode(code);
}
// getters e setters
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- linha 7: a classe deriva da classe [RuntimeException]. O compilador não obriga, portanto, a tratá-la com try/catch.
- linha 6: a anotação @ApplicationException faz com que a exceção não seja «engolida» por uma exceção do tipo [EjbException].
Para compreender a anotação @ApplicationException, voltemos à arquitetura utilizada no lado do servidor:
![]() |
A exceção do tipo [RdvMedecinsException] será lançada pelos métodos do EJB da camada [DAO] no interior do contentor EJB3 e interceptada por este. Sem a anotação @ApplicationException, o contentor EJB3 encapsula a exceção ocorrida numa exceção do tipo [EjbException] e relança-a. Pode não se desejar este encapsulamento e permitir que uma exceção do tipo [RdvMedecinsException] saia do contentor EJB3. É isso que a anotação @ApplicationException permite. Além disso, o atributo (rollback=true) desta anotação indica ao contentor EJB3 que, se a exceção do tipo [RdvMedecinsException] ocorrer no interior de um método executado no âmbito de uma transação com um SGBD, esta deve ser revertida. Em termos técnicos, isto denomina-se efetuar um rollback da transação.
3.4.4. A EJB da camada [DAO]
![]() |
![]() |
A interface Java [IDao] da camada [DAO] é a seguinte:
package rdvmedecins.dao;
import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
public interface IDao {
// lista de clientes
public List<Client> getAllClients();
// lista de médicos
public List<Medecin> getAllMedecins();
// lista de horários disponíveis de um médico
public List<Creneau> getAllCreneaux(Medecin medecin);
// lista de consultas de um médico, num determinado dia
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// encontrar um cliente identificado pelo seu ID
public Client getClientById(Long id);
// encontrar um cliente identificado pelo seu ID
public Medecin getMedecinById(Long id);
// encontrar uma consulta identificada pelo seu ID
public Rv getRvById(Long id);
// encontrar um intervalo horário identificado pelo seu ID
public Creneau getCreneauById(Long id);
// adicionar um RV
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// eliminar um RV
public void supprimerRv(Rv rv);
}
Esta interface foi criada após a identificação das necessidades da camada [web]:
- linha 14: a lista de clientes. Vamos precisar dela para preencher a lista suspensa de clientes,
- linha 16: a lista de médicos. Precisaremos dela para preencher a lista suspensa de médicos,
- linha 18: a lista dos horários disponíveis de um médico. Precisaremos dela para apresentar a agenda do médico para um determinado dia,
- linha 20: a lista de consultas de um médico para um determinado dia. Combinada com o método anterior, permitirá exibir a agenda do médico para um determinado dia com os seus horários já reservados,
- linha 22: permite localizar um cliente a partir do seu número. O método permitirá localizar um cliente a partir de uma escolha na lista suspensa de clientes,
- linha 24: o mesmo se aplica aos médicos,
- linha 26: procura uma consulta pelo seu número. Pode ser utilizado quando se elimina uma consulta para verificar previamente se esta existe efetivamente,
- linha 28: procura um intervalo de tempo a partir do seu número. Permite identificar o intervalo que um utilizador pretende adicionar ou eliminar,
- linha 30: para adicionar uma consulta,
- linha 32: para eliminar uma consulta.
A interface local [IDaoLocal] do EJB limita-se a derivar da interface anterior [IDao]:
package rdvmedecins.dao;
import javax.ejb.Local;
@Local
public interface IDaoLocal extends IDao{
}
O mesmo se aplica à interface remota [IDaoRemote]:
package rdvmedecins.dao;
import javax.ejb.Remote;
@Remote
public interface IDaoRemote extends IDao{
}
A interface EJB [DaoJpa] implementa ambas as interfaces, a local e a remota:
package rdvmedecins.dao;
...
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
- a linha 5 indica que a interface remota EJB tem o nome «rdvmedecins.dao». Além disso, a anotação @Singleton (Java EE6) garante que apenas será criada uma instância do EJB. A anotação @Stateless (Java EE5) define um EJB que pode ser criado em múltiplas instâncias para alimentar um conjunto de EJB,
- a linha 6 indica que todos os métodos do EJB são executados no âmbito de uma transação gerida pelo contentor EJB3,
- a linha 7 mostra que o EJB implementa as interfaces local e remota e é também serializável.
O código completo do EJB é o seguinte:
package rdvmedecins.dao;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import rdvmedecins.exceptions.RdvMedecinsException;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
@PersistenceContext
private EntityManager em;
// lista de clientes
public List<Client> getAllClients() {
try {
return em.createQuery("select rc from Client rc").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 1);
}
}
// lista de médicos
public List<Medecin> getAllMedecins() {
try {
return em.createQuery("select rm from Medecin rm").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 2);
}
}
// lista dos horários disponíveis de um determinado médico
// médico: o médico
public List<Creneau> getAllCreneaux(Medecin medecin) {
try {
return em.createQuery("select rc from Creneau rc join rc.medecin m where m.id=:idMedecin").setParameter("idMedecin", medecin.getId()).getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 3);
}
}
// lista de consultas de um determinado médico, num determinado dia
// médico: o médico
// dia: o dia
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
try {
return em.createQuery("select rv from Rv rv join rv.creneau c join c.idMedecin m where m.id=:idMedecin and rv.jour=:jour").setParameter("idMedecin", medecin.getId()).setParameter("jour", jour).getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 3);
}
}
// adição de uma consulta
// dia: dia da consulta
// intervalo: intervalo horário da consulta
// cliente: cliente para quem foi marcada a consulta
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
try {
Rv rv = new Rv(null, jour);
rv.setClient(client);
rv.setCreneau(creneau);
em.persist(rv);
return rv;
} catch (Throwable th) {
throw new RdvMedecinsException(th, 4);
}
}
// eliminação de uma consulta
// consulta: a consulta eliminada
public void supprimerRv(Rv rv) {
try {
em.remove(em.merge(rv));
} catch (Throwable th) {
throw new RdvMedecinsException(th, 5);
}
}
// recuperar um determinado cliente
public Client getClientById(Long id) {
try {
return (Client) em.find(Client.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// recuperar um médico específico
public Medecin getMedecinById(Long id) {
try {
return (Medecin) em.find(Medecin.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// recuperar uma consulta específica
public Rv getRvById(Long id) {
try {
return (Rv) em.find(Rv.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// recuperar um horário específico
public Creneau getCreneauById(Long id) {
try {
return (Creneau) em.find(Creneau.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
}
- linha 22: o objeto EntityManager que gere o acesso ao contexto de persistência. Ao instanciar a classe, este campo será inicializado pelo contentor EJB graças à anotação @PersistenceContext da linha 21,
- linha 27: consulta JPQL (Java Persistence Query Language) que devolve todas as linhas da tabela [clients] sob a forma de uma lista de objetos [Client],
- linha 36: consulta análoga para os médicos,
- linha 46: uma consulta JPQL que realiza uma junção entre as tabelas [creneaux] e [medecins]. É parametrizada pelo ID do médico,
- linha 57: uma consulta JPQL que realiza uma junção entre as tabelas [rv], [creneaux] e [medecins] e que tem dois parâmetros: o ID do médico e o dia da consulta,
- linhas 69-73: criação de uma consulta e, em seguida, o seu registo na base de dados,
- linha 83: eliminação de uma consulta da base de dados,
- linha 92: executa um select na base de dados para encontrar um determinado cliente,
- linha 101: o mesmo para um médico,
- linha 110: o mesmo para uma consulta,
- linha 119: o mesmo para um intervalo horário,
- todas as operações com o contexto de persistência em da linha 22 estão sujeitas a encontrar um problema com a base de dados. Por isso, estão todas rodeadas por um try / catch. A eventual exceção é encapsulada na exceção «própria» RdvMedecinsException.
3.4.5. Implementação do controlador JDBC a partir de MySQL
Na arquitetura abaixo:
![]() |
O EclipseLink necessita do controlador JDBC do MySQL. É necessário instalar este controlador nas bibliotecas do servidor Glassfish, na pasta <glassfish>/domains/domain1/lib/ext, em que <glassfish> é a pasta de instalação do servidor Glassfish. Pode-se obtê-lo da seguinte forma:
![]() |
A pasta onde deve ser colocado o controlador JDBC do MySQL é <Pasta Domains>[1]/domain1/lib/ext [2]. Este controlador está disponível no URL [http://www.mysql.fr/downloads/connector/j/]. Após a sua instalação, é necessário reiniciar o servidor Glassfish para que este reconheça esta nova biblioteca.
3.4.6. Implantação da camada EJB da camada [DAO]
Voltemos à arquitetura construída até ao momento:
![]() |
O conjunto [web, métier, DAO, JPA] deve ser implementado no servidor Glassfish. Procedemos da seguinte forma:
![]() |
- em [1], compilamos o projeto Maven,
- em [2], executamo-lo,
- em [3], foi implementado no servidor Glassfish (separador [Services])
Pode ser interessante consultar os registos do Glassfish:
![]() |
Em [1], os registos do Glassfish estão disponíveis no separador [Output / Glassfish Server 3+]. São os seguintes:
As linhas identificadas por [Config] e [Précis] são os registos de EclipseLink; as identificadas por [Infos] provêm do Glassfish.
- linhas 1-12: o EclipseLink processa as entidades JPA que detetou,
- linhas 13-17: informações que indicam que o processamento das entidades JPA decorreu normalmente,
- linha 18: o EclipseLink dá sinal de si,
- linha 19: EclipseLink reconhece que se trata de SGBD MySQL,
- linhas 20-24: o EclipseLink tenta estabelecer ligação com o BD,
- linhas 25-28: conseguiu,
- linhas 29-33: tenta voltar a ligar-se, desta vez utilizando especificamente uma plataforma MySQL (linha 30),
- linhas 34-37: também bem-sucedido,
- linha 38: confirmação de que a unidade de persistência [dbrdvmedecins-PU] pôde ser instanciada,
- linha 39: os nomes portáveis das interfaces remota e local do EJB [DaoJpa], sendo que «portável» significa que são reconhecidos por todos os servidores de aplicações Java EE 6,
- linha 40: os nomes das interfaces remota e local do EJB [DaoJpa], específicos do Glassfish. No teste que se segue, utilizaremos o nome «rdvmedecins.dao».
As linhas 39 e 40 são importantes. Ao escrever o cliente de um EJB no Glassfish, é necessário conhecê-las.
3.4.7. Testes do EJB da camada [DAO]
Agora que o EJB da camada [DAO] da nossa aplicação foi implementado, podemos testá-lo. Vamos fazê-lo no âmbito de uma aplicação cliente/servidor:
![]() |
O cliente irá testar a interface remota do EJB [DAO] implementado no servidor Glassfish.
Começamos por criar um novo projeto Maven:
![]() |
- No [1], criamos um novo projeto,
- em [2,3], criamos um projeto Maven do tipo [Java Application],
- em [4], atribuímos-lhe um nome e colocamo-lo na mesma pasta que o EJB e o [DAO],
![]() |
- em [5], o projeto gerado,
- em [6], foi gerada uma classe [App.java]. Vamos eliminá-la,
- em [7], foi gerado um ramo [Source Packages]. Ainda não o tínhamos encontrado. Podemos colocar testes JUnit neste ramo. É o que faremos. Não iremos manter a classe de teste [AppTest] gerada,
- em [8], as dependências do projeto Maven. O ramo [Dependencies] está vazio. Teremos de adicionar novas dependências a este ramo. O ramo [Test Dependencies] reúne as dependências necessárias para os testes. Aqui, a biblioteca utilizada é a do framework JUnit 3.8. Teremos de a alterar.
O projeto evolui da seguinte forma:
![]() |
- para [1], o projeto onde as duas classes geradas foram eliminadas, bem como a dependência JUnit.
Voltemos à arquitetura cliente/servidor que será utilizada para o teste:
![]() |
O cliente precisa de conhecer a interface remota disponibilizada pelo EJB e pelo [DAO]. Além disso, irá trocar entidades JPA com o EJB. Por isso, necessita da definição dessas entidades. Para que o projeto de teste do EJB tenha acesso a estas informações, vamos adicionar o projeto do EJB [DAO] como dependência ao projeto:
![]() |
- no [1], adicionamos uma dependência do ramo [Test Dependencies],
- no [2], seleciona-se o separador [Open Projects],
- em [3], seleciona-se o projeto Maven de EJB [DAO],
![]() |
- em [4], a dependência adicionada.
Voltemos à arquitetura cliente/servidor do teste:
![]() |
Durante a execução, o cliente e o servidor comunicam através da rede TCP-IP. Não vamos programar estas trocas de dados. Para cada servidor de aplicações, existe uma biblioteca a integrar nas dependências do cliente. A biblioteca para o Glassfish chama-se [gf-client]. Adicionamo-la:
![]() |
- no [1], adicionamos uma dependência,
- no [2], definimos as características do artefacto pretendido,
- no [3], são adicionadas inúmeras dependências. O Maven irá descarregá-las. Este processo pode demorar vários minutos. Em seguida, são armazenadas no repositório local do Maven.
Podemos agora criar o teste JUnit:
![]() |
- em [2], clicamos com o botão direito do rato em [Test Packages] para criar um novo teste JUnit,
![]() |
- em [3], atribua um nome à classe de teste, bem como um pacote para a mesma [4],
- em [5], escolhe-se o framework JUnit 4.x,
- em [6], a classe de teste gerada,
- em [7], as novas dependências do projeto Maven.
O ficheiro [pom.xml] fica então da seguinte forma:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>mv-client-rdvmedecins-ejb-dao</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-client-rdvmedecins-ejb-dao</name>
<url>http://maven.apache.org</url>
<repositories>
<repository>
<url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
<id>eclipselink</id>
<layout>default</layout>
<name>Repository for library Library[eclipselink]</name>
</repository>
<repository>
<url>http://repo1.maven.org/maven2/</url>
<id>junit_4</id>
<layout>default</layout>
<name>Repository for library Library[junit_4]</name>
</repository>
</repositories>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.appclient</groupId>
<artifactId>gf-client</artifactId>
<version>3.1.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
De notar:
- nas linhas 32-51, as dependências do projeto,
- linhas 13-26: foram definidos dois repositórios Maven, um para o EclipseLink (linhas 14-19) e outro para o JUnit4 (linhas 20-25).
A classe de teste será a seguinte:
package rdvmedecins.tests.dao;
import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import junit.framework.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import rdvmedecins.dao.IDaoRemote;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
public class JUnitTestDao {
// camada [dao] testada
private static IDaoRemote dao;
// data de hoje
Date jour = new Date();
@BeforeClass
public static void init() throws NamingException {
// inicialização do ambiente JNDI
InitialContext initialContext = new InitialContext();
// instanciação da camada DAO
dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
}
@Test
public void test1() {
// exibição de clientes
List<Client> clients =dao.getAllClients();
display("Liste des clients :", clients);
// exibição de médicos
List<Medecin> medecins =dao.getAllMedecins();
display("Liste des médecins :", medecins);
// exibição dos horários disponíveis de um médico
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = dao.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// lista de consultas de um médico, num determinado dia
display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// adicionar um RV
Rv rv = null;
Creneau creneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
rv = dao.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// adicionar um RV no mesmo horário do mesmo dia
// deve provocar uma exceção
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
Boolean erreur = false;
try {
rv = dao.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// regista-se o erro
erreur=true;
}
// verifica-se se ocorreu um erro
Assert.assertTrue(erreur);
// lista de RV
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// eliminar um RV
System.out.println("Suppression du Rv ajouté");
dao.supprimerRv(rv);
System.out.println("Rv supprimé");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
}
// método utilitário — apresenta os elementos de uma coleção
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- linhas 23-29: o método marcado com @BeforeClass é executado antes de todos os outros. Aqui, cria-se uma referência à interface remota do EJB [DaoJpa]. Recorde-se que lhe tínhamos atribuído o nome JNDI «rdvmedecins.dao»,
- linhas 34-35: apresentam a lista de clientes,
- linhas 37-38: apresentam a lista de médicos,
- linhas 40-42: apresentam os horários disponíveis do primeiro médico,
- linha 44: exibe as consultas do primeiro médico para o dia da linha 21,
- linhas 46-51: adicionam uma consulta ao primeiro médico, para o seu horário n.º 2 e no dia da linha 21,
- linha 52: exibe, para verificação, as consultas do primeiro médico para o dia da linha 21. Deve haver pelo menos uma, a que acabou de ser adicionada,
- linhas 55-70: adiciona-se a mesma consulta. Como a tabela [RV] tem uma restrição de unicidade, esta adição deve provocar uma exceção. Verifica-se isso na linha 70,
- linha 72: exibem, para verificação, as consultas do primeiro médico para o dia da linha 21. A consulta que se pretendia adicionar não deve constar aí,
- linhas 74-76: elimina-se a única consulta que foi adicionada,
- linha 77: exibem, para verificação, as consultas do primeiro médico para o dia da linha 21. A consulta que acabámos de eliminar não deve constar aí.
Este teste é um teste falso JUnit. Contém apenas uma asserção (linha 70). Trata-se de um teste visual com os defeitos que lhe são inerentes.
Se tudo correr bem, os testes devem ser bem-sucedidos:
![]() |
- em [1], cria-se o projeto de teste,
- em [2], executa-se o teste,
- em [3], o teste foi bem-sucedido.
Vamos analisar mais detalhadamente os resultados do teste:
O leitor é convidado a ler estes registos em simultâneo com o código que os gerou. Vamos debruçar-nos sobre a exceção que ocorreu ao adicionar um compromisso já existente, nas linhas 41-49. A pilha de exceções é reproduzida nas linhas 42-48. Trata-se de uma exceção inesperada. Voltemos ao código do método de adição de um compromisso:
// adicionar um compromisso
// dia: dia do Rv
// intervalo: intervalo horário da consulta
// cliente: cliente para quem foi marcada a consulta
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
try {
Rv rv = new Rv(null, jour);
rv.setClient(client);
rv.setCreneau(creneau);
System.out.println(String.format("avant persist : %s",rv));
em.persist(rv);
System.out.println(String.format("après persist : %s",rv));
return rv;
} catch (Throwable th) {
throw new RdvMedecinsException(th, 4);
}
}
Vejamos os registos do Glassfish durante a adição dos dois compromissos:
- linha 2: antes do primeiro persist,
- linha 3: após o primeiro persist,
- linha 4: o comando INSERT que vai ser executado. Note-se que não ocorre ao mesmo tempo que a operação persist. Se fosse esse o caso, este registo teria aparecido antes da linha 2. A operação INSERT ocorre, então, normalmente no final da transação em que o método é executado,
- linha 6: EclipseLink pergunta a MySQL qual é a última chave primária utilizada. Obterá a chave primária da consulta adicionada. Este valor irá preencher o campo id da entidade [Rv] persistida,
- linhas 7-8: a consulta SELECT que irá apresentar os compromissos do médico,
- linhas 9-10: as exibições no ecrã da segunda consulta persist,
- linhas 11-12: a ordem INSERT que será executada. Deve provocar uma exceção. Esta aparece nas linhas 15-16 e é clara. É inicialmente lançada pelo controlador JDBC do MySQL devido à violação da restrição de unicidade das consultas. Deduz-se que estas exceções deveriam aparecer nos registos do teste JUnit. No entanto, não é esse o caso:
Recorde-se a arquitetura cliente/servidor do teste:
![]() |
Quando o EJB [DAO] lança uma exceção, esta tem de ser serializada para chegar ao cliente. Provavelmente foi esta operação que falhou por uma razão que não compreendi. Como a nossa aplicação completa não funcionará em modo cliente/servidor, podemos ignorar este problema.
Agora que o EJB da camada [DAO] está operacional, podemos passar para o EJB da camada [métier].
3.5. A camada [métier]
Voltemos à arquitetura da aplicação em desenvolvimento:
![]() |
Vamos criar um novo projeto Maven para a camada EJB [métier]. Como se pode ver acima, este terá uma dependência do projeto Maven que foi criado para as camadas [DAO] e [JPA].
3.5.1. O projeto NetBeans
Criamos um novo projeto Maven do tipo EJB. Para tal, basta seguir o procedimento já utilizado e descrito na página 174.
![]() |
- no [1], o projeto Maven da camada [métier],
- em [2], adiciona-se uma dependência,
- no [3], seleciona-se o projeto Maven das camadas [DAO] e [JPA],
- em [4], seleciona-se o escopo [provided]. Recorde-se que isto significa que é necessário para a compilação, mas não para a execução do projeto. Com efeito, oEJB da camada [métier] será implementado no servidor Glassfish juntamente com o EJB das camadas [DAO] e [JPA]. Assim, quando for executado, o EJB das camadas [DAO] e [JPA] já estará presente,
![]() |
- no [6], o novo projeto com a sua dependência.
Apresentemos agora os códigos-fonte da camada [métier]:
![]() |
O EJB [Metier] terá a seguinte interface [IMetier]:
package rdvmedecins.metier.service;
import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
public interface IMetier {
// camada DAO
// lista de clientes
public List<Client> getAllClients();
// lista de médicos
public List<Medecin> getAllMedecins();
// lista de horários de um médico
public List<Creneau> getAllCreneaux(Medecin medecin);
// lista das consultas de um médico, num determinado dia
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// encontrar um cliente identificado pelo seu ID
public Client getClientById(Long id);
// encontrar um cliente identificado pelo seu ID
public Medecin getMedecinById(Long id);
// encontrar uma consulta identificada pelo seu ID
public Rv getRvById(Long id);
// encontrar um intervalo horário identificado pelo seu ID
public Creneau getCreneauById(Long id);
// adicionar um RV
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// eliminar um RV
public void supprimerRv(Rv rv);
// função
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
}
Para compreender esta interface, é necessário ter em conta a arquitetura do projeto:
![]() |
Definimos a interface da camada [DAO] (parágrafo 3.4.4) e indicámos que esta respondia às necessidades da camada [web], ou seja, às necessidades do utilizador. A camada [web] comunica com a camada [DAO] apenas através da camada [métier]. Isto explica por que razão se encontram na camada [métier] todos os métodos da camada [DAO]. Estes métodos limitar-se-ão a delegar o pedido da camada [web] à camada [DAO]. Nada mais do que isso.
Durante a análise da aplicação, surge uma necessidade: ser capaz de apresentar numa página web a agenda de um médico para um determinado dia, de modo a saber os horários ocupados e livres desse dia. É tipicamente o que acontece quando a secretária responde a um pedido por telefone. Pede-se-lhe uma consulta para um determinado dia com um determinado médico. Para responder a esta necessidade, a camada [métier] disponibiliza o método da linha 46.
// função
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
Pode-se questionar onde colocar este método:
- poderia ser colocado na camada [DAO]. No entanto, este método não responde propriamente a uma necessidade de acesso aos dados, mas sim a uma necessidade de negócio,
- poderia ser colocado na camada [web]. Isso seria uma má ideia. Pois se alterarmos a camada [web] para uma camada [Swing], perderemos o método, embora a necessidade continue a existir.
O método recebe como parâmetros o médico e o dia para o qual se pretende a agenda de marcações. Devolve um objeto [AgendaMedecinJour] que representa a agenda do médico para esse dia:
package rdvmedecins.metier.entites;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.jpa.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// campos
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// construtores
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// getters e setters
...
}
- linha 12: o médico a quem pertence a agenda,
- linha 13: o dia da agenda,
- linha 14: os horários disponíveis do médico para esse dia.
- A classe apresenta construtores (linhas 17, 21), bem como um método toString adaptado (linha 27).
A classe [CreneauMedecinJour] (linha 14) é a seguinte:
package rdvmedecins.metier.entites;
import java.io.Serializable;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// campos
private Creneau creneau;
private Rv rv;
// construtores
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau,rv);
}
// getters e setters
...
}
- linha 12: um horário disponível do médico,
- linha 13: a consulta associada, null, se o horário estiver disponível.
Vemos, assim, que o campo creneauxMedecinJour da linha 14 da classe [AgendaMedecinJour] permite-nos obter todos os horários do médico com a informação «ocupado» ou «disponível» para cada um deles. Este era o objetivo do novo método [getAgendaMedecinJour] da interface [IMetier].
O nosso EJB [Metier] terá uma interface local e uma interface remota que se limitarão a derivar da interface principal [IMetier]:
package rdvmedecins.metier.service;
import javax.ejb.Local;
@Local
public interface IMetierLocal extends IMetier{
}
package rdvmedecins.metier.service;
import javax.ejb.Remote;
@Remote
public interface IMetierRemote extends IMetier{
}
O EJB e o [Metier] implementam estas interfaces da seguinte forma:
package rdvmedecins.metier.service;
import java.io.Serializable;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import javax.ejb.EJB;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import rdvmedecins.dao.IDaoLocal;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.entites.CreneauMedecinJour;
@Singleton
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote, Serializable {
// camada DAO
@EJB
private IDaoLocal dao;
public Metier() {
}
@Override
public List<Client> getAllClients() {
return dao.getAllClients();
}
@Override
public List<Medecin> getAllMedecins() {
return dao.getAllMedecins();
}
@Override
public List<Creneau> getAllCreneaux(Medecin medecin) {
return dao.getAllCreneaux(medecin);
}
@Override
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
return dao.getRvMedecinJour(medecin, jour);
}
@Override
public Client getClientById(Long id) {
return dao.getClientById(id);
}
@Override
public Medecin getMedecinById(Long id) {
return dao.getMedecinById(id);
}
@Override
public Rv getRvById(Long id) {
return dao.getRvById(id);
}
@Override
public Creneau getCreneauById(Long id) {
return dao.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return dao.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(Rv rv) {
dao.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour) {
// lista de horários disponíveis do médico
List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
// lista de marcações desse mesmo médico para esse mesmo dia
List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
// cria-se um dicionário a partir das consultas marcadas
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// cria-se a agenda para o dia solicitado
AgendaMedecinJour agenda = new AgendaMedecinJour();
// o médico
agenda.setMedecin(medecin);
// o dia
agenda.setJour(jour);
// os intervalos de reserva
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// preenchimento dos intervalos de reserva
for (int i = 0; i < creneauxHoraires.size(); i++) {
// linha i da agenda
creneauxMedecinJour[i] = new CreneauMedecinJour();
// ID do horário
creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
// o intervalo está livre ou reservado?
if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
// o intervalo está ocupado — regista-se a reserva
Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
creneauxMedecinJour[i].setRv(resa);
}
}
// envia-se o resultado
return agenda;
}
}
- na linha 22, a classe [Metier] é um singleton de EJB,
- linha 23, cada método do EJB decorre no âmbito de uma transação. Isto significa que a transação tem início no início do método, na camada [métier]. Esta irá chamar métodos da camada [DAO]. Estes decorrerão no âmbito da mesma transação,
- na linha 24, o EJB implementa as suas interfaces local e remota e é, além disso, serializável,
- linha 27: uma referência à EJB da camada [DAO],
- linha 29: esta será injetada pelo contentor EJB do servidor Glassfish, graças à anotação @EJB. Assim, quando os métodos da classe [Metier] são executados, a referência à EJB da camada [DAO] já foi inicializada,
- linhas 33-81: esta referência é utilizada para delegar à camada [DAO] a chamada efetuada à camada [métier],
- linha 84: o método getAgendaMedecinJour que permite obter a agenda de um médico para um determinado dia. Deixamos que o leitor acompanhe os comentários.
3.5.2. Implementação da camada [métier]
A camada [métier] depende da camada [DAO]. Cada camada foi implementada com um EJB. Para testar o EJB e o [métier], é necessário implementar os dois EJB. Para tal, é necessário um projeto empresarial.
![]() |
- [1], criamos um novo projeto,
- do tipo Maven [2] e Aplicação Empresarial [3],
- atribuímos-lhe um nome [4]. O sufixo ear será adicionado automaticamente,
![]() |
- em [5], escolhe-se o servidor Glassfish e Java EE 6,
- em [6], uma aplicação empresarial contém módulos, geralmente módulos EJB e módulos web. Aqui, a aplicação empresarial irá conter os módulos dos dois EJB que criámos. Como esses módulos já existem, não assinalamos as caixas de seleção,
- no [7,8], foram criados dois projetos. O [8] é o projeto empresarial que vamos utilizar. O [7] é um projeto cuja função desconheço. Não tive de o utilizar e, como não aprofundei os meus conhecimentos sobre o Maven, não sei para que pode servir. Por isso, vamos ignorá-lo.
Agora que o projeto da empresa está criado, podemos definir os seus módulos.
![]() |
- no [1], criamos uma nova dependência,
- em [2], selecionamos o projeto EJB [DAO],
- em [3], declara-se que se trata de um EJB. Não se deve deixar o tipo em branco, pois, nesse caso, será utilizado o tipo jar e, neste contexto, esse tipo não é adequado,
- no [4], utiliza-se o âmbito [compile],
- no [5], o projeto com a sua nova dependência,
![]() |
- no [6, 7, 8], recomeçamos para adicionar o EJB da camada [métier],
- em [9], as duas dependências,
- em [10], compilamos o projeto,
![]() |
- em [11], executa-se o projeto,
- em [12], no separador [Services], verifica-se que o projeto foi implementado no servidor Glassfish. Isto significa que os dois EJB estão agora presentes no servidor.
Nos registos do servidor Glassfish, encontram-se informações sobre a implementação dos dois EJB:
![]() |
- e [1], no separador de registos do Glassfish.
Encontram-se aí os seguintes registos:
- linhas 1-5: as entidades JPA foram reconhecidas,
- linha 7: indica que a criação da unidade de persistência [dbrdvmedecins2-PU] foi bem-sucedida e que a ligação à base de dados associada foi estabelecida,
- linha 8: os nomes portáveis das interfaces remota e local do EJB, [DaoJpa] e portable significam que foram reconhecidos por todos os servidores de aplicações,
- linha 9: o mesmo, mas com nomes próprios do GlassFish,
- linhas 10-11: o mesmo para o EJB e o [Metier].
Vamos reter o nome portátil da interface remota do EJB [Metier]:
java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote
Vamos precisar dele durante os testes da camada [métier].
3.5.3. Teste da camada [métier]
Tal como fizemos com a camada [DAO], vamos testar a camada [métier] no âmbito de uma aplicação cliente/servidor:
![]() |
O cliente irá testar a interface remota da camada EJB [Metier] implementada no servidor Glassfish.
Começamos por criar um novo projeto Maven. Para tal, seguimos o procedimento utilizado para criar o projeto de teste da camada [dao] (ver parágrafo 3.4.7), excluindo a criação do teste JUnit. O projeto assim criado é o seguinte
![]() |
- em [1], o projeto criado com as suas dependências: em relação ao EJB da camada [dao], em relação ao EJB da camada [métier], da biblioteca [gf-client].
Nesta altura, o ficheiro [pom.xml] do projeto é o seguinte:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>mv-client-rdvmedecins-ejb-metier</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-client-rdvmedecins-ejb-metier</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.glassfish.appclient</groupId>
<artifactId>gf-client</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-metier</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
Certifique-se de que possui as dependências descritas nas linhas 17 a 33. O teste será uma simples classe de consola:
![]() |
O código da classe [ClientRdvMedecinsMetier] é o seguinte:
package istia.st.client;
import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.service.IMetierRemote;
public class ClientRdvMedecinsMetier {
// o nome da interface remota do EJB [Metier]
private static String IDaoRemoteName = "java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
// data de hoje
private static Date jour = new Date();
public static void main(String[] args) {
try {
// contexto JNDI do servidor Glassfish
InitialContext initialContext = new InitialContext();
// referência na camada remota [metier]
IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
// visualização de clientes
List<Client> clients = metier.getAllClients();
display("Liste des clients :", clients);
// visualização de médicos
List<Medecin> medecins = metier.getAllMedecins();
display("Liste des médecins :", medecins);
// visualização dos horários de um médico
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = metier.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// lista de consultas de um médico, num determinado dia
display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// visualização da agenda
AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// adicionar um RV
Rv rv = null;
Creneau creneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
rv = metier.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// visualização da agenda
agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// eliminar um RV
System.out.println("Suppression du Rv ajouté");
metier.supprimerRv(rv);
System.out.println("Rv supprimé");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// visualização da agenda
agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
} catch (Throwable ex) {
System.out.println("Erreur...");
while (ex != null) {
System.out.println(String.format("%s : %s", ex.getClass().getName(), ex.getMessage()));
ex = ex.getCause();
}
}
}
// método utilitário - apresenta os elementos de uma coleção
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- linha 18: o nome portátil da interface remota do EJB [Metier] foi obtido nos registos do GlassFish,
- linhas 24-27: obtém-se uma referência à interface remota do EJB [Metier],
- linhas 29-30: apresentam os clientes,
- linhas 32-33: apresentam os médicos,
- linhas 35-37: apresentam os horários disponíveis de um médico,
- linha 39: exibe as consultas de um médico num determinado dia,
- linhas 41-42: a agenda desse mesmo médico para o mesmo dia,
- linhas 44-49: adiciona-se uma consulta,
- linha 50: exibem-se as consultas do médico. Deve haver mais uma,
- linhas 52-53: exibe-se a agenda do médico. Deve ser visível a consulta adicionada,
- linhas 55-57: elimina-se a consulta que acabou de ser adicionada,
- linha 58: isto deve refletir-se na lista de consultas do médico,
- linhas 60-61: e na sua agenda.
Executamos o teste:
![]() | ![]() |
As exibições no ecrã obtidas são as seguintes:
Liste des clients :
Client[1,Mr,Jules,MARTIN]
Client[2,Mme,Christine,GERMAN]
Client[3,Mr,Jules,JACQUARD]
Client[4,Melle,Brigitte,BISTROU]
Liste des médecins :
Médecin[1,Mme,Marie,PELISSIER]
Médecin[2,Mr,Jacques,BROMARD]
Médecin[3,Mr,Philippe,JANDOT]
Médecin[4,Melle,Justine,JACQUEMOT]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER]
Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Ajout d'un Rv le [Wed May 23 16:25:26 CEST 2012] dans le créneau Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] pour le client Client[1,Mr,Jules,MARTIN]
Rv ajouté
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Suppression du Rv ajouté
Rv supprimé
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
- linha 37: a agenda da Sra. PELISSIER, a 23 de maio de 2012. Não há nenhum horário reservado,
- linha 39: adição de um compromisso,
- linha 42: a nova agenda da Sra. PELISSIER. Está agora reservado um horário para o Sr. MARTIN,
- linha 44: o compromisso foi eliminado,
- linha 46: a agenda da Sra. PELISSIER mostra que não há nenhum horário reservado.
Consideramos agora que as camadas [DAO] e [métier] estão operacionais. Resta-nos escrever a camada [web] com o framework JSF. Para tal, vamos utilizar os conhecimentos adquiridos no início deste documento.
3.6. A camada [web]
Voltemos à arquitetura que estamos a construir:
![]() |
Vamos construir a última camada, a camada [web].
3.6.1. O projeto NetBeans
Estamos a criar um projeto Maven:
![]() |
- em [1], criamos um novo projeto,
- em [2, 3], um projeto Maven do tipo [Web Application],
- em [4], atribuímos-lhe um nome,
![]() |
- em [5], escolhe-se o servidor Glassfish e o Java EE 6 Web,
- em [6], o projeto assim criado,
- em [7], o projeto após terem sido eliminadas a página [index.jsp] e o pacote presente em [Source Packages],
![]() |
- em [8, 9], nas propriedades do projeto, adiciona-se um framework,
- em [10], seleciona-se Java Server Faces,
![]() |
- em [11], a configuração do Java Server Faces. Mantêm-se os valores predefinidos. Note-se que é utilizado o JSF 2,
- em [12], o projeto é então alterado em dois pontos: é gerado um ficheiro [web.xml], bem como uma página [index.html].
O ficheiro [web.xml] é o seguinte:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
</web-app>
Já nos deparámos com este ficheiro.
- linhas 7-11: definem o servlet que irá processar todos os pedidos feitos à aplicação. Trata-se do servlet de JSF,
- linhas 12-15: definem os URL processados por esta servlet. Trata-se dos URL com o formato /faces/*,
- linhas 21-23: definem a página [index.xhtml] como página inicial.
Esta página é a seguinte:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
Hello from Facelets
</h:body>
</html>
Já a vimos anteriormente. Podemos executar este projeto:
![]() |
- em [1], executamos o projeto e obtemos o resultado [2] no navegador.
Apresentamos agora o projeto completo para, em seguida, detalhar os seus diferentes elementos.
![]() |
- em [1], as páginas XHTML do projeto,
- em [2], os códigos Java,
- em [3], os ficheiros de mensagens, uma vez que a aplicação está internacionalizada,
![]() |
- em [4], as dependências do projeto.
3.6.2. As dependências do projeto
Voltemos à arquitetura do projeto:
![]() |
A camada JSF assenta nas camadas [métier], [DAO] e [JPA]. Estas três camadas estão encapsuladas nos dois projetos Maven que criámos, o que explica as dependências do projeto [4]. Vamos simplesmente mostrar como estas dependências são adicionadas:
![]() |
- no [1], colocaremos «ejb» para indicar que a dependência é de um projeto EJB,
- no [2], indicaremos [provided]. Com efeito, o projeto web será implementado em simultâneo com os dois projetos EJB. Por isso, não é necessário incluir os ficheiros JAR do EJB.
3.6.3. A configuração do projeto
A configuração do projeto é a mesma dos projetos JSF que analisámos no início deste documento. Enumeramos os ficheiros de configuração sem os voltar a explicar.
![]() | ![]() |
[web.xml]: configura a aplicação web.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Production</param-value>
</context-param>
<context-param>
<param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
<error-page>
<error-code>500</error-code>
<location>/faces/exception.xhtml</location>
</error-page>
<error-page>
<exception-type>Exception</exception-type>
<location>/faces/exception.xhtml</location>
</error-page>
</web-app>
Note-se, na linha 26, que a página [index.xhtml] é a página inicial da aplicação.
[faces-config.xml]: configura a aplicação JSF
<?xml version='1.0' encoding='UTF-8'?>
<!-- =========== FULL CONFIGURATION FILE ================================== -->
<faces-config version="2.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
<application>
<resource-bundle>
<base-name>
messages
</base-name>
<var>msg</var>
</resource-bundle>
<message-bundle>messages</message-bundle>
</application>
</faces-config>
[beans.xml]: vazio, mas necessário para a anotação @Named
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>
[styles.css]: a folha de estilo da aplicação
.reservationsHeaders {
text-align: center;
font-style: italic;
color: Snow;
background: Teal;
}
.creneau {
height: 25px;
text-align: center;
background: MediumTurquoise;
}
.client {
text-align: left;
background: PowderBlue;
}
.action {
width: 6em;
text-align: left;
color: Black;
background: MediumTurquoise;
}
.erreursHeaders {
background: Teal;
background-color: #ff6633;
color: Snow;
font-style: italic;
text-align: center
}
.erreurClasse {
background: MediumTurquoise;
background-color: #ffcc66;
height: 25px;
text-align: center
}
.erreurMessage {
background: PowderBlue;
background-color: #ffcc99;
text-align: left
}
[messages_fr.properties]: o ficheiro de mensagens em francês
# layout
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers
layout.entete.langue1=Fran\u00e7ais
layout.entete.langue2=Anglais
# exceção
exception.header=L'exception suivante s'est produite
exception.httpCode=Code HTTP de l'erreur
exception.message=Message de l'exception
exception.requestUri=Url demand\u00e9e lors de l'erreur
exception.servletName=Nom de la servlet demand\u00e9e lorsque l'erreur s'est produite
# formulário 1
form1.titre=R\u00e9servations
form1.medecin=M\u00e9decin
form1.jour=Jour (jj/mm/aaaa)
form1.button.agenda=Agenda
form1.jour.required=date requise
form1.jour.erreur=date erron\u00e9e
# formulário 2
form2.titre=Agenda de {0} {1} {2} le {3}
form2.titre_detail=Agenda de {0} {1} {2} le {3}
form2.creneauHoraire=Cr\u00e9neau horaire
form2.client=Client
form2.accueil=Accueil
form2.supprimer=Supprimer
form2.reserver=R\u00e9server
# formulário 3
form3.titre=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.titre_detail=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.client=Client
form3.valider=Valider
form3.annuler=Annuler
# erro
erreur.titre=Une erreur s'est produite.
erreur.message=Message d'erreur
erreur.accueil=Page d'accueil
erreur.classe=Cause
[messages_en.properties]: o ficheiro de mensagens em inglês
# layout
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university
layout.entete.langue1=French
layout.entete.langue2=English
# exceção
exception.header=The following exceptions occurred
exception.httpCode=Error HTTP code
exception.message=Exception message
exception.requestUri=Url targeted when error occurred
exception.servletName=Servlet targeted's name when error occurred
# formulário 1
form1.titre=Reservations
form1.medecin=Doctor
form1.jour=Date (dd/mm/yyyy)
form1.button.agenda=Diary
form1.jour.required=The date is required
form1.jour.erreur=The date is invalid
# formulário 2
form2.titre={0} {1} {2}'' diary on {3}
form2.titre_detail={0} {1} {2}'' diary on {3}
form2.creneauHoraire=Time Period
form2.client=Client
form2.accueil=Welcome Page
form2.supprimer=Delete
form2.reserver=Reserve
# formulário 3
form3.titre=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.titre_detail=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,número,#00} - {6,número,#00}:{7,número,#00}
form3.client=Client
form3.valider=Submit
form3.annuler=Cancel
# erro
erreur.titre=An error occurred
erreur.message=Error message
erreur.accueil=Welcome Page
erreur.classe=Cause
3.6.4. As vistas do projeto
Recorde-se o funcionamento da aplicação. A página inicial é a seguinte:
![]() |
A partir desta primeira página, o utilizador (Secretariado, Médico) irá realizar uma série de ações. Apresentamo-las abaixo. A vista à esquerda mostra a página a partir da qual o utilizador efetua um pedido; a vista à direita, a resposta enviada pelo servidor.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Por fim, também é possível obter uma página de erros:
![]() |
Estas diferentes visualizações são obtidas através das seguintes páginas do projeto web:
![]() |
- em [1], as páginas [basdepage, entete, layout] garantem a formatação de todas as visualizações,
- em [2], a vista produzida por [layout.xhtml].
A tecnologia utilizada aqui é a dos facelets. Esta foi descrita no parágrafo 2.11. Limitamo-nos a apresentar o código das páginas XHTML utilizadas para a formatação:
[entete.xhtml]
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['layout.entete']}"/></h2>
<div align="left">
<h:commandLink value="#{msg['layout.entete.langue1']}" actionListener="#{changeLocale.setFrenchLocale}"/>
<h:outputText value=" "/>
<h:commandLink value="#{msg['layout.entete.langue2']}" actionListener="#{changeLocale.setEnglishLocale}"/>
</div>
</body>
</html>
Note-se, nas linhas 10 a 12, os dois links para alterar o idioma da aplicação.
[basdepage.xhtml]
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<body>
<h:outputText value="#{msg['layout.basdepage']}"/>
</body>
</html>
[layout.xhtml]
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{changeLocale.locale}">
<h:head>
<title>RdvMedecins</title>
<h:outputStylesheet library="css" name="styles.css"/>
</h:head>
<h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
<h:form id="formulaire">
<table style="width: 1200px">
<tr>
<td colspan="2" bgcolor="#ccccff">
<ui:include src="entete.xhtml"/>
</td>
</tr>
<tr>
<td style="width: 100px; height: 200px" bgcolor="#ffcccc">
</td>
<td>
<ui:insert name="contenu" >
<h2>Contenu</h2>
</ui:insert>
</td>
</tr>
<tr bgcolor="#ffcc66">
<td colspan="2">
<ui:include src="basdepage.xhtml"/>
</td>
</tr>
</table>
</h:form>
</h:body>
</f:view>
</html>
Esta página é o modelo (template) da página [index.xhtml]:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="contenu">
<h:panelGroup rendered="#{form.form1Rendered}">
<ui:include src="form1.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.form2Rendered}">
<ui:include src="form2.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.form3Rendered}">
<ui:include src="form3.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.erreurRendered}">
<ui:include src="erreur.xhtml"/>
</h:panelGroup>
</ui:define>
</ui:composition>
</html>
As linhas 8 a 21 definem a área denominada «conteúdo» (linha 8) em [layout.xhtml] (linha 7). Trata-se da área central das visualizações:
![]() |
A página [index.xhtml] é a única página da aplicação. Não haverá, portanto, qualquer navegação entre páginas. Ela apresenta uma das quatro páginas [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. Esta apresentação é controlada por quatro valores booleanos [form1Rendered, form2Rendered, form3Rendered, erreurRendered] do bean de formulário que iremos descrever em breve.
3.6.5. Os beans do projeto
![]() |
As classes do pacote [utils] já foram apresentadas:
- a classe [ChangeLocale] é a classe responsável pela mudança de idioma. Já foi analisada (parágrafo 2.4.4).
- a classe [Messages] é uma classe que facilita a internacionalização das mensagens de uma aplicação. Foi analisada no parágrafo 2.8.5.7.
3.6.5.1. O bean Application
O bean [Application] é o seguinte:
package beans;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.metier.service.IMetierLocal;
@Named(value = "application")
@ApplicationScoped
public class Application implements Serializable{
// camada de negócio
@EJB
private IMetierLocal metier;
// cache
private List<Medecin> medecins;
private List<Client> clients;
private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
private Map<Long, Client> hClients = new HashMap<Long, Client>();
// erros
private List<Erreur> erreurs = new ArrayList<Erreur>();
private Boolean erreur = false;
public Application() {
}
@PostConstruct
public void init() {
// os médicos e os clientes são armazenados na cache
try {
medecins = metier.getAllMedecins();
clients = metier.getAllClients();
} catch (Throwable th) {
// regista-se o erro
erreur = true;
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
while (th.getCause() != null) {
th = th.getCause();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
}
return;
}
// verificação das listas
if (medecins.size() == 0) {
// regista-se o erro
erreur = true;
erreurs.add(new Erreur("", "La liste des médecins est vide"));
}
if (clients.size() == 0) {
// regista-se o erro
erreur = true;
erreurs.add(new Erreur("", "La liste des clients est vide"));
}
// erro?
if (erreur) {
return;
}
// os dicionários
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
}
}
// getters e setters
...
}
- linhas 15-16: a classe [Application] é um bean com âmbito de aplicação. É criada uma única vez no início do ciclo de vida da aplicação JSF e está acessível a todos os pedidos de todos os utilizadores. Nela colocam-se, geralmente, dados de leitura única. Neste caso, iremos colocar nela a lista de médicos e a lista de clientes. Partimos, portanto, do princípio de que estas listas não se alteram com frequência. As páginas XHTML acedem a ela através do nome «application»,
- linhas 20-21: uma referência à interface local do EJB [Metier] será injetada pelo contentor EJB do Glassfish. Recordemos a arquitetura da aplicação:
![]() |
A aplicação JSF e as aplicações EJB e [Metier] serão executadas na mesma JVM (Java Virtual Machine). Assim, a camada [JSF] irá utilizar a interface local da EJB. Neste caso, o bean da aplicação utiliza a EJB e a [Metier]. Mesmo que não fosse esse o caso, seria normal encontrar aí uma referência à camada [métier]. Trata-se, de facto, de uma informação que pode ser partilhada por todos os pedidos de todos os utilizadores, sendo, portanto, um dado com âmbito Application.
- linhas 34-35: o método init é executado imediatamente após a instanciação da classe [Application] (presença da anotação @PostConstruct),
- Nas linhas 36 a 73, o método cria os seguintes elementos: a lista de médicos na linha 23, a lista de clientes na linha 24, um dicionário de médicos indexado pelo seu ID na linha 25 e o mesmo para os clientes na linha 26. Podem ocorrer erros. Estes são registados na lista da linha 28.
A classe [Erreur] é a seguinte:
package beans;
public class Erreur {
public Erreur() {
}
// campo
private String classe;
private String message;
// construtor
public Erreur(String classe, String message){
this.setClasse(classe);
this.message=message;
}
// getters e setters
...
}
- linha 9, o nome de uma classe de exceção, caso tenha sido lançada uma exceção,
- linha 10: uma mensagem de erro.
3.6.5.2. O bean [Form]
O seu código é o seguinte:
package beans;
...
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// bean de aplicação
@Inject
private Application application;
// modelo
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private String form3Titre;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneau;
private Medecin medecin;
private Client client;
private Long idClient;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
@PostConstruct
private void init() {
// A inicialização decorreu corretamente?
if (application.getErreur()) {
// recuperar a lista de erros
erreurs = application.getErreurs();
// a vista de erros é apresentada
setForms(false, false, false, true);
}
}
// exibição da vista
private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
this.form1Rendered = form1Rendered;
this.form2Rendered = form2Rendered;
this.form3Rendered = form3Rendered;
this.erreurRendered = erreurRendered;
}
.................................................
}
- linhas 5-7: a classe [Form] é um bean com o nome «form» e com âmbito de sessão. Recorde-se que, nesse caso, a classe deve ser serializável.
- linhas 13-14: o bean «form» tem uma referência ao bean «application». Esta referência será injetada pelo contentor de servlets no qual a aplicação é executada (presença da anotação @Inject).
- linhas 17-31: o modelo das páginas [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. A exibição destas páginas é controlada pelos valores booleanos das linhas 19-22. Note-se que, por predefinição, é a página [form1.xhtml] que é apresentada,
- linhas 33-34: o método init é executado logo após a instanciação da classe (presença da anotação @PostConstruct),
- linhas 35-41: o método init é utilizado para determinar qual a página que deve ser apresentada em primeiro lugar: normalmente a página [form1.xhtml] (linha 19), a menos que a inicialização da aplicação tenha falhado (linha 36), caso em que será apresentada a página [erreur.xhtml] (linha 40).
A página [erreur.xhtml] é a seguinte:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['erreur.titre']}"/></h2>
<p>
<h:commandButton value="#{msg['erreur.accueil']}" actionListener="#{form.accueil()}"/>
</p>
<hr/>
<h:dataTable value="#{form.erreurs}" var="erreur" headerClass="erreursHeaders" columnClasses="erreurClasse,erreurMessage">
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.classe']}"/>
</f:facet>
<h:outputText value="#{erreur.classe}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.message']}"/>
</f:facet>
<h:outputText value="#{erreur.message}"/>
</h:column>
</h:dataTable>
</body>
</html>
Utiliza uma baliza <h:dataTable> (linhas 14-27) para apresentar a lista de erros. Isto resulta numa página semelhante à seguinte:

Vamos agora definir as diferentes fases do ciclo de vida da aplicação.
3.6.6. Interações entre páginas e modelo
3.6.6.1. A exibição da página inicial
Se tudo correr bem, a primeira página apresentada é a [form1.xhtml]. O resultado é a seguinte visualização:
![]() |
A página [form1.xhtml] é a seguinte:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['form1.titre']}"/></h2>
<h:panelGrid columns="3">
<h:panelGroup>
<div align="center"><h3><h:outputText value="#{msg['form1.medecin']}"/></h3></div>
</h:panelGroup>
<h:panelGroup>
<div align="center"><h3><h:outputText value="#{msg['form1.jour']}"/></h3></div>
</h:panelGroup>
<h:panelGroup/>
<h:selectOneMenu value="#{form.idMedecin}">
<f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>
</h:selectOneMenu>
<h:inputText id="jour" value="#{form.jour}" required="true" requiredMessage="#{msg['form1.jour.required']}" converterMessage="#{msg['form1.jour.erreur']}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:inputText>
<h:message for="jour" styleClass="error"/>
</h:panelGrid>
<h:commandButton value="#{msg['form1.button.agenda']}" actionListener="#{form.getAgenda}"/>
</body>
</html>
Esta página é alimentada pelo seguinte modelo:
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
// Bean Application
@Inject
private Application application;
// modelo
private Long idMedecin;
private Date jour = new Date();
// lista de médicos
public List<Medecin> getMedecins() {
return application.getMedecins();
}
// agenda
public void getAgenda() {
...
}
- O campo da linha 9 alimenta, em leitura e escrita, o valor da lista da linha 18 da página. Na exibição inicial da página, esta define o valor selecionado na lista suspensa. Na exibição inicial, idMedecin é igual a null, pelo que será selecionado o primeiro médico;
- o método das linhas 13-15 gera os elementos da lista suspensa de médicos (linha 19 da página). Cada opção gerada terá como rótulo (itemLabel) o título, apelido e nome próprio do médico e, como valor (itemValue), o ID do médico,
- o campo da linha 10 alimenta, em leitura/gravação, o campo de introdução de dados da linha 21 da página. Na exibição inicial, é, portanto, a data de hoje que é apresentada,
- linhas 17-19: o método getAgenda gere o clique no botão [Agenda] da linha 26 da página. Como não há navegação (é sempre a página [index.html] que é solicitada), utilizar-se-á frequentemente o atributo actionListener em vez do atributo action. Neste caso, o método chamado no modelo não devolve qualquer resultado.
Quando se clica no botão [Agenda],
- são enviados valores: o valor selecionado na lista suspensa de médicos é registado no campo idMedecin do modelo e o dia escolhido no campo «dia»,
- é chamado o método getAgenda do modelo.
O método getAgenda é o seguinte:
// Bean de aplicação
@Inject
private Application application;
// modelo
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
private Medecin medecin;
private List<Erreur> erreurs;
// agenda
public void getAgenda() {
try {
// procurar o médico
medecin = application.gethMedecins().get(idMedecin);
// título do formulário 2
form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
// Agenda do médico para um determinado dia
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// exibir o formulário 2
setForms(false, true, false, false);
} catch (Throwable th) {
// visualização dos erros
prepareVueErreur(th);
}
}
// preparação vueErreur
private void prepareVueErreur(Throwable th) {
// cria-se a lista de erros
erreurs = new ArrayList<Erreur>();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
while (th.getCause() != null) {
th = th.getCause();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
}
// a visualização dos erros é apresentada
setForms(false, false, false, true);
}
Recorde-se o que o método getAgenda deve apresentar:
![]() |
- linha 21: recupera-se o médico selecionado no dicionário de médicos, que foi armazenado no bean application. Para tal, utiliza-se o seu ID, que foi enviado para idMedecin,
- linha 23: prepara-se o título da página [form2.xhtml] que vai ser apresentada. Esta mensagem é retirada do ficheiro de mensagens para que possa ser internacionalizada. Esta técnica foi descrita no parágrafo 2.8.5.7, página 135.
- linha 25: recorre-se à camada [métier] para calcular a agenda do médico selecionado para o dia selecionado,
- linha 27: é exibido o [form2.xhtml],
- linha 28: se ocorrer uma exceção, é então criada uma lista de erros (linhas 37-42) e a página [erreur.xhtml] é apresentada (linha 44).
3.6.6.2. Exibir a agenda de um médico
A página [form2.xhtml] corresponde à seguinte visualização:
![]() |
O código da página [form2.xhtml] é o seguinte:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jsp/jstl/core">
<body>
<h2><h:outputText value="#{form.form2Titre}"/></h2>
<h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
<h:dataTable value="#{form.agendaMedecinJour.creneauxMedecinJour}" var="creneauMedecinJour" headerClass="reservationsHeaders" columnClasses="creneau,client,action">
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['form2.creneauHoraire']}"/>
</f:facet>
<h:outputText value="#{creneauMedecinJour.creneau.hdebut}:#{creneauMedecinJour.creneau.mdebut} - #{creneauMedecinJour.creneau.hfin}:#{creneauMedecinJour.creneau.mfin}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
<c:if test="#{creneauMedecinJour.rv==null}">
<h:outputText value=""/>
<c:otherwise>
<h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}"/>
</c:otherwise>
</c:if>
</h:column>
<h:column>
<f:facet name="header"/>
<h:commandLink action="#{form.action()}" value="#{creneauMedecinJour.rv==null ? msg['form2.reserver'] : msg['form2.supprimer']}">
<f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneau}"/>
</h:commandLink>
</h:column>
</h:dataTable>
</body>
</html>
Recorde-se que o método getAgenda inicializou dois campos no modelo:
// modelo
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
Estes dois campos alimentam a página [form2.xhtml]:
- linha 10, o título da página,
- linha 12: a agenda do médico é apresentada por uma baliza <h:dataTable> com três colunas,
- linhas 13-18: a primeira coluna apresenta os horários disponíveis,
- linhas 19-30: a segunda coluna apresenta o nome do cliente que, eventualmente, reservou o horário ou nada, caso contrário. Para efetuar esta escolha, utilizam-se as balizas da biblioteca JSTL Core referenciada na linha 7,
- linhas 30-35: a terceira coluna apresenta o link [Réserver] se o intervalo de tempo estiver livre, e o link [Supprimer] se o intervalo de tempo estiver ocupado.
Os links da terceira coluna estão associados ao seguinte modelo:
// modelo
private Long idCreneau;
// ação em RV
public void action() {
...
}
- o método action é chamado quando o utilizador clica no link Reservar / Eliminar (linha 32). Note-se que aqui foi utilizado o atributo action. O método a que este atributo aponta deve ter a assinatura String action(), uma vez que o método tem de devolver uma chave de navegação. No entanto, neste caso, a assinatura é `void action()`. Isto não provocou qualquer erro e pode-se supor que, neste caso, não há navegação. Era isso que se pretendia. Substituir `actionListener` por `action` provocava um mau funcionamento,
- o campo idCreneau da linha 2 irá recuperar o ID do intervalo horário do link em que se clicou (linha 33 da página).
3.6.6.3. Eliminação de um compromisso
Vamos analisar o código que gere a eliminação de um compromisso. Isso corresponde à seguinte sequência de vistas:
![]() |
O código relacionado com esta operação é o seguinte:
// bean Application
@Inject
private Application application;
// modelo
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneau;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
// ação sobre RV
public void action() {
// procura-se um horário disponível na agenda
int i = 0;
Boolean trouvé = false;
while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
trouvé = true;
} else {
i++;
}
}
// Encontrou-se alguma?
if (!trouvé) {
// É estranho — volta a apresentar o form2
setForms(false, true, false, false);
return;
}
// Encontrámos
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// de acordo com a ação pretendida
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// reserva
public void reserver() {
...
}
public void supprimer() {
try {
// eliminação de um compromisso
application.getMetier().supprimerRv(creneauChoisi.getRv());
// atualiza-se a agenda
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// exibe o form2
setForms(false, true, false, false);
} catch (Throwable th) {
// visualização de erros
prepareVueErreur(th);
}
}
- linha 16: quando o método action é iniciado, o ID do intervalo horário selecionado foi gravado em idCreneau (linha 11),
- linhas 18-26: procura-se recuperar o intervalo horário a partir do seu id (linha 21). Procura-se no calendário atual, agendaMedecinJour da linha 10. Normalmente, deve ser encontrado. Se não for o caso, não se faz nada (linhas 28-32),
- linha 34: se tivermos encontrado o intervalo de tempo procurado, recuperamos uma referência que armazenamos na linha 12,
- linha 36: verifica-se se o intervalo de tempo selecionado tinha um compromisso. Se sim, este é eliminado (linha 39); caso contrário, reserva-se um (linha 37),
- linha 51: a consulta do intervalo escolhido é eliminada. É a camada [métier] que realiza esta tarefa,
- linha 53: solicita-se à camada [métier] a nova agenda do médico. É claro que aí se verá menos uma consulta. Mas, como a aplicação é multiutilizador, é possível ver as alterações feitas por outros utilizadores,
- linha 55: volta a ser apresentada a página [form2.xhtml],
- linha 58: uma vez que a camada [métier] foi solicitada, podem ocorrer exceções. Neste caso, a pilha de exceções é registada na lista de erros da linha 13 e exibida através da vista [erreur.xhtml].
3.6.6.4. Marcação de consultas
A marcação de consultas segue a seguinte sequência:
![]() |
O modelo envolvido nesta ação é o seguinte:
// modelo
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form3Titre;
private AgendaMedecinJour agendaMedecinJour;
private Medecin medecin;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
// ação no RV
public void action() {
...
// foi encontrado
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// de acordo com a ação pretendida
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// reserva
public void reserver() {
try {
// título do formulário 3
form3Titre = Messages.getMessage(null, "form3.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour),
creneauChoisi.getCreneau().getHdebut(), creneauChoisi.getCreneau().getMdebut(), creneauChoisi.getCreneau().getHfin(), creneauChoisi.getCreneau().getMfin()}).getSummary();
// cliente selecionado na lista suspensa
idClient=null;
// é apresentado o formulário 3
setForms(false, false, true, false);
} catch (Throwable th) {
// visualização de erros
prepareVueErreur(th);
}
}
- linha 14: se o intervalo de tempo escolhido não tiver qualquer compromisso, trata-se de uma reserva,
- linha 30: prepara-se o título da página [form3.xhtml] com a mesma técnica utilizada para o título da página [form2.xhtml],
- linha 34: neste formulário, existe um menu suspenso cujo valor é fornecido por idClient. Define-se o valor deste campo para null para não selecionar ninguém,
- linha 36: exibe-se a página [form3.xhtml],
- linha 39: ou a página de erros, caso tenha ocorrido uma exceção.
A página [form3.xhtml] é a seguinte:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{form.form3Titre}"/></h2>
<h:panelGrid columns="2">
<h:outputText value="#{msg['form3.client']}"/>
<h:selectOneMenu value="#{form.idClient}">
<f:selectItems value="#{form.clients}" var="client" itemLabel="#{client.titre} #{client.prenom} #{client.nom}" itemValue="#{client.id}"/>
</h:selectOneMenu>
<h:panelGroup>
<h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
<h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
</h:panelGroup>
</h:panelGrid>
</body>
</html>
Esta página é alimentada pelo seguinte modelo:
// bean Application
@Inject
private Application application;
// modelo
private Long idClient;
// lista de clientes
public List<Client> getClients() {
return application.getClients();
}
- linha 6: o n.º do cliente preenche o atributo value da lista suspensa de clientes na linha 12 da página. Este atributo define o elemento selecionado na lista suspensa,
- linhas 9-11: o método getClients preenche o conteúdo do menu suspenso (linha 13). O texto (itemLabel) de cada opção é [Titre Prénom Nom] do cliente, e o valor associado (itemValue) é o ID do cliente. É, portanto, este valor que será enviado.
3.6.6.5. Validação de um compromisso
A validação de uma marcação corresponde à seguinte sequência:
![]() |
e corresponde ao clique no botão [Valider]:
<h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
É, portanto, o método [Form].validerRv que irá gerir este evento. O seu código é o seguinte:
// bean da aplicação
@Inject
private Application application;
// modelo
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private Long idCreneau;
private Long idClient;
private List<Erreur> erreurs;
// validação de compromisso
public void validerRv() {
try {
// recupera-se uma instância do intervalo horário selecionado
Creneau creneau = application.getMetier().getCreneauById(idCreneau);
// adiciona-se o compromisso
application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
// atualiza-se a agenda
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// exibe-se o form2
setForms(false, true, false, false);
} catch (Throwable th) {
// visualização de erros
prepareVueErreur(th);
}
}
- linha 12: antes de o método validerRv ser executado, o campo idClient recebeu o ID do cliente selecionado pelo utilizador,
- linha 19: a partir do ID do intervalo horário memorizado numa etapa anterior (o bean tem alcance de sessão), solicita-se à camada [métier] uma referência ao próprio intervalo horário,
- linha 21: solicita-se à camada [métier] que adicione uma consulta para o dia escolhido (dia), o intervalo horário escolhido (intervalo) e o cliente escolhido (idClient),
- linha 23: solicita-se à camada [métier] que atualize a agenda do médico. Ver-se-á a consulta adicionada, bem como todas as alterações que outros utilizadores da aplicação possam ter feito,
- linha 25: volta a apresentar-se a agenda [form2.xhtml],
- linha 28: exibe-se a página de erro caso ocorra algum erro.
3.6.6.6. Cancelamento de uma marcação
Isto corresponde à seguinte sequência:
![]() |
O botão [Annuler] na página [form3.xhtml] é o seguinte:
<h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
O método [Form].annulerRv é, portanto, chamado:
// cancelamento da marcação
public void annulerRv() {
// exibe o formulário 2
setForms(false, true, false, false);
}
3.6.6.7. Voltar à página inicial
Resta ainda uma ação a analisar, a da sequência seguinte:
![]() |
O código do botão [Accueil] na página [form2.xhtml] é o seguinte:
<h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
O método [Form].accueil é o seguinte:
public void accueil() {
// exibe a página inicial
setForms(true, false, false, false);
}
3.7. Conclusion
Criámos a seguinte aplicação:
![]() |
Concentrámo-nos mais nas funcionalidades da aplicação do que na sua apresentação ao utilizador. Esta será melhorada com a utilização da biblioteca de componentes PrimeFaces. Criámos uma aplicação básica, mas que, mesmo assim, é representativa de uma arquitetura Java EE em camadas, utilizando EJB. A aplicação pode ser melhorada de várias formas:
- é necessária uma autenticação. Nem todos têm autorização para adicionar/eliminar compromissos,
- deveria ser possível percorrer a agenda para a frente e para trás ao procurar um dia com horários disponíveis,
- deveria ser possível solicitar a lista dos dias em que existem horários livres para um médico. Com efeito, se este for oftalmologista, as suas consultas são geralmente marcadas com seis meses de antecedência,
- ...
3.8. Testes com o Eclipse
3.8.1. A camada [DAO]
![]() |
- em [1], importa-se o projeto EJB da camada [DAO] e o seu cliente,
- em [2], seleciona-se o projeto EJB da camada [DAO] e executa-se [3],
- em [4], executa-se num servidor,
![]() |
- em [5], apenas o servidor Glassfish é proposto, pois é o único que possui um contentor EJB,
- no [6], o módulo EJB foi implementado,
![]() |
- no [7], são apresentados os registos:
São os mesmos que tínhamos com o NetBeans.
![]() |
- em [7A] [7B] executa-se o teste JUnit do cliente,
![]() |
- em [8], o teste é bem-sucedido,
- em [9], os registos da consola.
![]() |
Em [10], descarrega-se a aplicação EJB.
3.8.2. A fralda [métier]
![]() |
- em [1], importam-se os quatro projetos Maven da camada [métier],
- em [2], seleciona-se o projeto da empresa e executa-se-o em [3], num servidor Glassfish [4] [5],
![]() |
- em [6], o projeto empresarial foi implementado no Glassfish,
![]() |
- em [7], analisamos os registos do Glassfish,
Na linha 3, anotamos o nome portátil do EJB [Metier] e colamo-lo na consola do cliente deste EJB:
public class ClientRdvMedecinsMetier {
// o nome da interface remota do EJB [Metier]
private static String IDaoRemoteName = "java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
// data de hoje
private static Date jour = new Date();
![]() |
- no [8], executamos o cliente de consola,
- no [9], os seus registos.
![]() |
- em [10], descarrega-se a aplicação empresarial;
3.8.3. A camada [web]
![]() |
- em [1], importam-se os três projetos Maven da camada [web]. O projeto com a extensão «ear» é o projeto empresarial que deve ser implementado no Glassfish,
- em [2], executa-se o projeto,
![]() |
- no servidor Glassfish [3],
- em [4], a aplicação empresarial foi corretamente implementada,
![]() |
- em [5], solicitamos o URL da aplicação no navegador interno do Eclipse.


























































































































