Skip to content

3. Applicazione di esempio – 01: rdvmedecins-jsf2-ejb

Il testo che segue fa riferimento, all'indirizzo , ai seguenti documenti:

  • [ref7]: Introduzione a Java EE 5 (giugno 2010) [http://tahe.developpez.com/java/javaee]. Questo documento permette di scoprire JSF 1 e i EJB3.
  • [ref8]: Persistenza Java nella pratica (giugno 2007) [http://tahe.developpez.com/java/jpa]. Questo documento permette di scoprire la persistenza dei dati con JPA (Java Persistence API).
  • [ref9]: Realizzazione di un servizio web Java EE con NetBeans e il server GlassFish (gennaio 2009) [http://tahe.developpez.com/java/webservice-jee]. Questo documento illustra la realizzazione di un servizio web.

L'applicazione di esempio che verrà esaminata proviene da [ref9].

3.1. L'application

Un’azienda di servizi informatici [ISTIA-AGI] desidera offrire un servizio di prenotazione appuntamenti. Il primo mercato di riferimento è quello dei medici che esercitano in proprio. Questi ultimi, in genere, non dispongono di segreteria. I clienti che desiderano fissare un appuntamento telefonano quindi direttamente al medico. Quest’ultimo viene così disturbato frequentemente nel corso della giornata, il che riduce la sua disponibilità nei confronti dei pazienti. L’azienda [ISTIA-AGI] intende proporre loro un servizio di prenotazione appuntamenti che funziona secondo il seguente principio:

  • un servizio di segreteria gestisce le prenotazioni per un gran numero di medici. Tale servizio può essere gestito da una sola persona, il cui stipendio viene ripartito tra tutti i medici che utilizzano il servizio.
  • la segreteria e tutti i medici sono collegati a Internet
  • gli appuntamenti RV vengono registrati in un database centralizzato, accessibile via Internet sia dalla segreteria che dai medici
  • La registrazione dei RV viene normalmente effettuata dalla segreteria. Può essere effettuata anche dai medici stessi. Ciò avviene in particolare quando, al termine di una visita, il medico assegna personalmente un nuovo RV al proprio paziente.

L’architettura del servizio di registrazione del codice RV è la seguente:

I medici guadagnano in efficienza se non devono più gestire i RV. Se sono in numero sufficiente, il loro contributo alle spese di funzionamento della segreteria sarà modesto.

La società [ISTIA-AGI] decide di realizzare l’applicazione in due versioni:

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

3.2. Funzionamento dell'applicazione

Chiameremo l'applicazione [RdvMedecins]. Di seguito presentiamo alcune schermate che ne illustrano il funzionamento.

La pagina iniziale dell’applicazione è la seguente:

Da questa prima pagina, l’utente (Segreteria, Medico) potrà eseguire una serie di azioni. Le illustriamo di seguito. La schermata a sinistra mostra la pagina da cui l’utente effettua una richiesta, quella a destra la risposta inviata dal server.

Infine, è possibile ottenere 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: numero identificativo del medico - chiave primaria della tabella
  • 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.
  • NOM: il cognome del medico
  • PRENOM: il suo nome
  • TITRE: il suo titolo (Sig.na, Sig.ra, Sig.)

3.3.2. La tabella [CLIENTS]

I pazienti dei diversi medici sono registrati nella tabella [CLIENTS]:

  • ID: numero identificativo del cliente - chiave primaria della tabella
  • 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.
  • NOM: il nome del cliente
  • PRENOM: il suo nome
  • TITRE: il suo titolo (Sig.na, Sig.ra, Sig.)

3.3.3. La tabella [CRENEAUX]

Elenca le fasce orarie in cui sono possibili i RV:

  • 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.
  • ID_MEDECIN: numero identificativo del medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna MEDECINS (ID).
  • HDEBUT: ora di inizio della fascia oraria
  • MDEBUT: minuti di inizio della fascia oraria
  • HFIN: ora di fine della fascia oraria
  • MFIN: minuti di fine della fascia oraria

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

3.3.4. La tabella [RV]

Elenca i RV assegnati a ciascun medico:

  • ID: numero che identifica in modo univoco il RV – chiave primaria
  • JOUR: giorno del RV
  • ID_CRENEAU: fascia oraria del RV – chiave esterna sul campo [ID] della tabella [CRENEAUX] – determina sia la fascia oraria che il medico interessato.
  • ID_CLIENT: numero del cliente per il quale è stata effettuata la prenotazione – chiave esterna sul campo [ID] della tabella [CLIENTS]

Questa tabella presenta un e vincolo di unicità sui valori delle colonne collegate (JOUR, ID_CRENEAU):

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

Se una riga della tabella [RV] presenta il valore (JOUR1, ID_CRENEAU1) per le colonne (JOUR, ID_CRENEAU), tale valore non può comparire in nessun altro punto. Altrimenti, ciò significherebbe che due RV sono stati registrati contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, il driver JDBC del database avvia un SQLException quando si verifica questo caso.

La riga di id pari a 3 (cfr. [1] sopra) indica che un RV è stato prenotato per la fascia oraria n. 20 e il cliente n. 4 il 23/08/2006. La tabella [CRENEAUX] ci indica che la fascia n. 20 corrisponde alla fascia oraria 16:20 - 16:40 e appartiene al medico n. 1 (la sig.ra Marie PELISSIER). La tabella [CLIENTS] ci indica che il cliente n. 4 è la signorina Brigitte BISTROU.

3.3.5. Generazione del database

Per creare le tabelle e compilarle è possibile utilizzare lo script [dbrdvmedecins2.sql], disponibile nella sezione degli esempi del sito. Con [WampServer] (cfr. paragrafo 1.3.3), è possibile procedere come segue:

  • in [1], cliccare sull’icona di [WampServer] e selezionare l’opzione [PhpMyAdmin] [2],
  • in [3], nella finestra che si è aperta, selezionare il link [Bases de données],
  • in [2], si crea un database a cui è stato assegnato il nome [4] e la codifica [5],
  • in [7], il database è stato creato. Si fa clic sul relativo link,
  • in [8], si importa un file SQL,
  • che si seleziona nel file system tramite il pulsante [9],
  • in [11], si seleziona lo script SQL e in [12] lo si esegue,
  • in [13], le quattro tabelle del database sono state create. Si segue uno dei link,
  • in [14], il contenuto della tabella.

In seguito, non torneremo più su questo database. Il lettore è tuttavia invitato a seguirne l’evoluzione nel corso dei programmi, soprattutto quando qualcosa non funziona.

3.4. I livelli [DAO] e [JPA]

Torniamo all’architettura che dobbiamo costruire:

Realizzeremo quattro progetti Maven:

  • un progetto per gli strati [DAO] e [JPA],
  • un progetto per lo strato [métier],
  • un progetto per il livello [web],
  • un progetto aziendale che riunirà i tre progetti precedenti.

Ora creiamo il progetto Maven dei livelli [DAO] e [JPA].

Nota: la comprensione dei livelli [métier], [DAO], [JPA] richiede conoscenze di Java EE. A tal fine, è possibile consultare [ref7] (cfr. paragrafo 3).

3.4.1. Il progetto NetBeans

È il seguente:

  • in [1], si crea un progetto Maven di tipo [EJB Module] [2],
  • in [3], si assegna un nome al progetto,
  • in [4], si sceglie come 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. È interessante conoscere 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.

Descriviamo ora alcuni di questi strumenti di generazione automatica. Per comprendere il codice generato, è necessario avere una buona conoscenza delle entità JPA, [ref8] e delle entità EJB, [ref7] (cfr. paragrafo 3).

3.4.2.1. Creazione di una connessione NetBeans al database

  • avviare SGBD e MySQL 5 affinché BD sia disponibile,
  • creare una connessione NetBeans al database [dbrdvmedecins2],
  • nella scheda [Services] [1], nel ramo [Databases] [2], selezionare il driver JDBC MySQL [3],
  • quindi selezionare l'opzione [4] "Connect Using" che consente di creare una connessione con un database MySQL,
  • in [5], inserire le informazioni richieste. In [6], il nome del database, in [7] l'utente del database e la sua password,
  • in [8] è possibile verificare le informazioni fornite,
  • in [9], il messaggio che si dovrebbe ricevere se i dati sono corretti,
  • in [10], la connessione è stata stabilita. Qui si vedono le quattro tabelle del database a cui ci si è collegati.

3.4.2.2. Creazione di un'unità di persistenza

Torniamo all’architettura in fase di realizzazione:

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

  • le caratteristiche JDBC di accesso al database (URL, utente, password),
  • le classi che fungeranno da immagini delle tabelle del database,
  • l’implementazione JPA utilizzata. Infatti, JPA è una specifica implementata da diversi prodotti. In questo caso, utilizzeremo EclipseLink, che è l'implementazione predefinita utilizzata dal server GlassFish. Ciò ci evita di aggiungere a GlassFish le librerie di un'altra implementazione.

NetBeans può generare questo file di persistenza tramite un wizard.

  • fare clic con il tasto destro del mouse sul progetto e selezionare la creazione di un'unità di persistenza [1],
  • in [2], assegnare un nome all’unità di persistenza che si sta creando,
  • in [3], selezionare l’implementazione JPA EclipseLink (JPA 2.0),
  • in [4], indicare che le transazioni con il database saranno gestite dal contenitore EJB del server Glassfish,
  • in [5], specificare che le tabelle di BD sono già state create e che quindi non vengono create nuovamente,
  • in [6], creare una nuova fonte di dati per il server Glassfish,
  • in [7], assegnare un nome JNDI (Java Naming Directory Interface),
  • in [8], collegare questo nome alla connessione MySQL creata nella fase 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>

Riporta le informazioni fornite nella procedura guidata:

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

Normalmente, in questo file è indicato il tipo di implementazione JPA utilizzata. Nella procedura guidata abbiamo indicato EclipseLink. Poiché si tratta dell'implementazione JPA utilizzata di default dal server Glassfish, non è menzionata nel file [persistence.xml].

Nella scheda [Design] è possibile avere una visione d'insieme del file [persistence.xml]:

Per ottenere i log di EclipseLink, utilizzeremo 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: si indica che si utilizza l’implementazione JPA di EclipseLink,
  • righe 7-9: raccolgono le proprietà di configurazione del provider JPA, in questo caso EclipseLink,
  • riga 8: questa proprietà consente di registrare gli ordini SQL che verranno emessi da EclipseLink.

Il file [glassfish-resources.xml] che è stato 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 riporta le informazioni che abbiamo fornito nei due assistenti utilizzati in precedenza:

  • righe 5-11: le caratteristiche JDBC del database MySQL5 [dbrdvmedecins2],
  • riga 13: il nome JNDI della fonte dati.

Questo file verrà utilizzato per creare la fonte dati JNDI [jdbc/dbrdvmedecins2] del server Glassfish. Si tratta di un’operazione specifica per questo server. Per un altro server, occorrerebbe procedere in modo diverso, generalmente tramite uno strumento di amministrazione. Tale strumento esiste anche per Glassfish.

Infine, sono state aggiunte alcune 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 artifatti hanno l'attributo provided. Si ricorda che ciò significa che sono necessari per la compilazione ma non per l’esecuzione. Infatti, durante l’esecuzione, vengono forniti (provided) dal server Glassfish,
  • righe 41-48: definiscono un nuovo repository di artefatti Maven, quello in cui si trovano gli artefatti EclipseLink.

3.4.2.3. Generazione delle entità JPA

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

  • in [1], si creano le entità JPA da un database,
  • in [2], si seleziona la fonte dati [jdbc / dbrdvmedecins2] creata in precedenza,
  • in [3], l'elenco delle tabelle di questa fonte di dati,
  • in [4], le si selezionano tutte,
  • in [5], le tabelle selezionate,
  • in [6], si assegna un nome alle classi Java associate alle quattro tabelle,
  • nonché un nome di pacchetto [7],
  • in [8], JPA raggruppa le righe delle tabelle di BD in collezioni. Scegliamo la lista come collezione,
  • in [9], le classi Java create dall'assistente.

3.4.2.4. Le entità JPA generate

L’entità [Medecin] è la rappresentazione della tabella [medecins]. La classe Java è piena di annotazioni che rendono il codice poco leggibile a prima vista. Se si mantiene solo ciò che è essenziale per comprendere il ruolo dell’entità, si ottiene 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;

// costruttori
....

   // getter e setter
....

  @Override
  public int hashCode() {
  ...
  }

  @Override
  public boolean equals(Object object) {
  ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • alla riga 4, l’annotazione @Entity rende la classe [Medecin] un’entità JPA, c.a.d. una classe collegata a una tabella di BD tramite API e JPA,
  • riga 5, il nome della tabella BD associata all’entità JPA. Ogni campo della tabella corrisponde a un campo nella 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 titolo della classe [Medecin] corrisponde al campo [TITRE] (riga 13) della tabella [medecins],
  • righe 16-17: il campo «nome» della classe [Medecin] corrisponde al campo [NOM] (riga 16) della tabella [medecins],
  • righe 19-20: il campo "versione" della classe [Medecin] corrisponde al campo [VERSION] (riga 19) della tabella [medecins]. In questo caso, l’assistente non riconosce che la colonna è in realtà una colonna di versione che deve essere incrementata ad ogni modifica della riga a cui appartiene. Per assegnarle questo ruolo, è necessario aggiungere l’annotazione @Version. Lo faremo in un passaggio successivo,
  • righe 22-23: il campo prenom della classe [Medecin] corrisponde al campo [PRENOM] della tabella [medecins],
  • righe 10-11: il campo id corrisponde alla chiave primaria [ID] della tabella. Le annotazioni delle righe 8-9 precisano 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 delle righe che inserirà nella tabella [Medecins]. Esistono diverse strategie possibili. In questo caso, la strategia GenerationType.IDENTITY indica che il livello JPA utilizzerà la modalità auto_increment della tabella MySQL,
  • righe 25-26: la tabella [creneaux] ha una chiave esterna sulla tabella [medecins]. Una fascia oraria appartiene a un medico. Viceversa, a un medico sono associate diverse fasce orarie. Si ha quindi una relazione uno (medico) a molti (slot), una relazione qualificata dall’annotazione @OneToMany da JPA (riga 25). Il campo della riga 26 conterrà tutte le fasce orarie del medico. Ciò avviene senza alcuna programmazione. Per comprendere appieno la riga 25, è necessario presentare 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;

// costruttori
...
// getter e setter
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}

Commentiamo solo le nuove annotazioni:

  • abbiamo detto che la tabella [creneaux] aveva una chiave esterna verso la tabella [medecins]: una fascia oraria è associata a un medico. Più fasce orarie possono essere associate allo stesso medico. Esiste una relazione dalla tabella [creneaux] alla tabella [medecins] che è definita come "molti (slot)" a "uno (medico)". È l'annotazione @ManyToOne della riga 32 che serve a definire la chiave esterna,
  • mentre la riga 31, con l’annotazione @JoinColumn, specifica la relazione della chiave esterna: la colonna [ID_MEDECIN] della tabella [creneaux] è chiave esterna sulla colonna [ID] della tabella [medecins],
  • riga 33: un riferimento al medico titolare della fascia oraria. Anche in questo caso lo si ottiene senza programmazione.

Il collegamento tramite chiave esterna tra l’entità [Creneau] e l’entità [Medecin] è quindi concretizzato da due annotazioni:

  • nell’entità [Creneau]:

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

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

Le due annotazioni riflettono la stessa relazione: quella della chiave esterna dalla tabella [creneaux] alla tabella [medecins]. Si dice che siano inverse l'una rispetto all'altra. Solo la relazione @ManyToOne è indispensabile. Essa definisce in modo inequivocabile la relazione di chiave esterna. La relazione @OneToMany è facoltativa. Se presente, si limita a fare riferimento alla relazione @ManyToOne a cui è associata. Questo è il significato dell’attributo mappedBy della riga 1 dell’entità [Medecin]. Il valore di questo attributo è il nome del campo dell’entità [Creneau] che presenta l’annotazione @ManyToOne, la quale specifica la chiave esterna. Sempre nella stessa riga 1 dell’entità [Medecin], l’attributo cascade=CascadeType.ALL definisce il comportamento dell’entità [Medecin] rispetto all’entità [Creneau]:

  • se si inserisce una nuova entità [Medecin] nel database, allora devono essere inserite anche le entità [Creneau] del campo della riga 2,
  • se si modifica un'entità [Medecin] nel database, allora devono essere modificate anche le entità [Creneau] del campo della riga 2,
  • se si elimina un'entità [Medecin] dal database, allora devono essere eliminate anche le entità [Creneau] del campo della riga 2.

Forniamo il codice delle altre due entità senza commenti particolari, poiché non introducono nuove notazioni.

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;

// costruttori
...
// getter e setter
...

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

   // costruttori
...

   // getter e setter
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • la riga 13 descrive il campo «giorno» di tipo Java Date. Si indica che nella tabella [rv], la colonna [JOUR] (riga 12) è di tipo data (senza ora),
  • righe 16-18: definiscono la relazione di chiave esterna che la tabella [rv] ha con la tabella [creneaux],
  • righe 20-22: definiscono la relazione di chiave esterna che la tabella [rv] ha con la tabella [clients].

La generazione automatica delle entità JPA ci permette di ottenere una base di lavoro. A volte è sufficiente, altre volte no. È il caso in questione:

  • è necessario aggiungere l’annotazione @Version ai vari campi versione delle entità,
  • è necessario scrivere metodi toString più espliciti di quelli generati,
  • le entità [Medecin] e [Client] sono analoghe. Le faremo derivare da una classe [Personne],
  • elimineremo le relazioni inverse @OneToMany delle relazioni @ManyToOne. Non sono indispensabili e comportano complicazioni di programmazione,
  • eliminiamo la validazione @NotNull sulle chiavi primarie. Quando si salva un'entità JPA insieme a MySQL, l'entità iniziale ha una chiave primaria null. È solo dopo la persistenza nel database che la chiave primaria dell’elemento persistito assume un valore.

Con queste specifiche, le diverse classi diventano le seguenti:

La classe Persona 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;
// costruttori
...

// getter e setter
  ...

  @Override
  public String toString() {
    return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
  }
  
}
  • riga 8: si noti che la classe [Personne] non è essa stessa un'entità (@Entity). Sarà la classe padre delle entità. L'annotazione @MappedSuperClass indica questa situazione.

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


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
  private static final long serialVersionUID = 1L;

// costruttori
...

  @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 [Personne].

L'entità [Medecin], che incapsula le righe della tabella [medecins], segue lo stesso modello:


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;

  // costruttori
...

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

  // costruttori
  ...

  // getter e setter
  ...
 
  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    // TODO: Avviso - questo metodo non funzionerà nel caso in cui i campi id non siano impostati
    ...
  }

  @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» esistente tra la tabella [creneaux] e la tabella [medecins] del database: un medico ha più fasce orarie, una fascia oraria 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;

   // costruttori
...

   // getter e setter
...

  @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» esistente tra la tabella [rv] e la tabella [clients] (un cliente può comparire in più Rv) del database, mentre le righe 25-27 modellano la relazione "molti a uno" esistente tra la tabella [rv] e la tabella [creneaux] (una fascia oraria può comparire in più Rv).

3.4.3. La classe di eccezione

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


package rdvmedecins.exceptions;

import java.io.Serializable;
import javax.ejb.ApplicationException;

@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException implements Serializable{

  // campi privati
  private int code = 0;

  // costruttori
  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);
  }

  // getter e setter
  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • riga 7: la classe deriva dalla classe [RuntimeException]. Il compilatore non impone quindi di gestirla con try / catch.
  • riga 6: l'annotazione @ApplicationException fa sì che l'eccezione non venga "trascurata" da un'eccezione di tipo [EjbException].

Per comprendere l’annotazione @ApplicationException, torniamo all’architettura utilizzata sul lato server:

L’eccezione di tipo [RdvMedecinsException] verrà generata dai metodi di EJB del livello [DAO] all’interno del contenitore EJB3 e da quest’ultimo intercettata. Senza l'annotazione @ApplicationException, il contenitore EJB3 incapsula l'eccezione verificatasi in un'eccezione di tipo [EjbException] e la rilancia. Si potrebbe non volere tale incapsulamento e lasciare che dal contenitore EJB3 fuoriesca un'eccezione di tipo [RdvMedecinsException]. È ciò che consente l'annotazione @ApplicationException. Inoltre, l’attributo (rollback=true) di questa annotazione indica al contenitore EJB3 che, se l’eccezione di tipo [RdvMedecinsException] si verifica all’interno di un metodo eseguito in una transazione con un SGBD, questa deve essere annullata. In termini tecnici, ciò si definisce «eseguire un rollback» della transazione.

3.4.4. L’ e EJB del livello [DAO]

L' e 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 {

  // elenco dei clienti
  public List<Client> getAllClients();
  // elenco dei medici
  public List<Medecin> getAllMedecins();
  // elenco delle fasce orarie di un medico
  public List<Creneau> getAllCreneaux(Medecin medecin);
  // elenco degli appuntamenti di un medico in un determinato giorno
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
  // trovare un cliente identificato dal suo ID
  public Client getClientById(Long id);
  // trovare un cliente identificato dal suo ID
  public Medecin getMedecinById(Long id);
  // trovare un appuntamento identificato dal proprio ID
  public Rv getRvById(Long id);
  // trovare una fascia oraria identificata dal proprio ID
  public Creneau getCreneauById(Long id);
  // aggiungere un RV
  public Rv ajouterRv(Date jour, Creneau creneau, Client client);
  // eliminare un RV
  public void supprimerRv(Rv rv);
}

Questa interfaccia è stata realizzata dopo aver identificato le esigenze 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 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, ci consentirà di visualizzare l'agenda del medico per un determinato giorno con le fasce orarie già prenotate,
  • riga 22: consente di individuare un cliente in base al suo numero. Il metodo ci permetterà di individuare un cliente selezionandolo dall’elenco a discesa dei clienti,
  • riga 24: lo stesso vale per i medici,
  • riga 26: individua un appuntamento in base al suo numero. Può essere utilizzato quando si elimina un appuntamento per verificare preventivamente che esista effettivamente,
  • riga 28: individua una fascia oraria in base al suo numero. Consente di identificare la fascia che un utente desidera aggiungere o eliminare,
  • riga 30: per aggiungere un appuntamento,
  • riga 32: per eliminare un appuntamento.

L'interfaccia locale [IDaoLocal] di EJB si limita a derivare 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'interfaccia EJB [DaoJpa] implementa entrambe le interfacce, locale e 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 porta il nome "rdvmedecins.dao". Inoltre, l'annotazione @Singleton (Java EE6) fa sì che venga creata una sola istanza di EJB. L'annotazione @Stateless (Java EE5) definisce un EJB che può essere creato in più istanze per alimentare un pool di EJB,
  • la riga 6 indica che tutti i metodi di EJB vengono eseguiti all'interno di una transazione gestita dal contenitore EJB3,
  • la riga 7 mostra che EJB implementa le interfacce locale e remota ed è anche serializzabile.

Il codice completo di EJB è 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;

  // elenco dei clienti
  public List<Client> getAllClients() {
    try {
      return em.createQuery("select rc from Client rc").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 1);
    }
  }

  // elenco dei medici
  public List<Medecin> getAllMedecins() {
    try {
      return em.createQuery("select rm from Medecin rm").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 2);
    }
  }

  // elenco delle fasce orarie di un determinato medico
  // medico: il medico
  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);
    }
  }

  // elenco degli appuntamenti di un determinato medico, in un determinato giorno
  // medico: il medico
  // giorno: il giorno
  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);
    }
  }

  // aggiunta di un appuntamento
  // giorno: giorno dell'appuntamento
  // fascia oraria: fascia oraria dell'appuntamento
  // cliente: cliente per il quale è stato fissato l’appuntamento
  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);
    }
  }

  // eliminazione di un appuntamento
  // appuntamento: l'appuntamento cancellato
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }

  // recuperare un determinato cliente
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // recuperare un determinato medico
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // recuperare un appuntamento specifico
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // recuperare una fascia oraria specifica
  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. All'istanziazione della classe, questo campo verrà inizializzato dal contenitore EJB grazie all'annotazione @PersistenceContext della riga 21,
  • riga 27: query JPQL (Java Persistence Query Language) che restituisce tutte le righe della tabella [clients] sotto forma di un elenco di oggetti [Client],
  • riga 36: query analoga per i medici,
  • riga 46: una query JPQL che esegue un join tra le tabelle [creneaux] e [medecins]. È parametrizzata dall’ID del medico,
  • riga 57: una query JPQL che esegue un join tra le tabelle [rv], [creneaux] e [medecins] e che ha due parametri: l'ID del medico e il giorno dell'appuntamento,
  • righe 69-73: creazione di un appuntamento e successivo salvataggio dello stesso 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: idem per un medico,
  • riga 110: lo stesso per un appuntamento,
  • riga 119: idem per una fascia oraria,
  • tutte le operazioni che utilizzano il contesto di persistenza em della riga 22 potrebbero incontrare un problema con il database. Pertanto, sono tutte racchiuse in un try / catch. L'eventuale eccezione è incapsulata nell'eccezione "interna" RdvMedecinsException.

3.4.5. Implementazione del driver JDBC da MySQL

Nell’architettura riportata di seguito:

EclipseLink richiede il driver JDBC di MySQL. È necessario installarlo nelle librerie del server Glassfish nella cartella <glassfish>/domains/domain1/lib/ext, dove <glassfish> è la cartella di installazione del server Glassfish. È possibile ottenerlo nel modo seguente:

La cartella in cui inserire il driver JDBC di MySQL è <cartella Domains>[1]/domain1/lib/ext [2]. Questo driver è disponibile in 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 EJB del livello [DAO]

Torniamo all’architettura realizzata finora:

Il pacchetto [web, métier, DAO, JPA] deve essere distribuito sul server Glassfish. Procediamo:

  • in [1], si compila il progetto Maven,
  • in [2], lo si esegue,
  • in [3], è stato distribuito sul server Glassfish (scheda [Services])

Si può essere curiosi di dare un'occhiata ai 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 identificate da [Config] e [Précis] sono i log di EclipseLink, mentre quelle identificate da [Infos] 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 è avvenuta normalmente,
  • riga 18: EclipseLink si segnala,
  • riga 19: EclipseLink riconosce di avere a che fare con SGBD MySQL,
  • righe 20-24: EclipseLink tenta di connettersi a BD,
  • righe 25-28: ci è riuscito,
  • righe 29-33: sta tentando di riconnettersi, questa volta utilizzando specificatamente una piattaforma MySQL (riga 30),
  • righe 34-37: anche in questo caso l'operazione ha avuto esito positivo,
  • riga 38: conferma che l’unità di persistenza [dbrdvmedecins-PU] è stata istanziata,
  • riga 39: i nomi portabili delle interfacce remota e locale di EJB e [DaoJpa], dove «portabile» significa che sono riconosciuti da tutti i server di applicazioni Java EE 6,
  • riga 40: i nomi delle interfacce remote e locali di EJB e [DaoJpa], specifici per Glassfish. Nel test che seguirà useremo il nome "rdvmedecins.dao".

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

3.4.7. Test del EJB del livello [DAO]

Ora che il EJB del livello [DAO] della nostra applicazione è stato implementato, possiamo testarlo. Lo faremo nell’ambito di un’applicazione client/server:

Il client testerà l'interfaccia remota del 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 inseriamo nella stessa cartella di EJB e [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'avevamo ancora incontrato. In questo ramo è possibile inserire dei test JUnit. Lo faremo. Non manterremo la classe di test [AppTest] generata,
  • in [8], le dipendenze del progetto Maven. Il ramo [Dependencies] è vuoto. Dovremo inserirvi nuove dipendenze. Il ramo [Test Dependencies] raccoglie le dipendenze necessarie per i test. Qui, la libreria utilizzata è quella del framework JUnit 3.8. Dovremo cambiarla.

Il progetto si evolve come segue:

  • in [1], il progetto in cui sono state eliminate le due classi generate e la dipendenza JUnit.

Torniamo all’architettura client/server che verrà utilizzata per il test:

Il client deve conoscere l’interfaccia remota offerta da EJB e [DAO]. Inoltre, scambierà con EJB le entità JPA. Ha quindi bisogno della definizione di tali entità. Affinché il progetto di test del EJB abbia accesso a queste informazioni, aggiungeremo il progetto del EJB [DAO] come dipendenza al progetto:

  • in [1], si aggiunge una dipendenza dal ramo [Test Dependencies],
  • in [2], si seleziona la scheda [Open Projects],
  • in [3], si seleziona il progetto Maven di EJB [DAO],
  • in [4], la dipendenza 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 di applicazioni, 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], si specificano le caratteristiche dell’artefatto desiderato,
  • in [3] vengono aggiunte numerose dipendenze. Maven le scaricherà. L'operazione potrebbe richiedere diversi minuti. Successivamente vengono memorizzate nel repository locale di Maven.

Ora possiamo creare il test JUnit:

  • in [2], si fa clic con il tasto destro su [Test Packages] per creare un nuovo test JUnit,
  • in [3], si assegna un nome alla classe di test e un pacchetto per essa [4],
  • in [5], si sceglie 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>

Da notare:

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

  // livello [dao] testato
  private static IDaoRemote dao;
  // data odierna
  Date jour = new Date();

  @BeforeClass
  public static void init() throws NamingException {
    // inizializzazione dell'ambiente JNDI
    InitialContext initialContext = new InitialContext();
    // istanziazione del livello DAO
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }

  @Test
  public void test1() {
    // visualizzazione clienti
    List<Client> clients =dao.getAllClients();
    display("Liste des clients :", clients);
    // visualizzazione medici
    List<Medecin> medecins =dao.getAllMedecins();
    display("Liste des médecins :", medecins);
    // visualizzazione delle fasce orarie di un medico
    Medecin medecin = medecins.get(0);
    List<Creneau> creneaux = dao.getAllCreneaux(medecin);
    display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
    // elenco degli appuntamenti di un medico in un determinato giorno
    display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // aggiungere un 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));
    // aggiungere un RV nella stessa fascia oraria dello stesso giorno
    // deve generare un'eccezione
    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();
      }
      // si registra l'errore
      erreur=true;
    }
    // si verifica che si sia verificato un errore
    Assert.assertTrue(erreur);
    // elenco dei RV
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // elimina un 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));
  }

  // metodo di utilità - visualizza gli elementi di una collezione
  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 contrassegnato con @BeforeClass viene eseguito prima di tutti gli altri. Qui si crea un riferimento all'interfaccia remota di EJB [DaoJpa]. Ricordiamo che gli era stato assegnato il nome JNDI "rdvmedecins.dao",
  • righe 34-35: visualizzano l’elenco dei clienti,
  • righe 37-38: visualizzano l'elenco dei medici,
  • righe 40-42: visualizzano le fasce orarie del primo medico,
  • riga 44: visualizza gli appuntamenti del primo medico per il giorno indicato nella riga 21,
  • righe 46-51: aggiungono un appuntamento al primo medico, per la sua fascia oraria n. 2 e il giorno indicato nella riga 21,
  • riga 52: visualizza, a scopo di verifica, gli appuntamenti del primo medico per il giorno indicato nella riga 21. Ne deve esserci almeno uno, ovvero quello appena aggiunto,
  • righe 55-70: si aggiunge lo stesso appuntamento. Poiché la tabella [RV] ha un vincolo di unicità, questa aggiunta deve generare un'eccezione. Ce ne assicuriamo alla riga 70,
  • riga 72: visualizziamo a scopo di verifica gli appuntamenti del primo medico per il giorno indicato nella riga 21. Quello che volevamo aggiungere non deve essere presente,
  • righe 74-76: si elimina l'unico appuntamento che è stato aggiunto,
  • riga 77: vengono visualizzati, a scopo di verifica, gli appuntamenti del primo medico per il giorno indicato alla riga 21. Quello appena eliminato non deve essere presente.

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

Se tutto va bene, i test dovrebbero superare:

  • in [1], si crea il progetto di test,
  • in [2], si esegue il test,
  • in [3], il test è stato superato.

Diamo un'occhiata più da vicino ai risultati 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 soffermeremo sull’eccezione verificatasi durante l’aggiunta di un appuntamento già esistente, righe 41-49. La pila delle eccezioni è riportata alle righe 42-48. Si tratta di un evento inaspettato. Torniamo al codice del metodo di aggiunta di un appuntamento:


  // Aggiunta di un appuntamento
  // giorno: giorno dell'appuntamento
  // fascia oraria: fascia oraria dell'appuntamento
  // cliente: cliente per il quale è stato fissato l'appuntamento
  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: il comando INSERT che verrà eseguito. Si noti che non avviene contemporaneamente all’operazione persist. Se così fosse, questo log sarebbe apparso prima della riga 2. L’operazione INSERT avviene quindi normalmente alla fine della transazione in cui viene eseguito il metodo,
  • riga 6: EclipseLink chiede a MySQL quale sia l’ultima chiave primaria utilizzata. Otterrà la chiave primaria dell’appuntamento aggiunto. Questo valore alimenterà il campo id dell’entità [Rv] persistita,
  • righe 7-8: la query SELECT che visualizzerà gli appuntamenti del medico,
  • righe 9-10: le visualizzazioni a schermo del secondo persist,
  • righe 11-12: il comando INSERT che verrà eseguito. Deve generare un'eccezione. Questa appare alle righe 15-16 ed è chiara. Viene generata inizialmente dal driver JDBC di MySQL a causa della violazione del vincolo di unicità degli appuntamenti. Ne deduciamo che dovremmo vedere queste eccezioni nei log del 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

Ricordiamo l’architettura client/server del test:

Quando il EJB [DAO] genera un'eccezione, questa deve essere serializzata per raggiungere il client. Probabilmente è questa operazione che ha dato esito negativo per un motivo che non ho compreso. Poiché la nostra applicazione completa non funzionerà in modalità client/server, possiamo ignorare questo problema.

Ora che il EJB del livello [DAO] è operativo, possiamo passare al EJB del livello [métier].

3.5. Il livello [métier]

Torniamo all’architettura dell’applicazione in fase di sviluppo:

Creeremo un nuovo progetto Maven per i livelli EJB e [métier]. Come si vede sopra, dipenderà dal progetto Maven che è stato creato per i livelli [DAO] e [JPA].

3.5.1. Il progetto NetBeans

Creiamo un nuovo progetto Maven di tipo EJB. A tal fine, è sufficiente seguire la procedura già utilizzata e descritta a pagina 174.

  • in [1], il progetto Maven del livello [métier],
  • in [2], si aggiunge una dipendenza,
  • in [3], si seleziona il progetto Maven dei livelli [DAO] e [JPA],
  • in [4], si seleziona l’ambito [provided]. Si ricorda che ciò significa che è necessario per la compilazione ma non per l’esecuzione del progetto. Infatti, l’EJB del livello [métier] verrà distribuito sul server Glassfish insieme al EJB dei livelli [DAO] e [JPA]. Pertanto, al momento dell’esecuzione, il livello EJB, derivato dai livelli [DAO] e [JPA], sarà già presente,
  • in [6], il nuovo progetto con la sua dipendenza.

Presentiamo ora i codici sorgente del livello [métier]:

EJB [Metier] 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 {

    // livello DAO
    // elenco dei clienti
    public List<Client> getAllClients();

    // elenco dei medici
    public List<Medecin> getAllMedecins();

    // elenco delle fasce orarie di un medico
    public List<Creneau> getAllCreneaux(Medecin medecin);

    // elenco degli appuntamenti di un medico in un determinato giorno
    public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);

    // trovare un cliente identificato dal suo ID
    public Client getClientById(Long id);

    // trovare un cliente identificato dal suo ID
    public Medecin getMedecinById(Long id);

    // trovare un appuntamento identificato dal proprio ID
    public Rv getRvById(Long id);

    // trovare una fascia oraria identificata dal proprio ID
    public Creneau getCreneauById(Long id);

    // aggiungere un RV
    public Rv ajouterRv(Date jour, Creneau creneau, Client client);

    // eliminare un RV
    public void supprimerRv(Rv rv);
    
    // professione
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);

}

Per comprendere questa interfaccia, è necessario ricordare l’architettura del progetto:

Abbiamo definito l’interfaccia del livello [DAO] (paragrafo 3.4.4) e abbiamo indicato che essa soddisfa le esigenze del livello [web], ovvero le esigenze degli utenti. Il livello [web] comunica con il livello [DAO] solo tramite il livello [métier]. Questo spiega perché nel livello [métier] si ritrovano tutti i metodi del livello [DAO]. Questi metodi si limiteranno a delegare la richiesta del livello [web] al livello [DAO]. Nient’altro.

Durante l’analisi dell’applicazione, emerge un’esigenza: poter visualizzare su una pagina web l’agenda di un medico per un determinato giorno, al fine di conoscere gli slot occupati e quelli liberi della giornata. Si tratta di una situazione tipica quando la segretaria risponde a una richiesta al telefono. Le viene chiesto un appuntamento per un determinato giorno con un determinato medico. Per soddisfare questa esigenza, il livello [métier] offre il metodo della riga 46.


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

Ci si potrebbe chiedere dove collocare questo metodo:

  • si potrebbe inserirlo nel livello [DAO]. Tuttavia, questo metodo non risponde realmente a un’esigenza di accesso ai dati, ma piuttosto a un’esigenza aziendale;
  • si potrebbe collocarlo nel livello [web]. Sarebbe però una cattiva idea. Infatti, se si modifica il livello [web] in un livello [Swing], si perderà il metodo mentre l’esigenza rimane sempre presente.

Il metodo riceve come parametri il medico e il giorno per il quale si desidera l’agenda delle prenotazioni. Restituisce un oggetto [AgendaMedecinJour] che rappresenta l’agenda del medico e del 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;
    // campi
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;

    // costruttori
    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());
    }

    // getter e setter
...
  
}
  • riga 12: il medico a cui appartiene l’agenda,
  • riga 13: il giorno dell’agenda,
  • riga 14: le fasce orarie del medico per quel giorno.
  • la classe presenta dei costruttori (righe 17, 21) e un metodo toString adattato (riga 27).

La classe [CreneauMedecinJour] (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;
    // campi
    private Creneau creneau;
    private Rv rv;

    // costruttori
    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);
    }

    // getter e setter

  ...
}
  • riga 12: una fascia oraria del medico,
  • riga 13: l’appuntamento associato, null se la fascia oraria è libera.

Si vede quindi che il campo creneauxMedecinJour della riga 14 della classe [AgendaMedecinJour] ci permette di ottenere tutte le fasce orarie del medico con l’indicazione “occupato” o “libero” per ciascuna di esse. Questo era l’obiettivo del nuovo metodo [getAgendaMedecinJour] dell’interfaccia [IMetier].

Il nostro EJB [Metier] avrà un’interfaccia locale e un’interfaccia remota che si limiteranno a derivare dall’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{

}

EJB e [Metier] implementano queste interfacce nel modo seguente:


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 {

  // livello DAO
  @EJB
  private IDaoLocal dao;

  public Metier() {
  }

  @Override
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }

  @Override
  public List<Medecin> getAllMedecins() {
    return dao.getAllMedecins();
  }

  @Override
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    return dao.getAllCreneaux(medecin);
  }

  @Override
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
    return dao.getRvMedecinJour(medecin, jour);
  }

  @Override
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }

  @Override
  public Medecin getMedecinById(Long id) {
    return dao.getMedecinById(id);
  }

  @Override
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }

  @Override
  public Creneau getCreneauById(Long id) {
    return dao.getCreneauById(id);
  }

  @Override
  public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
    return dao.ajouterRv(jour, creneau, client);
  }

  @Override
  public void supprimerRv(Rv rv) {
    dao.supprimerRv(rv);
  }

  @Override
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour) {
    // elenco delle fasce orarie del medico
    List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
    // elenco delle prenotazioni dello stesso medico per lo stesso giorno
    List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
    // si crea un dizionario a partire dagli appuntamenti fissati
    Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
    for (Rv resa : reservations) {
      hReservations.put(resa.getCreneau().getId(), resa);
    }
    // si crea l'agenda per il giorno richiesto
    AgendaMedecinJour agenda = new AgendaMedecinJour();
    // il medico
    agenda.setMedecin(medecin);
    // il giorno
    agenda.setJour(jour);
    // le fasce orarie di prenotazione
    CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
    agenda.setCreneauxMedecinJour(creneauxMedecinJour);
    // compilazione delle fasce orarie di prenotazione
    for (int i = 0; i < creneauxHoraires.size(); i++) {
      // riga i dell'agenda
      creneauxMedecinJour[i] = new CreneauMedecinJour();
      // ID della fascia oraria
      creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
      // la fascia oraria è libera o prenotata?
      if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
        // la fascia oraria è occupata - si registra la prenotazione
        Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
        creneauxMedecinJour[i].setRv(resa);
      }
    }
    // si restituisce il risultato
    return agenda;
  }
}
  • alla riga 22, la classe [Metier] è un singleton di EJB,
  • riga 23, ogni metodo di EJB viene eseguito all’interno di una transazione. Ciò significa che la transazione ha inizio all’inizio del metodo, nel livello [métier]. Quest’ultimo richiamerà i metodi del livello [DAO]. Questi si svolgeranno all’interno della stessa transazione,
  • riga 24, il EJB implementa le sue interfacce locale e remota ed è inoltre serializzabile,
  • riga 27: un riferimento a EJB dal livello [DAO],
  • riga 29: questa verrà iniettata dal contenitore EJB del server Glassfish, grazie all’annotazione @EJB. Pertanto, quando vengono eseguiti i metodi della classe [Metier], il riferimento a EJB del livello [DAO] è stato inizializzato,
  • righe 33-81: questo riferimento viene utilizzato per delegare al livello [DAO] la chiamata effettuata al livello [métier],
  • riga 84: il metodo getAgendaMedecinJour che consente di ottenere l'agenda di un medico per un determinato giorno. Lasciamo che sia il lettore a seguire i commenti.

3.5.2. Implementazione del livello [métier]

Il livello [métier] dipende dal livello [DAO]. Ogni livello è stato implementato con un EJB. Per testare il EJB e il [métier], dobbiamo distribuire entrambi i EJB. A tal fine, abbiamo bisogno di un progetto aziendale.

  • [1], si crea un nuovo progetto,
  • di tipo Maven [2] e Applicazione aziendale [3],
  • gli si assegna un nome [4]. Il suffisso ear verrà aggiunto automaticamente,
  • in [5], si sceglie il server Glassfish e Java EE 6,
  • in [6]; un'applicazione aziendale contiene moduli, in genere moduli EJB e moduli web. In questo caso, l’applicazione aziendale conterrà i moduli dei due EJB che abbiamo creato. Poiché questi moduli esistono già, non si spuntano le caselle,
  • in [7,8] sono stati creati due progetti. [8] è il progetto aziendale che useremo. [7] è un progetto di cui ignoro la funzione. Non ho avuto occasione di utilizzarlo e, non avendo approfondito Maven, non so a cosa possa servire. Quindi lo ignoreremo.

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

  • in [1], creiamo una nuova dipendenza,
  • in [2], si seleziona il progetto EJB [DAO],
  • in [3], si dichiara che si tratta di un EJB. Non lasciare il tipo vuoto perché in tal caso verrà utilizzato il tipo jar e in questo caso tale tipo non è adatto,
  • in [4], si utilizza l'ambito [compile],
  • in [5], il progetto con la sua nuova dipendenza,
  • in [6, 7, 8], si ricomincia per aggiungere EJB dal livello [métier],
  • in [9], le due dipendenze,
  • in [10], si compila il progetto,
  • in [11], lo si esegue,
  • in [12], nella scheda [Services], si vede che il progetto è stato distribuito sul server Glassfish. Ciò significa che i due EJB sono ora presenti sul server.

Nei log del server Glassfish si trovano informazioni sulla distribuzione dei due EJB:

  • e [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 la creazione dell'unità di persistenza [dbrdvmedecins2-PU] è andata a buon fine e che è stata stabilita la connessione al database associato,
  • riga 8: i nomi portabili delle interfacce remote e locali di EJB e [DaoJpa]. portable significa che sono riconosciute da tutti i server applicativi,
  • riga 9: lo stesso concetto, ma con nomi proprietari di Glassfish,
  • righe 10-11: lo stesso vale per EJB e [Metier].

Prenderemo in considerazione il nome portabile dell'interfaccia remota di 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 i test del livello [métier].

3.5.3. Test del livello [métier]

Come abbiamo fatto per il livello [DAO], testeremo il livello [métier] nell’ambito di un’applicazione client/server:

Il client testerà l’interfaccia remota del livello EJB [Metier] distribuito sul server Glassfish.

Iniziamo creando un nuovo progetto Maven. A tal fine, seguiamo la procedura utilizzata per creare il progetto di test del livello [dao] (cfr. paragrafo 3.4.7), esclusa la creazione del test JUnit. Il progetto così creato è il seguente

  • in [1], il progetto creato con le sue dipendenze: rispetto a EJB del livello [dao], rispetto a EJB del livello [métier], della 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>

Ci si assicurerà di avere le dipendenze descritte alle righe 17-33. Il test consisterà in una semplice classe console:

Il codice della 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 {

  // il nome dell'interfaccia remota di EJB [Metier]
  private static String IDaoRemoteName = "java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
  // data odierna
  private static Date jour = new Date();

  public static void main(String[] args) {
    try {
      // contesto JNDI del server Glassfish
      InitialContext initialContext = new InitialContext();
      // riferimento sul livello remoto [metier]
      IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
      // visualizzazione clienti
      List<Client> clients = metier.getAllClients();
      display("Liste des clients :", clients);
      // visualizzazione medici
      List<Medecin> medecins = metier.getAllMedecins();
      display("Liste des médecins :", medecins);
      // visualizzazione delle fasce orarie di un medico
      Medecin medecin = medecins.get(0);
      List<Creneau> creneaux = metier.getAllCreneaux(medecin);
      display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
      // elenco degli appuntamenti di un medico in un determinato giorno
      display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // visualizzazione agenda
      AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // aggiungere un RV
      Rv rv = null;
      Creneau creneau = creneaux.get(2);
      Client client = clients.get(0);
      System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
      rv = metier.ajouterRv(jour, creneau, client);
      System.out.println("Rv ajouté");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // visualizzazione dell'agenda
      agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // eliminare un 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));
      // visualizzazione calendario
      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();
      }
    }
  }

  // metodo di utilità - visualizza gli elementi di una collezione
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • riga 18: il nome portatile dell'interfaccia remota dell'EJB [Metier] è stato ricavato dai log di GlassFish,
  • righe 24-27: si ottiene un riferimento all'interfaccia remota dell'EJB [Metier],
  • righe 29-30: visualizzano i clienti,
  • righe 32-33: visualizzano i medici,
  • righe 35-37: visualizzano gli slot di un medico,
  • riga 39: visualizza gli appuntamenti di un medico in un determinato giorno,
  • righe 41-42: l'agenda dello stesso medico per lo stesso giorno,
  • righe 44-49: si aggiunge un appuntamento,
  • riga 50: si visualizzano gli appuntamenti del medico. Ne dovrebbe esserci uno in più,
  • righe 52-53: visualizza l'agenda del medico. Si dovrebbe vedere l'appuntamento aggiunto,
  • righe 55-57: si elimina l'appuntamento appena aggiunto,
  • riga 58: ciò deve riflettersi nell'elenco degli appuntamenti del medico,
  • righe 60-61: e nella sua agenda.

Si esegue il test:

 

Le schermate ottenute 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: l'agenda della signora PELISSIER, 23 maggio 2012. Nessuna fascia oraria è riservata,
  • riga 39: aggiunta di un appuntamento,
  • riga 42: la nuova agenda della signora PELISSIER. Una fascia oraria è ora riservata al signor MARTIN,
  • riga 44: l'appuntamento è stato cancellato,
  • riga 46: l'agenda della sig.ra PELISSIER mostra che non è stato riservato alcun intervallo di tempo.

Consideriamo ora che i livelli [DAO] e [métier] siano operativi. Resta da scrivere il livello [web] con il framework JSF. A tal fine, utilizzeremo le conoscenze acquisite all’inizio di questo documento.

3.6. Il livello [web]

Torniamo all’architettura in fase di realizzazione:

Realizzeremo l'ultimo livello, ovvero 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], si sceglie il server Glassfish e Java EE 6 Web,
  • in [6], il progetto così creato,
  • in [7], il progetto una volta eliminate la pagina [index.jsp] e il pacchetto presente in [Source Packages],
  • in [8, 9], nelle proprietà del progetto, si aggiunge un framework,
  • in [10], si seleziona Java Server Faces,
  • in [11], la configurazione di Java Server Faces. Si mantengono i valori predefiniti. Si noti che viene utilizzato JSF 2,
  • in [12], il progetto viene quindi modificato in due punti: viene generato un file [web.xml] e 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 elaborerà tutte le richieste inviate all’applicazione. Si tratta del servlet di JSF,
  • righe 12-15: definiscono le URL gestite da questa servlet. Si tratta delle URL della forma /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à vista. Possiamo eseguire questo progetto:

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

Presentiamo ora il progetto completo per poi descriverne in dettaglio i vari elementi.

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

3.6.2. Le dipendenze del progetto

Torniamo all’architettura del progetto:

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

  • in [1], inseriremo ejb per indicare che la dipendenza riguarda il progetto EJB,
  • in [2], inseriremo [provided]. Infatti, il progetto web verrà distribuito contemporaneamente ai due progetti EJB. Pertanto non è necessario includere i file JAR del progetto EJB.

3.6.3. La configurazione del progetto

La configurazione del progetto è quella dei progetti JSF che abbiamo esaminato all’inizio di questo documento. Elenchiamo i file di configurazione senza ripeterne la spiegazione.

 

[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, alla riga 26, che la pagina [index.xhtml] è la pagina iniziale 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]: vuota ma necessaria 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
# eccezione
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
# modulo 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
# modulo 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
# modulo 3
form3.titre=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,numero,#00} - {6,numero,#00}:{7,numero,#00}
form3.titre_detail=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,numero,#00} - {6,numero,#00}:{7,numero,#00}
form3.client=Client
form3.valider=Valider
form3.annuler=Annuler
# errore
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
# eccezione
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
# modulo 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
# modulo 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
# modulo 3
form3.titre=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,numero,#00} - {6,numero,#00}:{7,numero,#00}
form3.titre_detail=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,numero,#00} - {6,numero,#00}:{7,numero,#00}
form3.client=Client
form3.valider=Submit
form3.annuler=Cancel
# errore
erreur.titre=An error occurred
erreur.message=Error message
erreur.accueil=Welcome Page
erreur.classe=Cause

3.6.4. Le schermate del progetto

Ricordiamo il funzionamento dell'applicazione. La pagina iniziale è la seguente:

 

Da questa prima pagina, l’utente (Segreteria, Medico) effettuerà una serie di operazioni. Le presentiamo di seguito. La schermata a sinistra mostra la pagina da cui l’utente effettua una richiesta, quella a destra la risposta inviata dal server.

Infine, è possibile ottenere anche una pagina di errore:

Queste diverse visualizzazioni si ottengono con le seguenti pagine del progetto web:

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

In questo caso è stata utilizzata la tecnologia dei facelet, descritta nel paragrafo 2.11. Ci limitiamo a fornire il codice delle pagine XHTML utilizzate per l'impaginazione:

[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 (template) della 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 da 8 a 21 definiscono l'area denominata "contenuto" (riga 8) in [layout.xhtml] (riga 7). Si tratta dell'area centrale delle viste:

 

La pagina [index.xhtml] è l’unica pagina dell’applicazione. Non ci sarà quindi alcuna navigazione tra le pagine. Essa visualizza una delle quattro pagine [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. Questa visualizzazione è controllata da quattro valori booleani [form1Rendered, form2Rendered, form3Rendered, erreurRendered] del bean form 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. È già stata analizzata (paragrafo 2.4.4).
  • la classe [Messages] è una classe che facilita l’internazionalizzazione dei messaggi di un’applicazione. È stata analizzata nel paragrafo 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{

  // livello business
  @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>();
  // errori
  private List<Erreur> erreurs = new ArrayList<Erreur>();
  private Boolean erreur = false;

  public Application() {
  }

  @PostConstruct
  public void init() {
    // si memorizzano nella cache medici e clienti
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // si registra l'errore
      erreur = true;
      erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
      while (th.getCause() != null) {
        th = th.getCause();
        erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
      }
      return;
    }
    // verifica degli elenchi
    if (medecins.size() == 0) {
      // si registra l'errore
      erreur = true;
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // si annota l'errore
      erreur = true;
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // errore?
    if (erreur) {
      return;
    }

    // i dizionari
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
  }

  // getter e setter
  ...
}
  • righe 15-16: la classe [Application] è un bean con ambito Application. Viene creata una sola volta all’inizio del ciclo di vita dell’applicazione JSF ed è accessibile a tutte le richieste di tutti gli utenti. In genere vi si inseriscono dati di sola lettura. In questo caso, vi inseriremo l’elenco dei medici e quello 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 di EJB [Metier] verrà iniettato dal contenitore EJB di Glassfish. Ricordiamo l’architettura dell’applicazione:

L’applicazione JSF e EJB [Metier] verranno eseguite nella stessa JVM (Java Virtual Machine). Pertanto, il livello [JSF] utilizzerà l'interfaccia locale di EJB. In questo caso, il bean dell'applicazione utilizza EJB e [Metier]. Anche se così non fosse, sarebbe normale trovarvi un riferimento al livello [métier]. Si tratta infatti di un’informazione che può essere condivisa da tutte le richieste di tutti gli utenti, quindi un dato di ambito Application.

  • righe 34-35: il metodo init viene eseguito subito dopo l’istanziazione della classe [Application] (presenza dell’annotazione @PostConstruct),
  • Nelle righe 36-73, il metodo crea i seguenti elementi: l'elenco dei medici alla riga 23, quello 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, che vengono registrati nell'elenco alla riga 28.

La classe [Erreur] è la seguente:


package beans;

public class Erreur {
  
  public Erreur() {
  }
  
  // campo
  private String classe;
  private String message;

  // costruttore
  public Erreur(String classe, String message){
    this.setClasse(classe);
    this.message=message;
  }
  
  // getter e setter
...  
}
  • 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 dell'applicazione
  @Inject
  private Application application;

  // modello
  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() {
    // L'inizializzazione è andata a buon fine?
    if (application.getErreur()) {
      // si recupera l'elenco degli errori
      erreurs = application.getErreurs();
      // viene visualizzata la schermata degli errori
      setForms(false, false, false, true);
    }
  }

  // visualizzazione della vista
  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» e con ambito di sessione. Si ricorda che, in questo caso, la classe deve essere serializzabile.
  • righe 13-14: il bean «form» ha un riferimento al bean «application». Questo verrà iniettato dal contenitore di servlet in cui viene eseguita l’applicazione (presenza dell’annotazione @Inject).
  • righe 17-31: il modello delle pagine [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. La visualizzazione di queste pagine è controllata dai valori booleani delle righe 19-22. Si noti che, per impostazione predefinita, viene visualizzata la pagina [form1.xhtml],
  • righe 33-34: il metodo init viene eseguito subito 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 andata a buon fine (riga 36), nel qual caso verrà visualizzata la pagina [erreur.xhtml] (riga 40).

La pagina [erreur.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 pagine e modello

3.6.6.1. Visualizzazione della pagina iniziale

Se tutto va bene, la prima pagina visualizzata è [form1.xhtml]. Si ottiene la seguente visualizzazione:

 

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 è generata dal seguente modello:


@Named(value = "form")
@SessionScoped
public class Form implements Serializable {

  // Bean Application
  @Inject
  private Application application;
  // modello
  private Long idMedecin;
  private Date jour = new Date();
  
// elenco dei medici
  public List<Medecin> getMedecins() {
    return application.getMedecins();
  }
  // agenda
  public void getAgenda() {
    ...
}
  • il campo della riga 9 fornisce in lettura e scrittura il valore dell'elenco della riga 18 della pagina. Alla visualizzazione iniziale della pagina, essa imposta il valore selezionato nel menu a discesa. Alla visualizzazione iniziale, idMedecin è uguale a null, quindi sarà selezionato il primo medico;
  • il metodo delle righe 13-15 genera gli elementi del 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 della riga 10 alimenta in lettura/scrittura il campo di immissione della riga 21 della pagina. Alla visualizzazione iniziale, viene quindi visualizzata la data odierna,
  • righe 17-19: il metodo getAgenda gestisce il clic sul pulsante [Agenda] della riga 26 della pagina. Poiché non vi è alcuna navigazione (viene sempre richiesta la pagina [index.html]), spesso si utilizzerà l’attributo actionListener al posto dell’attributo action. In questo caso, il metodo chiamato nel modello non restituisce alcun risultato.

Quando si fa clic sul pulsante [Agenda],

  • vengono inviati alcuni valori: il valore selezionato nel menu a tendina dei medici viene registrato nel campo idMedecin del modello e il giorno scelto nel campo «giorno»,
  • viene richiamato il metodo getAgenda del modello.

Il metodo getAgenda è il seguente:


  // Bean dell'applicazione
  @Inject
  private Application application;

  // modello
  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 {
      // ricerca del medico
      medecin = application.gethMedecins().get(idMedecin);
      // titolo del modulo 2
      form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
      // agenda del medico per un determinato giorno
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // visualizzazione del modulo 2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // panoramica degli errori
      prepareVueErreur(th);
    }
  }

  // preparazione vueErreur
  private void prepareVueErreur(Throwable th) {
    // si crea l'elenco degli errori
    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()));
    }
// viene visualizzata la vista degli errori
    setForms(false, false, false, true);
}

Ricordiamo cosa deve visualizzare il metodo getAgenda:

  • riga 21: si recupera il medico selezionato dal dizionario dei medici che è stato memorizzato nel bean application. A tal fine si utilizza il suo ID, che è stato inviato in idMedecin,
  • riga 23: si prepara il titolo della pagina [form2.xhtml] che verrà visualizzata. Questo messaggio viene prelevato dal file dei messaggi in modo da poter essere internazionalizzato. Questa tecnica è stata descritta al paragrafo 2.8.5.7, pagina 135.
  • riga 25: si richiama il livello [métier] per calcolare l'agenda del medico selezionato per il giorno specificato,
  • riga 27: viene visualizzato [form2.xhtml],
  • riga 28: in caso di eccezione, viene generato un elenco di errori (righe 37-42) e viene visualizzata la pagina [erreur.xhtml] (riga 44).

3.6.6.2. Visualizzare l'agenda di un medico

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

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:


// modello
  private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;

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

  • riga 10, il titolo della pagina,
  • riga 12: l'agenda del medico viene visualizzata tramite un tag <h:dataTable> a tre colonne,
  • righe 13-18: la prima colonna mostra le fasce orarie,
  • righe 19-30: la seconda colonna mostra il nome del cliente che ha eventualmente prenotato la fascia oraria o, in caso contrario, nulla. Per effettuare questa scelta, si utilizzano i tag della libreria JSTL Core a cui si fa riferimento alla riga 7,
  • righe 30-35: la terza colonna mostra il link [Réserver] se la fascia oraria è libera, il link [Supprimer] se la fascia oraria è occupata.

I link della terza colonna sono collegati al seguente modello:


// modello
  private Long idCreneau;

  // azione su RV
  public void action() {
    ...
}
  • il metodo action viene chiamato quando l'utente clicca sul link Prenota / Elimina (riga 32). Si noti che qui è stato utilizzato l'attributo action. Il metodo a cui punta questo attributo dovrebbe avere la firma String action(), poiché il metodo deve quindi restituire una chiave di navigazione. In questo caso, invece, è void action(). Ciò non ha causato alcun errore e si può supporre che in questo caso non vi sia alcuna navigazione. Questo era proprio l’obiettivo desiderato. L’inserimento di actionListener al posto di action causava un malfunzionamento,
  • il campo idCreneau della riga 2 recupererà l’ID della fascia oraria del 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;

  // modello
  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;

  // azione su RV
  public void action() {
    // si cerca la fascia oraria nell'agenda
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // È stato trovato?
    if (!trouvé) {
      // È strano: si visualizza nuovamente form2
      setForms(false, true, false, false);
      return;
    }
    // Trovato
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // in base all'azione desiderata
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // prenotazione

  public void reserver() {
    ...
  }

  public void supprimer() {
    try {
      // cancellazione di un appuntamento
      application.getMetier().supprimerRv(creneauChoisi.getRv());
      // si aggiorna l'agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // visualizza form2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // visualizzazione errori
      prepareVueErreur(th);
    }
  }
  • riga 16: quando viene avviato il metodo action, l’ID della fascia oraria selezionata è stato inserito in idCreneau (riga 11),
  • righe 18-26: si cerca di recuperare la fascia oraria a partire dal suo id (riga 21). Lo si cerca nell'agenda corrente, agendaMedecinJour della riga 10. Normalmente dovrebbe essere presente. In caso contrario, non si esegue alcuna operazione (righe 28-32),
  • riga 34: se si è trovata la fascia oraria cercata, se ne recupera un riferimento che viene memorizzato alla riga 12,
  • riga 36: si verifica se la fascia oraria scelta conteneva un appuntamento. In caso affermativo, lo si elimina (riga 39), altrimenti se ne prenota uno (riga 37),
  • riga 51: l’appuntamento della fascia oraria scelta viene cancellato. È il livello [métier] che svolge questo compito,
  • riga 53: si richiede al livello [métier] la nuova agenda del medico. Ovviamente vi si vedrà un appuntamento in meno. Ma poiché l’applicazione è multiutente, è possibile visualizzare le modifiche apportate da altri utenti,
  • riga 55: si visualizza nuovamente la pagina [form2.xhtml],
  • riga 58: poiché è stato richiamato il livello [métier], potrebbero verificarsi delle eccezioni. In tal caso, si memorizza lo stack delle eccezioni nell’elenco degli errori della riga 13 e le si visualizza tramite la vista [erreur.xhtml].

3.6.6.4. Fissazione di un appuntamento

La prenotazione di un appuntamento segue la sequenza seguente:

Il modello coinvolto in questa azione è il seguente:


// modello
  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;

  // azione su RV
  public void action() {
...
    // trovato
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // in base all'azione desiderata
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // prenotazione

    public void reserver() {
    try {
      // titolo del modulo 3
      form3Titre = Messages.getMessage(null, "form3.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour),
                creneauChoisi.getCreneau().getHdebut(), creneauChoisi.getCreneau().getMdebut(), creneauChoisi.getCreneau().getHfin(), creneauChoisi.getCreneau().getMfin()}).getSummary();
      // cliente selezionato dal menu a tendina
      idClient=null;
      // viene visualizzato il modulo 3
      setForms(false, false, true, false);
    } catch (Throwable th) {
      // visualizzazione errori
      prepareVueErreur(th);
    }
  }
  • riga 14: se la fascia oraria scelta non ha alcun appuntamento, si tratta di una prenotazione,
  • riga 30: si prepara il titolo della pagina [form3.xhtml] con la stessa tecnica utilizzata per il titolo della pagina [form2.xhtml],
  • riga 34: in questo modulo è presente un menu a tendina il cui valore viene fornito da idClient. Si imposta il valore di questo campo su null per non selezionare nessuno,
  • riga 36: si visualizza la pagina [form3.xhtml],
  • riga 39: oppure la pagina degli errori 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 è alimentata dal seguente modello:


// Bean dell'applicazione
  @Inject
  private Application application;

  // modello
  private Long idClient;

  // elenco dei clienti
  public List<Client> getClients() {
    return application.getClients();
  }
  • riga 6: il numero del cliente popola l'attributo value del menu a tendina dei clienti nella riga 12 della pagina. Imposta l'elemento selezionato nel menu a tendina,
  • righe 9-11: il metodo getClients popola il contenuto del menu a tendina (riga 13). La dicitura (itemLabel) di ciascuna opzione è [Titre Prénom Nom] del cliente, mentre il valore associato (itemValue) è l’ID del cliente. È quindi questo valore che verrà inviato.

3.6.6.5. Conferma di un appuntamento

La convalida di un appuntamento segue la sequenza seguente:

e corrisponde al clic sul pulsante [Valider]:


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

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


  // Bean dell'applicazione
  @Inject
  private Application application;
  
  // modello
  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;

  // convalida appuntamento
  public void validerRv() {
    try {
      // si recupera un'istanza della fascia oraria selezionata
      Creneau creneau = application.getMetier().getCreneauById(idCreneau);
      // si aggiunge l'appuntamento
      application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
      // si aggiorna l'agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // si visualizza form2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // visualizzazione degli errori
      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: a partire dall’ID della fascia oraria memorizzata in una fase precedente (il bean ha ambito di sessione), si richiede al livello [métier] un riferimento alla fascia oraria stessa,
  • riga 21: si richiede al livello [métier] di aggiungere un appuntamento per il giorno scelto (giorno), la fascia oraria scelta (fascia) e il cliente scelto (idClient),
  • riga 23: si richiede al livello [métier] di aggiornare l’agenda del medico. Verrà visualizzato l’appuntamento aggiunto insieme a tutte le modifiche che altri utenti dell’applicazione potrebbero aver apportato,
  • riga 25: si visualizza nuovamente l'agenda [form2.xhtml],
  • riga 28: si 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 [Annuler] nella pagina [form3.xhtml] è il seguente:


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

Viene quindi richiamato il metodo [Form].annulerRv:


  // annullamento dell'appuntamento
  public void annulerRv() {
    // visualizza il modulo 2
    setForms(false, true, false, false);
}

3.6.6.7. Torna alla pagina iniziale

Rimane un’azione da esaminare, quella della sequenza seguente:

Il codice del pulsante [Accueil] nella pagina [form2.xhtml] è il seguente:


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

Il metodo [Form].accueil è il seguente:


  public void accueil() {
    // visualizzazione della pagina iniziale
    setForms(true, false, false, false);
}

3.7. Conclusion

Abbiamo creato la seguente applicazione:

Ci siamo concentrati sulle funzionalità dell'applicazione piuttosto che sul suo aspetto per l'utente. Quest'ultimo verrà migliorato con l'utilizzo della libreria di componenti PrimeFaces. Abbiamo realizzato un'applicazione di base, ma comunque rappresentativa di un'architettura Java a livelli EE che utilizza EJB. L'applicazione può essere migliorata in vari modi:

  • è necessaria l'autenticazione. Non tutti sono autorizzati ad aggiungere o eliminare appuntamenti;
  • si dovrebbe poter scorrere l’agenda avanti e indietro quando si cerca un giorno con fasce orarie libere,
  • si dovrebbe poter richiedere l’elenco dei giorni in cui ci sono fasce orarie libere per un medico. Infatti, se si tratta di un oftalmologo, i suoi appuntamenti vengono generalmente prenotati con sei mesi di anticipo,
  • ...

3.8. I test con Eclipse

3.8.1. Il livello [DAO]

  • in [1], si importa il progetto EJB dal livello [DAO] e il relativo client,
  • in [2], si seleziona il progetto EJB dal livello [DAO] e lo si esegue in [3],
  • in [4], lo si esegue su un server,
  • in [5], viene proposto solo il server Glassfish poiché è 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.

Sono gli stessi che avevamo con NetBeans.

  • in [7A] [7B] si esegue il test JUnit del cliente,
  • in [8], il test ha esito positivo,
  • in [9], i log della console.

In [10], si scarica l'applicazione EJB.

3.8.2. Lo strato [métier]

  • in [1], si importano i quattro progetti Maven dal livello [métier],
  • in [2], si seleziona il progetto aziendale e lo si esegue in [3], su un server Glassfish [4] [5],
  • in [6], il progetto aziendale è stato distribuito su Glassfish,
  • in [7], si esaminano 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, si prende nota del nome portatile di EJB [Metier] e lo si incolla nella console client di questo EJB:


public class ClientRdvMedecinsMetier {

  // il nome dell'interfaccia remota di EJB [Metier]
  private static String IDaoRemoteName = "java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
  // data odierna
private static Date jour = new Date();
  • nel [8], si esegue il client console,
  • in [9], i relativi log.
  • in [10], si esegue il download dell'applicazione aziendale;

3.8.3. Il livello [web]

  • in [1], si importano i tre progetti Maven del livello [web]. Quello con il suffisso "ear" è il progetto aziendale da distribuire su Glassfish,
  • in [2], lo si esegue,
  • sul server Glassfish [3],
  • in [4], l'applicazione aziendale è stata correttamente distribuita,
  • in [5], si richiede l'URL dell'applicazione nel browser interno di Eclipse.