Skip to content

4. Il servizio web J2EE degli appuntamenti

Torniamo all'architettura dell'applicazione da realizzare:

In questa sezione ci occuperemo della realizzazione del servizio web J2EE [1] eseguito su un server Sun / Glassfish.

4.1. Il database

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

Image

4.1.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.)

4.1.2. La tabella [CLIENTS]

I clienti 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.)

4.1.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 (dott.ssa Marie PELISSIER).

4.1.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 signora Marie PELISSIER). La tabella [CLIENTS] ci indica che il cliente n. 4 è la signorina Brigitte BISTROU.

4.2. Generazione del database

Create il database MySql [dbrdvmedecins] con lo strumento che preferite. Per creare le tabelle e inserirvi i dati, potrete utilizzare lo script [createbd.sql] che vi verrà fornito. Il suo contenuto è il seguente:

create table CLIENTS (
        ID bigint not null auto_increment,
        VERSION integer not null,
        TITRE varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(30) not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table CRENEAUX (
        ID bigint not null auto_increment,
        VERSION integer not null,
        HDEBUT integer not null,
        MDEBUT integer not null,
        HFIN integer not null,
        MFIN integer not null,
        ID_MEDECIN bigint not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table MEDECINS (
        ID bigint not null auto_increment,
        VERSION integer not null,
        TITRE varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(30) not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table RV (
        ID bigint not null auto_increment,
        JOUR date not null,
        ID_CLIENT bigint not null,
        ID_CRENEAU bigint not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    alter table CRENEAUX 
        add index FK9BD7A197FE16862 (ID_MEDECIN), 
        add constraint FK9BD7A197FE16862 
        foreign key (ID_MEDECIN) 
        references MEDECINS (ID);

    alter table RV 
        add index FKA4494D97AD2 (ID_CLIENT), 
        add constraint FKA4494D97AD2 
        foreign key (ID_CLIENT) 
        references CLIENTS (ID);

    alter table RV 
        add index FKA441A673246 (ID_CRENEAU), 
        add constraint FKA441A673246 
        foreign key (ID_CRENEAU) 
        references CRENEAUX (ID);

INSERT INTO CLIENTS ( VERSION, NOM, PRENOM, TITRE) VALUES (1, 'MARTIN', 'Jules', 'Mr');
...

INSERT INTO MEDECINS ( VERSION, NOM, PRENOM, TITRE) VALUES (1, 'PELISSIER', 'Marie', 'Mme');
...

INSERT INTO CRENEAUX ( VERSION, ID_MEDECIN, HDEBUT, MDEBUT, HFIN, MFIN) VALUES (1, 1, 8, 0, 8, 20);
...

INSERT INTO RV ( JOUR, ID_CRENEAU, ID_CLIENT) VALUES ('2006-08-22', 1, 2);
...

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

COMMIT WORK;

4.3. Gli elementi dell’architettura lato server

Torniamo all’architettura dell’applicazione da realizzare:

Sul lato server, l’applicazione sarà costituita da:

  1. da un livello JPA che consente di lavorare con il BD tramite oggetti
  1. da un EJB incaricato di gestire le operazioni con il livello JPA
  2. da un servizio web incaricato di esporre ai client remoti l’interfaccia dell’EJB sotto forma di servizio web.

Gli elementi (b) e (c) implementano il livello [dao] rappresentato nello schema precedente. È noto che un’applicazione può accedere a un EJB remoto tramite i protocolli RMI e JNDI. In pratica, ciò limita i client a quelli Java. Un servizio web utilizza un protocollo di comunicazione standardizzato implementato da diversi linguaggi: .NET, PHP, C++, ... È proprio questo che vogliamo dimostrare qui utilizzando un client .NET.

Per una breve introduzione ai servizi web, si può consultare il corso [ref1], paragrafo 14, pagina 109.

Un servizio web può essere implementato in due modi:

  • tramite una classe annotata con @WebService che viene eseguita in un contenitore web
  • tramite un EJB annotato con @WebService che viene eseguito in un contenitore EJB

In questo caso utilizzeremo la prima soluzione:

Nel corso [ref1], paragrafo 14, pagina 109, si trova un esempio che utilizza la seconda soluzione.

4.4. Configurazione di Hibernate e del server GlassFish

A seconda della versione, il server Glassfish V2 fornito con NetBeans potrebbe non disporre delle librerie Hibernate necessarie al livello JPA/Hibernate. Se nel prosieguo del tutorial vi accorgete che Glassfish non offre un’implementazione JPA/Hibernate o che, durante la distribuzione dei servizi, un’eccezione segnala che le librerie Hibernate non sono state trovate, dovete aggiungere le librerie nella cartella [<glassfish>/domains/domain1/lib/ext] e quindi riavviare il server Glassfish:

  • in [1], la cartella <glassfish>/.../lib/ext
  • in [2], le librerie Hibernate e alcuni driver JDBC
  • in [3], il driver JDBC di MySQL

Le librerie Hibernate sono contenute nel file zip allegato al tutorial.

4.5. Gli strumenti di generazione automatica di NetBeans

Torniamo all’architettura che dobbiamo realizzare:

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 preziose indicazioni 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, [ref1], EJB e [ref2].

Creazione di una connessione NetBeans al database

  • avviare SGBD e MySQL 5 affinché BD sia disponibile
  • creare una connessione NetBeans al database [dbrdvmedecins]
  • nella scheda [Files], nel ramo [Databases] [1], selezionare il driver JDBC MySQL [2]
  • quindi selezionare l'opzione [3] "Connect Using" che consente di creare una connessione con un database MySQL
  • in [4], inserire le informazioni richieste
  • quindi confermare in [5]
  • in [6], la connessione è stata creata. Sono visibili le quattro tabelle del database collegato.

Creazione di un progetto EJB

  • in [1], creare una nuova applicazione, un modulo EJB
  • in [2], selezionare la categoria [Java EE] e in [3] il tipo [EJB Module]
  • in [4] scegliere una cartella per il progetto e in [5] assegnargli un nome - quindi completare la procedura guidata
  • in [6] il progetto generato

Aggiunta di una risorsa JDBC al server Glassfish

Aggiungeremo una risorsa JDBC al server Glassfish.

  • nella scheda [Services], avviare il server Glassfish [2, 3]
  • nella scheda [Projects], fare clic con il tasto destro del mouse sul progetto EJB e in [5] selezionare l’opzione [New / Other] che consente di aggiungere un elemento al progetto.

Image

  • in [6], selezionare la categoria [Glassfish] e in [7] specificare che si desidera creare una risorsa JDBC selezionando il tipo [JDBC Resource]
  • in [8], specificare che questa risorsa JDBC utilizzerà il proprio pool di connessioni
  • in [9], assegnare un nome alla risorsa JDBC
  • in [10], passare alla fase successiva
  • in [11], si definiscono le caratteristiche del pool di connessioni della risorsa JDBC
  • in [12], assegnare un nome al pool di connessioni
  • in [13], selezionare la connessione NetBeans [dbrdvmedecins] creata in precedenza
  • in [14], passare alla fase successiva
  • in [15], normalmente non c'è nulla da modificare in questa pagina. Le proprietà della connessione al database MySQL [dbrdvmedecins] sono state ricavate da quelle della connessione NetBeans [dbrdvmedecins] creata in precedenza
  • in [16], passare alla fase successiva
  • in [17], mantenere i valori predefiniti proposti
  • in [18], completare la procedura guidata. Questa crea il file [sun-resources.xml] [19] il cui contenuto è il seguente:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Resource Definitions //EN" "http://www.sun.com/software/appserver/dtds/sun-resources_1_3.dtd">
<resources>
  <jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins" object-type="user" pool-name="dbrdvmedecinsPool">
    <description/>
  </jdbc-resource>
  <jdbc-connection-pool ...">
    <property name="URL" value="jdbc:mysql://localhost:3306/dbrdvmedecins"/>
    <property name="User" value="root"/>
    <property name="Password" value="()"/>
  </jdbc-connection-pool>
</resources>

Il file sopra riportato riporta tutte le informazioni inserite nella procedura guidata in formato XML. Verrà utilizzato da IDE NetBeans per richiedere al server GlassFish di creare la risorsa "jdbc/dbrdvmedecins" definita alla riga 4.

Creazione di un'unità di persistenza

L'unità di persistenza [persistence.xml] configura il livello JPA: indica l'implementazione JPA utilizzata (Toplink, Hibernate, ...) e la configura.

  • in [1], fare clic con il tasto destro del mouse sul progetto EJB e selezionare [New / Other] in [2]
  • in [3], selezionare la categoria [Persistence], quindi in [4], specificare che si desidera creare un'unità di persistenza JPA
  • in [5], assegnare un nome all’unità di persistenza creata
  • in [6], scegliere [Hibernate] come implementazione JPA
  • in [7], selezionare la risorsa Glassfish "jdbc/dbrdvmedecins" appena creata
  • in [8], specificare che non deve essere eseguita alcuna azione sul database durante l’istanziazione del livello JPA
  • completare la procedura guidata
  • in [9], il file [persistence.xml] creato dalla procedura guidata

Il suo contenuto è il seguente:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.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_1_0.xsd">
  <persistence-unit name="serveur-ejb-dao-jpa-hibernate-generePU" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties/>
  </persistence-unit>
</persistence>

Ancora una volta, riprende in un formato XML le informazioni fornite nella procedura guidata. Questo file non è sufficiente per lavorare con il database MySQL5 "dbrdvmedecins". Dovremmo indicare a Hibernate il tipo di SGBD da gestire. Ciò verrà fatto in un secondo momento.

Creazione delle entità JPA

 
  • in [1], fare clic con il tasto destro del mouse sul progetto e in [2] selezionare l’opzione [New / Other]
  • in [3], selezionare la categoria [Persistence], quindi in [4], specificare che si desidera creare entità JPA a partire da un database esistente.
  • in [5], selezionare la fonte JDBC "jdbc/dbrdvmedecins" che abbiamo creato
  • in [6], le quattro tabelle del database associato
  • in [7,8], includerle tutte nella generazione delle entità JPA
  • in [9], proseguire con la procedura guidata
  • in [10], le entità JPA che verranno generate
  • in [11], assegnare un nome al pacchetto delle entità JPA
  • in [12], scegliere il tipo Java che incapsulerà gli elenchi di oggetti restituiti dal livello JPA
  • completare la procedura guidata
  • in [13], le quattro entità JPA generate, una per ogni tabella del database.

Ecco, ad esempio, il codice dell’entità [Rv] che rappresenta una riga della tabella [rv] del database [dbrdvmedecins].

package jpa;
...
@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)
  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;
  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneaux idCreneau;
  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Clients idClient;

  public Rv() {
  }

...
}

Creazione del livello EJB di accesso alle entità JPA

  • in [1], fare clic con il tasto destro del mouse sul progetto e in [2], selezionare l'opzione [New / Other]
  • in [3], selezionare la categoria [Persistence], quindi in [4] il tipo [Session Beans for Entity Classes]
  • in [5], le entità JPA create in precedenza vengono visualizzate
  • in [6], selezionarle tutte
  • in [7], sono state selezionate
  • in [8], proseguire con la procedura guidata
  • in [9], assegnare un nome al pacchetto degli EJB che verranno generati
  • in [10], specificare che gli EJB devono implementare sia un'interfaccia locale che una remota
  • chiudere la procedura guidata
  • in [11], gli EJB generati

Ecco, ad esempio, il codice dell'EJB che gestisce l'accesso all'entità [Rv], ovvero alla tabella [rv] del database [dbrdvmedecins]:

package ejb;
...
@Stateless
public class RvFacade implements RvFacadeLocal, RvFacadeRemote {
  @PersistenceContext
  private EntityManager em;

  public void create(Rv rv) {
    em.persist(rv);
  }

  public void edit(Rv rv) {
    em.merge(rv);
  }

  public void remove(Rv rv) {
    em.remove(em.merge(rv));
  }

  public Rv find(Object id) {
    return em.find(Rv.class, id);
  }

  public List<Rv> findAll() {
    return em.createQuery("select object(o) from Rv as o").getResultList();
  }

}

Come già detto, la generazione automatica del codice può essere molto utile per avviare un progetto e familiarizzare con le entità JPA e EJB. Di seguito riscriveremo i livelli JPA e EJB con il nostro codice, ma il lettore vi ritroverà le informazioni che abbiamo appena visto nella generazione automatica dei livelli.

4.6. Il progetto NetBeans del modulo EJB

Creiamo un nuovo modulo EJB vuoto (cfr. paragrafo 4.5):

 
  • il pacchetto [rdvmedecins.entites] raggruppa le entità del livello JPA
  • il pacchetto [rdvmedecins.dao] implementa l’EJB del livello [dao]
  • il pacchetto [rdvmedecins.exceptions] implementa una classe di eccezione specifica dell’applicazione

Di seguito, diamo per scontato che il lettore abbia seguito tutti i passaggi descritti nel paragrafo 4.5. Alcuni di essi dovranno essere ripetuti.

4.6.1. Configurazione del livello JPA

Ricordiamo l’architettura della nostra applicazione client/server:

Il progetto NetBeans:

 

Il livello [JPA] è configurato dai file [persistence.xml] e [sun-resources.xml] sopra riportati. Questi due file sono generati da procedure guidate già illustrate:

  • la generazione del file [sun-resources.xml] è stata descritta nel paragrafo 4.5.
  • la generazione del file [persistence.xml] è stata descritta nel paragrafo 4.5.

Il file [persistence.xml] generato deve essere modificato come segue:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.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_1_0.xsd">
  <persistence-unit name="dbrdvmedecins" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <properties>
       <!-- Dialetto -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>
  • riga 3: il tipo di transazioni è JTA: le transazioni saranno gestite dal contenitore EJB3 di GlassFish
  • riga 4: viene utilizzata un’implementazione JPA/Hibernate. A tal fine, la libreria Hibernate è stata aggiunta al server GlassFish (cfr. paragrafo 4.4).
  • riga 5: la fonte dati JTA utilizzata dal livello JPA ha il nome JNDI «jdbc/dbrdvmedecins».
  • riga 8: questa riga non viene generata automaticamente. Deve essere aggiunta manualmente. Indica a Hibernate che il SGBD utilizzato è MySQL5.

La fonte dati "jdbc/dbrdvmedecins" è configurata nel seguente file [sun-resources.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Resource Definitions //EN" "http://www.sun.com/software/appserver/dtds/sun-resources_1_3.dtd">
<resources>
  <jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins" object-type="user" pool-name="dbrdvmedecinsPool">
    <description/>
  </jdbc-resource>
  <jdbc-connection-pool ...>
    <property name="URL" value="jdbc:mysql://localhost/dbrdvmedecins"/>
    <property name="User" value="root"/>
    <property name="Password" value="()"/>
  </jdbc-connection-pool>
</resources>
  • righe 8-10: le caratteristiche JDBC della fonte dati (URL del database, nome utente e password). Il database MySQL dbrdvmedecins è quello descritto al paragrafo 4.1.
  • riga 7: le caratteristiche del pool di connessioni associato a questa fonte di dati

4.6.2. Le entità del livello JPA

Ricordiamo l’architettura della nostra applicazione client/server:

Il progetto NetBeans:

Il pacchetto [rdvmedecins.entites] implementa il livello [Jpa].

Nel paragrafo 4.5 abbiamo visto come generare automaticamente le entità JPA di un'applicazione. In questa sede non utilizzeremo tale tecnica, ma definiremo noi stessi le entità. Queste riprenderanno tuttavia gran parte del codice generato nel paragrafo 4.5. In questo caso, vogliamo che le entità [Medecin] e [Client] siano classi figlie di una classe [Personne].

La classe Personne viene utilizzata per rappresentare medici e clienti:

package rdvmedecins.entites;
...
@MappedSuperclass
public class Personne implements Serializable {
   // caratteristiche di una persona

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Version
  @Column(name = "VERSION", nullable = false)
  private Integer version;

  @Column(name = "TITRE", length = 5, nullable = false)
  private String titre;
  @Column(name = "NOM", length = 30, nullable = false)
  private String nom;
  @Column(name = "PRENOM", length = 30, nullable = false)
  private String prenom;

   // costruttore predefinito
  public Personne() {
  }

   // costruttore con parametri
  public Personne(String titre, String nom, String prenom) {
     // si utilizzano i setter
...
  }

   // costruttore per copia
  public Personne(Personne personne) {
     // si utilizzano i setter
 ...
  }

   // toString
  @Override
  public String toString() {
    return "[" + titre + "," + prenom + "," + nom + "]";
  }

// getter e setter
....
}
  • riga 3: 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.entites;
....
@Entity
@Table(name = "CLIENTS")
public class Client extends Personne implements Serializable {

   // costruttore predefinito
  public Client() {
  }

   // costruttore con parametri
  public Client(String titre, String nom, String prenom) {
     // genitore
    super(titre, nom, prenom);
  }

   // costruttore per copia
  public Client(Client client) {
     // genitore
    super(client);
  }
}
  • riga 3: la classe [Client] è un'entità JPA
  • riga 4: è associata alla tabella [clients]
  • riga 5: deriva dalla classe [Personne]

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

package rdvmedecins.entites;
...
@Entity
@Table(name = "MEDECINS")
public class Medecin extends Personne implements Serializable {

   // costruttore predefinito
  public Medecin() {
  }

   // costruttore con parametri
  public Medecin(String titre, String nom, String prenom) {
     // genitore
    super(titre, nom, prenom);
  }

   // costruttore per copia
  public Medecin(Medecin medecin) {
     // genitore
    super(medecin);
  }
}

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

package rdvmedecins.entites;
....
@Entity
@Table(name = "CRENEAUX")
public class Creneau implements Serializable {

   // caratteristiche di uno slot di RV
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Version
  @Column(name = "VERSION", nullable = false)
  private Integer version;
  @ManyToOne
  @JoinColumn(name = "ID_MEDECIN", nullable = false)
  private Medecin medecin;
  @Column(name = "HDEBUT", nullable = false)
  private Integer hdebut;
  @Column(name = "MDEBUT", nullable = false)
  private Integer mdebut;
  @Column(name = "HFIN", nullable = false)
  private Integer hfin;
  @Column(name = "MFIN", nullable = false)
  private Integer mfin;

   // produttore predefinito
  public Creneau() {

  }

   // costruttore con parametri
  public Creneau(Medecin medecin, Integer hDebut,Integer mDebut, Integer hFin, Integer mFin) {
     // si utilizzano i setter
...
  }

   // costruttore con copia
  public Creneau(Creneau creneau) {
     // si utilizzano i setter
...
  }

   // toString
  @Override
  public String toString() {
    return "[" + getId() + "," + getVersion() + "," + getMedecin() + "," + getHdebut() + ":" + getMdebut() + "," + getHfin() + ":" + getMfin() + "]";
  }

   // setter - getter
...
}
  • le righe 15-17 modellano la relazione «uno a molti» esistente tra la tabella [creneaux] e la tabella [medecins] del database.

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

package rdvmedecins.entites;
...
@Entity
@Table(name = "RV")
public class Rv implements Serializable {
   // caratteristiche

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Column(name = "JOUR", nullable = false)
  @Temporal(TemporalType.DATE)
  private Date jour;
  @ManyToOne
  @JoinColumn(name = "ID_CLIENT", nullable = false)
  private Client client;
  @ManyToOne
  @JoinColumn(name = "ID_CRENEAU", nullable = false)
  private Creneau creneau;

   // costruttore predefinito
  public Rv() {
  }

   // costruttore con parametri
  public Rv(Date jour, Client client, Creneau creneau) {
     // si utilizzano i setter
...
  }

   // costruttore per copia
  public Rv(Rv rv) {
     // tramite i setter
...
  }

   // toString
  @Override
  public String toString() {
    return "[" + getId() + "," + new SimpleDateFormat("dd/MM/yyyy").format(getJour()) + "," + getClient() + "," + getCreneau() + "]";
  }

// getter e setter
...
}
  • le righe 15-17 modellano la relazione «uno a molti» esistente tra la tabella [rv] e la tabella [clients] del database, mentre le righe 18-20 modellano la relazione "uno a molti" che esiste tra la tabella [rv] e la tabella [creneaux]

4.6.3. La classe di eccezione

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

package rdvmedecins.exceptions;

import javax.ejb.ApplicationException;

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

  private static final long serialVersionUID = 1L;

   // 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 - setter
...
}
  • riga 6: la classe deriva dalla classe [RuntimeException]. Il compilatore non impone quindi di gestirla con try / catch.
  • riga 5: 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 dell'EJB del livello [dao] all'interno del contenitore EJB3 e intercettata da quest'ultimo. 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, quest’ultima deve essere annullata. In termini tecnici, questa operazione è denominata «rollback» della transazione.

4.6.4. L'EJB del livello [dao]

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

package rdvmedecins.dao;
...
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, String 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);
   // ricerca di 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(String jour, Creneau creneau, Client client);
   // eliminare un RV
  public void supprimerRv(Rv rv);
}

L'interfaccia locale [IDaoLocal] dell'EJB si limita a derivare dalla precedente interfaccia [IDao]:

1
2
3
4
5
6
7
package rdvmedecins.dao;

import javax.ejb.Local;

@Local
public interface IDaoLocal extends IDao{
}

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

1
2
3
4
5
6
7
package rdvmedecins.dao;

import javax.ejb.Remote;

@Remote
public interface IDaoRemote extends IDao {
}

L'EJB [DaoJpa] implementa entrambe le interfacce, locale e remota:

1
2
3
4
5
6
7
package rdvmedecins.dao;
...
@Stateless(mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal,IDaoRemote {
...
}
  • la riga 3 indica che l'EJB remoto ha il nome "rdvmedecins.dao"
  • la riga 4 indica che tutti i metodi dell'EJB si svolgono all'interno di una transazione gestita dal contenitore EJB3.
  • la riga 5 mostra che l'EJB implementa le interfacce locale e remota.

Il codice completo dell'EJB è il seguente:

package rdvmedecins.dao;
...
@Stateless(mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal,IDaoRemote {

  @PersistenceContext
  private EntityManager em;

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

   // elenco dei medici
  public List<Medecin> getAllMedecins() {
    try {
      return em.createQuery("select m from Medecin m").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 c from Creneau c join c.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, String jour) {
    try {
      return em.createQuery("select rv from Rv rv join rv.creneau c join c.medecin m where m.id=:idMedecin and rv.jour=:jour").setParameter("idMedecin", medecin.getId()).setParameter("jour", new SimpleDateFormat("yyyy:MM:dd").parse(jour)).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
  }

   // 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(String jour, Creneau creneau, Client client) {
    try {
      Rv rv = new Rv(new SimpleDateFormat("yyyy:MM:dd").parse(jour), client, creneau);
      em.persist(rv);
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }

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

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

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

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

   // 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, 10);
    }
  }
}
  • riga 8: 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 7.
  • riga 15: query JPQL che restituisce tutte le righe della tabella [clients] sotto forma di un elenco di oggetti [Client].
  • riga 22: query analoga per i medici
  • riga 32: una query JPQL che esegue un join tra le tabelle [creneaux] e [medecins]. È configurata in base all’ID del medico.
  • riga 43: 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 55-57: creazione di un appuntamento e successivo salvataggio dello stesso nel database.
  • riga 67: eliminazione di un appuntamento dal database.
  • riga 76: esegue una query SELECT sul database per trovare un determinato cliente
  • riga 85: idem per un medico
  • riga 94: lo stesso per un appuntamento
  • riga 103: idem per una fascia oraria
  • tutte le operazioni che utilizzano il contesto di persistenza em della riga 9 potrebbero incontrare un problema con il database. Pertanto, sono tutte racchiuse in un blocco try/catch. L'eventuale eccezione è incapsulata nell'eccezione "interna" RdvMedecinsException.

Una volta compilato, il modulo EJB genera un file .jar denominato :

4.7. Distribuzione dell’EJB del livello [dao] con NetBeans

NetBeans consente di distribuire facilmente sul server GlassFish l’EJB creato in precedenza.

  • Nelle proprietà del progetto EJB, verificare le opzioni di esecuzione [1].
  • In [2], il nome del server su cui verrà distribuito l'EJB
  • nella scheda [Services] [3], si avvia [4].
  • in [5], il server Glassfish una volta avviato. Non dispone ancora di alcun modulo EJB.
  • Avviare il server MySQL e assicurarsi che il database [dbrdvmedecins] sia online. A tal fine, è possibile utilizzare la connessione NetBeans creata al paragrafo 4.5.
  • Nella scheda [Projects] [6], si distribuisce il modulo EJB [7]: è necessario che SGBD MySQL5 sia in esecuzione affinché la risorsa JDBC "jdbc/dbrdvmedecins" utilizzata dall’EJB sia accessibile.
  • In [8], l’EJB distribuito appare nella struttura ad albero del server GlassFish
  • In [9], si rimuove l'EJB distribuito
  • in [10], l'EJB non compare più nella struttura ad albero del server GlassFish.

4.8. Distribuzione dell'EJB del livello [dao] con GlassFish

Qui mostriamo come distribuire sul server GlassFish un EJB a partire dal suo archivio .jar.

  • Avviare il server MySQL e assicurarsi che il database [dbrdvmedecins] sia online. A tal fine, è possibile utilizzare la connessione NetBeans creata al paragrafo 4.5.

Ricordiamo la configurazione JPA del modulo EJB che verrà distribuito. Tale configurazione è definita nel file [persistence.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.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_1_0.xsd">
  <persistence-unit name="dbrdvmedecins" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <properties>
       <!-- Dialetto -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>

La riga 5 indica che il livello JPA utilizza una fonte di dati JTA, c.a.d gestita dal contenitore EJB3, denominata "jdbc/dbrdvmedecins".

Nel paragrafo 4.5 abbiamo visto come creare questa risorsa JDBC da NetBeans. Qui mostriamo come farlo direttamente con GlassFish. Seguiamo la procedura descritta nel paragrafo 13.1.2, a pagina 79 di [ref1].

Iniziamo eliminando la risorsa per poterla ricreare. Lo facciamo da NetBeans:

  • in [1], le risorse JDBC del server GlassFish
  • in [2], la risorsa "jdbc/dbrdvmedecins" del nostro EJB
  • in [3], il pool di connessioni di questa risorsa JDBC
  • in [4], si elimina il pool di connessioni. Ciò comporterà l’eliminazione di tutte le risorse JDBC che lo utilizzano, quindi la risorsa "jdbc/dbrdvmedecins".
  • In [5] e [6], la risorsa JDBC e il pool di connessioni sono stati eliminati.

Ora utilizziamo la console di amministrazione del server GlassFish per creare la risorsa JDBC e distribuire l'EJB.

  • nella scheda [services] [1] di NetBeans, avviare il server GlassFish [2], quindi accedere alla sua console di amministrazione
  • all'indirizzo [4], effettuare l'accesso come amministratore (password: adminadmin se non è stata modificata durante l'installazione o successivamente).
  • in [5], selezionare il ramo [Connection Pools] delle risorse di Glassfish
  • in [6], creare un nuovo pool di connessioni. Si ricorda che un pool di connessioni è una tecnica per limitare il numero di aperture/chiusure di connessioni con un SGBD. All’avvio del server, N (un numero definito dalla configurazione) connessioni vengono aperte con il SGBD. Queste connessioni aperte vengono poi messe a disposizione degli EJB che le richiedono per eseguire un’operazione con il SGBD. Non appena l’operazione è terminata, l’EJB restituisce la connessione al pool. La connessione non viene mai chiusa. Viene condivisa tra i diversi thread che accedono al SGBD
  • in [7]; assegnate un nome al pool
  • in [8]; la classe che modella la fonte dati è la classe [javax.sql.DataSource]
  • in [9], il SGBD che contiene la fonte dati è qui MySQl.
  • in [10], passare alla fase successiva
  • In [11], l'attributo "Connection Validation Required" fa sì che, prima di fornire una connessione, il pool verifichi che sia operativa. Se così non fosse, ne crea una nuova. Ciò consente a un'applicazione di continuare a funzionare dopo un'interruzione momentanea con SGBD. Durante l’interruzione, nessuna connessione è utilizzabile e vengono segnalate delle eccezioni al client. Una volta terminata l’interruzione, i client che continuano a richiedere connessioni le ottengono nuovamente: grazie all’attributo "Connection Validation Required", tutte le connessioni del pool verranno ricreate. Senza questo attributo, il pool rileverebbe che le connessioni iniziali sono state interrotte ma non tenterebbe di crearne di nuove.
  • In [12], si richiede il livello di isolamento “Read Committed” per le transazioni. Questo livello garantisce che una transazione T2 non possa leggere i dati modificati da una transazione T1 finché quest’ultima non sia completamente terminata.
  • In [13], si richiede che tutte le transazioni utilizzino il livello di isolamento specificato in [12]
  • in [14] e [15], specificare l’URL di BD il cui pool gestisce le connessioni
  • in [16], l'utente sarà root
  • in [17], aggiungere una proprietà
  • in [18], aggiungere la proprietà "Password" con il valore () in [19]. Sebbene lo screenshot [19] non lo mostri, non bisogna inserire la stringa vuota, ma () (parentesi aperta, parentesi chiusa) per indicare una password vuota. Se l’utente root del vostro SGBD MySQL ha una password non vuota, inserite tale password.
  • in [20], completate la procedura guidata di creazione del pool di connessioni per il database MySQL [dbrdvmedecins].
  • in [21], il pool è stato creato. Fare clic sul relativo link.
  • in [22], il pulsante [Ping] consente di creare una connessione con il database [dbrdvmedecins]
  • in [23], se tutto va bene, un messaggio indica che la connessione è stata stabilita con successo

Una volta creato il pool di connessioni, è possibile creare una risorsa JDBC:

  • in [1], si seleziona il ramo [JDBC Resources] dall’albero degli oggetti del server
  • in [2], si crea una nuova risorsa JDBC
  • in [3], si assegna un nome alla risorsa JDBC. Il nome deve corrispondere a quello utilizzato nel file [persistence.xml]:
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
  • in [4], si specifica il pool di connessioni che la nuova risorsa JDBC deve utilizzare: quello appena creato
  • in [5], si completa la procedura guidata di creazione
  • in [6] la nuova risorsa JDBC

Ora che la risorsa JDBC è stata creata, è possibile distribuire l'archivio jar dell'EJB:

  • in [1], selezionate il ramo [Enterprise Applications]
  • in [2], utilizzando il pulsante [Deploy], specificare che si desidera distribuire una nuova applicazione
  • in [3], specificare che l’applicazione è un modulo EJB
  • in [4], selezionate il file JAR dell’EJB [serveur-ejb-dao-jpa-hibernate.jar] che vi è stato fornito per il TP.
  • in [5], è possibile modificare il nome del modulo EJB, se lo si desidera
  • in [6], completate la procedura guidata di distribuzione del modulo EJB
  • Nel [7], il modulo EJB è stato implementato. Ora è possibile utilizzarlo.

4.9. Test dell'EJB del livello [dao]

Ora che l’EJB del livello [dao] della nostra applicazione è stato distribuito, possiamo testarlo. Lo faremo utilizzando il seguente client Java:

La classe [MainTestsDaoRemote] [1] è una classe di test JUnit 4. Le librerie in [2] sono costituite, da un lato, da:

  • dal file JAR dell'EJB del livello [dao] [3] (cfr. paragrafo 4.6.4).
  • dalle librerie GlassFish [4] necessarie ai client remoti degli EJB.

La classe di test è la seguente:

package dao;
...
public class MainTestsDaoRemote {

   // livello [dao] testato
  private static IDaoRemote dao;

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

  @Test
  public void test1() {
     // dati del test
    String jour = "2006:08:23";
     // visualizzazione clienti
    List<Client> clients = null;
    try {
      clients = dao.getAllClients();
      display("Liste des clients :", clients);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // visualizzazione medici
    List<Medecin> medecins = null;
    try {
      medecins = dao.getAllMedecins();
      display("Liste des médecins :", medecins);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // visualizzazione delle fasce orarie di un medico
    Medecin medecin = medecins.get(0);
    List<Creneau> creneaux = null;
    try {
      creneaux = dao.getAllCreneaux(medecin);
      display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // elenco degli appuntamenti di un medico in un determinato giorno
    try {
      display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // 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));
    try {
      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, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // 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));
    try {
      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, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // eliminare un RV
    System.out.println("Suppression du Rv ajouté");
    try {
      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, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
  }

   // metodo di utilità - visualizza gli elementi di una raccolta
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • riga 13: si noti l’istanziazione del proxy dell’EJB remoto. Si utilizza il suo nome JNDI "rdvmedecins.dao".
  • I metodi di test utilizzano i metodi esposti dall'EJB (cfr. paragrafo 4.6.4).

Se tutto va bene, i test dovrebbero superare:

 

Ora che l’EJB del livello [dao] è operativo, è possibile procedere alla sua esposizione pubblica tramite un servizio web.

4.10. Il servizio web del livello [dao]

Per una breve introduzione al concetto di servizio web, si rimanda al paragrafo 14, pagina 111 di [ref1].

Torniamo all’architettura del server della nostra applicazione client/server:

Ci interessiamo qui di seguito al servizio web del livello [dao]. L’unico ruolo di questo servizio è quello di rendere disponibile l’interfaccia dell’EJB del livello [dao] a client multipiattaforma in grado di dialogare con un servizio web.

Ricordiamo che esistono due modi per implementare un servizio web:

  • tramite una classe annotata con @WebService che viene eseguita in un contenitore web
  • tramite un EJB annotato con @WebService che viene eseguito in un contenitore EJB

Qui utilizziamo la prima soluzione. In NetBeans, dobbiamo creare un progetto aziendale con due moduli:

  • il modulo EJB che verrà eseguito nel contenitore EJB: l’EJB del livello [dao].
  • il modulo web che verrà eseguito nel contenitore web: il servizio web che stiamo realizzando.

Realizzeremo questo progetto aziendale in due modi.

4.10.1. Progetto NetBeans - Versione 1

Per prima cosa creiamo un progetto NetBeans di tipo «Web Application»:

  • in [1], creiamo un nuovo progetto nella categoria “Java Web” [2] di tipo “Web Application” [3].
  • in [4], si assegna un nome al progetto e in [5] si specifica la cartella in cui deve essere generato
  • in [6], si imposta il server dell’applicazione che eseguirà l’applicazione web
  • in [7], si imposta il contesto dell'applicazione
  • in [8], si convalida la configurazione del progetto.
  • in [9], il progetto generato. Il servizio web che stiamo realizzando utilizzerà il EJB del progetto precedente [10]. Pertanto, deve fare riferimento al file .jar del modulo EJB [10].
  • In [11], si aggiunge un progetto NetBeans alle librerie del progetto web [12]
  • in [13], si seleziona la cartella del modulo EJB nel file system e si conferma.
  • in [14], il modulo EJB è stato aggiunto alle librerie del progetto web.

In [15], implementiamo il servizio web con la seguente classe [WsDaoJpa]:

package rdvmedecins.ws;
...
@WebService()
public class WsDaoJpa implements IDao {

  @EJB
  private IDaoLocal dao;

   // elenco dei clienti
  @WebMethod
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }

   // elenco dei medici
  @WebMethod
  public List<Medecin> getAllMedecins() {
    return dao.getAllMedecins();
  }

   // elenco delle fasce orarie di un determinato medico
   // medecin: il medico
  @WebMethod
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    return dao.getAllCreneaux(medecin);
  }

   // elenco degli appuntamenti di un determinato medico, in un determinato giorno
   // medico: il medico
   // giorno: il giorno
  @WebMethod
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour) {
    return dao.getRvMedecinJour(medecin, jour);
  }

   // aggiunta di un appuntamento
   // giorno: giorno dell'appuntamento
   // fascia oraria: fascia oraria dell'appuntamento
   // cliente: cliente per il quale è stato fissato l’appuntamento
  @WebMethod
  public Rv ajouterRv(String jour, Creneau creneau, Client client) {
    return dao.ajouterRv(jour, creneau, client);
  }

   // eliminazione di un appuntamento
   // appuntamento: l'appuntamento cancellato
  @WebMethod
  public void supprimerRv(Rv rv) {
    dao.supprimerRv(rv);
  }

   // recuperare un determinato cliente
  @WebMethod
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }

   // recuperare un determinato medico
  @WebMethod
  public Medecin getMedecinById(Long id) {
    return dao.getMedecinById(id);
  }

   // recuperare un appuntamento specifico
  @WebMethod
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }

   // recuperare una fascia oraria specifica
  @WebMethod
  public Creneau getCreneauById(Long id) {
    return dao.getCreneauById(id);
  }
}
  • alla riga 4, la classe [WsdaoJpa] implementa l'interfaccia [IDao]. Ricordiamo che questa interfaccia è definita nell'archivio dell'EJB del livello [dao] nella forma seguente:
package rdvmedecins.dao;
...
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, String 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(String jour, Creneau creneau, Client client);
   // eliminare un RV
  public void supprimerRv(Rv rv);
}
  • riga 3: l’annotazione @WebService rende la classe [WsDaoJpa] un servizio web.
  • righe 6-7: il riferimento all'EJB del livello [dao] verrà iniettato dal server dell'applicazione nel campo della riga 7. Ricordiamo che è sempre l’implementazione locale (in questo caso IDaoLocal) a essere così iniettata. Tale iniezione è possibile poiché il servizio web viene eseguito nella stessa JVM dell’EJB.
  • Tutti i metodi del servizio web sono contrassegnati con l’annotazione @WebMethod per renderli visibili ai client remoti. Un metodo non contrassegnato con l’annotazione @WebMethod sarebbe interno al servizio web e non visibile ai client remoti. Ogni metodo M del servizio web si limita a chiamare il metodo M corrispondente dell'EJB iniettato alla riga 7.

La creazione di questo servizio web è rappresentata da un nuovo ramo nel progetto NetBeans:

In [1] si vede il servizio web WsDaoJpa e in [2] i metodi che esso espone ai clienti remoti.

Ricordiamo l’architettura del servizio web in fase di sviluppo:

I componenti del servizio web che stiamo per implementare sono:

  • [1]: il modulo web che abbiamo appena realizzato
  • [2]: il modulo EJB che abbiamo creato in una fase precedente e dal quale dipende il servizio web

Per distribuirli insieme, è necessario riunire i due moduli in un progetto NetBeans denominato “aziendale”:

In [1] si crea un nuovo progetto aziendale [2, 3].

  • in [4,5], si assegna un nome al progetto e si specifica la cartella di creazione
  • in [6], si sceglie il server applicativo su cui verrà distribuita l’applicazione aziendale
  • in [7], un progetto aziendale può avere tre componenti: applicazione web, modulo EJB, applicazione client. In questo caso, il progetto viene creato senza alcuna componente. Queste verranno aggiunte in un secondo momento.
  • in [8], l’applicazione aziendale appena creata.
  • in [9], fare clic con il tasto destro su [Java EE Modules] e aggiungere un nuovo modulo
  • in [10], vengono visualizzati solo i moduli NetBeans attualmente aperti in IDE. Qui selezioniamo il modulo web [serveur-webservice-1-ejb-dao-jpa-hibernate] e il modulo EJB [serveur-ejb-dao-jpa-hibernate] che abbiamo creato.
  • In [11], i due moduli aggiunti al progetto aziendale.

Non resta che distribuire questa applicazione aziendale sul server GlassFish. Successivamente, è necessario avviare SGBD e MySQL affinché la fonte dati JDBC "jdbc/dbrdvmedecins" utilizzata dal modulo EJB sia accessibile.

  • in [1], si avvia il server Glassfish
  • se il modulo EJB [serveur-ejb-dao-jpa-hibernate] è distribuito, lo si rimuove [2]
  • in [3], si distribuisce l'applicazione aziendale
  • in [4], l'applicazione è distribuita. Si nota che contiene entrambi i moduli: Web ed EJB.

4.10.2. Progetto NetBeans - versione 2

Mostriamo ora come distribuire il servizio web quando non si dispone del codice sorgente del modulo EJB, ma solo del suo archivio .jar.

Il nuovo progetto NetBeans del servizio web sarà il seguente:

Gli elementi salienti del progetto sono i seguenti:

  • [1]: il servizio web è implementato da un progetto NetBeans di tipo [Web Application].
  • [2]: il servizio web è implementato dalla classe [WsDaoJpa] già esaminata
  • [3]: l'archivio EJB del livello [dao] che consente alla classe [WsDaoJpa] di accedere alle definizioni delle diverse classi, interfacce ed entità dei livelli [dao] e [jpa].

Successivamente, realizziamo il progetto aziendale necessario per l’implementazione del servizio web:

  • [1], creiamo un'applicazione aziendale [ea-rdvmedecins], inizialmente priva di qualsiasi modulo.
  • in [2], si aggiunge il modulo web [serveur-webservice-ejb-dao-jpa-hibernate] precedente
  • in [3], il risultato.

Così com’è, l’applicazione aziendale [ea-rdvmedecins] non può essere distribuita sul server Glassfish da NetBeans. Si verifica un errore. È quindi necessario distribuire manualmente l’archivio EAR dell’applicazione [ea-rdvmedecins]:

  • l'archivio [ea-rdvmedecins.ear] si trova nella cartella [dist] [2] della scheda [Files] di NetBeans.
  • In questo archivio [3] si trovano i due elementi dell’applicazione aziendale:
  • l'archivio dell'EJB [serveur-ejb-dao-jpa-hibernate]. Questo archivio è presente perché faceva parte delle librerie a cui faceva riferimento il servizio web.
  • l'archivio del servizio web [serveur-webservice- ejb-dao-jpa-hibernate].
  • L'archivio [ea-rdvmedecins.ear] è costituito da un semplice Build [4] dell'applicazione aziendale.
  • In [5], l’operazione di distribuzione non va a buon fine.

Per distribuire l’archivio [ea-rdvmedecins.ear] dell’applicazione aziendale, procediamo come illustrato durante la distribuzione dell’archivio EJB [serveur-ejb-dao-jpa-hibernate.jar] al paragrafo 4.2. Utilizziamo nuovamente il client web di amministrazione del server GlassFish. Non ripeteremo i passaggi già descritti.

Per prima cosa, inizieremo con lo «scaricare» l’applicazione aziendale distribuita al paragrafo 4.10.1:

  • [1]: selezionare il ramo [Enterprise Applications] del server Glassfish
  • in [2] selezionare l’applicazione aziendale da scaricare, quindi in [3] scaricarla
  • in [4] l’applicazione aziendale è stata scaricata
  • in [1], scegliere il ramo [Enterprise Applications] del server Glassfish
  • in [2], distribuire una nuova applicazione aziendale
  • in [3], selezionare il tipo [Enterprise Application]
  • in [4], specificare il file .ear del progetto NetBeans [ea-rdvmedecins]
  • in [5], distribuite questo archivio
  • in [6]; l'applicazione è stata distribuita
  • in [7], il servizio web [WsDaoJpa] appare nel ramo [Web Services] del server Glassfish. Lo si seleziona.
  • In [8] sono disponibili diverse informazioni sul servizio web. La più interessante per un cliente è l’informazione [9]: l’URI del servizio web.
  • In [10] è possibile testare il servizio web
  • In [11], l' , l'URI del servizio web a cui è stato aggiunto il parametro ?tester. Questo URI mostra una pagina di test. Tutti i metodi (@WebMethod) esposti dal servizio web vengono visualizzati e possono essere testati. In questo caso, si testa il metodo [13] che richiede l'elenco dei clienti.
  • In [14], presentiamo solo una vista parziale della pagina di risposta. Tuttavia, si può notare che il metodo getAllClients ha effettivamente restituito l'elenco dei clienti. Lo screenshot ci mostra che invia la risposta in formato XML.

Un servizio web è interamente descritto da un file XML denominato file WSDL:

  • in [1] nello strumento di amministrazione web del server Glassfish, selezionate il servizio web [WsDaoJpa]
  • in [2], seguire il link [View WSDL]
  • in [3]: l'URI del file WSDL. Si tratta di un'informazione importante da conoscere. È necessaria per configurare i client di questo servizio web.
  • in [4], la descrizione XML del servizio web. Non commenteremo questo contenuto complesso.

4.10.3. Test JUnit del servizio web

Creiamo un progetto NetBeans per «ripetere» i test già eseguiti con un client EJB, utilizzando questa volta un client per il servizio web recentemente implementato. Seguiamo qui una procedura analoga a quella descritta al paragrafo 14.2.1, pagina 115 di [ref1].

  • in [1], un progetto Java classico
  • in [2], la classe di test
  • in [3], il client utilizza l'archivio dell'EJB per accedere alle definizioni dell'interfaccia del livello [dao] e delle entità JPA. Si ricorda che questo archivio si trova nella sottocartella [dist] della cartella del modulo EJB.

Per accedere al servizio web remoto, è necessario generare delle classi proxy:

Nello schema sopra riportato, il livello [2] [C=Client] comunica con il livello [1] [S=Serveur]. Per comunicare con il livello [S], il client [C] deve creare una connessione di rete con il livello [S] e comunicare con esso secondo un protocollo specifico. Le connessioni di rete sono di tipo TCP e il protocollo di trasporto è HTTP. Il livello [S], che rappresenta il servizio web, è implementato da una servlet Java eseguita dal server Glassfish. Non abbiamo scritto questa servlet. La sua generazione è automatizzata da Glassfish a partire dalle annotazioni @Webservice e @WebMethod della classe [WsDaoJpa] che abbiamo scritto. Allo stesso modo, automatizzeremo la generazione del livello [C] del client. Il livello [C] viene talvolta definito livello proxy del servizio web remoto, poiché il termine proxy indica un elemento intermedio in una catena software. In questo caso, il proxy C funge da intermediario tra il client che scriveremo e il servizio web che abbiamo distribuito.

Con NetBeans 6.5, il proxy C può essere generato nel modo seguente (per il seguito, è necessario che il servizio web sia attivo sul server GlassFish):

  • in [1], aggiungere un nuovo elemento al progetto Java
  • in [2], selezionare il ramo [Web services]
  • in [3], selezionare [Web Service Client]
  • in [4], specificare l'URI del file WSDL del servizio web. Questo URI è stato illustrato nel paragrafo 4.10.2.
  • in [5], lasciare il valore predefinito [JAX-WS]. L'altro valore possibile è [JAX-RPC]
  • Dopo aver confermato la procedura guidata per la creazione del proxy del servizio web, il progetto NetBeans è stato arricchito con un ramo [Web Service References] [6]. Questo ramo mostra i metodi esposti dal servizio web remoto.
  • nella scheda [Files] [7], sono stati aggiunti codici sorgente Java [8]. Essi corrispondono al proxy C generato.
  • In [9] è riportato il codice di una delle classi. Si nota [10] che sono state inserite in un pacchetto [rdvmedecins.ws]. Non commenteremo il codice di queste classi, che risulta nuovamente piuttosto complesso.

Per il client Java che stiamo realizzando, il proxy C generato funge da intermediario. Per accedere al metodo M del servizio web remoto, il client Java chiama il metodo M del proxy C. Il client Java chiama così metodi locali (eseguiti nella stessa JVM) e, in modo trasparente per lui, queste chiamate locali vengono tradotte in chiamate remote.

Resta da capire come chiamare i metodi M del proxy C. Torniamo alla nostra classe di test JUnit:

In [1], la classe di test [MainTestsDaoRemote] è quella già utilizzata durante il test dell’EJB del livello [dao]:

package dao;
...
public class MainTestsDaoRemote {

   // livello [dao] testato
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
  }

  @Test
  public void test1() {
...
  }
}
  • riga [13], il test test1 è stato mantenuto invariato.
  • riga [9], il contenuto del metodo [init] è stato eliminato.

A questo punto, il progetto presenta degli errori poiché il metodo di test [test1] utilizza le entità [Client], [Medecin], [Creneau], [Rv], che non si trovano più negli stessi pacchetti di prima. Si trovano nel pacchetto del proxy C generato. Si eliminano le istruzioni import interessate e le si rigenerano tramite l'operazione «Fix Imports».

Torniamo al codice della classe di test [MainTestsDaoRemote]:

package dao;
...

public class MainTestsDaoRemote {

   // livello [dao] testato
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
}

Il metodo [init] alla riga 10 deve inizializzare il riferimento al livello [dao] alla riga 7. Dobbiamo capire come utilizzare il proxy C generato nel nostro codice. NetBeans ci aiuta in questo processo.

  • Selezionare in [1] il metodo [getAllClients] del servizio web con il mouse, quindi trascinare tale metodo e rilasciarlo all’interno del metodo [init] della classe di test.

Si ottiene il risultato [2]. Questo scheletro di codice ci mostra come utilizzare il proxy C generato:

1
2
3
4
5
6
7
8
9
    try { // Operazione di chiamata al servizio web
      rdvmedecins.ws.WsDaoJpaService service = new rdvmedecins.ws.WsDaoJpaService();
      rdvmedecins.ws.WsDaoJpa port = service.getWsDaoJpaPort();
       // elaborazione del risultato di TODO qui
      java.util.List<rdvmedecins.ws.Client> result = port.getAllClients();
      System.out.println("Result = "+result);
    } catch (Exception ex) {
       // TODO gestione delle eccezioni personalizzate qui
}
  • la riga [5] ci mostra che il metodo [getAllClients] è un metodo dell’oggetto di tipo [WsDaoJpa] definito alla riga 3. Il tipo [WsDaoJpa] è un'interfaccia che presenta gli stessi metodi di quelli esposti dal servizio web remoto.
  • Alla riga [3], l’oggetto [WsDaoJpa port] viene ottenuto da un altro oggetto di tipo [WsDaoJpaService] definito alla riga 2. Il tipo [WsDaoJpaService] rappresenta il proxy C generato localmente.
  • L'accesso al servizio web remoto potrebbe non riuscire, pertanto l'intero codice è racchiuso in un blocco try/catch.
  • Gli oggetti del proxy C si trovano nel pacchetto [rdvmedecins.ws]

Una volta compreso questo codice, si vede che il riferimento locale del servizio web remoto può essere ottenuto tramite il codice:

WsDaoJpa dao=new WsDaoJpaService().getWsDaoJpaPort();

Il codice della classe di test JUnit diventa quindi il seguente:

package dao;

import rdvmedecins.ws.Client;
import rdvmedecins.ws.Creneau;
import rdvmedecins.ws.Medecin;
import rdvmedecins.ws.Rv;
import rdvmedecins.ws.WsDaoJpa;
import rdvmedecins.ws.WsDaoJpaService;
...

public class MainTestsDaoRemote {

   // livello [dao] testato
  private static WsDaoJpa dao;

  @BeforeClass
  public static void init(){
    dao=new WsDaoJpaService().getWsDaoJpaPort();
  }

  @Test
  public void test1() {
...
  }

   // metodo di utilità - visualizza gli elementi di una collezione
  private static void display(String message, List elements) {
 ...
  }
}

Ora siamo pronti per i test:

In [1] viene eseguito il test JUnit. In [2] il test ha esito positivo. Osservando i messaggi visualizzati sulla console di NetBeans, si trovano righe come le seguenti:

Liste des clients :
rdvmedecins.ws.Client@1982fc1
rdvmedecins.ws.Client@676437
rdvmedecins.ws.Client@1e4853f
rdvmedecins.ws.Client@1e808ca

Sul lato server, l'entità [Client] dispone di un metodo toString che visualizza i diversi campi di un oggetto di tipo [Client]. Durante la generazione automatica del proxy C, le entità vengono create nel proxy C ma solo con i campi privati accompagnati dai relativi metodi get / set. Pertanto, il metodo toString non è stato generato nell'entità [Client] del proxy C. Ciò spiega la visualizzazione precedente. Ciò non toglie nulla al test JUnit: è stato superato. D'ora in poi considereremo il servizio web operativo.