Skip to content

3. Esempio di applicazione – 01: rdvmedecins-jsf2-ejb

Il testo seguente fa riferimento ai seguenti documenti:

L'applicazione di esempio da studiare proviene da [rif. 9].

3.1. L'applicazione

Un'azienda di servizi IT [ISTIA-AGI] intende offrire un servizio di prenotazione appuntamenti. Il mercato di riferimento principale è costituito dai medici che esercitano in proprio. Questi medici generalmente non dispongono di personale amministrativo. I clienti che desiderano fissare un appuntamento chiamano quindi direttamente il medico. Ciò spesso interrompe il lavoro del medico durante la giornata, riducendo la sua disponibilità nei confronti dei pazienti. L'azienda [ISTIA-AGI] desidera offrire loro un servizio di prenotazione appuntamenti basato sul seguente principio:

  • un addetto alla reception gestisce la prenotazione degli appuntamenti per un gran numero di medici. Questo addetto alla reception può essere una sola persona. Il suo stipendio viene ripartito tra tutti i medici che utilizzano il servizio di prenotazione.
  • L'ufficio amministrativo e tutti i medici sono collegati a Internet
  • Gli appuntamenti vengono registrati in un database centralizzato, accessibile via Internet dall'ufficio amministrativo e dai medici
  • Gli appuntamenti vengono normalmente fissati dall'ufficio amministrativo. Possono essere fissati anche dai medici stessi. Ciò avviene in particolare quando, al termine di una visita, il medico fissa un nuovo appuntamento per il paziente.

L'architettura del servizio di prenotazione degli appuntamenti è la seguente:

I medici diventano più efficienti se non devono più gestire gli appuntamenti. Se il loro numero è sufficiente, il loro contributo ai costi operativi dell’ufficio amministrativo sarà minimo.

L'azienda [ISTIA-AGI] ha deciso di sviluppare l'applicazione in due versioni:

  • una versione server JSF / EJB3 / JPA EclipseLink / Glassfish:
  • e una versione server JSF / Spring / JPA Hibernate / Tomcat:

3.2. Come funziona l'applicazione

Chiameremo l'applicazione [RdvMedecins]. Di seguito sono riportati alcuni screenshot che mostrano come funziona.

La pagina iniziale dell'applicazione è la seguente:

Da questa pagina iniziale, l'utente (addetto alla reception, medico) eseguirà una serie di azioni. Le presentiamo di seguito. La vista a sinistra mostra la pagina da cui l'utente effettua una richiesta; la vista a destra mostra la risposta inviata dal server.

Infine, potrebbe comparire anche una pagina di errore:

3.3. Il database

Torniamo all'architettura dell'applicazione da realizzare:

Il database, che chiameremo [ dbrdvmedecins2], è un database MySQL5 con quattro tabelle:

  

3.3.1. La tabella [MEDECINS]

Contiene informazioni sui medici gestiti dall'applicazione [RdvMedecins].

  • ID: il numero ID del medico — la chiave primaria della tabella
  • VERSION: un numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • LAST_NAME: il cognome del medico
  • FIRST_NAME: il nome del medico
  • TITLE: il titolo (Sig.ra, Sig.ra, Sig.)

3.3.2. La tabella [CLIENTI]

I clienti dei vari medici sono memorizzati nella tabella [CLIENTS]:

  • ID: il numero ID del cliente - la chiave primaria della tabella
  • VERSION: un numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • COGNOME: il cognome del cliente
  • NOME: il nome del cliente
  • TITOLO: il titolo (Sig.ra, Sig.ra, Sig.)

3.3.3. La tabella [SLOTS]

Elenca le fasce orarie in cui sono disponibili gli appuntamenti:

  • ID: numero identificativo della fascia oraria - chiave primaria della tabella (riga 8)
  • VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • DOCTOR_ID: numero ID che identifica il medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna DOCTORS(ID).
  • START_TIME: ora di inizio della fascia oraria
  • MSTART: minuto di inizio della fascia oraria
  • HFIN: ora di fine della fascia oraria
  • MFIN: minuti di fine della fascia oraria

La seconda riga della tabella [SLOTS] (vedi [1] sopra) indica, ad esempio, che la fascia oraria n. 2 inizia alle 8:20 e termina alle 8:40 e appartiene al medico n. 1 (dott.ssa Marie PELISSIER).

3.3.4. La tabella [RV]

Elenca gli appuntamenti fissati per ciascun medico:

  • ID: identificatore univoco dell'appuntamento – chiave primaria
  • GIORNO: giorno dell'appuntamento
  • SLOT_ID: fascia oraria dell'appuntamento – chiave esterna sul campo [ID] della tabella [SLOTS] – determina sia la fascia oraria che il medico coinvolto.
  • CLIENT_ID: ID del cliente per il quale è stata effettuata la prenotazione – chiave esterna sul campo [ID] della tabella [CLIENTS]

Questa tabella ha un vincolo di unicità sui valori delle colonne unite (GIORNO, SLOT_ID):

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

Se una riga della tabella [RV] presenta il valore (DAY1, SLOT_ID1) nelle colonne (DAY, SLOT_ID), tale valore non può comparire in nessun altro punto. In caso contrario, ciò significherebbe che sono stati prenotati due appuntamenti contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, quando ciò si verifica, il driver JDBC del database genera un'eccezione SQLException.

La riga con ID pari a 3 (vedi [1] sopra) indica che è stato prenotato un appuntamento per lo slot n. 20 e il cliente n. 4 il 23/08/2006. La tabella [SLOTS] ci dice che lo slot n. 20 corrisponde alla fascia oraria 16:20 – 16:40 e appartiene al medico n. 1 (Sig.ra Marie PELISSIER). La tabella [CLIENTS] ci dice che il cliente n. 4 è la sig.ra Brigitte BISTROU.

3.3.5. Generazione del database

Per creare le tabelle e popolarle, è possibile utilizzare lo script [dbrdvmedecins2.sql], disponibile sul sito web degli esempi. Utilizzando [WampServer] (vedere la sezione 1.3.3), procedere come segue:

  • In [1], clicca sull'icona [WampServer] e seleziona l'opzione [PhpMyAdmin] [2],
  • in [3], nella finestra che si apre, selezionare il link [Databases],
  • in [2], crea un database con il nome [4] e la codifica [5],
  • in [7], il database è stato creato. Clicca sul suo link,
  • in [8], importare un file SQL,
  • che si seleziona dal file system utilizzando il pulsante [9],
  • in [11], selezionare lo script SQL e in [12] eseguirlo,
  • in [13], le quattro tabelle nel database sono state create. Segui uno dei link,
  • in [14], il contenuto della tabella.

Non torneremo più su questo database. Tuttavia, invitiamo il lettore a seguirne l'evoluzione nel corso dei programmi, specialmente quando le cose non funzionano.

3.4. I livelli [DAO] e [JPA]

Torniamo all'architettura che dobbiamo costruire:

Realizzeremo quattro progetti Maven:

  • un progetto per i livelli [DAO] e [JPA],
  • un progetto per il livello [business],
  • un progetto per il livello [web],
  • un progetto enterprise che riunirà i tre progetti precedenti.

Ora creeremo il progetto Maven per i livelli [DAO] e [JPA].

Nota: per comprendere i livelli [business], [DAO] e [JPA] è necessaria una conoscenza di Java EE. A tal fine, è possibile fare riferimento a [rif7] (vedere il paragrafo 3).

3.4.1. Il progetto NetBeans

Ecco cosa fare:

  • In [1], creiamo un progetto Maven di tipo [Modulo EJB] [2],
  • in [3], diamo un nome al progetto,
  • in [4], selezioniamo il server GlassFish,
  • in [5], il progetto generato.

3.4.2. Generazione del livello [JPA]

Torniamo all'architettura che dobbiamo costruire:

Con NetBeans è possibile generare automaticamente il livello [JPA] e il livello [EJB] che controlla l'accesso alle entità JPA generate. È utile acquisire familiarità con questi metodi di generazione automatica, poiché il codice generato fornisce indicazioni preziose su come scrivere le entità JPA o il codice EJB che le utilizza.

Descriveremo ora alcuni di questi strumenti di generazione automatica. Per comprendere il codice generato, è necessaria una solida conoscenza delle entità JPA [rif. 8] e degli EJB [rif. 7] (vedere la Sezione 3).

3.4.2.1. Creazione di una connessione NetBeans al database

  • Avviare il DBMS MySQL 5 in modo che il database sia disponibile,
  • creare una connessione NetBeans al database [dbrdvmedecins2],
  • nella scheda [Servizi] [1], nella sezione [Database] [2], selezionare il driver JDBC di MySQL [3],
  • quindi selezionare l'opzione [4] "Connetti utilizzando" per creare una connessione a un database MySQL,
  • in [5], inserisci le informazioni richieste. In [6], il nome del database; in [7], l'utente e la password del database;
  • in [8], è possibile verificare le informazioni fornite,
  • in [9], il messaggio che dovrebbe apparire se le informazioni sono corrette,
  • in [10], la connessione è stabilita. È possibile visualizzare le quattro tabelle nel database collegato.

3.4.2.2. Creazione di un'unità di persistenza

Torniamo all'architettura che stiamo realizzando:

Attualmente stiamo realizzando il livello [JPA]. La sua configurazione avviene in un file [persistence.xml] in cui sono definite le unità di persistenza. Ciascuna di esse richiede le seguenti informazioni:

  • i dettagli di connessione JDBC per il database (URL, nome utente, password),
  • le classi che rappresenteranno le tabelle del database,
  • l'implementazione JPA utilizzata. Infatti, JPA è una specifica implementata da vari prodotti. In questo caso, useremo EclipseLink, che è l'implementazione predefinita utilizzata dal server GlassFish. Questo ci evita di dover aggiungere librerie di un'altra implementazione a GlassFish.

NetBeans può generare questo file di persistenza utilizzando una procedura guidata.

  • Fare clic con il tasto destro del mouse sul progetto e selezionare "Crea unità di persistenza" [1],
  • in [2], assegnare un nome all'unità di persistenza che si sta creando,
  • in [3], selezionare l'implementazione JPA di EclipseLink (JPA 2.0),
  • in [4], specificare che le transazioni del database saranno gestite dal contenitore EJB del server GlassFish,
  • in [5], specificare che le tabelle del database sono già state create e quindi non verranno create,
  • in [6], creare una nuova origine dati per il server GlassFish,
  • in [7], fornire un nome JNDI (Java Naming Directory Interface),
  • in [8], collegare questo nome alla connessione MySQL creata nel passaggio precedente,
  • in [9], completare la procedura guidata,
  • in [10], il nuovo progetto,
  • in [11], il file [persistence.xml] è stato generato nella cartella [META-INF],
  • in [12], è stata generata una cartella [setup],
  • in [13], sono state aggiunte nuove dipendenze al progetto Maven.

Il file [META-INF/persistence.xml] generato è il seguente:


<?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>

Include le informazioni fornite nella procedura guidata:

  • riga 3: il nome dell'unità di persistenza,
  • riga 3: il tipo di transazioni del database, in questo caso transazioni JTA (Java Transaction API) gestite dal contenitore EJB3 del server GlassFish,
  • riga 4: il nome JNDI dell'origine dati.

Normalmente, questo file specifica il tipo di implementazione JPA in uso. Nella procedura guidata abbiamo selezionato EclipseLink. Poiché si tratta dell'implementazione JPA predefinita utilizzata dal server GlassFish, non viene menzionata nel file [persistence.xml].

Nella scheda [Design], è possibile visualizzare una panoramica del file [persistence.xml]:

Per ottenere i log di EclipseLink, useremo il seguente file [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>
  • riga 4: specifica che viene utilizzata l'implementazione JPA di EclipseLink,
  • righe 7–9: contengono le proprietà di configurazione per il provider JPA, in questo caso EclipseLink,
  • riga 8: questa proprietà abilita la registrazione delle istruzioni SQL che EclipseLink eseguirà.

Il file [glassfish-resources.xml] creato è il seguente:


<?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>

Questo file contiene le informazioni che abbiamo inserito nei due procedure guidate utilizzate in precedenza:

  • righe 5–11: le proprietà JDBC del database MySQL5 [dbrdvmedecins2],
  • riga 13: il nome JNDI dell'origine dati.

Questo file verrà utilizzato per creare la fonte dati JNDI [jdbc/dbrdvmedecins2] per il server GlassFish. Si tratta di una configurazione specifica per questo server. Per un altro server sarebbe necessario un approccio diverso, che in genere prevede l'uso di uno strumento di amministrazione. Uno strumento di questo tipo è disponibile anche per GlassFish.

Infine, sono state aggiunte le dipendenze al progetto. Il file [pom.xml] è il seguente:


<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>
  • Righe 32–37: un livello [JPA] richiede l'artefatto [javaee-api];
  • righe 16, 22, 28: gli artefatti richiesti dall'implementazione JPA/EclipseLink qui utilizzata.
  • righe 18, 24, 30, 36: tutti gli artefatti hanno l'attributo provided. Si noti che ciò significa che sono richiesti per la compilazione ma non per l'esecuzione. Infatti, in fase di esecuzione, sono forniti dal server GlassFish,
  • Righe 41–48: definiscono un nuovo repository di artefatti Maven, dove si possono trovare gli artefatti EclipseLink.

3.4.2.3. Generazione di entità JPA

Le entità JPA possono essere generate utilizzando una procedura guidata di NetBeans:

  • in [1], creare entità JPA da un database,
  • in [2], selezionare l'origine dati creata in precedenza [jdbc / dbrdvmedecins2],
  • in [3], l'elenco delle tabelle per questa origine dati,
  • in [4], selezionarle tutte,
  • in [5], le tabelle selezionate,
  • in [6], si assegnano i nomi alle classi Java associate alle quattro tabelle,
  • così come un nome di pacchetto [7],
  • In [8], JPA raggruppa le righe delle tabelle del database in collezioni. Scegliamo una lista come collezione,
  • in [9], le classi Java create dalla procedura guidata.

3.4.2.4. Le entità JPA generate

L'entità [Medecin] rappresenta la tabella [medecins]. La classe Java è disseminata di annotazioni che rendono il codice di difficile lettura a prima vista. Se manteniamo solo ciò che è essenziale per comprendere il ruolo dell'entità, otteniamo il seguente codice:


package rdvmedecins.jpa;
 
...
@Entity
@Table(name = "medecins")
public class Medecin implements Serializable {
  
@Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;
  
  @Column(name = "TITRE")
  private String titre;
 
  @Column(name = "NOM")
  private String nom;
 
  @Column(name = "VERSION")
  private int version;
 
  @Column(name = "PRENOM")
  private String prenom;
 
  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
  private List<Creneau> creneauList;
 
// manufacturers
....
 
  // getters and setters
....
 
  @Override
  public int hashCode() {
  ...
  }
 
  @Override
  public boolean equals(Object object) {
  ...
  }
 
  @Override
  public String toString() {
    ...
  }
  
}
  • Riga 4: L'annotazione @Entity rende la classe [Medecin] un'entità JPA, ovvero una classe collegata a una tabella del database tramite l'API JPA.
  • riga 5: il nome della tabella del database associata all'entità JPA. Ogni campo della tabella corrisponde a un campo della classe Java,
  • riga 6: la classe implementa l'interfaccia Serializable. Ciò è necessario nelle applicazioni client/server, dove le entità vengono serializzate tra il client e il server.
  • righe 10–11: il campo id della classe [Medecin] corrisponde al campo [ID] (riga 10) della tabella [medecins],
  • righe 13–14: il campo *title* nella classe [Doctor] corrisponde al campo [TITLE] (riga 13) nella tabella [doctors],
  • righe 16–17: il campo name della classe [Doctor] corrisponde al campo [NAME] (riga 16) della tabella [doctors],
  • righe 19-20: il campo version della classe [Medecin] corrisponde al campo [VERSION] (riga 19) della tabella [medecins]. Qui, la procedura guidata non riconosce che la colonna è in realtà una colonna di versione che deve essere incrementata ogni volta che viene modificata la riga a cui appartiene. Per assegnarle questo ruolo, è necessario aggiungere l'annotazione @Version. Lo faremo in un passaggio successivo,
  • righe 22–23: il campo first_name della classe [Doctor] corrisponde al campo [FIRST_NAME] della tabella [doctors],
  • righe 10–11: il campo id corrisponde alla chiave primaria [ID] della tabella. Le annotazioni alle righe 8–9 chiariscono questo punto,
  • riga 8: l'annotazione @Id indica che il campo annotato è associato alla chiave primaria della tabella,
  • riga 9: il livello [JPA] genererà la chiave primaria per le righe che inserisce nella tabella [Doctors]. Esistono diverse strategie possibili. Qui, la strategia GenerationType.IDENTITY indica che il livello JPA utilizzerà la modalità auto_increment della tabella MySQL,
  • righe 25–26: la tabella [slots] ha una chiave esterna sulla tabella [doctors]. Uno slot appartiene a un medico. Al contrario, a un medico sono associati diversi slot. Abbiamo quindi una relazione uno-a-molti (un medico a molti slot), una relazione qualificata dall'annotazione @OneToMany in JPA (riga 25). Il campo alla riga 26 conterrà tutti gli slot del medico. Ciò si ottiene senza alcuna programmazione. Per comprendere appieno la riga 25, dobbiamo introdurre la classe [Creneau].

Essa è la seguente:


package rdvmedecins.jpa;
 
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
 
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;
 
  @Column(name = "MDEBUT")
  private int mdebut;
 
  @Column(name = "HFIN")
  private int hfin;
 
  @Column(name = "HDEBUT")
  private int hdebut;
 
  @Column(name = "MFIN")
  private int mfin;
 
  @Column(name = "VERSION")
  private int version;
 
  @JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Medecin idMedecin;
 
  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
  private List<Rv> rvList;
 
// manufacturers
...
// getters and setters
...
 
  @Override
  public int hashCode() {
    ...
  }
 
  @Override
  public boolean equals(Object object) {
    ...
  }
 
  @Override
  public String toString() {
    ...
  }
  
}

Commentiamo solo le nuove annotazioni:

  • abbiamo specificato che la tabella [slots] ha una chiave esterna verso la tabella [doctors]: uno slot è associato a un medico. Più slot possono essere associati allo stesso medico. Abbiamo una relazione dalla tabella [slots] alla tabella [doctors] definita come molti-a-uno (slot verso medico). L'annotazione @ManyToOne alla riga 32 viene utilizzata per definire la chiave esterna,
  • la riga 31, con l'annotazione @JoinColumn, specifica la relazione di chiave esterna: la colonna [ID_MEDECIN] nella tabella [slots] è una chiave esterna sulla colonna [ID] nella tabella [doctors],
  • Riga 33: un riferimento al medico a cui appartiene lo slot. Anche in questo caso ciò si ottiene senza alcuna codifica.

La relazione di chiave esterna tra l'entità [Creneau] e l'entità [Medecin] è quindi implementata da due annotazioni:

  • nell'entità [Creneau]:

@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
private Medecin idMedecin;
  • nell'entità [Doctor]:

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

Entrambe le annotazioni riflettono la stessa relazione: quella della chiave esterna dalla tabella [Appointments] alla tabella [Doctors]. Si dice che siano inverse l'una dell'altra. Solo la relazione @ManyToOne è essenziale. Essa definisce in modo univoco la relazione della chiave esterna. La relazione @OneToMany è facoltativa. Se presente, fa semplicemente riferimento alla relazione @ManyToOne a cui è associata. Questo è il significato dell'attributo mappedBy alla riga 1 dell'entità [Doctor]. Il valore di questo attributo è il nome del campo nell'entità [Slot] che ha l'annotazione @ManyToOne che specifica la chiave esterna. Sempre alla riga 1 dell'entità [Medecin], l'attributo cascade=CascadeType.ALL definisce il comportamento dell'entità [Medecin] rispetto all'entità [Creneau]:

  • se una nuova entità [Doctor] viene inserita nel database, allora devono essere inserite anche le entità [TimeSlot] nel campo alla riga 2,
  • se un'entità [Doctor] viene modificata nel database, allora anche le entità [Slot] nel campo alla riga 2 devono essere modificate,
  • se un'entità [Doctor] viene eliminata dal database, allora anche le entità [Slot] nel campo alla riga 2 devono essere eliminate.

Forniamo il codice per le altre due entità senza commenti specifici, poiché non introducono alcuna nuova notazione.

L'entità [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;
 
// manufacturers
...
// getters and setters
...
 
  @Override
  public int hashCode() {
    ...
  }
 
  @Override
  public boolean equals(Object object) {
    ...
  }
 
  @Override
  public String toString() {
    ...
  }
  
}
  • Le righe 24–25 riflettono la relazione di chiave esterna tra la tabella [rv] e la tabella [clients].

L'entità [Rv]:


package rdvmedecins.jpa;
 
...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;
 
  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;
 
  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneau idCreneau;
 
  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client idClient;
 
  // manufacturers
...
 
  // getters and setters
...
 
  @Override
  public int hashCode() {
    ...
  }
 
  @Override
  public boolean equals(Object object) {
    ...
  }
 
  @Override
  public String toString() {
    ...
  }
  
}
  • La riga 13 specifica che il campo `jour` è di tipo Java Date. Indica che nella tabella [rv], la colonna [JOUR] (riga 12) è di tipo data (senza ora),
  • righe 16–18: definiscono la relazione di chiave esterna dalla tabella [rv] alla tabella [slots],
  • Righe 20–22: definiscono la relazione di chiave esterna dalla tabella [rv] alla tabella [clients].

La generazione automatica delle entità JPA ci fornisce una base di lavoro. A volte questo è sufficiente, a volte no. È il caso qui:

  • dobbiamo aggiungere l'annotazione @Version ai vari campi di versione delle entità,
  • dobbiamo scrivere metodi toString più espliciti di quelli generati,
  • le entità [Medecin] e [Client] sono analoghe. Le faremo derivare da una classe [Person],
  • Rimuoveremo le relazioni inverse @OneToMany dalle relazioni @ManyToOne. Non sono essenziali e complicano la programmazione,
  • rimuoviamo la validazione @NotNull sulle chiavi primarie. Quando si persiste un'entità JPA con MySQL, l'entità ha inizialmente una chiave primaria nulla. È solo dopo la persistenza nel database che la chiave primaria dell'entità persistita assume un valore.

Con queste specifiche, le varie classi diventano le seguenti:

La classe Person viene utilizzata per rappresentare medici e clienti:


package rdvmedecins.jpa;
 
import java.io.Serializable;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
 
@MappedSuperclass
public class Personne implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;
 
  @Basic(optional = false)
  @Size(min = 1, max = 5)
  @Column(name = "TITRE")
  private String titre;
 
  @Basic(optional = false)
  @NotNull
  @Size(min = 1, max = 30)
  @Column(name = "NOM")
  private String nom;
 
  @Basic(optional = false)
  @NotNull
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @Basic(optional = false)
  @NotNull
  @Size(min = 1, max = 30)
  @Column(name = "PRENOM")
  private String prenom;
// manufacturers
...
 
// getters and setters
  ...
 
  @Override
  public String toString() {
    return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
  }
  
}
  • Riga 8: Si noti che la classe [Person] non è di per sé un'entità (@Entity). Fungerà da classe padre per le entità. L'annotazione @MappedSuperClass lo indica.

L'entità [Client] incapsula le righe della tabella [clients]. Deriva dalla precedente classe [Person]:


package rdvmedecins.jpa;
 
import java.io.Serializable;
import javax.persistence.*;
 
@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
  private static final long serialVersionUID = 1L;
 
// manufacturers
...
 
  @Override
  public int hashCode() {
...
  }
 
  @Override
  public boolean equals(Object object) {
  ...
  }
 
  @Override
  public String toString() {
    return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
  }
  
}
  • Riga 6: la classe [Client] è un'entità JPA,
  • riga 7: è associata alla tabella [clients],
  • riga 8: deriva dalla classe [Person].

L'entità [Doctor], che incapsula le righe della tabella [doctors], segue lo stesso schema:


package rdvmedecins.jpa;
 
import java.io.Serializable;
import javax.persistence.*;
 
@Entity
@Table(name = "medecins")
public class Medecin extends Personne implements Serializable {
  private static final long serialVersionUID = 1L;
 
  // manufacturers
...
 
  @Override
  public int hashCode() {
    ...
  }
 
  @Override
  public boolean equals(Object object) {
    ...
  }
 
  @Override
  public String toString() {
    return String.format("Médecin[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
  }
  
}

L'entità [Creneau] incapsula le righe della tabella [creneaux]:


package rdvmedecins.jpa;
 
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
 
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
 
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "MDEBUT")
  private int mdebut;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "HFIN")
  private int hfin;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "HDEBUT")
  private int hdebut;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "MFIN")
  private int mfin;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Medecin medecin;
 
  // manufacturers
  ...
 
  // getters and setters
  ...
 
  @Override
  public int hashCode() {
    ...
  }
 
  @Override
  public boolean equals(Object object) {
    // TODO: Warning - this method won't work in the case the id fields are not set
    ...
  }
 
  @Override
  public String toString() {
    return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
  }
}
  • Le righe 45–47 modellano la relazione "molti-a-uno" tra la tabella [slots] e la tabella [doctors] nel database: un medico ha più turni, mentre un turno appartiene a un solo medico.

L'entità [Rv] incapsula le righe della tabella [rv]:


package rdvmedecins.jpa;
 
import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
 
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
 
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;
  
  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneau creneau;
  
  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client client;
 
  // manufacturers
...
 
  // getters and setters
...
 
  @Override
  public int hashCode() {
    ...
  }
 
  @Override
  public boolean equals(Object object) {
    ...
  }
 
  @Override
  public String toString() {
    return String.format("Rv[%s, %s, %s]", id, creneau, client);
  }
}
  • Le righe 29–31 modellano la relazione "molti-a-uno" tra la tabella [rv] e la tabella [clients] (un cliente può comparire in più voci Rv) nel database, mentre le righe 25–27 modellano la relazione "molti-a-uno" tra la tabella [rv] e la tabella [slots] (uno slot può comparire in più voci Rv).

3.4.3. La classe di eccezione

La classe di eccezione dell'applicazione [ RdvMedecinsException] è la seguente:


package rdvmedecins.exceptions;
 
import java.io.Serializable;
import javax.ejb.ApplicationException;
 
@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException implements Serializable{
 
  // private fields
  private int code = 0;
 
  // manufacturers
  public RdvMedecinsException() {
    super();
  }
 
  public RdvMedecinsException(String message) {
    super(message);
  }
 
  public RdvMedecinsException(String message, Throwable cause) {
    super(message, cause);
  }
 
  public RdvMedecinsException(Throwable cause) {
    super(cause);
  }
 
  public RdvMedecinsException(String message, int code) {
    super(message);
    setCode(code);
  }
 
  public RdvMedecinsException(Throwable cause, int code) {
    super(cause);
    setCode(code);
  }
 
  public RdvMedecinsException(String message, Throwable cause, int code) {
    super(message, cause);
    setCode(code);
  }
 
  // getters - setters
  public int getCode() {
    return code;
  }
 
  public void setCode(int code) {
    this.code = code;
  }
}
  • Riga 7: La classe estende la classe [RuntimeException]. Pertanto, il compilatore non richiede che venga gestita con blocchi try/catch.
  • Riga 6: L'annotazione @ApplicationException garantisce che l'eccezione non venga "assorbita" da un'[EjbException].

Per comprendere l'annotazione @ApplicationException, rivediamo l'architettura lato server:

L'eccezione [RdvMedecinsException] verrà generata dai metodi EJB nel livello [DAO] all'interno del contenitore EJB3 e da esso intercettata. Senza l'annotazione @ApplicationException, il contenitore EJB3 incapsula l'eccezione verificatasi all'interno di una [EjbException] e la rigenera. Potresti non volere questo incapsulamento e preferire che un'eccezione di tipo [RdvMedecinsException] fuoriesca dal contenitore EJB3. Questo è ciò che consente l'annotazione @ApplicationException. Inoltre, l'attributo (rollback=true) di questa annotazione indica al contenitore EJB3 che, se si verifica un'eccezione [RdvMedecinsException] all'interno di un metodo eseguito come parte di una transazione con un DBMS, la transazione deve essere annullata. In termini tecnici, questo processo è chiamato rollback della transazione.

3.4.4. L' e EJB del livello [DAO]

L'interfaccia Java [ IDao] del livello [DAO] è la seguente:


package rdvmedecins.dao;
 
 
import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
 
public interface IDao {
 
  // customer list
  public List<Client> getAllClients();
  // list of doctors
  public List<Medecin> getAllMedecins();
  // list of physician slots
  public List<Creneau> getAllCreneaux(Medecin medecin);
  // list of doctor's appointments on a given day
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
  // find a customer identified by its id
  public Client getClientById(Long id);
  // find a customer identified by its id
  public Medecin getMedecinById(Long id);
  // find an Rv identified by its id
  public Rv getRvById(Long id);
  // find a time slot identified by its id
  public Creneau getCreneauById(Long id);
  // add a RV to the list
  public Rv ajouterRv(Date jour, Creneau creneau, Client client);
  // delete a RV
  public void supprimerRv(Rv rv);
}

Questa interfaccia è stata realizzata dopo aver identificato i requisiti del livello [web]:

  • riga 14: l'elenco dei clienti. Ne avremo bisogno per popolare l'elenco a discesa dei clienti,
  • riga 16: l'elenco dei medici. Ne avremo bisogno per popolare l'elenco a discesa dei medici,
  • riga 18: l'elenco delle fasce orarie disponibili di un medico. Ne avremo bisogno per visualizzare l'agenda del medico per un determinato giorno,
  • riga 20: l'elenco degli appuntamenti di un medico per un determinato giorno. In combinazione con il metodo precedente, questo ci permetterà di visualizzare l'agenda del medico per un determinato giorno con le fasce orarie già prenotate,
  • riga 22: ci permette di trovare un cliente in base al suo numero ID. Questo metodo ci consentirà di trovare un cliente selezionandolo dall'elenco a discesa dei clienti,
  • riga 24: come sopra per i medici,
  • riga 26: recupera un appuntamento in base al suo numero. Può essere utilizzato quando si elimina un appuntamento per verificare in anticipo che esista effettivamente,
  • riga 28: recupera una fascia oraria in base al suo numero. Permette di identificare la fascia che un utente vuole aggiungere o eliminare,
  • riga 30: per aggiungere un appuntamento,
  • riga 32: per cancellare un appuntamento.

L'interfaccia locale dell'EJB [IDaoLocal] deriva semplicemente dalla precedente interfaccia [IDao]:


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

Lo stesso vale per l'interfaccia remota [IDaoRemote]:


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

L'EJB [DaoJpa] implementa sia l'interfaccia locale che quella remota:


package rdvmedecins.dao;
 
...
 
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
  • La riga 5 indica che l'EJB remoto è denominato "rdvmedecins.dao". Inoltre, l'annotazione @Singleton (Java EE6) garantisce che venga creata una sola istanza dell'EJB. L'annotazione @Stateless (Java EE5) definisce un EJB che può essere creato in più istanze per popolare un pool di EJB,
  • la riga 6 indica che tutti i metodi EJB vengono eseguiti all'interno di una transazione gestita dal contenitore EJB3,
  • La riga 7 mostra che l'EJB implementa le interfacce locali e remote ed è anche serializzabile.

Il codice EJB completo è il seguente:


package rdvmedecins.dao;
 
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import rdvmedecins.exceptions.RdvMedecinsException;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
 
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
 
  @PersistenceContext
  private EntityManager em;
 
  // customer list
  public List<Client> getAllClients() {
    try {
      return em.createQuery("select rc from Client rc").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 1);
    }
  }
 
  // list of doctors
  public List<Medecin> getAllMedecins() {
    try {
      return em.createQuery("select rm from Medecin rm").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 2);
    }
  }
 
  // list of time slots for a given doctor
  // doctor: the doctor
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    try {
      return em.createQuery("select rc from Creneau rc join rc.medecin m where m.id=:idMedecin").setParameter("idMedecin", medecin.getId()).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 3);
    }
  }
 
  // list of appointments for a given doctor on a given day
  // doctor: the doctor
  // day: the day
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
    try {
      return em.createQuery("select rv from Rv rv join rv.creneau c join c.idMedecin m where m.id=:idMedecin and rv.jour=:jour").setParameter("idMedecin", medecin.getId()).setParameter("jour", jour).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 3);
    }
  }
 
  // add Rv
  // day : day of appointment
  // creneau: Rv time slot
  // customer: customer for whom the appointment is taken
  public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
    try {
      Rv rv = new Rv(null, jour);
      rv.setClient(client);
      rv.setCreneau(creneau);
      em.persist(rv);
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
  }

  // deleting an appointment
  // rv: Rv deleted
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }
 
  // retrieve a specific customer
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
 
  // retrieve a specific doctor
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
 
  // retrieve a given Rv
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
 
  // retrieve a given slot
  public Creneau getCreneauById(Long id) {
    try {
      return (Creneau) em.find(Creneau.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
}
  • riga 22: l'oggetto EntityManager che gestisce l'accesso al contesto di persistenza. Quando la classe viene istanziata, questo campo verrà inizializzato dal contenitore EJB utilizzando l'annotazione @PersistenceContext alla riga 21,
  • riga 27: query JPQL (Java Persistence Query Language) che restituisce tutte le righe della tabella [clients] come un elenco di oggetti [Client],
  • riga 36: una query simile per i medici,
  • riga 46: una query JPQL che esegue un join tra le tabelle [slots] e [doctors]. È parametrizzata dall'ID del medico,
  • riga 57: una query JPQL che esegue un join tra le tabelle [appointments], [slots] e [doctors] e che ha due parametri: l'ID del medico e la data dell'appuntamento,
  • righe 69–73: creazione di un appuntamento seguita dal suo salvataggio nel database,
  • riga 83: eliminazione di un appuntamento dal database,
  • riga 92: esegue una query SELECT sul database per trovare un determinato cliente,
  • riga 101: lo stesso per un medico,
  • riga 110: lo stesso per un appuntamento,
  • riga 119: lo stesso per una fascia oraria,
  • Tutte le operazioni che utilizzano il contesto di persistenza dalla riga 22 potrebbero incontrare un problema con il database. Pertanto, sono tutte racchiuse in un blocco try/catch. Qualsiasi eccezione è incapsulata nell'eccezione personalizzata RdvMedecinsException.

3.4.5. Implementazione del driver JDBC per MySQL

Nell'architettura riportata di seguito:

EclipseLink richiede il driver JDBC di MySQL. Questo deve essere installato nelle librerie del server GlassFish nella cartella <glassfish>/domains/domain1/lib/ext, dove <glassfish> è la directory di installazione del server GlassFish. È possibile ottenerlo come segue:

La cartella in cui deve essere collocato il driver JDBC di MySQL è <cartella Domains>[1]/domain1/lib/ext [2]. Questo driver è disponibile all'URL [http://www.mysql.fr/downloads/connector/j/]. Una volta installato, è necessario riavviare il server GlassFish affinché riconosca questa nuova libreria.

3.4.6. Distribuzione del livello [DAO] EJB

Torniamo all'architettura che abbiamo costruito finora:

L'intero stack [web, logica di business, DAO, JPA] deve essere distribuito sul server GlassFish. Ecco come procedere:

  • In [1], creiamo il progetto Maven,
  • In [2], lo eseguiamo,
  • in [3], è stato distribuito sul server GlassFish (scheda [Servizi])

Potresti essere curioso di controllare i log di GlassFish:

In [1], i log di GlassFish sono disponibili nella scheda [Output / GlassFish Server 3+]. Sono i seguenti:

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

Le righe contrassegnate con [Config] e [Details] sono log di EclipseLink; quelle contrassegnate con [Info] provengono da GlassFish.

  • Righe 1–12: EclipseLink elabora le entità JPA che ha individuato,
  • righe 13–17: informazioni che indicano che l'elaborazione delle entità JPA è proceduta normalmente,
  • Riga 18: EclipseLink segnala la propria presenza,
  • riga 19: EclipseLink riconosce di avere a che fare con il DBMS MySQL,
  • righe 20–24: EclipseLink tenta di connettersi al database,
  • righe 25–28: ci è riuscito,
  • righe 29–33: tenta di riconnettersi, questa volta utilizzando specificatamente una piattaforma MySQL (riga 30),
  • righe 34–37: di nuovo con esito positivo,
  • riga 38: conferma che l'unità di persistenza [dbrdvmedecins-PU] è stata istanziata,
  • riga 39: i nomi portabili delle interfacce remote e locali dell'EJB [DaoJpa], dove "portabile" significa riconosciuto da tutti i server di applicazioni Java EE 6,
  • riga 40: i nomi delle interfacce remote e locali dell'EJB [DaoJpa], specifici per GlassFish. Nel test successivo, useremo il nome "rdvmedecins.dao".

Le righe 39 e 40 sono importanti. Quando si scrive un client EJB su GlassFish, è necessario conoscerle.

3.4.7. Test del livello [DAO] EJB

Ora che l'EJB del livello [DAO] della nostra applicazione è stato distribuito, possiamo testarlo. Lo faremo nel contesto di un'applicazione client/server:

Il client testerà l'interfaccia remota dell'EJB [DAO] distribuito sul server GlassFish.

Iniziamo creando un nuovo progetto Maven :

  • In [1], creiamo un nuovo progetto,
  • in [2,3], creiamo un progetto Maven di tipo [Java Application],
  • in [4], gli diamo un nome e lo collochiamo nella stessa cartella dell'EJB [DAO],
  • In [5], il progetto generato,
  • in [6], è stata generata una classe [App.java]. La elimineremo,
  • in [7] è stato generato un ramo [Source Packages]. Non l'abbiamo ancora incontrato. Possiamo inserire i test JUnit in questo ramo. Lo faremo. Non manterremo la classe di test generata [AppTest],
  • in [8], le dipendenze del progetto Maven. Il ramo [Dependencies] è vuoto. Dovremo aggiungere nuove dipendenze lì. Il ramo [Test Dependencies] contiene le dipendenze necessarie per i test. Qui, la libreria utilizzata è il framework JUnit 3.8. Dovremo cambiarlo.

Il progetto si evolve come segue:

  • in [1], il progetto in cui sono state rimosse le due classi generate, insieme alla dipendenza JUnit.

Torniamo all'architettura client/server che verrà utilizzata per i test:

Il client deve conoscere l'interfaccia remota fornita dall'EJB [DAO]. Inoltre, scambierà entità JPA con l'EJB. Ha quindi bisogno delle definizioni di queste entità. Per garantire che il progetto di test EJB abbia accesso a queste informazioni, aggiungeremo il progetto EJB [DAO] come dipendenza al progetto:

  • In [1], aggiungere una dipendenza al ramo [Test Dependencies],
  • in [2], selezionare la scheda [Progetti aperti],
  • in [3], selezionare il progetto Maven EJB [DAO],
  • in [4], la dipendenza viene aggiunta.

Torniamo all'architettura client/server del test:

Durante l'esecuzione, il client e il server comunicano tramite la rete TCP-IP. Non programmeremo questi scambi. Per ogni server applicativo, esiste una libreria da integrare nelle dipendenze del client. Quella per Glassfish si chiama [gf-client]. La aggiungiamo:

  • in [1], aggiungiamo una dipendenza,
  • in [2], specifichiamo le caratteristiche dell'artefatto desiderato,
  • in [3], viene aggiunto un gran numero di dipendenze. Maven le scaricherà. L'operazione potrebbe richiedere alcuni minuti. Successivamente, vengono memorizzate nel repository Maven locale.

Ora possiamo creare il test JUnit:

  • in [2], fare clic con il tasto destro del mouse su [Test Packages] per creare un nuovo test JUnit,
  • in [3], assegniamo un nome alla classe di test e specifichiamo un pacchetto [4],
  • in [5], selezionare il framework JUnit 4.x,
  • in [6], la classe di test generata,
  • in [7], le nuove dipendenze del progetto Maven.

Il file [pom.xml] risulta quindi il seguente:


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

Nota:

  • righe 32–51, le dipendenze del progetto,
  • righe 13–26: sono stati definiti due repository Maven, uno per EclipseLink (righe 14–19) e l'altro per JUnit4 (righe 20–25).

La classe di test sarà la seguente:


package rdvmedecins.tests.dao;
 
import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import junit.framework.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import rdvmedecins.dao.IDaoRemote;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
 
public class JUnitTestDao {
 
  // layer [dao] tested
  private static IDaoRemote dao;
  // today's date
  Date jour = new Date();
 
  @BeforeClass
  public static void init() throws NamingException {
    // environment initialization JNDI
    InitialContext initialContext = new InitialContext();
    // dao layer instantiation
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }
 
  @Test
  public void test1() {
    // customer display
    List<Client> clients =dao.getAllClients();
    display("Liste des clients :", clients);
    // physician display
    List<Medecin> medecins =dao.getAllMedecins();
    display("Liste des médecins :", medecins);
    // display doctor's slots
    Medecin medecin = medecins.get(0);
    List<Creneau> creneaux = dao.getAllCreneaux(medecin);
    display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
    // list of doctor's appointments on a given day
    display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // add a RV
    Rv rv = null;
    Creneau creneau = creneaux.get(2);
    Client client = clients.get(0);
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    rv = dao.ajouterRv(jour, creneau, client);
    System.out.println("Rv ajouté");
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // add a RV in the same slot on the same day
    // must trigger an exception
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    Boolean erreur = false;
    try {
      rv = dao.ajouterRv(jour, creneau, client);
      System.out.println("Rv ajouté");
    } catch (Exception ex) {
      Throwable th = ex;
      while (th != null) {
        System.out.println(ex.getMessage());
        th = th.getCause();
      }
      // we note the error
      erreur=true;
    }
    // check for errors
    Assert.assertTrue(erreur);
    // RV list
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // delete a RV
    System.out.println("Suppression du Rv ajouté");
    dao.supprimerRv(rv);
    System.out.println("Rv supprimé");
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
  }
 
  // utility method - displays items in a collection
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • righe 23–29: il metodo annotato con @BeforeClass viene eseguito prima di tutti gli altri. Qui creiamo un riferimento all'interfaccia remota dell'EJB [DaoJpa]. Ricordiamo che gli abbiamo assegnato il nome JNDI "rdvmedecins.dao",
  • righe 34–35: visualizza l'elenco dei clienti,
  • righe 37-38: visualizza l'elenco dei medici,
  • righe 40–42: visualizzano le fasce orarie del primo medico,
  • riga 44: visualizza gli appuntamenti del primo medico nel giorno specificato alla riga 21,
  • righe 46–51: aggiungi un appuntamento per il primo medico, per la fascia oraria n. 2 e il giorno specificato alla riga 21,
  • riga 52: visualizza, a scopo di verifica, gli appuntamenti del primo medico per il giorno specificato nella riga 21. Ce ne deve essere almeno uno: quello appena aggiunto,
  • righe 55-70: aggiungono lo stesso appuntamento. Poiché la tabella [RV] ha un vincolo di unicità, questa aggiunta deve causare un'eccezione. Lo verifichiamo alla riga 70,
  • riga 72: visualizza gli appuntamenti del primo medico per il giorno indicato nella riga 21 a scopo di verifica. Quello che volevamo aggiungere non dovrebbe essere presente,
  • righe 74–76: eliminiamo l'unico appuntamento che era stato aggiunto,
  • riga 77: visualizza gli appuntamenti del primo medico nel giorno specificato alla riga 21 per verifica. Quello appena eliminato non dovrebbe essere presente.

Questo è un test JUnit fittizio. Contiene una sola asserzione (riga 70). Si tratta di un test visivo con i difetti che ne derivano.

Se tutto va bene, i test dovrebbero superare:

  • in [1], si compila il progetto di test,
  • in [2], il test viene eseguito,
  • in [3], il test è stato superato.

Diamo un'occhiata più da vicino all'output del test:

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

Si invita il lettore a leggere questi log insieme al codice che li ha generati. Ci concentreremo sull'eccezione verificatasi durante l'aggiunta di un appuntamento esistente, righe 41–49. La traccia dello stack dell'eccezione è mostrata nelle righe 42–48. È inaspettata. Torniamo al codice del metodo che aggiunge un appuntamento:


  // add Rv
  // day : day of appointment
  // creneau: Rv time slot
  // customer: customer for whom the appointment is taken
  public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
    try {
      Rv rv = new Rv(null, jour);
      rv.setClient(client);
      rv.setCreneau(creneau);
      System.out.println(String.format("avant persist : %s",rv));
      em.persist(rv);
      System.out.println(String.format("après persist : %s",rv));
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
}

Diamo un'occhiata ai log di GlassFish durante l'aggiunta dei due appuntamenti:

...
Infos: avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Infos: après persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Précis: INSERT INTO rv (JOUR, ID_CLIENT, ID_CRENEAU) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Précis: SELECT LAST_INSERT_ID()
Précis: SELECT t1.ID, t1.JOUR, t1.ID_CLIENT, t1.ID_CRENEAU FROM creneaux t0, rv t1 WHERE (((t0.ID_MEDECIN = ?) AND (t1.JOUR = ?)) AND (t0.ID = t1.ID_CRENEAU))
    bind => [2 parameters bound]
Infos: avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Infos: après persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Précis: INSERT INTO rv (JOUR, ID_CLIENT, ID_CRENEAU) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Précis: SELECT 1
Avertissement: Local Exception Stack: 
Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.2.v20111125-r10461): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '2012-05-23-3' for key 'UNQ1_RV'
Error Code: 1062
...
  • riga 2: prima del primo persist,
  • riga 3: dopo il primo persist,
  • riga 4: l'istruzione INSERT che verrà eseguita. Si noti che non avviene contemporaneamente all'operazione persist. Se così fosse, questo log sarebbe apparso prima della riga 2. L'operazione INSERT avviene normalmente alla fine della transazione in cui viene eseguito il metodo,
  • Riga 6: EclipseLink richiede a MySQL l'ultima chiave primaria utilizzata. Recupererà la chiave primaria dell'appuntamento appena aggiunto. Questo valore popolerà il campo id dell'entità [Rv] salvata,
  • Righe 7–8: La query SELECT che visualizzerà gli appuntamenti del medico,
  • Righe 9–10: Viene visualizzata la schermata per la seconda persistenza,
  • Righe 11–12: L'istruzione INSERT che verrà eseguita. Dovrebbe generare un'eccezione. Questa eccezione appare alle righe 15–16 ed è chiara. Viene inizialmente generata dal driver JDBC di MySQL a causa di una violazione del vincolo di unicità sugli appuntamenti. Possiamo dedurre che dovremmo vedere queste eccezioni nei log dei test JUnit. Tuttavia, non è così:
1
2
3
4
5
6
7
8
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe

Esaminiamo l'architettura client/server del test:

Quando l'EJB [DAO] genera un'eccezione, questa deve essere serializzata per raggiungere il client. È probabile che questa operazione sia fallita per un motivo che non ho compreso. Poiché la nostra applicazione completa non verrà eseguita in un ambiente client/server, possiamo ignorare questo problema.

Ora che l'EJB del livello [DAO] è operativo, possiamo passare all'EJB del livello [business].

3.5. Il livello [business]

Torniamo all'architettura dell'applicazione che stiamo realizzando:

Creeremo un nuovo progetto Maven per l'EJB [business]. Come mostrato sopra, dipenderà dal progetto Maven creato per i livelli [DAO] e [JPA].

3.5.1. Il progetto NetBeans

Stiamo creando un nuovo progetto Maven di tipo EJB. Per farlo, basta seguire la procedura già utilizzata e descritta a pagina 174.

  • In [1], il progetto Maven per il livello [business],
  • in [2], aggiungere una dipendenza,
  • in [3], selezionare il progetto Maven per i livelli [DAO] e [JPA],
  • in [4], selezionare l'ambito [provided]. Si noti che ciò significa che è richiesto per la compilazione ma non per l'esecuzione del progetto. Infatti, l'EJB del livello [business] verrà distribuito sul server GlassFish insieme agli EJB dei livelli [DAO] e [JPA]. Quindi, al momento dell'esecuzione, gli EJB dei livelli [DAO] e [JPA] saranno già presenti,
  • in [6], il nuovo progetto con le sue dipendenze.

Diamo ora un'occhiata al codice sorgente del livello [business]:

L'EJB [Business] avrà la seguente interfaccia [IMetier]:


package rdvmedecins.metier.service;
 
import java.util.Date;
import java.util.List;
 
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
 
public interface IMetier {
 
    // dao layer
    // customer list
    public List<Client> getAllClients();
 
    // list of doctors
    public List<Medecin> getAllMedecins();
 
    // list of physician slots
    public List<Creneau> getAllCreneaux(Medecin medecin);
 
    // list of doctor's appointments on a given day
    public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
 
    // find a customer identified by its id
    public Client getClientById(Long id);
 
    // find a customer identified by its id
    public Medecin getMedecinById(Long id);
 
    // find an Rv identified by its id
    public Rv getRvById(Long id);
 
    // find a time slot identified by its id
    public Creneau getCreneauById(Long id);
 
    // add a RV to the list
    public Rv ajouterRv(Date jour, Creneau creneau, Client client);
 
    // delete a RV
    public void supprimerRv(Rv rv);
    
    // job
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
 
}

Per comprendere questa interfaccia, è necessario richiamare l'architettura del progetto:

Abbiamo definito l'interfaccia del livello [DAO] (Sezione 3.4.4) e specificato che essa risponde alle esigenze del livello [web], ovvero ai requisiti degli utenti. Il livello [web] comunica con il livello [DAO] solo tramite il livello [business]. Questo spiega perché tutti i metodi del livello [DAO] si trovano nel livello [business]. Questi metodi si limiteranno a delegare la richiesta dal livello [web] al livello [DAO]. Nient'altro.

Durante l'analisi dell'applicazione è emersa una necessità: la possibilità di visualizzare su una pagina web l'agenda di un medico per un determinato giorno, in modo da mostrare quali fasce orarie sono prenotate e quali sono disponibili. Questo è tipicamente il caso in cui una segretaria riceve una richiesta per telefono. Il chiamante chiede un appuntamento in un giorno specifico con un medico specifico. Per soddisfare questa esigenza, il livello [business] fornisce il metodo alla riga 46.


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

Ci si potrebbe chiedere dove collocare questo metodo:

  • potrebbe essere collocato nel livello [DAO]. Tuttavia, questo metodo non risponde realmente a un'esigenza di accesso ai dati, ma piuttosto a un'esigenza aziendale;
  • potremmo collocarlo nel livello [web]. Sarebbe una cattiva idea. Perché se trasformassimo il livello [web] in un livello [Swing], perderemmo il metodo anche se l'esigenza continuasse a sussistere.

Il metodo accetta come parametri il medico e il giorno per il quale vogliamo l'agenda degli appuntamenti. Restituisce un oggetto [AgendaMedecinJour] che rappresenta l'agenda del medico per quel giorno:


package rdvmedecins.metier.entites;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.jpa.Medecin;
 
public class AgendaMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;
 
    // manufacturers
    public AgendaMedecinJour() {
 
    }
 
    public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
        this.medecin = medecin;
        this.jour = jour;
        this.creneauxMedecinJour = creneauxMedecinJour;
    }
 
    public String toString() {
        StringBuffer str = new StringBuffer("");
        for (CreneauMedecinJour cr : creneauxMedecinJour) {
            str.append(" ");
            str.append(cr.toString());
        }
        return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
    }
 
    // getters and setters
...
  
}
  • riga 12: il medico a cui appartiene questo orario,
  • riga 13: il giorno dell'orario,
  • riga 14: le fasce orarie del medico per quel giorno.
  • La classe include costruttori (righe 17, 21) e un metodo toString personalizzato (riga 27).

La classe [DoctorTimeSlotDay] (riga 14) è la seguente:


package rdvmedecins.metier.entites;
 
import java.io.Serializable;
import rdvmedecins.jpa.Creneau;
 
import rdvmedecins.jpa.Rv;
 
public class CreneauMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Creneau creneau;
    private Rv rv;
 
    // manufacturers
    public CreneauMedecinJour() {
 
    }
 
    public CreneauMedecinJour(Creneau creneau, Rv rv) {
        this.creneau=creneau;
    this.rv=rv;
    }
 
    // toString
    @Override
    public String toString() {
        return String.format("[%s %s]", creneau,rv);
    }
 
    // getters and setters
 
  ...
}
  • riga 12: una fascia oraria del medico,
  • riga 13: l'appuntamento associato, nullo se la fascia oraria è libera.

Possiamo vedere che il campo creneauxMedecinJour alla riga 14 della classe [AgendaMedecinJour] ci permette di recuperare tutte le fasce orarie del medico con lo stato "occupato" o "libero" per ciascuna di esse. Questo era lo scopo del nuovo metodo [getAgendaMedecinJour] di [IMetier].

Il nostro EJB [Metier] avrà un'interfaccia locale e un'interfaccia remota che estendono semplicemente l'interfaccia principale [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{
 
}

L'EJB [Metier] implementa queste interfacce come segue:


package rdvmedecins.metier.service;
 
import java.io.Serializable;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
 
import javax.ejb.EJB;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
 
import rdvmedecins.dao.IDaoLocal;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.entites.CreneauMedecinJour;
 
@Singleton
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote, Serializable {
 
  // dao layer
  @EJB
  private IDaoLocal dao;
 
  public Metier() {
  }
 
  @Override
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }
 
  @Override
  public List<Medecin> getAllMedecins() {
    return dao.getAllMedecins();
  }

  @Override
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    return dao.getAllCreneaux(medecin);
  }
 
  @Override
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
    return dao.getRvMedecinJour(medecin, jour);
  }
 
  @Override
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }
 
  @Override
  public Medecin getMedecinById(Long id) {
    return dao.getMedecinById(id);
  }
 
  @Override
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }
 
  @Override
  public Creneau getCreneauById(Long id) {
    return dao.getCreneauById(id);
  }
 
  @Override
  public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
    return dao.ajouterRv(jour, creneau, client);
  }
 
  @Override
  public void supprimerRv(Rv rv) {
    dao.supprimerRv(rv);
  }
 
  @Override
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour) {
    // list of doctor's time slots
    List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
    // list of bookings for the same doctor on the same day
    List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
    // a dictionary is created from the Rvs taken
    Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
    for (Rv resa : reservations) {
      hReservations.put(resa.getCreneau().getId(), resa);
    }
    // create the agenda for the requested day
    AgendaMedecinJour agenda = new AgendaMedecinJour();
    // the doctor
    agenda.setMedecin(medecin);
    // the day
    agenda.setJour(jour);
    // reservation slots
    CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
    agenda.setCreneauxMedecinJour(creneauxMedecinJour);
    // filling reservation slots
    for (int i = 0; i < creneauxHoraires.size(); i++) {
      // line i agenda
      creneauxMedecinJour[i] = new CreneauMedecinJour();
      // slot id
      creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
      // is the slot free or reserved?
      if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
        // the slot is occupied - we note the resa
        Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
        creneauxMedecinJour[i].setRv(resa);
      }
    }
    // we return the result
    return agenda;
  }
}
  • riga 22, la classe [Metier] è un EJB singleton,
  • Riga 23: ogni metodo EJB viene eseguito all'interno di una transazione. Ciò significa che la transazione inizia all'avvio del metodo, nel livello [business]. Questo livello chiamerà i metodi nel livello [DAO]. Questi metodi verranno eseguiti all'interno della stessa transazione,
  • riga 24: l'EJB implementa le sue interfacce locali e remote ed è anche serializzabile,
  • riga 27: un riferimento all'EJB nel livello [DAO],
  • riga 29: questo verrà iniettato dal contenitore EJB del server GlassFish, grazie all'annotazione @EJB. Quindi, quando i metodi della classe [Business] vengono eseguiti, il riferimento all'EJB del livello [DAO] è stato inizializzato,
  • righe 33–81: questo riferimento viene utilizzato per delegare la chiamata effettuata al livello [Business] al livello [DAO],
  • riga 84: il metodo getAgendaMedecinJour, che recupera l'agenda di un medico per un dato giorno. Lasceremo che sia il lettore a seguire i commenti.

3.5.2. Distribuzione del livello [business]

Il livello [business] dipende dal livello [DAO]. Ogni livello è stato implementato con un EJB. Per testare l'EJB [business], dobbiamo distribuire entrambi gli EJB. Per farlo, abbiamo bisogno di un progetto enterprise.

  • [1], creare un nuovo progetto,
  • di tipo Maven [2] e Applicazione Enterprise [3],
  • e assegnagli un nome [4]. Il suffisso "ear" verrà aggiunto automaticamente,
  • in [5], selezioniamo il server GlassFish e Java EE 6,
  • in [6], un'applicazione enterprise contiene moduli, in genere moduli EJB e moduli web. Qui, l'applicazione enterprise conterrà i moduli dei due EJB che abbiamo creato. Poiché questi moduli esistono già, non spuntiamo le caselle,
  • in [7,8], sono stati creati due progetti. [8] è il progetto enterprise che useremo. [7] è un progetto di cui non conosco lo scopo. Non ho mai dovuto usarlo e, dato che non ho approfondito Maven, non so a cosa serva. Quindi lo ignoreremo.

Ora che il progetto enterprise è stato creato, possiamo definirne i moduli.

  • In [1], creiamo una nuova dipendenza,
  • in [2], selezioniamo il progetto EJB [DAO],
  • in [3], dichiariamo che si tratta di un EJB. Non lasciare il tipo vuoto, perché in tal caso verrà utilizzato il tipo jar, e quel tipo non è adatto in questo caso,
  • in [4], usiamo l'ambito [compile],
  • In [5], il progetto con la sua nuova dipendenza,
  • in [6, 7, 8], ripetiamo il processo per aggiungere l'EJB dal livello [business],
  • in [9], le due dipendenze,
  • in [10], si compila il progetto,
  • in [11], lo eseguiamo,
  • in [12], nella scheda [Servizi], vediamo che il progetto è stato distribuito sul server GlassFish. Ciò significa che entrambi gli EJB sono ora presenti sul server.

Nei log del server GlassFish è possibile trovare informazioni sulla distribuzione dei due EJB:

  • in [1], nella scheda dei log di GlassFish.

Qui si trovano i seguenti log:

Infos: rdvmedecins.jpa.Creneau actually got transformed
Infos: rdvmedecins.jpa.Medecin actually got transformed
Infos: rdvmedecins.jpa.Personne actually got transformed
Infos: rdvmedecins.jpa.Client actually got transformed
Infos: rdvmedecins.jpa.Rv actually got transformed
Infos: EclipseLink, version: Eclipse Persistence Services - 2.3.2.v20111125-r10461
Infos: file:/D:/data/istia-1112/netbeans/dvp/jsf2-pf-pfm/maven/netbeans/rdvmedecins-jsf2-ejb/mv-rdvmedecins-metier-dao/mv-rdvmedecins-metier-dao-ear/target/gfdeploy/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT_jar/_dbrdvmedecins2-PU login successful
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: EJB5181:Portable JNDI names for EJB Metier: [java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]
  • Righe 1-5: Le entità JPA sono state riconosciute,
  • riga 7: indica che l'unità di persistenza [dbrdvmedecins2-PU] è stata creata con successo e che è stata stabilita la connessione al database associato,
  • riga 8: i nomi portabili delle interfacce remote e locali dell'EJB [DaoJpa]. "Portabile" significa riconosciuto da tutti i server applicativi,
  • riga 9: la stessa cosa ma con nomi specifici di GlassFish,
  • Righe 10–11: lo stesso per l'EJB [Metier].

Useremo il nome portatile dell'interfaccia remota dell'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

Ne avremo bisogno durante il test del livello [business].

3.5.3. Test del livello [business]

Come abbiamo fatto per il livello [DAO], testeremo il livello [business] nell'ambito di un'applicazione client/server:

Il client testerà l'interfaccia remota dell'EJB [Business] distribuito sul server GlassFish.

Iniziamo creando un nuovo progetto Maven. Per farlo, seguiamo la stessa procedura utilizzata per creare il progetto di test del livello [DAO] (vedere la sezione 3.4.7), tralasciando la creazione del test JUnit. Il progetto risultante è il seguente

  • [1] mostra il progetto creato con le sue dipendenze: relative all'EJB del livello [DAO], all'EJB del livello [Business] e alla libreria [gf-client].

A questo punto, il file [pom.xml] del progetto è il seguente:


<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>

Assicurati di avere le dipendenze descritte nelle righe 17–33. Il test sarà una semplice classe console:

Il codice per la classe [ClientRdvMedecinsMetier] è il seguente:


package istia.st.client;
 
import java.util.Date;
import java.util.List;
 
import javax.naming.InitialContext;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
 
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.service.IMetierRemote;
 
public class ClientRdvMedecinsMetier {
 
  // the remote interface name of the EJB [Metier]
  private static String IDaoRemoteName = "java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
  // today's date
  private static Date jour = new Date();
 
  public static void main(String[] args) {
    try {
      // context JNDI of Glassfish server
      InitialContext initialContext = new InitialContext();
      // reference on remote [metier] layer
      IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
      // customer display
      List<Client> clients = metier.getAllClients();
      display("Liste des clients :", clients);
      // physician display
      List<Medecin> medecins = metier.getAllMedecins();
      display("Liste des médecins :", medecins);
      // display doctor's slots
      Medecin medecin = medecins.get(0);
      List<Creneau> creneaux = metier.getAllCreneaux(medecin);
      display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
      // list of doctor's appointments on a given day
      display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // calendar display
      AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // add a RV to the list
      Rv rv = null;
      Creneau creneau = creneaux.get(2);
      Client client = clients.get(0);
      System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
      rv = metier.ajouterRv(jour, creneau, client);
      System.out.println("Rv ajouté");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // calendar display
      agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // delete a RV
      System.out.println("Suppression du Rv ajouté");
      metier.supprimerRv(rv);
      System.out.println("Rv supprimé");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // calendar display
      agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
    } catch (Throwable ex) {
      System.out.println("Erreur...");
      while (ex != null) {
        System.out.println(String.format("%s : %s", ex.getClass().getName(), ex.getMessage()));
        ex = ex.getCause();
      }
    }
  }
 
  // utility method - displays items in a collection
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • riga 18: il nome portabile dell'interfaccia remota dell'EJB [Metier] è stato ricavato dai log di GlassFish,
  • righe 24–27: viene ottenuto un riferimento all'interfaccia remota dell'EJB [Metier],
  • righe 29–30: visualizza i clienti,
  • righe 32–33: visualizzazione dei medici,
  • righe 35–37: visualizza le fasce orarie disponibili di un medico,
  • riga 39: visualizza gli appuntamenti di un medico in un dato giorno,
  • righe 41-42: l'agenda di questo medico per lo stesso giorno,
  • righe 44-49: aggiungi un appuntamento,
  • riga 50: visualizza gli appuntamenti del medico. Dovrebbe essercene un altro,
  • righe 52-53: visualizza l'agenda del medico. L'appuntamento aggiunto dovrebbe essere visibile,
  • righe 55-57: elimina l'appuntamento appena aggiunto,
  • riga 58: questo dovrebbe riflettersi nell'elenco degli appuntamenti del medico,
  • righe 60–61: e nel loro calendario.

Eseguiamo il test:

 

Le schermate risultanti sono le seguenti:


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]]
  • riga 37: agenda della sig.ra PELISSIER, 23 maggio 2012. Non sono riservati slot,
  • riga 39: aggiunta di un appuntamento,
  • riga 42: nuovo programma della sig.ra PELISSIER. Ora è riservato uno slot per il sig. MARTIN,
  • riga 44: l'appuntamento è stato cancellato,
  • riga 46: il calendario della sig.ra PELISSIER mostra che non è riservata alcuna fascia oraria.

Consideriamo ora operativi i livelli [CAD] e [business]. Dobbiamo ancora scrivere il livello [web] utilizzando il framework JSF. Per farlo, useremo le conoscenze acquisite all'inizio di questo documento.

3.6. Il livello [web]

Torniamo all'architettura attualmente in fase di realizzazione:

Realizzeremo l'ultimo livello, il livello [web].

3.6.1. Il progetto NetBeans

Stiamo creando un progetto Maven:

  • In [1], creiamo un nuovo progetto,
  • in [2, 3], un progetto Maven di tipo [Web Application],
  • in [4], gli diamo un nome,
  • in [5], selezionare il server GlassFish e Java EE 6 Web,
  • in [6], il progetto così creato,
  • in [7], il progetto dopo aver rimosso la pagina [index.jsp] e il pacchetto in [Source Packages],
  • in [8, 9], nelle proprietà del progetto, aggiungere un framework,
  • in [10], selezionare Java Server Faces,
  • in [11], la configurazione di Java Server Faces. Lasciare i valori predefiniti. Si noti che viene utilizzato JSF 2,
  • in [12], il progetto viene quindi modificato in due modi: viene generato un file [web.xml], così come una pagina [index.html].

Il file [web.xml] è il seguente:


<?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>

Abbiamo già incontrato questo file.

  • righe 7–11: definiscono il servlet che gestirà tutte le richieste effettuate all'applicazione. Si tratta del servlet JSF,
  • righe 12–15: definiscono gli URL gestiti da questo servlet. Si tratta di URL del tipo /faces/*,
  • righe 21–23: definiscono la pagina [index.xhtml] come pagina iniziale.

Questa pagina è la seguente:


<?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>

L'abbiamo già visto in precedenza. Possiamo eseguire questo progetto:

  • In [1], eseguiamo il progetto e otteniamo il risultato [2] nel browser.

Presenteremo ora il progetto completo e poi approfondiremo i suoi vari componenti.

  • in [1], le pagine XHTML del progetto,
  • in [2], il codice Java,
  • in [3], i file dei messaggi, poiché l'applicazione è internazionalizzata,
  • in [4], le dipendenze del progetto.

3.6.2. Dipendenze del progetto

Torniamo all'architettura del progetto:

Il livello JSF si basa sui livelli [business], [DAO] e [JPA]. Questi tre livelli sono incapsulati nei due progetti Maven che abbiamo creato, il che spiega le dipendenze del progetto [4]. Mostriamo semplicemente come vengono aggiunte queste dipendenze:

  • in [1], useremo ejb per indicare che la dipendenza riguarda un progetto EJB,
  • in [2], inseriremo [provided]. Questo perché il progetto web verrà distribuito insieme ai due progetti EJB. Pertanto, non è necessario includere i JAR EJB.

3.6.3. Configurazione del progetto

La configurazione del progetto è la stessa di quella dei progetti JSF trattati all'inizio di questo documento. Elenchiamo i file di configurazione senza spiegarli nuovamente.

 

[web.xml]: configura l'applicazione 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>

Si noti che alla riga 26, la pagina [index.xhtml] è la home page dell'applicazione.

[faces-config.xml]: configura l'applicazione 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]: vuoto ma necessario per l'annotazione @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]: il foglio di stile dell'applicazione


.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]: il file dei messaggi in francese


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

[messages_en.properties]: il file dei messaggi in inglese


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

3.6.4. Viste del progetto

Vediamo come funziona l'applicazione. La pagina iniziale è la seguente:

 

Da questa pagina iniziale, l'utente (personale amministrativo, medico) effettuerà una serie di operazioni. Le presentiamo di seguito. La vista a sinistra mostra la schermata da cui l'utente effettua una richiesta; la vista a destra mostra la risposta inviata dal server.

Infine, potrebbe anche essere visualizzata una pagina di errore:

Queste diverse visualizzazioni sono generate dalle seguenti pagine del progetto web:

  • in [1], le pagine [basdepage, entete, layout] gestiscono la formattazione di tutte le viste,
  • in [2], la vista generata da [layout.xhtml].

Qui è stata utilizzata la tecnologia Facelets. Questa è stata descritta nella Sezione 2.11. Forniremo semplicemente il codice delle pagine XHTML utilizzate per il layout:

[entete.xhtml]


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

Si notino le righe 10–12, i due link per cambiare la lingua dell'applicazione.

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

Questa pagina è il modello per la pagina [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>

Le righe 8–21 definiscono l'area denominata "content" (riga 8) in [layout.xhtml] (riga 7). Si tratta dell'area centrale delle viste:

 

La pagina [index.xhtml] è l'unica pagina dell'applicazione. Pertanto, non ci sarà alcuna navigazione tra le pagine. Visualizza una delle quattro pagine [form1.xhtml, form2.xhtml, form3.xhtml, error.xhtml]. Questa visualizzazione è controllata da quattro variabili booleane [form1Rendered, form2Rendered, form3Rendered, errorRendered] del bean del modulo, che descriveremo tra poco.

3.6.5. I bean del progetto

Le classi del pacchetto [utils] sono già state presentate:

  • la classe [ChangeLocale] è quella che gestisce il cambio di lingua. Ne abbiamo già parlato (Sezione 2.4.4).
  • La classe [Messages] è una classe che facilita l'internazionalizzazione dei messaggi di un'applicazione. È stata discussa nella sezione 2.8.5.7.

3.6.5.1. Il bean Application

Il bean [Application] è il seguente:


package beans;
 
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.metier.service.IMetierLocal;
 
@Named(value = "application")
@ApplicationScoped
public class Application implements Serializable{
 
  // business layer
  @EJB
  private IMetierLocal metier;
  // cache
  private List<Medecin> medecins;
  private List<Client> clients;
  private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
  private Map<Long, Client> hClients = new HashMap<Long, Client>();
  // errors
  private List<Erreur> erreurs = new ArrayList<Erreur>();
  private Boolean erreur = false;
 
  public Application() {
  }
 
  @PostConstruct
  public void init() {
    // caching doctors and customers
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // we note the error
      erreur = true;
      erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
      while (th.getCause() != null) {
        th = th.getCause();
        erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
      }
      return;
    }
    // list checking
    if (medecins.size() == 0) {
      // we note the error
      erreur = true;
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // we note the error
      erreur = true;
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // mistake?
    if (erreur) {
      return;
    }
 
    // dictionaries
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
  }

  // getters and setters
  ...
}
  • righe 15-16: la classe [Application] è un bean a livello di applicazione. Viene creata una sola volta all'inizio del ciclo di vita dell'applicazione JSF ed è accessibile a tutte le richieste di tutti gli utenti. Generalmente memorizziamo i dati di sola lettura nell'applicazione. Qui memorizzeremo l'elenco dei medici e l'elenco dei clienti. Partiamo quindi dal presupposto che questi non cambino spesso. Le pagine XHTML vi accedono tramite il nome dell'applicazione,
  • righe 20–21: un riferimento all'interfaccia locale dell'EJB [Business] verrà iniettato dal contenitore EJB di GlassFish. Rivediamo l'architettura dell'applicazione:

L'applicazione JSF e l'EJB [Metier] verranno eseguiti nella stessa JVM (Java Virtual Machine). Pertanto, il livello [JSF] utilizzerà l'interfaccia locale dell'EJB. In questo caso, il bean dell'applicazione utilizza l'EJB [Business]. Anche se così non fosse, sarebbe normale trovare un riferimento ad esso nel livello [business]. Si tratta infatti di informazioni che possono essere condivise da tutte le richieste di tutti gli utenti e quindi di dati con ambito Application.

  • Righe 34–35: Il metodo init viene eseguito immediatamente dopo l’istanziazione della classe [Application] (presenza dell’annotazione @PostConstruct).
  • Righe 36–73: il metodo crea i seguenti elementi: l'elenco dei medici alla riga 23, l'elenco dei clienti alla riga 24, un dizionario dei medici indicizzato in base al loro ID alla riga 25 e lo stesso per i clienti alla riga 26. Potrebbero verificarsi degli errori. Questi vengono registrati nell'elenco alla riga 28.

La classe [Error] è la seguente:


package beans;
 
public class Erreur {
  
  public Erreur() {
  }
  
  // field
  private String classe;
  private String message;
 
  // manufacturer
  public Erreur(String classe, String message){
    this.setClasse(classe);
    this.message=message;
  }
  
  // getters and setters
...  
}
  • riga 9: il nome di una classe di eccezione se è stata generata un'eccezione,
  • riga 10: un messaggio di errore.

3.6.5.2. Il bean [Form]

Il suo codice è il seguente:


package beans;
 
...
 
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
 
  public Form() {
  }
 
  // bean Application
  @Inject
  private Application application;
 
  // model
  private Long idMedecin;
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private String form2Titre;
  private String form3Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Long idCreneau;
  private Medecin medecin;
  private Client client;
  private Long idClient;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;
 
  @PostConstruct
  private void init() {
    // was the initialization successful?
    if (application.getErreur()) {
      // retrieve the list of errors
      erreurs = application.getErreurs();
      // the error view is displayed
      setForms(false, false, false, true);
    }
  }
 
  // view display
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.form3Rendered = form3Rendered;
    this.erreurRendered = erreurRendered;
  }
.................................................
}
  • righe 5-7: La classe [Form] è un bean denominato "form" con ambito di sessione. Si noti che la classe deve quindi essere serializzabile.
  • righe 13-14: il bean del modulo contiene un riferimento al bean dell'applicazione. Questo riferimento verrà iniettato dal contenitore servlet in cui è in esecuzione l'applicazione (presenza dell'annotazione @Inject).
  • righe 17-31: i modelli di pagina [form1.xhtml, form2.xhtml, form3.xhtml, error.xhtml]. La visualizzazione di queste pagine è controllata dai valori booleani nelle righe 19-22. Si noti che per impostazione predefinita viene visualizzata la pagina [form1.xhtml],
  • righe 33–34: il metodo init viene eseguito immediatamente dopo l'istanziazione della classe (presenza dell'annotazione @PostConstruct),
  • righe 35–41: il metodo init viene utilizzato per determinare quale pagina debba essere visualizzata per prima: normalmente la pagina [form1.xhtml] (riga 19) a meno che l'inizializzazione dell'applicazione non sia fallita (riga 36), nel qual caso verrà visualizzata la pagina [error.xhtml] (riga 40).

La pagina [error.xhtml] è la seguente:


<?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>

Utilizza un tag <h:dataTable> (righe 14–27) per visualizzare l'elenco degli errori. Il risultato è una pagina simile alla seguente:

Image

Ora definiremo le diverse fasi del ciclo di vita dell'applicazione.

3.6.6. Interazioni tra le pagine e il modello

3.6.6.1. Visualizzazione della pagina iniziale

Se tutto va bene, la prima pagina visualizzata è [form1.xhtml]. Il risultato è la seguente vista:

 

La pagina [form1.xhtml] è la seguente:


<?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>

Questa pagina è basata sul seguente modello:


@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
 
  // bean Application
  @Inject
  private Application application;
  // model
  private Long idMedecin;
  private Date jour = new Date();
  
// list of doctors
  public List<Medecin> getMedecins() {
    return application.getMedecins();
  }
  // agenda
  public void getAgenda() {
    ...
}
  • Il campo alla riga 9 legge e scrive il valore dell'elenco alla riga 18 della pagina. Al primo caricamento della pagina, imposta il valore selezionato nella casella combinata. Al caricamento iniziale, idMedecin è uguale a null, quindi verrà selezionato il primo medico.
  • il metodo alle righe 13–15 genera le voci per il menu a tendina dei medici (riga 19 della pagina). Ogni opzione generata avrà come etichetta (itemLabel) il titolo, il cognome e il nome del medico, e come valore (itemValue) l'ID del medico,
  • il campo alla riga 10 fornisce l'accesso in lettura/scrittura al campo di immissione alla riga 21 della pagina. Alla visualizzazione iniziale, viene mostrata la data corrente,
  • Righe 17–19: il metodo getAgenda gestisce il clic sul pulsante [Agenda] alla riga 26 della pagina. Poiché non c'è navigazione (è sempre la pagina [index.html] che viene richiesta), useremo spesso l'attributo actionListener invece dell'attributo action. In questo caso, il metodo chiamato nel modello non restituisce alcun risultato.

Quando si fa clic sul pulsante [Agenda],

  • vengono inviati dei valori: il valore selezionato nel menu a tendina dei medici viene memorizzato nel campo idMedecin del modello, e il giorno selezionato nel campo day;
  • viene chiamato il metodo getAgenda del modello.

Il metodo getAgenda è il seguente:


  // bean Application
  @Inject
  private Application application;
 
  // model
  private Long idMedecin;
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private String form2Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Medecin medecin;
  private List<Erreur> erreurs;
 
  // agenda
  public void getAgenda() {
    try {
      // we get the doctor back
      medecin = application.gethMedecins().get(idMedecin);
      // title form 2
      form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
      // the doctor's diary for a given day
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // form 2 is displayed
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
    }
  }
 
  // preparation vueErreur
  private void prepareVueErreur(Throwable th) {
    // create an error list
    erreurs = new ArrayList<Erreur>();
    erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
    while (th.getCause() != null) {
      th = th.getCause();
      erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
    }
// the error view is displayed
    setForms(false, false, false, true);
}

Rivediamo cosa dovrebbe visualizzare il metodo getAgenda:

  • riga 21: recuperiamo il medico selezionato dal dizionario dei medici memorizzato nel bean dell'applicazione. Per farlo, utilizziamo il suo ID, che è stato inviato a idMedecin,
  • riga 23: prepariamo il titolo della pagina [form2.xhtml] che verrà visualizzata. Questo messaggio viene recuperato dal file dei messaggi in modo da poter essere internazionalizzato. Questa tecnica è stata descritta nella sezione 2.8.5.7, pagina 135.
  • riga 25: chiamiamo il livello [business] per calcolare l'orario del medico selezionato per il giorno selezionato,
  • riga 27: viene visualizzato [form2.xhtml],
  • riga 28: se si verifica un'eccezione, viene generato un elenco di errori (righe 37–42) e viene visualizzata la pagina [error.xhtml] (riga 44).

3.6.6.2. Visualizza l'orario di un medico

La pagina [form2.xhtml] corrisponde alla seguente vista:

Il codice della pagina [form2.xhtml] è il seguente:


<?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>

Ricordiamo che il metodo getAgenda ha inizializzato due campi nel modello:


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

Questi due campi popolano la pagina [form2.xhtml]:

  • riga 10, il titolo della pagina,
  • riga 12: l'orario del medico viene visualizzato utilizzando un tag <h:dataTable> a tre colonne,
  • righe 13–18: la prima colonna mostra le fasce orarie,
  • righe 19–30: la seconda colonna visualizza il nome del cliente che potrebbe aver prenotato la fascia oraria, oppure nulla se non è stato prenotato. Per effettuare questa selezione, utilizziamo i tag della libreria JSTL Core a cui si fa riferimento alla riga 7,
  • righe 30–35: la terza colonna visualizza il link [Prenota] se la fascia oraria è disponibile e il link [Elimina] se la fascia oraria è prenotata.

I link nella terza colonna rimandano al seguente modello:


// modèle
  private Long idCreneau;
 
  // action sur RV
  public void action() {
    ...
}
  • Il metodo action viene chiamato quando l'utente fa clic sul link Prenota / Elimina (riga 32). Si noti che qui viene utilizzato l'attributo action. Il metodo indicato da questo attributo dovrebbe avere la firma String action(), poiché il metodo deve quindi restituire una chiave di navigazione. Tuttavia, qui è void action(). Ciò non ha causato un errore e possiamo supporre che in questo caso non vi sia navigazione. Questo è ciò che si intendeva. L'uso di actionListener al posto di action ha causato un malfunzionamento,
  • Il campo `idCreneau` alla riga 2 recupererà l'ID della fascia oraria associata al link su cui è stato cliccato (riga 33 della pagina).

3.6.6.3. Eliminazione di un appuntamento

Esaminiamo il codice che gestisce l'eliminazione di un appuntamento. Ciò corrisponde alla seguente sequenza di viste:

Il codice relativo a questa operazione è il seguente:


// bean Application
  @Inject
  private Application application;
 
  // model
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private AgendaMedecinJour agendaMedecinJour;
  private Long idCreneau;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;
 
  // action on RV
  public void action() {
    // search for the time slot in the calendar
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // have we found?
    if (!trouvé) {
      // it's weird - form2 is redisplayed
      setForms(false, true, false, false);
      return;
    }
    // we found
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // according to desired action
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // reservation
 
  public void reserver() {
    ...
  }
 
  public void supprimer() {
    try {
      // deleting an appointment
      application.getMetier().supprimerRv(creneauChoisi.getRv());
      // updating the agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // form2 is displayed
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
    }
  }
  • riga 16: all'avvio del metodo action, l'ID della fascia oraria selezionata è stato memorizzato in idCreneau (riga 11),
  • righe 18–26: tentiamo di recuperare la fascia oraria in base al suo ID (riga 21). La cerchiamo nel calendario corrente, agendaMedecinJour, dalla riga 10. Normalmente, dovremmo trovarla. In caso contrario, non facciamo nulla (righe 28–32),
  • riga 34: se la fascia oraria è stata trovata, ne recuperiamo un riferimento e lo memorizziamo nella riga 12,
  • riga 36: controlliamo se la fascia oraria selezionata aveva un appuntamento. In tal caso, lo eliminiamo (riga 39); altrimenti, ne prenotiamo uno (riga 37),
  • riga 51: l'appuntamento nella fascia oraria selezionata viene cancellato. Il livello [business] si occupa di questo,
  • riga 53: richiediamo l'agenda aggiornata del medico dal livello [business]. Ovviamente, vedremo un appuntamento in meno lì. Ma poiché l'applicazione è multiutente, potremmo vedere le modifiche apportate da altri utenti,
  • riga 55: la pagina [form2.xhtml] viene visualizzata nuovamente,
  • riga 58: poiché è stato chiamato il livello [business], potrebbero verificarsi delle eccezioni. In questo caso, memorizziamo lo stack delle eccezioni nell'elenco degli errori alla riga 13 e le visualizziamo utilizzando la vista [error.xhtml].

3.6.6.4. Pianificazione degli appuntamenti

La pianificazione degli appuntamenti segue questa sequenza:

Il modello coinvolto in questa azione è il seguente:


// model
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private String form3Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Medecin medecin;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;
 
  // action on RV
  public void action() {
...
    // we found
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // according to desired action
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // reservation
 
    public void reserver() {
    try {
      // title form 3
      form3Titre = Messages.getMessage(null, "form3.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour),
                creneauChoisi.getCreneau().getHdebut(), creneauChoisi.getCreneau().getMdebut(), creneauChoisi.getCreneau().getHfin(), creneauChoisi.getCreneau().getMfin()}).getSummary();
      // customer selected in combo
      idClient=null;
      // form 3 is displayed
      setForms(false, false, true, false);
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
    }
  }
  • riga 14: se la fascia oraria selezionata non ha alcun appuntamento, allora si tratta di una prenotazione,
  • riga 30: prepariamo il titolo della pagina [form3.xhtml] utilizzando la stessa tecnica usata per il titolo della pagina [form2.xhtml],
  • riga 34: in questo modulo c'è una casella combinata il cui valore è popolato da idClient. Impostiamo il valore di questo campo su null in modo che non sia selezionato nessuno,
  • riga 36: visualizziamo la pagina [form3.xhtml],
  • riga 39: oppure la pagina di errore se si è verificata un'eccezione.

La pagina [form3.xhtml] è la seguente:


<?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>

Questa pagina è basata sul seguente modello:


// bean Application
  @Inject
  private Application application;
 
  // model
  private Long idClient;
 
  // customer list
  public List<Client> getClients() {
    return application.getClients();
  }
  • riga 6: l'ID del cliente popola l'attributo value della casella combinata del cliente alla riga 12 della pagina. Imposta la voce selezionata della casella combinata,
  • righe 9–11: il metodo getClients popola il menu a tendina (riga 13). L'etichetta (itemLabel) per ciascuna opzione è [Titolo Nome Cognome] per il cliente, e il valore associato (itemValue) è l'ID del cliente. È quindi questo valore che verrà inviato.

3.6.6.5. Conferma di un appuntamento

La conferma di un appuntamento segue questa sequenza:

e corrisponde al clic sul pulsante [Conferma]:


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

Il metodo [Form].validerRv gestirà quindi questo evento. Il suo codice è il seguente:


  // bean Application
  @Inject
  private Application application;
  
  // model
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private Long idCreneau;
  private Long idClient;
  private List<Erreur> erreurs;
 
  // rv validation
  public void validerRv() {
    try {
      // retrieve an instance of the selected time slot
      Creneau creneau = application.getMetier().getCreneauById(idCreneau);
      // we add the Rv
      application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
      // updating the agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // form2 is displayed
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
    }
}
  • riga 12: Prima che il metodo `validerRv` venga eseguito, il campo `idClient` ha ricevuto l'ID del cliente selezionato dall'utente,
  • riga 19: utilizzando l'ID della fascia oraria memorizzato in un passaggio precedente (il bean è a livello di sessione), richiediamo un riferimento alla fascia oraria stessa dal livello [business],
  • riga 21: al livello [business] viene chiesto di aggiungere un appuntamento per il giorno selezionato (day), la fascia oraria selezionata (slot) e il cliente selezionato (idClient),
  • riga 23: chiediamo al livello [business] di aggiornare il calendario del medico. Vedremo l'appuntamento aggiunto insieme a eventuali modifiche apportate da altri utenti dell'applicazione,
  • riga 25: il calendario [form2.xhtml] viene visualizzato nuovamente,
  • riga 28: visualizza la pagina di errore se si verifica un errore.

3.6.6.6. Annullamento di un appuntamento

Ciò corrisponde alla seguente sequenza:

Il pulsante [Annulla] nella pagina [form3.xhtml] è il seguente:


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

Viene quindi chiamato il metodo [Form].cancelAppointment:


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

3.6.6.7. Torna alla home page

C'è un'altra azione da esaminare, nella sequenza seguente:

Il codice per il pulsante [Home] nella pagina [form2.xhtml] è il seguente:


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

Il metodo [Form].accueil è il seguente:


  public void accueil() {
    // on affiche la page d'accueil
    setForms(true, false, false, false);
}

3.7. Conclusione

Abbiamo realizzato la seguente applicazione:

Ci siamo concentrati sulle funzionalità dell'applicazione piuttosto che sulla sua interfaccia utente. Quest'ultima verrà migliorata utilizzando la libreria di componenti PrimeFaces. Abbiamo realizzato un'applicazione di base che rappresenta comunque un'architettura Java EE a livelli che utilizza gli EJB. L'applicazione può essere migliorata in vari modi:

  • è richiesta l'autenticazione. Non tutti sono autorizzati ad aggiungere o eliminare appuntamenti,
  • dovremmo poter scorrere il calendario in avanti e indietro quando cerchiamo un giorno con slot disponibili,
  • dovrebbe essere possibile richiedere un elenco dei giorni in cui un medico ha slot disponibili. Infatti, se il medico è un oftalmologo, gli appuntamenti vengono in genere prenotati con sei mesi di anticipo,
  • ...

3.8. Test con Eclipse

3.8.1. Il livello [DAO]

  • In [1], importare il progetto EJB per il livello [DAO] e il relativo client,
  • in [2], selezioniamo il progetto EJB del livello [DAO] ed eseguiamolo [3],
  • in [4], eseguirlo su un server,
  • In [5], viene offerto solo il server Glassfish perché è l'unico dotato di un contenitore EJB,
  • In [6], il modulo EJB è stato distribuito,
  • In [7], vengono visualizzati i log:
1
2
3
4
5
6
7
8
Infos: Hibernate Validator 4.2.0.Final

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

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

Questi sono quelli che avevamo con NetBeans.

  • In [7A] [7B] eseguiamo il test JUnit del cliente,
  • in [8], il test viene superato,
  • in [9], la console registra i log.

In [10], l'applicazione EJB viene scaricata.

3.8.2. Il livello [business]

  • In [1], importare i quattro progetti Maven dal livello [business],
  • In [2], seleziona il progetto enterprise ed eseguilo In [3], su un server GlassFish [4] [5],
  • in [6], il progetto enterprise è stato distribuito su GlassFish,
  • In [7], esaminiamo i log di GlassFish,
1
2
3
4
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: EJB5181:Portable JNDI names for EJB Metier: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]

Alla riga 3, prendiamo nota del nome portabile dell'EJB [Metier] e lo incolliamo nella console client per questo EJB:


public class ClientRdvMedecinsMetier {
 
  // the remote interface name of the EJB [Metier]
  private static String IDaoRemoteName = "java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
  // today's date
private static Date jour = new Date();
  • In [8], eseguiamo il client da console,
  • in [9], i relativi log.
  • in [10], l'applicazione aziendale viene scaricata;

3.8.3. Il livello [web]

  • In [1], importiamo i tre progetti Maven dal livello [web]. Quello con estensione .ear è il progetto enterprise che deve essere distribuito su GlassFish,
  • in [2], lo eseguiamo,
  • sul server GlassFish [3],
  • in [4], l'applicazione aziendale è stata distribuita con successo,
  • in [5], inseriamo l'URL dell'applicazione nel browser interno di Eclipse.