Skip to content

4. Il servizio web J2EE per gli appuntamenti

Torniamo all'architettura dell'applicazione da realizzare:

In questa sezione ci concentreremo sulla realizzazione del servizio web J2EE [1] in esecuzione 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: il numero identificativo del medico — la chiave primaria della tabella
  • VERSIONE: un numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • LAST_NAME: il cognome del medico
  • FIRST_NAME: il nome del medico
  • TITOLO: il titolo (Sig.ra, Sig.ra, Sig.)

4.1.2. La tabella [CLIENTI]

I clienti dei vari medici sono memorizzati 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.
  • COGNOME: il cognome del cliente
  • NOME: il nome del cliente
  • TITOLO: il titolo (Sig.ra, Sig.ra, Sig.)

4.1.3. La tabella [SLOTS]

Elenca le fasce orarie in cui sono disponibili gli appuntamenti:

  • ID: numero identificativo della fascia oraria - chiave primaria della tabella (riga 8)
  • VERSION: Un numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • DOC_ID: numero ID che identifica il medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna DOCTORS(ID).
  • START_TIME: Ora di inizio della fascia oraria
  • MSTART: 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 [SLOTS] (vedi [1] sopra) indica, ad esempio, che la fascia oraria n. 2 inizia alle 8:20 e termina alle 8:40 e appartiene al medico n. 1 (dott.ssa Marie PELISSIER).

4.1.4. La tabella [RV]

Elenca gli appuntamenti prenotati per ciascun medico:

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

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

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

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

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

4.2. Creazione del database

Crea il database MySQL [dbrdvmedecins] utilizzando lo strumento che preferisci. Per creare le tabelle e popolarle, puoi utilizzare lo script [createbd.sql] 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. Componenti dell'architettura lato server

Torniamo all'architettura dell'applicazione da realizzare:

Sul lato server, l'applicazione sarà composta da:

  1. un livello JPA che consente l'interazione con il database tramite oggetti
  1. un EJB responsabile della gestione delle operazioni con il livello JPA
  2. un servizio web responsabile dell'esposizione dell'interfaccia EJB ai client remoti sotto forma di servizio web.

Gli elementi (b) e (c) implementano il livello [DAO] mostrato nel diagramma precedente. Sappiamo che un'applicazione può accedere a un EJB remoto tramite i protocolli RMI e JNDI. In pratica, questo limita i client a quelli Java. Un servizio web utilizza un protocollo di comunicazione standardizzato che vari linguaggi implementano: .NET, PHP, C++, ... Questo è ciò che vogliamo dimostrare qui utilizzando un client .NET.

Per una breve introduzione ai servizi web, consultare il corso [rif1], 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
  • da un EJB contrassegnato con l'annotazione @WebService che viene eseguito in un contenitore EJB

Qui useremo la prima soluzione:

Nel corso [rif. 1], paragrafo 14, pagina 109, troverete un esempio che utilizza la seconda soluzione.

4.4. e la configurazione di Hibernate per il server GlassFish

A seconda della versione, il server GlassFish V2 incluso in NetBeans potrebbe non disporre delle librerie Hibernate richieste dal livello JPA/Hibernate. Se, mentre procedi con il tutorial, ti accorgi che GlassFish non fornisce un'implementazione JPA/Hibernate, o se durante la distribuzione del servizio si verifica un'eccezione che indica che le librerie Hibernate non sono state trovate, devi aggiungere le librerie alla cartella [<glassfish>/domains/domain1/lib/ext] e quindi riavviare il server GlassFish:

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

Le librerie Hibernate sono incluse nel file ZIP allegato al tutorial.

4.5. 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. È utile acquisire familiarità con questi metodi di generazione automatica perché il codice generato fornisce preziose indicazioni su come scrivere entità JPA o il codice EJB che le utilizza.

Descriveremo ora alcuni di questi strumenti di generazione automatica. Per comprendere il codice generato, è necessaria una solida conoscenza delle entità JPA [rif. 1] e degli EJB [rif. 2].

Creazione di una connessione NetBeans al database

  • Avviare il DBMS MySQL 5 in modo che il database sia disponibile
  • Creare una connessione NetBeans al database [dbrdvmedecins]
  • Nella scheda [File], nella sezione [Database] [1], selezionare il driver JDBC di MySQL [2]
  • Quindi seleziona l'opzione [3] "Connetti utilizzando" per creare una connessione a un database MySQL
  • In [4], inserisci le informazioni richieste
  • quindi confermare in [5]
  • In [6], la connessione viene creata. È possibile visualizzare le quattro tabelle nel 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], selezionare il tipo [Modulo EJB]
  • In [4], scegli una cartella per il progetto e in [5], assegnagli un nome, quindi completa 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 [Servizi], avvia il server GlassFish [2, 3]
  • Nella scheda [Progetti], fare clic con il tasto destro del mouse sul progetto EJB e in [5] selezionare l'opzione [Nuovo / Altro] per 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 [Risorsa JDBC]
  • 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], definire 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 al passaggio successivo
  • In [15], normalmente non c'è nulla da modificare in questa pagina. Le proprietà della connessione al database MySQL [dbrdvmedecins] sono state prese da quelle della connessione NetBeans [dbrdvmedecins] creata in precedenza
  • In [16], passare al passaggio successivo
  • in [17], mantenere i valori predefiniti forniti
  • in [18], completare la procedura guidata. In questo modo viene creato il file [sun-resources.xml] [19] con il seguente contenuto:
<?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 contiene tutte le informazioni inserite nella procedura guidata in formato XML. Verrà utilizzato dall'IDE NetBeans per indicare 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: specifica l'implementazione JPA utilizzata (TopLink, Hibernate, ecc.) e la configura.

  • In [1], fare clic con il tasto destro del mouse sul progetto EJB e selezionare [Nuovo / Altro] in [2]
  • In [3], selezionare la categoria [Persistenza], quindi in [4], indicare che si desidera creare un'unità di persistenza JPA
  • In [5], assegnare un nome all'unità di persistenza creata
  • In [6], scegli [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, converte le informazioni fornite nella procedura guidata in formato XML. Questo file non è sufficiente per lavorare con il database MySQL5 "dbrdvmedecins". Dovremmo specificare a Hibernate il tipo di DBMS da gestire. Lo faremo in seguito.

Creazione di entità JPA

 
  • In [1], fare clic con il tasto destro del mouse sul progetto e in [2] selezionare l'opzione [Nuovo / Altro]
  • In [3], selezionare la categoria [Persistenza], quindi in [4] indicare che si desidera creare entità JPA da un database esistente.
  • In [5], selezionare la sorgente JDBC "jdbc/dbrdvmedecins" che abbiamo creato
  • In [6], selezionare le quattro tabelle dal 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 per l'entità [Rv], che rappresenta una riga nella 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 per l'accesso alle entità JPA

  • In [1], fare clic con il tasto destro del mouse sul progetto e in [2] selezionare l'opzione [Nuovo / Altro]
  • in [3], selezionare la categoria [Persistenza], quindi in [4] selezionare il tipo [Session Bean per classi di entità]
  • in [5] vengono visualizzate le entità JPA create in precedenza
  • In [6], selezionarle tutte
  • In [7], sono state selezionate
  • In [8], prosegui con la procedura guidata
  • In [9], assegnare un nome al pacchetto per gli EJB che verranno generati
  • In [10], specificare che gli EJB devono implementare sia un'interfaccia locale che una remota
  • Completa la procedura guidata
  • in [11], gli EJB generati

Ecco, ad esempio, il codice EJB che gestisce l'accesso all'entità [Rv] e, quindi, alla tabella [rv] nel 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 accennato, la generazione automatica del codice può essere molto utile per avviare un progetto e familiarizzare con le entità JPA e gli EJB. Nelle sezioni seguenti riscriveremo i livelli JPA ed EJB con il nostro codice, ma il lettore riconoscerà le informazioni che abbiamo appena trattato nella generazione automatica dei livelli.

4.6. Il progetto NetBeans per il modulo EJB

Creiamo un nuovo modulo EJB vuoto (vedere la sezione 4.5):

 
  • Il pacchetto [rdvmedecins.entities] contiene le entità del livello JPA
  • il pacchetto [rdvmedecins.dao] implementa l'EJB per il livello [dao]
  • il pacchetto [rdvmedecins.exceptions] implementa una classe di eccezioni specifica dell'applicazione

Di seguito, si presume che il lettore abbia seguito tutti i passaggi della sezione 4.5. Sarà necessario ripeterne alcuni.

4.6.1. Configurazione del livello JPA

Rivediamo 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 dalle procedure guidate che abbiamo già incontrato:

  • La generazione del file [sun-resources.xml] è stata descritta nella Sezione 4.5.
  • La generazione del file [persistence.xml] è stata descritta nella sezione 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>
       <!-- Dialect -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>
  • Riga 3: Il tipo di transazione è 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 (vedere la sezione 4.4).
  • Riga 5: l'origine 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 DBMS utilizzato è MySQL5.

L'origine 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 proprietà JDBC dell'origine dati (URL del database, nome utente e password). Il database MySQL `dbrdvmedecins` è quello descritto nella Sezione 4.1.
  • Riga 7: le caratteristiche del pool di connessioni associato a questa origine dati

4.6.2. Le entità del livello JPA

Rivediamo l'architettura della nostra applicazione client/server:

Il progetto NetBeans:

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

Nella Sezione 4.5 abbiamo visto come generare automaticamente entità JPA per un'applicazione. Qui non useremo questa tecnica, ma definiremo le entità noi stessi. Tuttavia, queste incorporeranno gran parte del codice generato nella Sezione 4.5. In questo caso, vogliamo che le entità [Medecin] e [Client] siano classi figlie di una classe [Personne].

La classe Person viene utilizzata per rappresentare medici e clienti:

package rdvmedecins.entites;
...
@MappedSuperclass
public class Personne implements Serializable {
   // characteristics of a person

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

   // default builder
  public Personne() {
  }

   // builder with parameters
  public Personne(String titre, String nom, String prenom) {
     // we use setters
...
  }

   // copy builder
  public Personne(Personne personne) {
     // we use setters
 ...
  }

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

// getters and setters
....
}
  • Riga 3: Si noti che la classe [Person] non è di per sé 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 [Person]:

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

   // default builder
  public Client() {
  }

   // builder with parameters
  public Client(String titre, String nom, String prenom) {
     // parent
    super(titre, nom, prenom);
  }

   // copy builder
  public Client(Client client) {
     // parent
    super(client);
  }
}
  • Riga 3: la classe [Client] è un'entità JPA
  • riga 4: è associata alla tabella [clients]
  • riga 5: deriva dalla classe [Person]

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

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

   // default builder
  public Medecin() {
  }

   // builder with parameters
  public Medecin(String titre, String nom, String prenom) {
     // parent
    super(titre, nom, prenom);
  }

   // copy builder
  public Medecin(Medecin medecin) {
     // parent
    super(medecin);
  }
}

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

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

   // characteristics of a RV slot
  @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;

   // default builder
  public Creneau() {

  }

   // builder with parameters
  public Creneau(Medecin medecin, Integer hDebut,Integer mDebut, Integer hFin, Integer mFin) {
     // we use setters
...
  }

   // copy builder
  public Creneau(Creneau creneau) {
     // we use setters
...
  }

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

   // setters - getters
...
}
  • Le righe 15–17 modellano la relazione "uno-a-molti" tra la tabella [slots] e la tabella [doctors] nel database.

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

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

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

   // default builder
  public Rv() {
  }

   // builder with parameters
  public Rv(Date jour, Client client, Creneau creneau) {
     // we use setters
...
  }

   // copy builder
  public Rv(Rv rv) {
     // we use setters
...
  }

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

// getters and setters
...
}
  • Le righe 15–17 modellano la relazione "uno-a-molti" tra la tabella [rv] e la tabella [clients] nel database, mentre le righe 18–20 modellano la relazione "uno-a-molti" tra la tabella [rv] e la tabella [slots]

4.6.3. La classe di eccezione

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

package rdvmedecins.exceptions;

import javax.ejb.ApplicationException;

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

  private static final long serialVersionUID = 1L;

   // private fields
  private int code = 0;

   // manufacturers
  public RdvMedecinsException() {
    super();
  }

  public RdvMedecinsException(String message) {
    super(message);
  }

  public RdvMedecinsException(String message, Throwable cause) {
    super(message, cause);
  }

  public RdvMedecinsException(Throwable cause) {
    super(cause);
  }

  public RdvMedecinsException(String message, int code) {
    super(message);
    setCode(code);
  }

  public RdvMedecinsException(Throwable cause, int code) {
    super(cause);
    setCode(code);
  }

  public RdvMedecinsException(String message, Throwable cause, int code) {
    super(message, cause);
    setCode(code);
  }

   // getters - setters
...
}
  • Riga 6: La classe estende la classe [RuntimeException]. Pertanto, il compilatore non richiede che venga gestita con blocchi try/catch.
  • Riga 5: l'annotazione @ApplicationException garantisce che l'eccezione non venga "assorbita" da un'[EjbException].

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

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

4.6.4. L'EJB del livello [dao]

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

package rdvmedecins.dao;
...
public interface IDao {

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

L'interfaccia locale dell'EJB [IDaoLocal] estende semplicemente la 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 sia l'interfaccia locale che quella 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 è denominato "rdvmedecins.dao"
  • La riga 4 indica che tutti i metodi dell'EJB vengono eseguiti all'interno di una transazione gestita dal contenitore EJB3.
  • La riga 5 mostra che l'EJB implementa le interfacce locale e remota.

Il codice EJB completo è il seguente:

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

  @PersistenceContext
  private EntityManager em;

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

   // list of doctors
  public List<Medecin> getAllMedecins() {
    try {
      return em.createQuery("select m from Medecin m").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 2);
    }
  }

   // list of time slots for a given doctor
   // doctor: the doctor
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    try {
      return em.createQuery("select 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);
    }
  }

   // list of appointments for a given doctor on a given day
   // doctor: the doctor
   // day: the day
  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);
    }
  }

   // add Rv
   // day : day of appointment
   // creneau: Rv time slot
   // customer: customer for whom the appointment is taken
  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);
    }
  }

   // deleting an appointment
   // rv: Rv deleted
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

   // retrieve a specific customer
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 7);
    }
  }

   // retrieve a specific doctor
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 8);
    }
  }

   // retrieve a given Rv
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 9);
    }
  }

   // retrieve a given slot
  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. Quando la classe viene istanziata, questo campo verrà inizializzato dal contenitore EJB utilizzando l'annotazione @PersistenceContext alla riga 7.
  • riga 15: query JPQL che restituisce tutte le righe della tabella [clients] come un elenco di oggetti [Client].
  • riga 22: una query simile per i medici
  • Riga 32: una query JPQL che esegue un join tra le tabelle [slots] e [doctors]. È parametrizzata dall’ID del medico.
  • Riga 43: una query JPQL che esegue un join tra le tabelle [appointments], [slots] e [doctors] e che ha due parametri: l'ID del medico e la data dell'appuntamento.
  • Righe 55–57: creazione di un appuntamento e sua successiva persistenza nel database.
  • Riga 67: Elimina un appuntamento dal database.
  • Riga 76: esegue una query SELECT sul database per trovare un cliente specifico
  • Riga 85: Lo stesso per un medico
  • Riga 94: lo stesso per un appuntamento
  • Riga 103: lo stesso per una fascia oraria
  • Tutte le operazioni che coinvolgono il contesto di persistenza della riga 9 potrebbero incontrare un problema con il database. Pertanto, sono tutte racchiuse in un blocco try/catch. Qualsiasi eccezione è incapsulata nell'eccezione personalizzata 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 l'EJB creato in precedenza sul server GlassFish.

  • Nelle proprietà del progetto EJB, selezionare le opzioni di runtime [1].
  • In [2], il nome del server su cui verrà distribuito l'EJB
  • Nella scheda [Servizi] [3], avvialo [4].
  • In [5], il server GlassFish una volta avviato. Non dispone ancora di un modulo EJB.
  • Avviare il server MySQL e assicurarsi che il database [dbrdvmedecins] sia online. A tal fine, è possibile utilizzare la connessione NetBeans creata nella sezione 4.5.
  • Nella scheda [Progetti] [6], distribuire il modulo EJB [7]: il DBMS MySQL5 deve essere 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], rimuovere l'EJB distribuito
  • In [10], l'EJB non compare più nella struttura ad albero del server GlassFish.

4.8. Distribuzione dell'EJB dal livello [DAO] con GlassFish

Qui mostriamo come distribuire un EJB sul server GlassFish 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 nella Sezione 4.5.

Esaminiamo la configurazione JPA del modulo EJB da distribuire. Questa 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>
       <!-- Dialect -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>

La riga 5 indica che il livello JPA utilizza un'origine dati JTA, ovvero una gestita dal contenitore EJB3, denominata "jdbc/dbrdvmedecins".

Abbiamo visto nella Sezione 4.5 come creare questa risorsa JDBC utilizzando NetBeans. Qui mostriamo come farlo direttamente con GlassFish. Seguiamo la procedura descritta nella Sezione 13.1.2, pagina 79 di [rif1].

Iniziamo eliminando la risorsa in modo da 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 per questa risorsa JDBC
  • in [4], eliminiamo il pool di connessioni. Ciò comporterà l'eliminazione di tutte le risorse JDBC che lo utilizzano, inclusa 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 [Servizi] [1] di NetBeans, avviare il server GlassFish [2] e quindi accedere [3] alla sua console di amministrazione
  • In [4], accedi come amministratore (password: adminadmin se non l'hai modificata durante l'installazione o in seguito).
  • In [5], seleziona il ramo [Connection Pools] delle risorse di GlassFish
  • In [6], creare un nuovo pool di connessioni. Si noti che un pool di connessioni è una tecnica per limitare il numero di connessioni aperte e chiuse con un DBMS. All'avvio del server, vengono aperte N connessioni (un numero definito dalla configurazione) al DBMS. Queste connessioni aperte vengono quindi rese disponibili agli EJB che le richiedono per eseguire un'operazione con il DBMS. Non appena l'operazione è completata, l'EJB restituisce la connessione al pool. La connessione non viene mai chiusa; viene condivisa tra i vari thread che accedono al DBMS
  • in [7], assegnare un nome al pool
  • in [8], la classe che modella l'origine dati è la classe [javax.sql.DataSource]
  • in [9], il DBMS contenente l'origine dati è MySQL in questo caso.
  • in [10], procedere al passo successivo
  • in [11], l'attributo "Connection Validation Required" garantisce che, prima di concedere una connessione, il pool verifichi che sia operativa. In caso contrario, ne crea una nuova. Ciò consente a un'applicazione di continuare a funzionare dopo un'interruzione temporanea del DBMS. Durante l'interruzione, nessuna connessione è utilizzabile e vengono generate delle eccezioni per il client. Al termine dell'interruzione, i client che continuano a richiedere connessioni le ottengono nuovamente: grazie all'attributo "Convalida connessione richiesta", tutte le connessioni nel pool verranno ricreate. Senza questo attributo, il pool rileverebbe che le connessioni iniziali sono andate perse, ma non tenterebbe di crearne di nuove.
  • In [12], per le transazioni è specificato il livello di isolamento "Read Committed". Questo livello garantisce che una transazione T2 non possa leggere i dati modificati da una transazione T1 fino a quando quest'ultima non sia completamente completata.
  • In [13], specifichiamo che tutte le transazioni devono utilizzare il livello di isolamento specificato in [12]
  • In [14] e [15], specificare l'URL del database le cui connessioni sono gestite dal pool
  • In [16], l'utente sarà root
  • In [17], aggiungere una proprietà
  • In [18], aggiungere la proprietà "Password" con il valore () in [19]. Sebbene la schermata [19] non lo mostri, non inserire una stringa vuota; inserire invece () (parentesi aperta, parentesi chiusa) per indicare una password vuota. Se l'utente root del DBMS MySQL ha una password non vuota, inserire quella password.
  • In [20], completare la procedura guidata di creazione del pool di connessioni per il database MySQL [dbrdvmedecins].
  • In [21], il pool è stato creato. Fare clic sul relativo collegamento.
  • In [22], il pulsante [Ping] consente di stabilire 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], selezionare il ramo [Risorse JDBC] dall'albero degli oggetti del server
  • In [2], creare una nuova risorsa JDBC
  • In [3], assegnare un nome alla risorsa JDBC. Questo deve corrispondere al nome utilizzato nel file [persistence.xml]:
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
  • In [4], specifica il pool di connessioni che la nuova risorsa JDBC deve utilizzare: quello che hai appena creato
  • In [5], completiamo la procedura guidata di creazione
  • In [6], la nuova risorsa JDBC

Ora che la risorsa JDBC è stata creata, è possibile distribuire il file JAR dell'EJB:

  • In [1], selezionare il ramo [Enterprise Applications]
  • In [2], fare clic sul pulsante [Deploy] per indicare che si desidera distribuire una nuova applicazione
  • In [3], specificare che l'applicazione è un modulo EJB
  • In [4], selezionare il file JAR EJB [serveur-ejb-dao-jpa-hibernate.jar] fornito per l'esercitazione.
  • In [5], è possibile modificare il nome del modulo EJB, se lo si desidera
  • In [6], completare la procedura guidata di distribuzione del modulo EJB
  • In [7], il modulo EJB è stato distribuito. Ora è possibile utilizzarlo.

4.9. Test dell'EJB del livello [DAO]

Ora che l'EJB per il livello [DAO] della nostra applicazione è stato implementato, possiamo testarlo. Lo faremo utilizzando il seguente client Java:

La classe [MainTestsDaoRemote] [1] è una classe di test JUnit 4. Le librerie in [2] consistono in:

  • il file JAR EJB del livello [DAO] [3] (vedere la sezione 4.6.4).
  • le librerie GlassFish [4] necessarie per i client EJB remoti.

La classe di test è la seguente:

package dao;
...
public class MainTestsDaoRemote {

   // layer [dao] tested
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
     // environment initialization JNDI
    InitialContext initialContext = new InitialContext();
     // dao layer instantiation
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }

  @Test
  public void test1() {
     // tEST DATA
    String jour = "2006:08:23";
     // customer display
    List<Client> clients = null;
    try {
      clients = dao.getAllClients();
      display("Liste des clients :", clients);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // physician display
    List<Medecin> medecins = null;
    try {
      medecins = dao.getAllMedecins();
      display("Liste des médecins :", medecins);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // display doctor's slots
    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);
    }
     // list of doctor's appointments on a given day
    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);
    }
     // add a RV to the list
    Rv rv = null;
    Creneau creneau = creneaux.get(2);
    Client client = clients.get(0);
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    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);
    }
     // add a RV in the same slot on the same day
     // must trigger an exception
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    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);
    }
     // delete a 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);
    }
  }

   // utility method - displays items in a collection
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • Riga 13: Si noti l'istanziazione del proxy EJB remoto. Utilizziamo il suo nome JNDI "rdvmedecins.dao".
  • I metodi di test utilizzano i metodi esposti dall'EJB (vedere la sezione 4.6.4).

Se tutto va bene, i test dovrebbero superare:

 

Ora che l'EJB del livello [dao] è operativo, possiamo procedere a renderlo pubblico tramite un servizio web.

4.10. Il servizio web del livello [dao]

Per una breve introduzione al concetto di servizio web, consultare la sezione 14, pagina 111 di [rif1].

Torniamo all'architettura server della nostra applicazione client/server:

Qui ci concentriamo sul servizio web del livello [DAO]. L'unico scopo di questo servizio è rendere l'interfaccia EJB del livello [DAO] disponibile ai client multipiattaforma in grado di comunicare con un servizio web.

Ricordiamo che esistono due modi per implementare un servizio web:

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

In questo caso, utilizziamo la prima soluzione. Nell'IDE NetBeans, dobbiamo creare un progetto enterprise 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 attualmente sviluppando.

Realizzeremo questo progetto enterprise in due modi.

4.10.1. Progetto NetBeans - Versione 1

Per prima cosa, creiamo un progetto NetBeans di tipo "Applicazione Web":

  • In [1], creare un nuovo progetto nella categoria "Java Web" [2] del tipo "Applicazione Web" [3].
  • In [4] si assegna un nome al progetto, mentre in [5] si specifica la cartella in cui deve essere generato
  • In [6], specifichiamo il server delle applicazioni che eseguirà l'applicazione web
  • In [7], impostare il contesto dell'applicazione
  • In [8], convalidare la configurazione del progetto.
  • In [9], il progetto generato. Il servizio web che stiamo realizzando utilizzerà l'EJB del progetto precedente [10]. Pertanto, deve fare riferimento al file .jar del modulo EJB [10].
  • In [11], aggiungere un progetto NetBeans alle librerie del progetto web [12]
  • In [13], selezionare la cartella del modulo EJB nel file system e confermare.
  • 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;

   // customer list
  @WebMethod
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }

   // list of doctors
  @WebMethod
  public List<Medecin> getAllMedecins() {
    return dao.getAllMedecins();
  }

   // list of time slots for a given doctor
   // doctor: the doctor
  @WebMethod
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    return dao.getAllCreneaux(medecin);
  }

   // list of appointments for a given doctor on a given day
   // doctor: the doctor
   // day: the day
  @WebMethod
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour) {
    return dao.getRvMedecinJour(medecin, jour);
  }

   // add Rv
   // day : day of appointment
   // creneau: Rv time slot
   // customer: customer for whom the appointment is taken
  @WebMethod
  public Rv ajouterRv(String jour, Creneau creneau, Client client) {
    return dao.ajouterRv(jour, creneau, client);
  }

   // deleting an appointment
   // rv: Rv deleted
  @WebMethod
  public void supprimerRv(Rv rv) {
    dao.supprimerRv(rv);
  }

   // retrieve a specific customer
  @WebMethod
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }

   // retrieve a specific doctor
  @WebMethod
  public Medecin getMedecinById(Long id) {
    return dao.getMedecinById(id);
  }

   // retrieve a given Rv
  @WebMethod
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }

   // retrieve a given slot
  @WebMethod
  public Creneau getCreneauById(Long id) {
    return dao.getCreneauById(id);
  }
}
  • Riga 4: La classe [WsdaoJpa] implementa l'interfaccia [IDao]. Ricordiamo che questa interfaccia è definita nell'archivio EJB del livello [dao] come segue:
package rdvmedecins.dao;
...
public interface IDao {

   // customer list
  public List<Client> getAllClients();
   // list of doctors
  public List<Medecin> getAllMedecins();
   // list of physician slots
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // list of doctor's appointments on a given day
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour);
   // find a customer identified by its id
  public Client getClientById(Long id);
   // find a customer identified by its id
  public Medecin getMedecinById(Long id);
   // find an Rv identified by its id
  public Rv getRvById(Long id);
   // find a time slot identified by its id
  public Creneau getCreneauById(Long id);
   // add a RV to the list
  public Rv ajouterRv(String jour, Creneau creneau, Client client);
   // delete a 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 nel livello [DAO] verrà iniettato dal server dell'applicazione nel campo alla riga 7. Si noti che viene sempre iniettata l'implementazione locale (IDaoLocal in questo caso). Questa iniezione è possibile perché 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 chiama semplicemente il metodo M corrispondente dell'EJB iniettato alla riga 7.

La creazione di questo servizio web si riflette in un nuovo ramo nel progetto NetBeans:

In [1] vediamo il servizio web WsDaoJpa e in [2] i metodi che esso espone ai client remoti.

Esaminiamo 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 creato
  • [2]: il modulo EJB che abbiamo creato in un passaggio precedente, da cui dipende il servizio web

Per distribuirli insieme, dobbiamo combinare i due moduli in un progetto "enterprise" di NetBeans:

In [1], creare un nuovo progetto enterprise [2, 3].

  • In [4,5], assegniamo un nome al progetto e specifichiamo la directory di creazione
  • In [6], selezionare il server delle applicazioni su cui verrà distribuita l'applicazione aziendale
  • In [7], un progetto aziendale può avere tre componenti: applicazione web, modulo EJB e applicazione client. Qui, il progetto viene creato senza alcun componente. Questi verranno aggiunti in seguito.
  • 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 nell'IDE. Qui, selezioniamo il modulo web [web-service-server-1-ejb-dao-jpa-hibernate] e il modulo EJB [ejb-server-dao-jpa-hibernate] che abbiamo creato.
  • In [11], i due moduli aggiunti al progetto enterprise.

Ora dobbiamo distribuire questa applicazione enterprise sul server GlassFish. Successivamente, è necessario avviare il DBMS MySQL in modo che sia accessibile l'origine dati JDBC "jdbc/dbrdvmedecins" utilizzata dal modulo EJB.

  • In [1], avviamo il server GlassFish
  • Se il modulo EJB [ejb-server-dao-jpa-hibernate] è distribuito, lo scarichiamo [2]
  • In [3], distribuire l'applicazione enterprise
  • In [4], è distribuita. Possiamo vedere che contiene entrambi i moduli: Web ed EJB.

4.10.2. Progetto NetBeans - Versione 2

Ora mostreremo 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 per il 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], che abbiamo già studiato
  • [3]: l'archivio EJB per il livello [DAO], che consente alla classe [WsDaoJpa] di accedere alle definizioni delle varie classi, interfacce ed entità nei livelli [DAO] e [JPA].

Quindi creiamo il progetto enterprise necessario per distribuire il servizio web:

  • [1] Creiamo un'applicazione aziendale [ea-rdvmedecins], inizialmente priva di moduli.
  • In [2], aggiungiamo il precedente modulo web [webservice-ejb-dao-jpa-hibernate]
  • in [3], il risultato.

Allo stato attuale, 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] in NetBeans.
  • In questo archivio [3] si trovano i due componenti dell'applicazione enterprise:
  • l'archivio EJB [ejb-server-dao-jpa-hibernate]. Questo archivio è presente perché faceva parte delle librerie a cui fa riferimento il servizio web.
  • l'archivio del servizio web [webservice-server-ejb-dao-jpa-hibernate].
  • L'archivio [ea-rdvmedecins.ear] viene creato da una semplice compilazione [4] dell'applicazione enterprise.
  • In [5], l'operazione di distribuzione fallisce.

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

Per prima cosa, inizieremo "scaricando" l'applicazione aziendale distribuita nella sezione 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], selezionare il ramo [Enterprise Applications] del server GlassFish
  • in [2], distribuisci una nuova applicazione enterprise
  • In [3], selezionare il tipo [Enterprise Application]
  • In [4], specificare il file .ear per il progetto NetBeans [ea-rdvmedecins]
  • In [5], distribuire questo archivio
  • In [6], l'applicazione è stata distribuita
  • in [7], il servizio web [WsDaoJpa] appare nel ramo [Web Services] del server GlassFish. Selezionarlo.
  • In [8] sono disponibili vari dettagli sul servizio web. L'informazione più rilevante per un client è [9]: l'URI del servizio web.
  • In [10], è possibile testare il servizio web
  • In [11], l'URI del servizio web a cui è stato aggiunto il parametro ?test. Questo URI visualizza una pagina di test. Tutti i metodi (@WebMethod) esposti dal servizio web vengono visualizzati e possono essere testati. Qui, testiamo il metodo [13], che recupera l'elenco dei client.
  • In [14], mostriamo solo una vista parziale della pagina di risposta. Tuttavia, possiamo vedere che il metodo getAllClients ha effettivamente restituito l'elenco dei clienti. Lo screenshot mostra che invia la sua risposta in formato XML.

Un servizio web è descritto in modo completo da un file XML chiamato file WSDL:

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

4.10.3. Test JUnit per il servizio web

Creiamo un progetto NetBeans per "eseguire" i test precedentemente effettuati con un client EJB, questa volta utilizzando un client per il servizio web appena implementato. Qui seguiamo una procedura simile a quella descritta nella sezione 14.2.1, pagina 115 di [rif1].

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

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

Nel diagramma sopra, il livello [2] [C=Client] comunica con il livello [1] [S=Server]. Per interagire con il livello [S], il client [C] deve stabilire una connessione di rete con il livello [S] e comunicare con esso utilizzando un protocollo specifico. Le connessioni di rete sono connessioni TCP e il protocollo di trasporto è HTTP. Il livello [S], che rappresenta il servizio web, è implementato da un servlet Java in esecuzione sul server GlassFish. Non abbiamo scritto questo servlet. La sua generazione è automatizzata da GlassFish sulla base delle annotazioni @WebService e @WebMethod nella classe [WsDaoJpa] che abbiamo scritto. Allo stesso modo, automatizzeremo la generazione del livello client [C]. Il livello [C] è talvolta chiamato livello proxy per il servizio web remoto, dove il termine "proxy" si riferisce a un elemento intermediario in una catena software. In questo caso, il proxy C funge da intermediario tra il client che stiamo per scrivere e il servizio web che abbiamo distribuito.

Con NetBeans 6.5, il proxy C può essere generato come segue (per il resto del processo, il servizio web deve essere in esecuzione sul server GlassFish):

  • In [1], aggiungere un nuovo elemento al progetto Java
  • in [2], selezionare il ramo [Servizi Web]
  • in [3], selezionare [Web Service Client]
  • in [4], fornire l'URI del file WSDL del servizio web. Questo URI è stato presentato nella sezione 4.10.2.
  • In [5], lasciare il valore predefinito [JAX-WS]. L'altro valore possibile è [JAX-RPC]
  • Dopo aver confermato la procedura guidata di creazione del proxy del servizio web, il progetto NetBeans è stato espanso per includere un ramo [Riferimenti ai servizi web] [6]. Questo ramo mostra i metodi esposti dal servizio web remoto.
  • Nella scheda [Files] [7], è stato aggiunto il codice sorgente Java [8]. Esso corrisponde al proxy C generato.
  • In [9] è riportato il codice di una delle classi. Possiamo vedere [10] che sono state inserite in un pacchetto [rdvmedecins.ws]. Non commenteremo il codice di queste classi, che è ancora una volta 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 quindi metodi locali (eseguiti nella stessa JVM) e, in modo trasparente per esso, queste chiamate locali vengono tradotte in chiamate remote.

Dobbiamo ancora 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 per testare l'EJB del livello [dao]:

package dao;
...
public class MainTestsDaoRemote {

   // layer [dao] tested
  private static IDaoRemote dao;

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

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

A questo punto, il progetto contiene degli errori perché il metodo di test [test1] utilizza le entità [Client], [Medecin], [Creneau] e [Rv], che non si trovano più negli stessi pacchetti di prima. Ora si trovano nel pacchetto proxy C generato. Rimuoviamo le istruzioni di importazione pertinenti e le rigeneriamo utilizzando l'operazione "Fix Imports".

Torniamo al codice della classe di test [MainTestsDaoRemote]:

package dao;
...

public class MainTestsDaoRemote {

   // layer [dao] tested
  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 sapere come utilizzare il proxy C generato nel nostro codice. NetBeans ci aiuta in questo processo.

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

Otteniamo 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 { // Call Web Service Operation
      rdvmedecins.ws.WsDaoJpaService service = new rdvmedecins.ws.WsDaoJpaService();
      rdvmedecins.ws.WsDaoJpa port = service.getWsDaoJpaPort();
       // TODO process result here
      java.util.List<rdvmedecins.ws.Client> result = port.getAllClients();
      System.out.println("Result = "+result);
    } catch (Exception ex) {
       // TODO handle custom exceptions here
}
  • La riga [5] mostra che il metodo [getAllClients] è un metodo dell'oggetto [WsDaoJpa] definito alla riga 3. Il tipo [WsDaoJpa] è un'interfaccia che espone gli stessi metodi esposti dal servizio web remoto.
  • Nella riga [3], l'oggetto [WsDaoJpaPort] viene ottenuto da un altro oggetto di tipo [WsDaoJpaService] definito nella riga 2. Il tipo [WsDaoJpaService] rappresenta il proxy C generato localmente.
  • L'accesso al servizio web remoto potrebbe non andare a buon fine, pertanto l'intero blocco di codice è racchiuso in un blocco try/catch.
  • Gli oggetti proxy C si trovano nel pacchetto [rdvmedecins.ws]

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

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

Il codice per la 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 {

   // layer [dao] tested
  private static WsDaoJpa dao;

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

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

   // utility method - displays items in a collection
  private static void display(String message, List elements) {
 ...
  }
}

Ora siamo pronti per il test:

In [1] viene eseguito il test JUnit. In [2] il test viene superato. Se osserviamo l'output sulla console di NetBeans, vediamo 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 vari campi di un oggetto [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. Questo spiega la visualizzazione precedente. Ciò non sminuisce il test JUnit: è stato superato. Considereremo ora di avere un servizio web operativo.