Skip to content

3. Voorbeeldtoepassing – 01: rdvmedecins-jsf2-ejb

De volgende tekst verwijst naar de volgende documenten:

  • [ref7]: Inleiding tot Java EE 5 (juni 2010) [http://tahe.developpez.com/java/javaee]. Dit document biedt een inleiding tot JSF 1 en de EJB3.
  • [ref8]: Java-persistentie in de praktijk (juni 2007) [http://tahe.developpez.com/java/jpa]. Dit document biedt inzicht in gegevenspersistentie met JPA (Java Persistence API).
  • [ref9]: Een Java-webservice bouwen met NetBeans en de GlassFish-server (januari 2009) EE. Dit document behandelt het bouwen van een webservice.

De voorbeeldtoepassing die wordt behandeld, is afkomstig uit [ref9].

3.1. L'application

Een IT-dienstverlener, [ISTIA-AGI], wil een dienst voor het maken van afspraken aanbieden. De eerste doelgroep bestaat uit zelfstandige artsen. Deze hebben doorgaans geen secretariaat. Klanten die een afspraak willen maken, bellen dan rechtstreeks naar de arts. Hierdoor wordt de arts gedurende de dag vaak gestoord, wat ten koste gaat van de tijd die hij aan zijn patiënten kan besteden. Het bedrijf [ISTIA-AGI] wil hen een afsprakenplanningsdienst aanbieden die volgens het volgende principe werkt:

  • een secretariaat verzorgt de afspraken voor een groot aantal artsen. Dit secretariaat kan worden beperkt tot één persoon. Het salaris van deze persoon wordt gedeeld door alle artsen die gebruikmaken van de dienst.
  • het secretariaat en alle artsen zijn aangesloten op het internet
  • de RV-afspraken worden opgeslagen in een gecentraliseerde database, die via internet toegankelijk is voor zowel het secretariaat als de artsen
  • Het vastleggen van RV gebeurt normaal gesproken door het secretariaat. Het kan ook door de artsen zelf worden gedaan. Dit is met name het geval wanneer de arts aan het einde van een consult zelf een nieuwe RV voor zijn patiënt vaststelt.

De architectuur van de dienst voor het vastleggen van RV is als volgt:

Artsen werken efficiënter als ze zich niet meer met de RV hoeven bezig te houden. Als er voldoende artsen meedoen, zal hun bijdrage aan de exploitatiekosten van het secretariaat gering zijn.

Het bedrijf [ISTIA-AGI] besluit de applicatie in twee versies te realiseren:

  • een versie JSF / EJB3 / JPA EclipseLink / Glassfish-server:
  • en vervolgens een versie JSF / Spring / JPA Hibernate / Tomcat-server:

3.2. Werking van de applicatie

We noemen de applicatie [RdvMedecins]. Hieronder vindt u schermafbeeldingen van de werking ervan.

De startpagina van de applicatie ziet er als volgt uit:

Vanaf deze eerste pagina zal de gebruiker (secretariaat, arts) een aantal handelingen uitvoeren. Deze worden hieronder weergegeven. Het linkerscherm toont het scherm waarop de gebruiker een verzoek indient, het rechterscherm toont het antwoord dat door de server wordt verzonden.

Ten slotte kan er ook een foutpagina verschijnen:

3.3. De database

Laten we teruggaan naar de architectuur van de te bouwen applicatie:

De database, die we [dbrdvmedecins2] zullen noemen, is een MySQL5-database met vier tabellen:

  

3.3.1. De tabel [MEDECINS]

Het bevat informatie over de artsen die worden beheerd door de applicatie [RdvMedecins].

  • ID: identificatienummer van de arts – primaire sleutel van de tabel
  • VERSION: identificatienummer van de versie van de rij in de tabel. Dit nummer wordt telkens met 1 verhoogd wanneer er een wijziging in de rij wordt aangebracht.
  • NOM: de achternaam van de arts
  • PRENOM: zijn of haar voornaam
  • TITRE: zijn/haar aanspreektitel (mevrouw, meisje, meneer)

3.3.2. De tabel [CLIENTS]

De cliënten van de verschillende artsen worden opgeslagen in de tabel [CLIENTS]:

  • ID: identificatienummer van de klant – primaire sleutel van de tabel
  • VERSION: nummer dat de versie van de rij in de tabel identificeert. Dit nummer wordt met 1 verhoogd telkens wanneer er een wijziging in de rij wordt aangebracht.
  • NOM: de naam van de klant
  • PRENOM: de voornaam
  • TITRE: zijn/haar aanspreektitel (mevrouw, meisje, meneer)

3.3.3. De tabel [CRENEAUX]

Deze tabel geeft een overzicht van de tijdvakken waarin de RV mogelijk zijn:

  • ID: nummer dat het tijdvak identificeert – primaire sleutel van de tabel (regel 8)
  • VERSION: nummer dat de versie van de rij in de tabel identificeert. Dit nummer wordt met 1 verhoogd telkens wanneer er een wijziging in de rij wordt aangebracht.
  • ID_MEDECIN: nummer dat de arts identificeert aan wie dit tijdslot toebehoort – vreemde sleutel op de kolom MEDECINS (ID).
  • HDEBUT: starttijd van het tijdvak
  • MDEBUT: minuten begin van het tijdslot
  • HFIN: einduur van het tijdvak
  • MFIN: eindminuten van het tijdslot

De tweede regel van de tabel [CRENEAUX] (zie [1] hierboven) geeft bijvoorbeeld aan dat tijdvak nr. 2 om 8.20 uur begint en om 8.40 uur eindigt en toebehoort aan arts nr. 1 (mevrouw Marie PELISSIER).

3.3.4. De tabel [RV]

Deze tabel geeft een overzicht van de RV die voor elke arts zijn vastgelegd:

  • ID: nummer dat de RV op unieke wijze identificeert – primaire sleutel
  • JOUR: dag van de RV
  • ID_CRENEAU: tijdvak van RV – externe sleutel op het veld [ID] van de tabel [CRENEAUX] – bepaalt zowel het tijdvak als de betreffende arts.
  • ID_CLIENT: nummer van de klant voor wie de reservering is gemaakt – externe sleutel op het veld [ID] van de tabel [CLIENTS]

Deze tabel heeft een uniekheids -beperking op de waarden van de gekoppelde kolommen (JOUR, ID_CRENEAU):

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

Als een rij in de tabel [RV] de waarde (JOUR1, ID_CRENEAU1) voor de kolommen (JOUR, ID_CRENEAU), dan mag deze waarde nergens anders voorkomen. Anders zou dit betekenen dat er twee RV’en tegelijkertijd voor dezelfde arts zijn vastgelegd. Vanuit het oogpunt van Java-programmering start de driver JDBC van de database een SQLException wanneer dit zich voordoet.

De regel met id gelijk aan 3 (zie [1] hierboven) betekent dat er op 23/08/2006 een RV is geboekt voor tijdvak nr. 20 en klant nr. 4. Uit de tabel [CRENEAUX] blijkt dat tijdvak nr. 20 overeenkomt met het tijdvak 16.20 - 16.40 uur en toebehoort aan arts nr. 1 (mevrouw Marie PELISSIER). Uit de tabel [CLIENTS] blijkt dat klant nr. 4 mevrouw Brigitte BISTROU is.

3.3.5. Het aanmaken van de database

Om de tabellen aan te maken en te vullen, kan het script [dbrdvmedecins2.sql] worden gebruikt, dat te vinden is op de voorbeeldpagina. Met [WampServer] (zie paragraaf 1.3.3) kan als volgt worden te werk gegaan:

  • in [1] klikt men op het pictogram van [WampServer] en kiest men de optie [PhpMyAdmin] [2],
  • naar [3]; in het venster dat nu is geopend, selecteer je de link [Bases de données],
  • naar [2], maak je een database aan met de naam [4] en de codering [5],
  • in [7] is de database aangemaakt. Klik op de link,
  • in [8], importeren we een bestand SQL,
  • dat je in het bestandssysteem selecteert met de knop [9],
  • in [11] selecteren we het script SQL en in [12] voeren we het uit,
  • in [13] zijn de vier tabellen van de database aangemaakt. We volgen een van de links,
  • in [14], de inhoud van de tabel.

Vervolgens zullen we niet meer op deze database terugkomen. Maar de lezer wordt uitgenodigd om de ontwikkeling ervan te volgen in de loop van de programma’s, vooral wanneer het niet werkt.

3.4. De lagen [DAO] en [JPA]

Laten we terugkeren naar de architectuur die we moeten bouwen:

We gaan vier Maven-projecten bouwen:

  • een project voor de lagen [DAO] en [JPA],
  • een project voor de laag [métier],
  • een project voor de laag [web],
  • een bedrijfsproject waarin de drie voorgaande projecten worden samengebracht.

We bouwen nu het Maven-project voor de lagen [DAO] en [JPA].

Opmerking: om de lagen [métier], [DAO] en [JPA] te begrijpen, is kennis van Java EE vereist. Hiervoor kunt u [ref7] raadplegen (zie paragraaf 3).

3.4.1. Het NetBeans-project

Dit is het volgende:

  • in [1] wordt een Maven-project van het type [EJB Module] [2] opgezet,
  • in [3] geven we het project een naam,
  • in [4] kiezen we de Glassfish-server,
  • in [5] wordt het project gegenereerd.

3.4.2. Genereren van de laag [JPA]

Laten we teruggaan naar de architectuur die we moeten bouwen:

Met NetBeans is het mogelijk om automatisch de laag [JPA] en de laag [EJB] te genereren, die de toegang tot de gegenereerde entiteiten JPA regelen. Het is interessant om deze methoden voor automatische generatie te kennen, omdat de gegenereerde code waardevolle aanwijzingen geeft over hoe JPA-entiteiten moeten worden geschreven of de EJB-code die deze gebruikt.

We beschrijven nu enkele van deze automatische generatietools. Om de gegenereerde code te begrijpen, moet men een goede kennis hebben van de entiteiten JPA, [ref8] en de EJB, [ref7] (zie paragraaf 3).

3.4.2.1. Een NetBeans-verbinding met de database maken

  • start de entiteiten SGBD en MySQL 5 op, zodat de entiteit BD beschikbaar is,
  • maak een NetBeans-verbinding met de database [dbrdvmedecins2],
  • in het tabblad [Services] [1], in de tak [Databases] [2], selecteer de driver JDBC MySQL [3],
  • selecteer vervolgens de optie [4] "Connect Using" om een verbinding tot stand te brengen met een database MySQL,
  • in [5] de gevraagde gegevens invoeren. In [6] de naam van de database, in [7] de gebruikersnaam en het wachtwoord van de database,
  • in [8] kun je de opgegeven gegevens testen,
  • in [9], het verwachte bericht wanneer deze gegevens correct zijn,
  • in [10] is de verbinding tot stand gebracht. Hier zijn de vier tabellen van de verbonden database te zien.

3.4.2.2. Een persistentie-eenheid aanmaken

Laten we terugkeren naar de architectuur die momenteel wordt opgebouwd:

We zijn bezig met het bouwen van de laag [JPA]. De configuratie hiervan gebeurt in een bestand [persistence.xml] waarin persistentie-eenheden worden gedefinieerd. Elk daarvan heeft de volgende informatie nodig:

  • de JDBC-toegangskenmerken voor de database (URL, gebruiker, wachtwoord),
  • de klassen die de afbeeldingen van de tabellen in de database zullen vormen,
  • de gebruikte implementatie JPA. JPA is namelijk een specificatie die door verschillende producten wordt geïmplementeerd. Hier gebruiken we EclipseLink, de standaardimplementatie die door de GlassFish-server wordt gebruikt. Zo hoeven we de bibliotheken van een andere implementatie niet aan GlassFish toe te voegen.

NetBeans kan dit persistentiebestand genereren met behulp van een wizard.

  • klik met de rechtermuisknop op het project en kies voor het aanmaken van een persistentie-eenheid [1],
  • in [2], geef de persistentie-eenheid die je aanmaakt een naam,
  • in [3], kies de implementatie JPA EclipseLink (JPA 2.0),
  • in [4] aangeven dat de transacties met de database worden beheerd door de container EJB van de Glassfish-server,
  • in [5] aangeven dat de tabellen van BD al zijn aangemaakt en dat ze daarom niet opnieuw worden aangemaakt,
  • in [6]: maak een nieuwe gegevensbron aan voor de Glassfish-server,
  • in [7], geef de naam JNDI (Java Naming Directory Interface) op,
  • in [8], koppel deze naam aan de verbinding MySQL die in de vorige stap is aangemaakt,
  • in [9], de wizard afronden,
  • in [10], het nieuwe project,
  • in [11] is het bestand [persistence.xml] gegenereerd in de map [META-INF],
  • in [12] is een map [setup] aangemaakt,
  • in [13] zijn er nieuwe afhankelijkheden aan het Maven-project toegevoegd.

Het gegenereerde bestand [META-INF/persistence.xml] ziet er als volgt uit:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
    <jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties/>
  </persistence-unit>
</persistence>

Het bevat de informatie die in de wizard is opgegeven:

  • regel 3: de naam van de persistentie-eenheid,
  • regel 3: het type transacties met de database, in dit geval JTA-transacties (Java Transaction API) die worden beheerd door de EJB3-container van de Glassfish-server,
  • regel 4: de naam JNDI van de gegevensbron.

Normaal gesproken staat in dit bestand het gebruikte implementatietype JPA vermeld. In de wizard hebben we EclipseLink opgegeven. Aangezien dit de implementatie JPA is die standaard door de Glassfish-server wordt gebruikt, wordt deze niet vermeld in het bestand [persistence.xml].

Op het tabblad [Design] krijgt men een totaaloverzicht van het bestand [persistence.xml]:

Om logbestanden van EclipseLink te verkrijgen, gebruiken we het volgende bestand [persistence.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/> 
    </properties>
  </persistence-unit>
</persistence>
  • regel 4: hier wordt aangegeven dat de implementatie JPA van EclipseLink wordt gebruikt,
  • regels 7-9: bevatten de configuratie-eigenschappen van de provider JPA, in dit geval EclipseLink,
  • regel 8: met deze eigenschap kunnen de opdrachten SQL worden gelogd die door EclipseLink zullen worden verzonden.

Het aangemaakte bestand [glassfish-resources.xml] ziet er als volgt uit:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
    <jdbc-connection-pool allow-non-component-callers="false" ... steady-pool-size="8" validate-atmost-once-period-in-seconds="0" wrap-jdbc-objects="false">
        <property name="serverName" value="localhost"/>
        <property name="portNumber" value="3306"/>
        <property name="databaseName" value="dbrdvmedecins2"/>
        <property name="User" value="root"/>
        <property name="Password" value=""/>
        <property name="URL" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
    </jdbc-connection-pool>
    <jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins2" object-type="user" pool-name="mysql_dbrdvmedecins2_rootPool"/>
</resources>

Dit bestand bevat de informatie die we in de twee eerder gebruikte wizards hebben opgegeven:

  • regels 5-11: de kenmerken JDBC van de database MySQL5 [dbrdvmedecins2],
  • regel 13: de naam JNDI van de gegevensbron.

Dit bestand wordt gebruikt om de gegevensbron JNDI [jdbc/dbrdvmedecins2] van de Glassfish-server aan te maken. Dit is volledig specifiek voor deze server. Voor een andere server zou men het anders moeten aanpakken, meestal met behulp van een beheertool. Deze bestaat ook voor Glassfish.

Ten slotte zijn er afhankelijkheden aan het project toegevoegd. Het bestand [pom.xml] ziet er als volgt uit:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>istia.st</groupId>
    <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>ejb</packaging>

    <name>mv-rdvmedecins-ejb-dao-jpa</name>

    ...
    <dependencies>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>eclipselink</artifactId>
            <version>2.3.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>javax.persistence</artifactId>
            <version>2.0.3</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
            <version>2.3.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

...
    <repositories>
        <repository>
            <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
            <id>eclipselink</id>
            <layout>default</layout>
            <name>Repository for library Library[eclipselink]</name>
        </repository>
    </repositories>
</project>
  • regels 32-37: een laag [JPA] vereist het artefact [javaee-api],
  • regels 16, 22, 28: de artefacten die nodig zijn voor de hier gebruikte implementatie JPA / EclipseLink.
  • regels 18, 24, 30, 36: alle artefacten hebben het attribuut provided. Ter herinnering: dit betekent dat ze nodig zijn voor de compilatie, maar niet voor de uitvoering. Tijdens de uitvoering worden ze namelijk (provided) door de Glassfish-server geleverd,
  • regels 41-48: hiermee wordt een nieuwe Maven-artefactrepository gedefinieerd, waarin de artefacten EclipseLink te vinden zijn.

3.4.2.3. Genereren van de entiteiten JPA

De entiteiten JPA kunnen worden gegenereerd met een wizard in NetBeans:

  • in [1] worden JPA-entiteiten aangemaakt op basis van een database,
  • in [2] selecteert men de eerder aangemaakte gegevensbron [jdbc / dbrdvmedecins2],
  • in [3] de lijst met tabellen van deze gegevensbron,
  • in [4] selecteert men ze allemaal,
  • in [5] de geselecteerde tabellen,
  • in [6], geven we een naam aan de Java-klassen die bij de vier tabellen horen,
  • evenals een pakketnaam [7],
  • in [8], JPA verzamelt rijen uit de tabellen van BD in collecties. We kiezen de lijst als collectie,
  • in [9], de door de wizard aangemaakte Java-klassen.

3.4.2.4. De gegenereerde entiteiten JPA

De entiteit [Medecin] is de weergave van de tabel [medecins]. De Java-klasse zit vol met annotaties die de code op het eerste gezicht moeilijk leesbaar maken. Als we alleen datgene behouden wat essentieel is voor het begrijpen van de rol van de entiteit, krijgen we de volgende code:


package rdvmedecins.jpa;

...
@Entity
@Table(name = "medecins")
public class Medecin implements Serializable {
  
@Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;
  
  @Column(name = "TITRE")
  private String titre;

  @Column(name = "NOM")
  private String nom;

  @Column(name = "VERSION")
  private int version;

  @Column(name = "PRENOM")
  private String prenom;

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

// constructors
....

   // getters en setters
....

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

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

  @Override
  public String toString() {
    ...
  }
  
}
  • regel 4: de annotatie @Entity maakt van de klasse [Medecin] een entiteit JPA, c.a.d. een klasse die via API en JPA gekoppeld is aan een tabel van BD,
  • regel 5: de naam van de tabel BD die is gekoppeld aan de entiteit JPA. Elk veld van de tabel komt overeen met een veld in de Java-klasse,
  • regel 6: de klasse implementeert de interface Serializable. Dit is nodig in client/server-toepassingen, waar entiteiten tussen de client en de server worden geserialiseerd.
  • regels 10-11: het veld id van de klasse [Medecin] komt overeen met het veld [ID] (regel 10) van de tabel [medecins],
  • regels 13-14: het veld ‘titel’ van de klasse [Medecin] komt overeen met het veld [TITRE] (regel 13) van de tabel [medecins],
  • regels 16-17: het veld ‘naam’ van de klasse [Medecin] komt overeen met het veld [NOM] (regel 16) van de tabel [medecins],
  • rijen 19-20: het veld ‘versie’ van de klasse [Medecin] komt overeen met het veld [VERSION] (rij 19) van de tabel [medecins]. Hier herkent de wizard niet dat de kolom in feite een versiekolom is die bij elke wijziging van de rij waartoe deze behoort, moet worden verhoogd. Om deze rol toe te kennen, moet de annotatie @Version worden toegevoegd. Dit zullen we in een volgende stap doen,
  • regels 22-23: het veld ‘prenom’ van de klasse [Medecin] komt overeen met het veld [PRENOM] van de tabel [medecins],
  • regels 10-11: het veld id komt overeen met de primaire sleutel [ID] van de tabel. De annotaties in de regels 8-9 verduidelijken dit punt,
  • regel 8: de annotatie @Id geeft aan dat het geannoteerde veld gekoppeld is aan de primaire sleutel van de tabel,
  • regel 9: de laag [JPA] genereert de primaire sleutel van de regels die deze in de tabel [Medecins] zal invoegen. Er zijn verschillende strategieën mogelijk. Hier geeft de strategie GenerationType.IDENTITY aan dat de laag JPA de modus auto_increment van de tabel MySQL zal gebruiken,
  • regels 25-26: de tabel [creneaux] heeft een vreemde sleutel naar de tabel [medecins]. Een tijdvak behoort toe aan een arts. Omgekeerd heeft een arts meerdere tijdvakken die aan hem zijn gekoppeld. We hebben dus een één-op-veel-relatie (één arts – meerdere tijdvakken), een relatie die wordt gekwalificeerd door de annotatie @OneToMany via JPA (regel 25). Het veld van regel 26 bevat alle tijdvakken van de arts. Dit gebeurt zonder programmering. Om regel 25 volledig te begrijpen, moeten we de klasse [Creneau] toelichten.

Deze is als volgt:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "MDEBUT")
  private int mdebut;

  @Column(name = "HFIN")
  private int hfin;

  @Column(name = "HDEBUT")
  private int hdebut;

  @Column(name = "MFIN")
  private int mfin;

  @Column(name = "VERSION")
  private int version;

  @JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Medecin idMedecin;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
  private List<Rv> rvList;

// constructors
...
// getters en setters
...

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

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

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

We geven alleen uitleg bij de nieuwe opmerkingen:

  • we hebben gezegd dat de tabel [creneaux] een vreemde sleutel heeft naar de tabel [medecins]: een tijdvak is gekoppeld aan een arts. Aan dezelfde arts kunnen meerdere tijdvakken worden gekoppeld. Er is een relatie van de tabel [creneaux] naar de tabel [medecins] die wordt gekwalificeerd als ‘meerdere (tijdvakken)’ naar ‘één (arts)’. De annotatie @ManyToOne op regel 32 dient om de vreemde sleutel te specificeren,
  • rij 31 met de annotatie @JoinColumn specificeert de vreemde-sleutelrelatie: de kolom [ID_MEDECIN] van de tabel [creneaux] is een vreemde sleutel op de kolom [ID] van de tabel [medecins],
  • regel 33: een verwijzing naar de arts die eigenaar is van het tijdslot. Ook dit wordt weer zonder programmering verkregen.

De vreemde-sleutelrelatie tussen de entiteit [Creneau] en de entiteit [Medecin] wordt dus weergegeven door twee aantekeningen:

  • in de entiteit [Creneau]:

@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
private Medecin idMedecin;
  • in de entiteit [Medecin]:

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

Beide annotaties geven dezelfde relatie weer: die van de vreemde sleutel van de tabel [creneaux] naar de tabel [medecins]. Men zegt dat ze elkaars omgekeerde zijn. Alleen de relatie @ManyToOne is onmisbaar. Deze kwalificeert de relatie ondubbelzinnig als een vreemde sleutel. De relatie @OneToMany is optioneel. Indien deze aanwezig is, verwijst deze alleen naar de relatie @ManyToOne waarmee deze is gekoppeld. Dit is de betekenis van het attribuut mappedBy in regel 1 van de entiteit [Medecin]. De waarde van dit attribuut is de naam van het veld van de entiteit [Creneau] dat de annotatie @ManyToOne heeft, waarmee de vreemde sleutel wordt gespecificeerd. Nog steeds in dezelfde regel 1 van de entiteit [Medecin], bepaalt het attribuut cascade=CascadeType.ALL het gedrag van de entiteit [Medecin] ten opzichte van de entiteit [Creneau]:

  • als er een nieuwe entiteit [Medecin] in de database wordt ingevoegd, dan moeten de entiteiten [Creneau] uit het veld van regel 2 ook worden ingevoegd,
  • als een entiteit [Medecin] in de database wordt gewijzigd, dan moeten de entiteiten [Creneau] in het veld van regel 2 ook worden gewijzigd,
  • als een entiteit [Medecin] uit de database wordt verwijderd, dan moeten de entiteiten [Creneau] in het veld van regel 2 ook worden verwijderd.

We geven de code van de twee andere entiteiten weer zonder specifieke opmerkingen, aangezien ze geen nieuwe notaties introduceren.

De entiteit [Client]


package rdvmedecins.jpa;

...
@Entity
@Table(name = "clients")
public class Client implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "TITRE")
  private String titre;

  @Column(name = "NOM")
  private String nom;

  @Column(name = "VERSION")
  private int version;

  @Column(name = "PRENOM")
  private String prenom;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idClient")
  private List<Rv> rvList;

// constructors
...
// getters en setters
...

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

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

  @Override
  public String toString() {
    ...
  }
  
}
  • De regels 24-25 geven de vreemde-sleutelrelatie weer tussen de tabel [rv] en de tabel [clients].

De entiteit [Rv]:


package rdvmedecins.jpa;

...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;

  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneau idCreneau;

  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client idClient;

   // constructors
...

   // getters en setters
...

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

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

  @Override
  public String toString() {
    ...
  }
  
}
  • regel 13 specificeert het veld ‘dag’ van het Java-type Date. Er wordt aangegeven dat in de tabel [rv] de kolom [JOUR] (regel 12) van het type datum (zonder tijd) is,
  • regels 16-18: specificeren de vreemde-sleutelrelatie van de tabel [rv] naar de tabel [creneaux],
  • regels 20-22: specificeren de vreemde-sleutelrelatie van de tabel [rv] naar de tabel [clients].

Door de automatische generatie van de entiteiten JPA krijgen we een werkbasis. Soms is dat voldoende, soms niet. Dat is hier het geval:

  • moet de annotatie @Version worden toegevoegd aan de verschillende versievelden van de entiteiten,
  • er moeten toString-methoden worden geschreven die explicieter zijn dan de gegenereerde,
  • de entiteiten [Medecin] en [Client] zijn vergelijkbaar. We laten ze afleiden van een klasse [Personne],
  • we gaan de omgekeerde relaties @OneToMany van de relaties @ManyToOne verwijderen. Ze zijn niet onmisbaar en zorgen voor complicaties bij het programmeren,
  • we verwijderen de validatie @NotNull op de primaire sleutels. Wanneer we een entiteit JPA samen met MySQL opslaan, heeft de oorspronkelijke entiteit een primaire sleutel null. Pas na opslag in de database krijgt de primaire sleutel van het opgeslagen element een waarde.

Met deze specificaties zien de verschillende klassen er als volgt uit:

De klasse Persoon wordt gebruikt om artsen en klanten weer te geven:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@MappedSuperclass
public class Personne implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Basic(optional = false)
  @Size(min = 1, max = 5)
  @Column(name = "TITRE")
  private String titre;

  @Basic(optional = false)
  @NotNull
  @Size(min = 1, max = 30)
  @Column(name = "NOM")
  private String nom;

  @Basic(optional = false)
  @NotNull
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @Basic(optional = false)
  @NotNull
  @Size(min = 1, max = 30)
  @Column(name = "PRENOM")
  private String prenom;
// constructors
...

// getters en setters
  ...

  @Override
  public String toString() {
    return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
  }
  
}
  • regel 8: merk op dat de klasse [Personne] zelf geen entiteit (@Entity) is. Deze klasse zal de bovenliggende klasse van entiteiten zijn. De annotatie @MappedSuperClass duidt deze situatie aan.

De entiteit [Client] omvat de rijen van de tabel [clients]. Deze is afgeleid van de voorgaande klasse [Personne]:


package rdvmedecins.jpa;

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

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

// constructors
...

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

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

  @Override
  public String toString() {
    return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
  }
  
}
  • regel 6: de klasse [Client] is een JPA-entiteit,
  • regel 7: deze is gekoppeld aan de tabel [clients],
  • regel 8: deze is afgeleid van de klasse [Personne].

De entiteit [Medecin], die de rijen van de tabel [medecins] omvat, volgt hetzelfde patroon:


package rdvmedecins.jpa;

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

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

  // constructors
...

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

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

  @Override
  public String toString() {
    return String.format("Médecin[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
  }
  
}

De entiteit [Creneau] omvat de rijen van de tabel [creneaux]:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {

  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "MDEBUT")
  private int mdebut;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "HFIN")
  private int hfin;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "HDEBUT")
  private int hdebut;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "MFIN")
  private int mfin;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Medecin medecin;

  // constructors
  ...

  // getters en setters
  ...
 
  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    // TODO: Waarschuwing - deze methode werkt niet als de id-velden niet zijn ingesteld
    ...
  }

  @Override
  public String toString() {
    return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
  }
}
  • de regels 45-47 modelleren de ‘meerdere-op-één’-relatie die bestaat tussen de tabel [creneaux] en de tabel [medecins] in de database: een arts heeft meerdere tijdvakken, een tijdvak behoort toe aan slechts één arts.

De entiteit [Rv] omvat de rijen van de tabel [rv]:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "rv")
public class Rv implements Serializable {

  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;
  
  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneau creneau;
  
  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client client;

   // constructors
...

   // getters en setters
...

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

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

  @Override
  public String toString() {
    return String.format("Rv[%s, %s, %s]", id, creneau, client);
  }
}
  • de rijen 29-31 modelleren de „meerdere-op-één”-relatie die bestaat tussen de tabel [rv] en de tabel [clients] (een klant kan in meerdere Rv's voorkomen) in de database, en de rijen 25-27 de "meerdere-op-één"-relatie die bestaat tussen de tabel [rv] en de tabel [creneaux] (een tijdslot kan in meerdere Rv's voorkomen).

3.4.3. De uitzonderingsklasse

De uitzonderingsklasse [RdvMedecinsException] van de applicatie is als volgt:


package rdvmedecins.exceptions;

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

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

  // privévelden
  private int code = 0;

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

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

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

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

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

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

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

  // getters - setters
  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • regel 7: de klasse is afgeleid van de klasse [RuntimeException]. De compiler dwingt dus niet om deze met try/catch te behandelen.
  • regel 6: de annotatie @ApplicationException zorgt ervoor dat de uitzondering niet wordt ‘opgeslokt’ door een uitzondering van het type [EjbException].

Om de annotatie @ApplicationException te begrijpen, gaan we terug naar de architectuur die aan de serverzijde wordt gebruikt:

De uitzondering van het type [RdvMedecinsException] wordt gegenereerd door de methoden van EJB in de laag [DAO] binnen de container EJB3 en wordt door deze container onderschept. Zonder de annotatie @ApplicationException kapselt de container EJB3 de opgetreden uitzondering in een uitzondering van het type [EjbException] en gooit deze opnieuw. Het kan zijn dat men deze inkapseling niet wenst en een uitzondering van het type [RdvMedecinsException] uit de container EJB3 wil laten ontsnappen. Dit wordt mogelijk gemaakt door de annotatie @ApplicationException. Bovendien geeft het attribuut (rollback=true) van deze annotatie aan de container EJB3 aan dat, indien de uitzondering van het type [RdvMedecinsException] optreedt binnen een methode die wordt uitgevoerd in een transactie met een SGBD, deze moet worden teruggedraaid. In technische termen wordt dit een rollback van de transactie uitvoeren genoemd.

3.4.4. De EJB van de laag [DAO]

De Java- [IDao] van de laag [DAO] is als volgt:


package rdvmedecins.dao;


import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;

public interface IDao {

  // klantenlijst
  public List<Client> getAllClients();
  // lijst met artsen
  public List<Medecin> getAllMedecins();
  // lijst met tijdvakken van een arts
  public List<Creneau> getAllCreneaux(Medecin medecin);
  // lijst met afspraken van een arts op een bepaalde dag
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
  // een klant zoeken op basis van zijn ID
  public Client getClientById(Long id);
  // een klant zoeken op basis van zijn ID
  public Medecin getMedecinById(Long id);
  // een afspraak zoeken op basis van de id
  public Rv getRvById(Long id);
  // een tijdslot zoeken op basis van de id
  public Creneau getCreneauById(Long id);
  // een RV toevoegen
  public Rv ajouterRv(Date jour, Creneau creneau, Client client);
  // een RV verwijderen
  public void supprimerRv(Rv rv);
}

Deze interface is ontwikkeld na het in kaart brengen van de behoeften van de laag [web]:

  • regel 14: de lijst met klanten. Deze hebben we nodig om de vervolgkeuzelijst met klanten te vullen,
  • regel 16: de lijst met artsen. Deze hebben we nodig om de vervolgkeuzelijst met artsen te vullen,
  • regel 18: de lijst met beschikbare tijdvakken van een arts. Deze hebben we nodig om de agenda van de arts voor een bepaalde dag weer te geven,
  • regel 20: de lijst met afspraken van een arts voor een bepaalde dag. In combinatie met de vorige methode kunnen we hiermee de agenda van de arts voor een bepaalde dag weergeven, inclusief de reeds gereserveerde tijdvakken,
  • regel 22: hiermee kunnen we een klant opzoeken aan de hand van zijn nummer. Met deze methode kunnen we een klant opzoeken door een keuze te maken in de vervolgkeuzelijst met klanten,
  • regel 24: hetzelfde geldt voor artsen,
  • regel 26: zoekt een afspraak op basis van het nummer. Kan worden gebruikt wanneer een afspraak wordt verwijderd, om vooraf te controleren of deze daadwerkelijk bestaat,
  • regel 28: zoekt een tijdslot op basis van het nummer. Hiermee kan het tijdslot worden geïdentificeerd dat een gebruiker wil toevoegen of verwijderen,
  • regel 30: om een afspraak toe te voegen,
  • regel 32: om een afspraak te verwijderen.

De lokale interface [IDaoLocal] van EJB is een afgeleide van de vorige interface [IDao]:


package rdvmedecins.dao;

import javax.ejb.Local;

@Local
public interface IDaoLocal extends IDao{

}

Hetzelfde geldt voor de externe interface [IDaoRemote]:


package rdvmedecins.dao;

import javax.ejb.Remote;

@Remote
public interface IDaoRemote extends IDao{

}

De EJB [DaoJpa] implementeert beide interfaces, zowel de lokale als de externe:


package rdvmedecins.dao;

...

@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
  • regel 5 geeft aan dat de remote EJB de naam "rdvmedecins.dao" draagt. Bovendien zorgt de annotatie @Singleton (Java EE6) ervoor dat er slechts één instantie van de EJB wordt aangemaakt. De annotatie @Stateless (Java EE5) definieert een EJB waarvan meerdere instanties kunnen worden aangemaakt om een pool van EJB te voeden,
  • regel 6 geeft aan dat alle methoden van EJB worden uitgevoerd binnen een transactie die wordt beheerd door de container EJB3,
  • regel 7 laat zien dat EJB de lokale en externe interfaces implementeert en tevens serialiseerbaar is.

De volledige code van EJB is als volgt:


package rdvmedecins.dao;

import java.io.Serializable;
import java.util.Date;
import java.util.List;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import rdvmedecins.exceptions.RdvMedecinsException;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;

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

  @PersistenceContext
  private EntityManager em;

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

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

  // lijst met beschikbare tijdvakken van een bepaalde arts
  // arts: de arts
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    try {
      return em.createQuery("select rc from Creneau rc join rc.medecin m where m.id=:idMedecin").setParameter("idMedecin", medecin.getId()).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 3);
    }
  }

  // lijst met afspraken van een bepaalde arts op een bepaalde dag
  // arts: de arts
  // dag: de dag
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
    try {
      return em.createQuery("select rv from Rv rv join rv.creneau c join c.idMedecin m where m.id=:idMedecin and rv.jour=:jour").setParameter("idMedecin", medecin.getId()).setParameter("jour", jour).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 3);
    }
  }

  // een afspraak toevoegen
  // dag: dag van de afspraak
  // tijdvak: tijdvak van de afspraak
  // klant: klant voor wie de afspraak is gemaakt
  public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
    try {
      Rv rv = new Rv(null, jour);
      rv.setClient(client);
      rv.setCreneau(creneau);
      em.persist(rv);
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
  }

  // verwijdering van een afspraak
  // afspraak: de verwijderde afspraak
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }

  // een bepaalde klant ophalen
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // een bepaalde arts ophalen
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // een bepaalde afspraak ophalen
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // een bepaald tijdvak ophalen
  public Creneau getCreneauById(Long id) {
    try {
      return (Creneau) em.find(Creneau.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
}
  • regel 22: het object EntityManager dat de toegang tot de persistentiecontext beheert. Bij het instantiëren van de klasse wordt dit veld geïnitialiseerd door de container EJB dankzij de annotatie @PersistenceContext op regel 21,
  • regel 27: query JPQL (Java Persistence Query Language) die alle rijen van de tabel [clients] retourneert in de vorm van een lijst met objecten [Client],
  • regel 36: een soortgelijke query voor artsen,
  • regel 46: een query JPQL die een join uitvoert tussen de tabellen [creneaux] en [medecins]. Deze wordt ingesteld op basis van de id van de arts,
  • regel 57: een query JPQL die een join uitvoert tussen de tabellen [rv], [creneaux] en [medecins] en twee parameters heeft: de id van de arts en de dag van de afspraak,
  • regels 69-73: aanmaken van een afspraak en deze vervolgens opslaan in de database,
  • regel 83: verwijdering van een afspraak uit de database,
  • regel 92: voert een select uit op de database om een bepaalde klant te vinden,
  • regel 101: idem voor een arts,
  • regel 110: idem voor een afspraak,
  • regel 119: idem voor een tijdslot,
  • bij alle bewerkingen met de persistentie-context em uit regel 22 kan een probleem met de database optreden. Daarom zijn ze allemaal omgeven door een try/catch-blok. De eventuele uitzondering wordt ingekapseld in de "huis"-uitzondering RdvMedecinsException.

3.4.5. Implementatie van de driver JDBC van MySQL

In de onderstaande architectuur:

EclipseLink heeft de driver JDBC van MySQL nodig. Deze moet worden geïnstalleerd in de bibliotheken van de Glassfish-server in de map <glassfish>/domains/domain1/lib/ext, waarbij <glassfish> de installatiemap van de Glassfish-server is. Deze kan als volgt worden verkregen:

De map waarin de driver JDBC van MySQL moet worden geplaatst, is <Domains folder>[1]/domain1/lib/ext [2]. Dit stuurprogramma is beschikbaar in de map URL [http://www.mysql.fr/downloads/connector/j/]. Nadat het is geïnstalleerd, moet de Glassfish-server opnieuw worden opgestart zodat deze de nieuwe bibliotheek herkent.

3.4.6. Implementatie van de EJB van de [DAO]-laag

Laten we terugkeren naar de architectuur die tot nu toe is opgebouwd:

De set [web, métier, DAO, JPA] moet op de Glassfish-server worden geïmplementeerd. Dat doen we als volgt:

  • in [1] bouwen we het Maven-project,
  • in [2] voeren we het uit,
  • in [3] is het geïmplementeerd op de Glassfish-server (tabblad [Services])

Misschien wil je de logbestanden van Glassfish eens bekijken:

In [1] zijn de logbestanden van Glassfish beschikbaar op het tabblad [Output / Glassfish Server 3+]. Dit zijn de volgende:

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

De regels met de identificatiecodes [Config] en [Précis] zijn de logbestanden van EclipseLink; de regels met de identificatiecode [Infos] zijn afkomstig van Glassfish.

  • regels 1-12: EclipseLink verwerkt de entiteiten JPA die het heeft gedetecteerd,
  • regels 13-17: informatie waaruit blijkt dat de verwerking van de entiteiten JPA normaal is verlopen,
  • regel 18: EclipseLink meldt zich,
  • regel 19: EclipseLink herkent dat het te maken heeft met SGBD MySQL,
  • regels 20-24: EclipseLink probeert verbinding te maken met BD,
  • regels 25-28: dat is gelukt,
  • regels 29-33: hij probeert opnieuw verbinding te maken, ditmaal specifiek via het platform MySQL (regel 30),
  • regels 34-37: ook hier geslaagd,
  • regel 38: bevestiging dat de persistentie-eenheid [dbrdvmedecins-PU] kon worden geïnstantieerd,
  • regel 39: de draagbare namen van de externe en lokale interfaces van de EJB en [DaoJpa], waarbij „draagbaar“ betekent dat ze door alle Java-applicatieservers EE 6 worden herkend,
  • regel 40: de namen van de externe en lokale interfaces van de EJB [DaoJpa], die specifiek zijn voor Glassfish. In de komende test zullen we de naam "rdvmedecins.dao" gebruiken.

De regels 39 en 40 zijn belangrijk. Wanneer men de client van een EJB op Glassfish schrijft, is het noodzakelijk deze te kennen.

3.4.7. Testen van de EJB van de [DAO]-laag

Nu de EJB van de [DAO]-laag van onze applicatie is geïmplementeerd, kunnen we deze testen. We gaan dit doen in het kader van een client/server-applicatie:

De client gaat de externe interface van de EJB [DAO] testen die op de Glassfish-server is geïmplementeerd.

We beginnen met het aanmaken van een nieuw Maven-project :

  • In [1] maken we een nieuw project aan,
  • in [2,3] maken we een Maven-project van het type [Java Application],
  • in [4] geven we het een naam en plaatsen we het in dezelfde map als EJB en [DAO],
  • in [5], het gegenereerde project,
  • in [6] is er een klasse [App.java] gegenereerd. Deze verwijderen we,
  • in [7] is een branch [Source Packages] gegenereerd. Deze waren we nog niet tegengekomen. We kunnen tests JUnit in deze branch plaatsen. Dat gaan we doen. We zullen de gegenereerde testklasse [AppTest] niet behouden,
  • in [8], de afhankelijkheden van het Maven-project. De branch [Dependencies] is leeg. We zullen daar nieuwe afhankelijkheden aan toevoegen. De branch [Test Dependencies] bevat de afhankelijkheden die nodig zijn voor de tests. Hier wordt de bibliotheek van het framework JUnit 3.8 gebruikt. We zullen deze moeten wijzigen.

Het project ontwikkelt zich als volgt:

  • naar [1], het project waarin de twee gegenereerde klassen zijn verwijderd, evenals de afhankelijkheid JUnit.

Laten we terugkeren naar de client/server-architectuur die voor de test zal worden gebruikt:

De client moet de externe interface kennen die wordt aangeboden door EJB en [DAO]. Bovendien zal hij met EJB entiteiten uitwisselen die afkomstig zijn van JPA. Hij heeft dus de definitie daarvan nodig. Om ervoor te zorgen dat het testproject van de EJB toegang heeft tot deze informatie, voegen we het project van de EJB en [DAO] toe als afhankelijkheid aan het project:

  • in [1] voegen we een afhankelijkheid toe aan de branch [Test Dependencies],
  • in [2] selecteer je het tabblad [Open Projects],
  • in [3] selecteert men het Maven-project van EJB [DAO],
  • in [4], de toegevoegde afhankelijkheid.

Laten we teruggaan naar de client/server-architectuur van de test:

Tijdens de uitvoering communiceren de client en de server via het netwerk TCP-IP. We gaan deze uitwisselingen niet programmeren. Voor elke applicatieserver is er een bibliotheek die in de afhankelijkheden van de client moet worden geïntegreerd. Die voor Glassfish heet [gf-client]. We voegen deze toe:

  • in [1] voegen we een afhankelijkheid toe,
  • in [2] geven we de specificaties van het gewenste artefact op,
  • in [3] worden heel veel afhankelijkheden toegevoegd. Maven gaat deze downloaden. Dit kan enkele minuten duren. Ze worden vervolgens opgeslagen in de lokale Maven-repository.

We kunnen nu de test JUnit aanmaken:

  • in [2] klikken we met de rechtermuisknop op [Test Packages] om een nieuwe test JUnit aan te maken,
  • in [3], geef je de testklasse een naam en een pakket voor deze klasse [4],
  • in [5], we kiezen het framework JUnit 4.x,
  • in [6] de gegenereerde testklasse,
  • in [7], de nieuwe afhankelijkheden van het Maven-project.

Het bestand [pom.xml] ziet er dan als volgt uit:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-client-rdvmedecins-ejb-dao</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-client-rdvmedecins-ejb-dao</name>
  <url>http://maven.apache.org</url>

  <repositories>
    <repository>
      <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
      <id>eclipselink</id>
      <layout>default</layout>
      <name>Repository for library Library[eclipselink]</name>
    </repository>
    <repository>
      <url>http://repo1.maven.org/maven2/</url>
      <id>junit_4</id>
      <layout>default</layout>
      <name>Repository for library Library[junit_4]</name>
    </repository>
  </repositories>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish.appclient</groupId>
      <artifactId>gf-client</artifactId>
      <version>3.1.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Let op:

  • regels 32-51: de projectafhankelijkheden,
  • regels 13-26: er zijn twee Maven-repositories gedefinieerd, één voor EclipseLink (regels 14-19) en één voor JUnit4 (regels 20-25).

De testklasse ziet er als volgt uit:


package rdvmedecins.tests.dao;

import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import junit.framework.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import rdvmedecins.dao.IDaoRemote;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;

public class JUnitTestDao {

  // laag [dao] getest
  private static IDaoRemote dao;
  // huidige datum
  Date jour = new Date();

  @BeforeClass
  public static void init() throws NamingException {
    // initialisatie van de JNDI-omgeving
    InitialContext initialContext = new InitialContext();
    // instantiëren van de DAO-laag
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }

  @Test
  public void test1() {
    // weergave van klanten
    List<Client> clients =dao.getAllClients();
    display("Liste des clients :", clients);
    // weergave van artsen
    List<Medecin> medecins =dao.getAllMedecins();
    display("Liste des médecins :", medecins);
    // weergave van de afspraken van een arts
    Medecin medecin = medecins.get(0);
    List<Creneau> creneaux = dao.getAllCreneaux(medecin);
    display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
    // lijst met afspraken van een arts op een bepaalde dag
    display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // een RV toevoegen
    Rv rv = null;
    Creneau creneau = creneaux.get(2);
    Client client = clients.get(0);
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    rv = dao.ajouterRv(jour, creneau, client);
    System.out.println("Rv ajouté");
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // een RV toevoegen in hetzelfde tijdvak op dezelfde dag
    // moet een uitzondering veroorzaken
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    Boolean erreur = false;
    try {
      rv = dao.ajouterRv(jour, creneau, client);
      System.out.println("Rv ajouté");
    } catch (Exception ex) {
      Throwable th = ex;
      while (th != null) {
        System.out.println(ex.getMessage());
        th = th.getCause();
      }
      // de fout wordt genoteerd
      erreur=true;
    }
    // er wordt gecontroleerd of er een fout is opgetreden
    Assert.assertTrue(erreur);
    // lijst van RV
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // een RV verwijderen
    System.out.println("Suppression du Rv ajouté");
    dao.supprimerRv(rv);
    System.out.println("Rv supprimé");
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
  }

  // hulpprogramma – toont de elementen van een verzameling
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • regels 23-29: de methode met de tag @BeforeClass wordt vóór alle andere uitgevoerd. Hier wordt een verwijzing aangemaakt naar de externe interface van EJB [DaoJpa]. We herinneren ons dat we deze de naam JNDI "rdvmedecins.dao" hadden gegeven,
  • regels 34-35: geven de lijst met klanten weer,
  • regels 37-38: geven de lijst met artsen weer,
  • regels 40-42: geven de tijdvakken van de eerste arts weer,
  • regel 44: toont de afspraken van de eerste arts voor de dag van regel 21,
  • regels 46-51: voegen een afspraak toe voor de eerste arts, voor zijn tijdslot nr. 2 en de dag van regel 21,
  • regel 52: toont ter controle de afspraken van de eerste arts voor de dag van regel 21. Er moet er ten minste één zijn, namelijk degene die zojuist is toegevoegd,
  • regels 55-70: we voegen dezelfde afspraak toe. Aangezien de tabel [RV] een uniekheidsbeperking heeft, moet deze toevoeging een uitzondering veroorzaken. We controleren dit in regel 70,
  • regel 72: geef ter controle de afspraken van de eerste arts weer voor de dag van regel 21. De afspraak die we wilden toevoegen, mag daar niet staan,
  • regels 74-76: we verwijderen de enige afspraak die is toegevoegd,
  • regel 77: toont ter controle de afspraken van de eerste arts voor de dag van regel 21. De afspraak die zojuist is verwijderd, mag daar niet staan.

Deze test is een nep-test JUnit. Er zit slechts één assertion in (regel 70). Het is een visuele test met de bijbehorende fouten.

Als alles goed gaat, moeten de tests slagen:

  • in [1], we bouwen het testproject,
  • in [2] voeren we de test uit,
  • in [3] is de test geslaagd.

Laten we de testuitvoer eens nader bekijken:

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

De lezer wordt verzocht deze logbestanden te lezen in combinatie met de code die ze heeft gegenereerd. We gaan even stilstaan bij de uitzondering die optrad bij het toevoegen van een reeds bestaande afspraak, regels 41-49. De uitzonderingsstack wordt weergegeven in regels 42-48. Deze is onverwacht. Laten we teruggaan naar de code van de methode voor het toevoegen van een afspraak:


  // een afspraak toevoegen
  // dag: dag van de afspraak
  // tijdvak: tijdvak van de afspraak
  // klant: klant voor wie de afspraak is gemaakt
  public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
    try {
      Rv rv = new Rv(null, jour);
      rv.setClient(client);
      rv.setCreneau(creneau);
      System.out.println(String.format("avant persist : %s",rv));
      em.persist(rv);
      System.out.println(String.format("après persist : %s",rv));
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
}

Laten we de logbestanden van Glassfish bekijken bij het toevoegen van de twee afspraken:

...
Infos: avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Infos: après persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Précis: INSERT INTO rv (JOUR, ID_CLIENT, ID_CRENEAU) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Précis: SELECT LAST_INSERT_ID()
Précis: SELECT t1.ID, t1.JOUR, t1.ID_CLIENT, t1.ID_CRENEAU FROM creneaux t0, rv t1 WHERE (((t0.ID_MEDECIN = ?) AND (t1.JOUR = ?)) AND (t0.ID = t1.ID_CRENEAU))
    bind => [2 parameters bound]
Infos: avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Infos: après persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Précis: INSERT INTO rv (JOUR, ID_CLIENT, ID_CRENEAU) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Précis: SELECT 1
Avertissement: Local Exception Stack: 
Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.2.v20111125-r10461): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '2012-05-23-3' for key 'UNQ1_RV'
Error Code: 1062
...
  • regel 2: vóór de eerste persist,
  • regel 3: na de eerste persist,
  • regel 4: de opdracht INSERT die zal worden uitgevoerd. Merk op dat deze niet gelijktijdig plaatsvindt met de bewerking persist. Als dat wel het geval was, zou dit logboek vóór regel 2 zijn verschenen. De bewerking INSERT vindt dan normaal gesproken plaats aan het einde van de transactie waarin de methode wordt uitgevoerd,
  • regel 6: EclipseLink vraagt aan MySQL wat de laatst gebruikte primaire sleutel is. Het krijgt de primaire sleutel van de toegevoegde afspraak. Deze waarde vult het veld id van de opgeslagen entiteit [Rv] in,
  • regels 7-8: de query SELECT die de afspraken van de arts weergeeft,
  • regels 9-10: de schermweergaven van de tweede persist,
  • regels 11-12: de opdracht INSERT die zal worden uitgevoerd. Deze moet een uitzondering veroorzaken. Deze verschijnt op regels 15-16 en is duidelijk. De uitzondering wordt aanvankelijk gegenereerd door de driver JDBC van MySQL vanwege een schending van de uniekheidsvoorwaarde voor afspraken. Hieruit volgt dat we deze uitzonderingen zouden moeten zien in de logbestanden van de test JUnit. Dit is echter niet het geval:
1
2
3
4
5
6
7
8
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe

Laten we de client/server-architectuur van de test nog eens in herinnering brengen:

Wanneer de EJB [DAO] een uitzondering genereert, moet deze worden geserialiseerd om de client te bereiken. Waarschijnlijk is deze bewerking mislukt om een reden die ik niet heb begrepen. Aangezien onze volledige applicatie niet in een client/server-opstelling zal draaien, kunnen we dit probleem negeren.

Nu de EJB van de laag [DAO] operationeel is, kunnen we doorgaan naar de EJB van de laag [métier].

3.5. De laag [métier]

Laten we terugkeren naar de architectuur van de applicatie die momenteel wordt gebouwd:

We gaan een nieuw Maven-project opzetten voor de lagen EJB en [métier]. Zoals hierboven te zien is, zal dit project afhankelijk zijn van het Maven-project dat is gebouwd voor de lagen [DAO] en [JPA].

3.5.1. Het NetBeans-project

We bouwen een nieuw Maven-project van het type EJB. Hiervoor volstaat het om de procedure te volgen die al is gebruikt en beschreven op pagina 174.

  • in [1], het Maven-project van de laag [métier],
  • in [2] voeg je een afhankelijkheid toe,
  • in [3], kies je het Maven-project van de lagen [DAO] en [JPA],
  • in [4] selecteren we de scope [provided]. Ter herinnering: dit betekent dat deze nodig is voor het compileren, maar niet voor het uitvoeren van het project. HetEJB van de laag [métier] wordt namelijk op de Glassfish-server geïmplementeerd samen met de EJB van de lagen [DAO] en [JPA]. Dus wanneer deze wordt uitgevoerd, zal de EJB van de lagen [DAO] en [JPA] al aanwezig zijn,
  • in [6], het nieuwe project met zijn afhankelijkheid.

Laten we nu de broncode van de laag [métier] bekijken:

EJB [Metier] zal de volgende interface [IMetier] hebben:


package rdvmedecins.metier.service;

import java.util.Date;
import java.util.List;

import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;

public interface IMetier {

    // DAO-laag
    // klantenlijst
    public List<Client> getAllClients();

    // lijst met artsen
    public List<Medecin> getAllMedecins();

    // lijst met tijdvakken van een arts
    public List<Creneau> getAllCreneaux(Medecin medecin);

    // lijst met afspraken van een arts op een bepaalde dag
    public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);

    // een klant zoeken op basis van zijn ID
    public Client getClientById(Long id);

    // een klant zoeken op basis van zijn ID
    public Medecin getMedecinById(Long id);

    // een afspraak zoeken op basis van de id
    public Rv getRvById(Long id);

    // een tijdslot zoeken op basis van de id
    public Creneau getCreneauById(Long id);

    // een RV toevoegen
    public Rv ajouterRv(Date jour, Creneau creneau, Client client);

    // een RV verwijderen
    public void supprimerRv(Rv rv);
    
    // beroep
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);

}

Om deze interface te begrijpen, moet men de architectuur van het project in gedachten houden:

We hebben de interface van de laag [DAO] gedefinieerd (paragraaf 3.4.4) en aangegeven dat deze voldoet aan de behoeften van de laag [web], de gebruikersbehoeften. De laag [web] communiceert uitsluitend via de laag [métier] met de laag [DAO]. Dit verklaart waarom in de laag [métier] alle methoden van de laag [DAO] te vinden zijn. Deze methoden zullen het verzoek van de laag [web] alleen maar doorgeven aan de laag [DAO]. Meer niet.

Tijdens de analyse van de applicatie komt een behoefte naar voren: de mogelijkheid om op een webpagina de agenda van een arts voor een bepaalde dag weer te geven, zodat de bezette en vrije tijdvakken van die dag zichtbaar zijn. Dit is typisch het geval wanneer de secretaresse een verzoek via de telefoon beantwoordt. Er wordt haar gevraagd om een afspraak te maken voor een bepaalde dag met een bepaalde arts. Om aan deze behoefte te voldoen, biedt de laag [métier] de methode op regel 46.


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

Men kan zich afvragen waar deze methode geplaatst moet worden:

  • men zou deze in de laag [DAO] kunnen plaatsen. Deze methode beantwoordt echter niet echt aan een behoefte aan toegang tot gegevens, maar eerder aan een bedrijfsbehoefte;
  • men zou deze in de laag [web] kunnen plaatsen. Dat zou echter een slecht idee zijn. Want als men de laag [web] verandert in een laag [Swing], gaat de methode verloren terwijl de behoefte nog steeds bestaat.

De methode ontvangt als parameters de arts en de dag waarvoor men de afsprakenagenda wil zien. Ze retourneert een object [AgendaMedecinJour] dat de agenda van de arts en de dag weergeeft:


package rdvmedecins.metier.entites;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.jpa.Medecin;

public class AgendaMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // velden
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;

    // bouwers
    public AgendaMedecinJour() {

    }

    public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
        this.medecin = medecin;
        this.jour = jour;
        this.creneauxMedecinJour = creneauxMedecinJour;
    }

    public String toString() {
        StringBuffer str = new StringBuffer("");
        for (CreneauMedecinJour cr : creneauxMedecinJour) {
            str.append(" ");
            str.append(cr.toString());
        }
        return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
    }

    // getters en setters
...
  
}
  • regel 12: de arts van wie de agenda is,
  • regel 13: de dag van de agenda,
  • regel 14: de tijdvakken van de arts voor die dag.
  • de klasse bevat constructors (regels 17, 21) en een aangepaste methode toString (regel 27).

De klasse [CreneauMedecinJour] (regel 14) is als volgt:


package rdvmedecins.metier.entites;

import java.io.Serializable;
import rdvmedecins.jpa.Creneau;

import rdvmedecins.jpa.Rv;

public class CreneauMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // velden
    private Creneau creneau;
    private Rv rv;

    // constructors
    public CreneauMedecinJour() {

    }

    public CreneauMedecinJour(Creneau creneau, Rv rv) {
        this.creneau=creneau;
    this.rv=rv;
    }

    // toString
    @Override
    public String toString() {
        return String.format("[%s %s]", creneau,rv);
    }

    // getters en setters

  ...
}
  • regel 12: een tijdvak van de arts,
  • regel 13: de bijbehorende afspraak, null als het tijdvak vrij is.

Zo zien we dat het veld creneauxMedecinJour op regel 14 van de klasse [AgendaMedecinJour] ons in staat stelt om alle tijdvakken van de arts te verkrijgen, met de informatie „bezet” of „vrij” voor elk ervan. Dat was het doel van de nieuwe methode [getAgendaMedecinJour] van de interface [IMetier].

Onze EJB [Metier] zal een lokale interface en een externe interface hebben, die beide gewoon afgeleid zijn van de hoofdinterface [IMetier]:


package rdvmedecins.metier.service;
import javax.ejb.Local;

@Local
public interface IMetierLocal extends IMetier{

}

package rdvmedecins.metier.service;
import javax.ejb.Remote;

@Remote
public interface IMetierRemote extends IMetier{

}

De EJB en [Metier] implementeren deze interfaces als volgt:


package rdvmedecins.metier.service;

import java.io.Serializable;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.ejb.EJB;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;

import rdvmedecins.dao.IDaoLocal;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.entites.CreneauMedecinJour;

@Singleton
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote, Serializable {

  // DAO-laag
  @EJB
  private IDaoLocal dao;

  public Metier() {
  }

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

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

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

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

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

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

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

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

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

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

  @Override
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour) {
    // lijst met beschikbare tijdvakken van de arts
    List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
    // lijst met afspraken van dezelfde arts voor dezelfde dag
    List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
    // er wordt een woordenboek aangemaakt op basis van de gemaakte afspraken
    Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
    for (Rv resa : reservations) {
      hReservations.put(resa.getCreneau().getId(), resa);
    }
    // de agenda voor de gevraagde dag wordt aangemaakt
    AgendaMedecinJour agenda = new AgendaMedecinJour();
    // de arts
    agenda.setMedecin(medecin);
    // de dag
    agenda.setJour(jour);
    // de reserveringsslots
    CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
    agenda.setCreneauxMedecinJour(creneauxMedecinJour);
    // invullen van de reserveringsslots
    for (int i = 0; i < creneauxHoraires.size(); i++) {
      // regel i agenda
      creneauxMedecinJour[i] = new CreneauMedecinJour();
      // ID van het tijdvak
      creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
      // is het tijdvak vrij of gereserveerd?
      if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
        // het tijdvak is bezet – de reservering wordt genoteerd
        Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
        creneauxMedecinJour[i].setRv(resa);
      }
    }
    // het resultaat wordt teruggestuurd
    return agenda;
  }
}
  • regel 22: de klasse [Metier] is een singleton van EJB,
  • regel 23: elke methode van de EJB vindt plaats binnen een transactie. Dit betekent dat de transactie begint aan het begin van de methode, in de laag [métier]. Deze roept methoden aan uit de laag [DAO]. Deze vinden plaats binnen dezelfde transactie,
  • regel 24: EJB implementeert zijn lokale en externe interfaces en is bovendien serialiseerbaar,
  • regel 27: een verwijzing naar EJB vanuit de laag [DAO],
  • regel 29: deze wordt geïnjecteerd door de container EJB van de Glassfish-server, dankzij de annotatie @EJB. Dus wanneer de methoden van de klasse [Metier] worden uitgevoerd, is de verwijzing naar de EJB van de laag [DAO] geïnitialiseerd,
  • regels 33-81: deze verwijzing wordt gebruikt om de aanroep naar de laag [métier] te delegeren aan de laag [DAO],
  • regel 84: de methode getAgendaMedecinJour waarmee de agenda van een arts voor een bepaalde dag kan worden opgevraagd. We laten de lezer de commentaren volgen.

3.5.2. Implementatie van de laag [métier]

De laag [métier] is afhankelijk van de laag [DAO]. Elke laag is geïmplementeerd met een EJB. Om de EJB en [métier] te testen, moeten we beide EJB-lagen implementeren. Hiervoor hebben we een bedrijfsproject nodig.

  • [1], maken we een nieuw project aan,
  • van het type Maven [2] en Bedrijfsapplicatie [3],
  • en geven we het een naam: [4]. Het achtervoegsel ear wordt automatisch toegevoegd,
  • tot [5], we kiezen de Glassfish-server en Java EE 6,
  • in [6]; een bedrijfsapplicatie bevat modules, meestal EJB-modules en webmodules. Hier zal de bedrijfsapplicatie de modules van de twee EJB bevatten die we hebben gebouwd. Aangezien deze modules al bestaan, vinken we de vakjes niet aan,
  • in [7,8] zijn twee projecten aangemaakt. [8] is het bedrijfsproject dat we gaan gebruiken. [7] is een project waarvan ik de functie niet ken. Ik heb het niet hoeven gebruiken en aangezien ik me niet verdiept heb in Maven, weet ik niet waarvoor het kan dienen. We zullen het dus negeren.

Nu het bedrijfsproject is aangemaakt, kunnen we de modules ervan definiëren.

  • in [1] maken we een nieuwe afhankelijkheid aan,
  • in [2] kiezen we het project van EJB [DAO],
  • in [3], wordt aangegeven dat het een EJB is. Laat het type niet leeg, want in dat geval wordt het type jar gebruikt en dat type is hier niet geschikt,
  • in [4] wordt het bereik [compile] gebruikt,
  • in [5], het project met zijn nieuwe afhankelijkheid,
  • in [6, 7, 8], beginnen we opnieuw om de EJB van de laag [métier] toe te voegen,
  • tot [9], de twee afhankelijkheden,
  • naar [10], we bouwen het project,
  • in [11] wordt het uitgevoerd,
  • in [12], op het tabblad [Services], zien we dat het project is geïmplementeerd op de Glassfish-server. Dit betekent dat de twee EJB's nu op de server aanwezig zijn.

In de logbestanden van de Glassfish-server vinden we informatie over de implementatie van de twee EJB:

  • en [1], het tabblad met de logbestanden van Glassfish.

Daar staan de volgende logbestanden:

Infos: rdvmedecins.jpa.Creneau actually got transformed
Infos: rdvmedecins.jpa.Medecin actually got transformed
Infos: rdvmedecins.jpa.Personne actually got transformed
Infos: rdvmedecins.jpa.Client actually got transformed
Infos: rdvmedecins.jpa.Rv actually got transformed
Infos: EclipseLink, version: Eclipse Persistence Services - 2.3.2.v20111125-r10461
Infos: file:/D:/data/istia-1112/netbeans/dvp/jsf2-pf-pfm/maven/netbeans/rdvmedecins-jsf2-ejb/mv-rdvmedecins-metier-dao/mv-rdvmedecins-metier-dao-ear/target/gfdeploy/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT_jar/_dbrdvmedecins2-PU login successful
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: EJB5181:Portable JNDI names for EJB Metier: [java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]
  • regels 1-5: de entiteiten JPA zijn herkend,
  • regel 7: geeft aan dat het aanmaken van de persistentie-eenheid [dbrdvmedecins2-PU] is geslaagd en dat de verbinding met de bijbehorende database tot stand is gebracht,
  • regel 8: de draagbare namen van de externe en lokale interfaces van de EJB, [DaoJpa] en portable, wat betekent dat ze door alle applicatieservers worden herkend,
  • regel 9: hetzelfde, maar met de eigen namen van Glassfish,
  • regels 10-11: hetzelfde geldt voor de EJB en [Metier].

We onthouden de draagbare naam van de externe interface van EJB [Metier]:

java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote

Deze hebben we nodig bij het testen van de laag [métier].

3.5.3. Testen van de laag [métier]

Net zoals we hebben gedaan voor de laag [DAO], gaan we de laag [métier] testen in het kader van een client/server-toepassing:

De client gaat de externe interface van de EJB [Metier] testen die op de Glassfish-server is geïmplementeerd.

We beginnen met het aanmaken van een nieuw Maven-project. Hiervoor volgen we dezelfde werkwijze als bij het aanmaken van het testproject voor de laag [dao] (zie paragraaf 3.4.7), met uitzondering van het aanmaken van de test JUnit. Het aldus aangemaakte project ziet er als volgt uit

  • in [1], het aangemaakte project met zijn afhankelijkheden: ten opzichte van EJB van de laag [dao], ten opzichte van EJB van de laag [métier], van de bibliotheek [gf-client].

Op dit moment ziet het bestand [pom.xml] van het project er als volgt uit:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-client-rdvmedecins-ejb-metier</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-client-rdvmedecins-ejb-metier</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.glassfish.appclient</groupId>
      <artifactId>gf-client</artifactId>
      <version>3.1.1</version>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-metier</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>
</project>

Zorg ervoor dat de afhankelijkheden zoals beschreven in de regels 17-33 aanwezig zijn. De test bestaat uit een eenvoudige consoleklasse:

De code van de klasse [ClientRdvMedecinsMetier] is als volgt:


package istia.st.client;

import java.util.Date;
import java.util.List;

import javax.naming.InitialContext;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;

import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.service.IMetierRemote;

public class ClientRdvMedecinsMetier {

  // de naam van de externe interface van de EJB [Metier]
  private static String IDaoRemoteName = "java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
  // huidige datum
  private static Date jour = new Date();

  public static void main(String[] args) {
    try {
      // context JNDI van de Glassfish-server
      InitialContext initialContext = new InitialContext();
      // verwijzing naar de externe laag [metier]
      IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
      // weergave van klanten
      List<Client> clients = metier.getAllClients();
      display("Liste des clients :", clients);
      // weergave van artsen
      List<Medecin> medecins = metier.getAllMedecins();
      display("Liste des médecins :", medecins);
      // weergave van de tijdvakken van een arts
      Medecin medecin = medecins.get(0);
      List<Creneau> creneaux = metier.getAllCreneaux(medecin);
      display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
      // lijst met afspraken van een arts op een bepaalde dag
      display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // agenda weergeven
      AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // een RV toevoegen
      Rv rv = null;
      Creneau creneau = creneaux.get(2);
      Client client = clients.get(0);
      System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
      rv = metier.ajouterRv(jour, creneau, client);
      System.out.println("Rv ajouté");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // agenda weergeven
      agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // een RV verwijderen
      System.out.println("Suppression du Rv ajouté");
      metier.supprimerRv(rv);
      System.out.println("Rv supprimé");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // agenda weergeven
      agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
    } catch (Throwable ex) {
      System.out.println("Erreur...");
      while (ex != null) {
        System.out.println(String.format("%s : %s", ex.getClass().getName(), ex.getMessage()));
        ex = ex.getCause();
      }
    }
  }

  // hulpprogramma - toont de items in een verzameling
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • regel 18: de draagbare naam van de externe interface van de EJB [Metier] is overgenomen uit de logbestanden van GlassFish,
  • regels 24-27: er wordt een verwijzing verkregen naar de externe interface van de EJB [Metier],
  • regels 29-30: geven de klanten weer,
  • regels 32-33: geven de artsen weer,
  • regels 35-37: tonen de beschikbare tijdvakken van een arts,
  • regel 39: toont de afspraken van een arts op een bepaalde dag,
  • regels 41-42: de agenda van diezelfde arts voor diezelfde dag,
  • regels 44-49: er wordt een afspraak toegevoegd,
  • regel 50: de afspraken van de arts worden weergegeven. Er moet er één extra bij staan,
  • regels 52-53: de agenda van de arts wordt weergegeven. De toegevoegde afspraak moet zichtbaar zijn,
  • regels 55-57: de zojuist toegevoegde afspraak wordt verwijderd,
  • regel 58: dit moet terug te zien zijn in de lijst met afspraken van de arts,
  • regels 60-61: en in zijn agenda.

We voeren de test uit:

 

De verkregen scherm en zijn als volgt:


Liste des clients :
Client[1,Mr,Jules,MARTIN]
Client[2,Mme,Christine,GERMAN]
Client[3,Mr,Jules,JACQUARD]
Client[4,Melle,Brigitte,BISTROU]
Liste des médecins :
Médecin[1,Mme,Marie,PELISSIER]
Médecin[2,Mr,Jacques,BROMARD]
Médecin[3,Mr,Philippe,JANDOT]
Médecin[4,Melle,Justine,JACQUEMOT]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER]
Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Ajout d'un Rv le [Wed May 23 16:25:26 CEST 2012] dans le créneau Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] pour le client Client[1,Mr,Jules,MARTIN]
Rv ajouté
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Suppression du Rv ajouté
Rv supprimé
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
  • regel 37: de agenda van mevrouw PELISSIER, op 23 mei 2012. Er is geen tijdvak gereserveerd,
  • regel 39: toevoeging van een afspraak,
  • regel 42: de nieuwe agenda van mevrouw PELISSIER. Er is nu een tijdvak gereserveerd voor de heer MARTIN,
  • regel 44: de afspraak is verwijderd,
  • regel 46: uit de agenda van mevrouw PELISSIER blijkt dat er geen tijdvak is gereserveerd.

We gaan er nu vanuit dat de lagen [DAO] en [métier] operationeel zijn. We moeten nu nog de laag [web] schrijven met het framework JSF. Hiervoor gaan we gebruikmaken van de kennis die we aan het begin van dit document hebben opgedaan.

3.6. De laag [web]

Laten we terugkeren naar de architectuur die momenteel wordt opgebouwd:

We gaan de laatste laag bouwen, namelijk de laag [web].

3.6.1. Het NetBeans-project

We bouwen een Maven-project:

  • in [1] maken we een nieuw project aan,
  • in [2, 3], een Maven-project van het type [Web Application],
  • in [4] geven we het een naam,
  • in [5], we kiezen de Glassfish-server en Java EE 6 Web,
  • in [6], het zo aangemaakte project,
  • in [7], het project nadat de pagina [index.jsp] en het pakket in [Source Packages] zijn verwijderd,
  • in [8, 9], in de projecteigenschappen, voegen we een framework toe,
  • in [10] kiest men voor Java Server Faces,
  • in [11], de configuratie van Java Server Faces. We laten de standaardwaarden staan. Merk op dat JSF 2 wordt gebruikt,
  • in [12] wordt het project vervolgens op twee punten aangepast: er wordt een bestand [web.xml] gegenereerd, evenals een pagina [index.html].

Het bestand [web.xml] ziet er als volgt uit:


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>/faces/*</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>

We zijn dit bestand al eerder tegengekomen.

  • regels 7-11: definiëren de servlet die alle verzoeken aan de applicatie zal verwerken. Dit is de servlet van JSF,
  • regels 12-15: definiëren de URL die door deze servlet worden verwerkt. Dit zijn de URL in de vorm /faces/*,
  • regels 21-23: hierin wordt de pagina [index.xhtml] gedefinieerd als startpagina.

Deze pagina is de volgende:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
  <h:head>
    <title>Facelet Title</title>
  </h:head>
  <h:body>
    Hello from Facelets
  </h:body>
</html>

Deze zijn we al tegengekomen. We kunnen dit project uitvoeren:

  • in [1], we voeren het project uit en krijgen het resultaat [2] in de browser te zien.

We presenteren nu het volledige project om vervolgens de verschillende onderdelen ervan in detail te bespreken.

  • naar [1], de pagina’s XHTML van het project,
  • in [2], de Java-codes,
  • in [3], de berichtbestanden, aangezien de applicatie geïnternationaliseerd is,
  • in [4], de afhankelijkheden van het project.

3.6.2. De afhankelijkheden van het project

Laten we teruggaan naar de architectuur van het project:

De laag JSF is gebaseerd op de lagen [métier], [DAO] en [JPA]. Deze drie lagen zijn opgenomen in de twee Maven-projecten die we hebben opgebouwd, wat de afhankelijkheden van het project [4] verklaart. Laten we even laten zien hoe deze afhankelijkheden worden toegevoegd:

  • in [1] voegen we ‘ejb’ toe om aan te geven dat de afhankelijkheid betrekking heeft op het project EJB,
  • in [2] zetten we [provided]. Het webproject wordt namelijk tegelijk met de twee projecten EJB geïmplementeerd. Het is dus niet nodig om de JAR-bestanden van EJB mee te nemen.

3.6.3. De configuratie van het project

De projectconfiguratie is dezelfde als die van de JSF-projecten die we aan het begin van dit document hebben besproken. We sommen de configuratiebestanden op zonder ze opnieuw uit te leggen.

 

[web.xml]: configureert de webapplicatie.


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
  <context-param>
    <param-name>javax.faces.PROJECT_STAGE</param-name>
    <param-value>Production</param-value>
  </context-param>
  <context-param>
    <param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
    <param-value>true</param-value>
  </context-param> 
  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/faces/*</url-pattern>
  </servlet-mapping>
  <session-config>
    <session-timeout>
      30
    </session-timeout>
  </session-config>
  <welcome-file-list>
    <welcome-file>faces/index.xhtml</welcome-file>
  </welcome-file-list>
  <error-page>
    <error-code>500</error-code>
    <location>/faces/exception.xhtml</location>
  </error-page>
  <error-page>
    <exception-type>Exception</exception-type>
    <location>/faces/exception.xhtml</location>
  </error-page>

</web-app>

Let op: op regel 26 is de pagina [index.xhtml] de startpagina van de applicatie.

[faces-config.xml]: configureert de applicatie JSF


<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
    <message-bundle>messages</message-bundle>
  </application>
</faces-config>

[beans.xml]: leeg, maar nodig voor de annotatie @Named


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

[styles.css]: het stylesheet van de applicatie


.reservationsHeaders {
   text-align: center;
   font-style: italic;
   color: Snow;
   background: Teal;
}

.creneau {
   height: 25px;
   text-align: center;
   background: MediumTurquoise;
}
.client {
   text-align: left;
   background: PowderBlue;
}

.action {
   width: 6em;
   text-align: left;
   color: Black;
   background: MediumTurquoise;
}
.erreursHeaders {
   background: Teal;
   background-color: #ff6633;
   color: Snow;
   font-style: italic;
   text-align: center

}

.erreurClasse {
   background: MediumTurquoise;
   background-color: #ffcc66;
   height: 25px;
   text-align: center
}

.erreurMessage {
   background: PowderBlue;
   background-color: #ffcc99;
   text-align: left
}

[messages_fr.properties]: het bestand met de berichten in het Frans


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

[messages_en.properties]: het bestand met de Engelse meldingen


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

3.6.4. De weergaven van het project

Laten we nog even de werking van de applicatie in herinnering brengen. De startpagina ziet er als volgt uit:

 

Vanaf deze eerste pagina zal de gebruiker (secretariaat, arts) een aantal handelingen uitvoeren. Deze worden hieronder weergegeven. De afbeelding links toont het scherm waarop de gebruiker een verzoek indient, de afbeelding rechts het antwoord dat door de server wordt verzonden.

Ten slotte kan er ook een foutpagina verschijnen:

Deze verschillende weergaven worden verkregen via de volgende pagina's van het webproject:

  • in [1] zorgen de pagina's [basdepage, entete, layout] voor de opmaak van alle weergaven,
  • in [2] is de weergave die wordt gegenereerd door [layout.xhtml].

Hier is gebruikgemaakt van de facelets-technologie. Deze is beschreven in paragraaf 2.11. We beperken ons tot het weergeven van de code van de pagina's XHTML die worden gebruikt voor de opmaak:

[entete.xhtml]


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

Let op de regels 10-12: de twee links om de taal van de applicatie te wijzigen.

[basdepage.xhtml]


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
  <body>
    <h:outputText value="#{msg['layout.basdepage']}"/>
  </body>
</html>

[layout.xhtml]


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <f:view locale="#{changeLocale.locale}">
    <h:head>
      <title>RdvMedecins</title>
      <h:outputStylesheet library="css" name="styles.css"/>
    </h:head>
    <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
      <h:form id="formulaire">
        <table style="width: 1200px">
          <tr>
            <td colspan="2" bgcolor="#ccccff">
              <ui:include src="entete.xhtml"/>
            </td>
          </tr>
          <tr>
            <td style="width: 100px; height: 200px" bgcolor="#ffcccc">
            </td>
            <td>
              <ui:insert name="contenu" >
                <h2>Contenu</h2>
              </ui:insert>
            </td>
          </tr>
          <tr bgcolor="#ffcc66">
            <td colspan="2">
              <ui:include src="basdepage.xhtml"/>
            </td>
          </tr>         
        </table>
      </h:form>
    </h:body>
  </f:view>
</html>

Deze pagina is de sjabloon (template) van de pagina [index.xhtml]:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <ui:composition template="layout.xhtml">
    <ui:define name="contenu">
      <h:panelGroup rendered="#{form.form1Rendered}">
        <ui:include src="form1.xhtml"/>
      </h:panelGroup>
      <h:panelGroup rendered="#{form.form2Rendered}">
        <ui:include src="form2.xhtml"/>
      </h:panelGroup>
      <h:panelGroup rendered="#{form.form3Rendered}">
        <ui:include src="form3.xhtml"/>
      </h:panelGroup>
      <h:panelGroup rendered="#{form.erreurRendered}">
        <ui:include src="erreur.xhtml"/>
      </h:panelGroup>
    </ui:define>
  </ui:composition>
</html>

De regels 8-21 definiëren het gebied met de naam "inhoud" (regel 8) in [layout.xhtml] (regel 7). Dit is het centrale gebied van de weergaven:

 

De pagina [index.xhtml] is de enige pagina van de applicatie. Er is dus geen navigatie tussen pagina’s. De pagina geeft een van de vier pagina’s [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml] weer. Deze weergave wordt geregeld door vier booleaanse variabelen [form1Rendered, form2Rendered, form3Rendered, erreurRendered] van de form-bean, die we straks zullen beschrijven.

3.6.5. De beans van het project

De klassen van het pakket [utils] zijn al besproken:

  • de klasse [ChangeLocale] is de klasse die zorgt voor de taalwisseling. Deze is al besproken (paragraaf 2.4.4).
  • de klasse [Messages] is een klasse die de internationalisering van de berichten van een applicatie vergemakkelijkt. Deze is besproken in paragraaf 2.8.5.7.

3.6.5.1. De Application-bean

De bean [Application] is als volgt:


package beans;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.metier.service.IMetierLocal;

@Named(value = "application")
@ApplicationScoped
public class Application implements Serializable{

  // bedrijfslaag
  @EJB
  private IMetierLocal metier;
  // cache
  private List<Medecin> medecins;
  private List<Client> clients;
  private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
  private Map<Long, Client> hClients = new HashMap<Long, Client>();
  // fouten
  private List<Erreur> erreurs = new ArrayList<Erreur>();
  private Boolean erreur = false;

  public Application() {
  }

  @PostConstruct
  public void init() {
    // artsen en klanten worden in de cache opgeslagen
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // de fout wordt genoteerd
      erreur = true;
      erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
      while (th.getCause() != null) {
        th = th.getCause();
        erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
      }
      return;
    }
    // lijsten controleren
    if (medecins.size() == 0) {
      // de fout wordt genoteerd
      erreur = true;
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // de fout wordt genoteerd
      erreur = true;
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // fout?
    if (erreur) {
      return;
    }

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

  // getters en setters
  ...
}
  • regels 15-16: de klasse [Application] is een bean met het bereik Application. Deze wordt eenmalig aangemaakt aan het begin van de levenscyclus van de applicatie JSF en is toegankelijk voor alle verzoeken van alle gebruikers. Hierin worden doorgaans alleen-lezen gegevens geplaatst. Hier zullen we de lijst met artsen en die met klanten in opslaan. We gaan er dus vanuit dat deze niet vaak veranderen. De pagina’s XHTML hebben er toegang toe via de applicatienaam,
  • regels 20-21: een verwijzing naar de lokale interface van de EJB [Metier] wordt geïnjecteerd door de Glassfish-container EJB. Laten we de architectuur van de applicatie nog eens in herinnering brengen:

De applicaties JSF, EJB en [Metier] worden uitgevoerd in dezelfde JVM (Java Virtual Machine). De laag [JSF] zal dus gebruikmaken van de lokale interface van de EJB. Hier maakt de applicatie-bean gebruik van de EJB en de [Metier]. Zelfs als dat niet het geval was, zou het normaal zijn om daar een verwijzing naar de laag [métier] te vinden. Dit is namelijk informatie die door alle verzoeken van alle gebruikers kan worden gedeeld, dus gegevens met een bereik van Application.

  • regels 34-35: de methode init wordt direct na het instantiëren van de klasse [Application] uitgevoerd (aanwezigheid van de annotatie @PostConstruct),
  • In de regels 36-73 maakt de methode de volgende elementen aan: de lijst met artsen in regel 23, de lijst met klanten in regel 24, een woordenboek met artsen, geïndexeerd op hun id, in regel 25, en hetzelfde voor de klanten in regel 26. Er kunnen fouten optreden. Deze worden vastgelegd in de lijst in regel 28.

De klasse [Erreur] ziet er als volgt uit:


package beans;

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

  // constructor
  public Erreur(String classe, String message){
    this.setClasse(classe);
    this.message=message;
  }
  
  // getters en setters
...  
}
  • regel 9: de naam van een uitzonderingsklasse als er een uitzondering is gegenereerd,
  • regel 10: een foutmelding.

3.6.5.2. De bean [Form]

De code ervan is als volgt:


package beans;

...

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

  public Form() {
  }

  // Application-bean
  @Inject
  private Application application;

  // model
  private Long idMedecin;
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private String form2Titre;
  private String form3Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Long idCreneau;
  private Medecin medecin;
  private Client client;
  private Long idClient;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;

  @PostConstruct
  private void init() {
    // Is de initialisatie gelukt?
    if (application.getErreur()) {
      // de lijst met fouten wordt opgehaald
      erreurs = application.getErreurs();
      // het foutenscherm wordt weergegeven
      setForms(false, false, false, true);
    }
  }

  // weergave van het overzicht
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.form3Rendered = form3Rendered;
    this.erreurRendered = erreurRendered;
  }
.................................................
}
  • regels 5-7: de klasse [Form] is een bean met de naam 'form' en een bereik van 'session'. We herinneren eraan dat de klasse in dat geval serialiseerbaar moet zijn.
  • regels 13-14: de bean ‘form’ heeft een verwijzing naar de bean ‘application’. Deze wordt geïnjecteerd door de servletcontainer waarin de applicatie wordt uitgevoerd (aanwezigheid van de annotatie @Inject).
  • regels 17-31: het sjabloon van de pagina’s [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. De weergave van deze pagina’s wordt bepaald door de booleaanse waarden in de regels 19-22. Merk op dat standaard de pagina [form1.xhtml] wordt weergegeven,
  • regels 33-34: de methode init wordt direct na het instantiëren van de klasse uitgevoerd (aanwezigheid van de annotatie @PostConstruct),
  • regels 35-41: de methode `init` wordt gebruikt om te bepalen welke pagina als eerste moet worden weergegeven: normaal gesproken de pagina [form1.xhtml] (regel 19), tenzij de initialisatie van de applicatie is mislukt (regel 36), in welk geval de pagina [erreur.xhtml] wordt weergegeven (regel 40).

De pagina [erreur.xhtml] is de volgende:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <body>
    <h2><h:outputText value="#{msg['erreur.titre']}"/></h2>
    <p>
      <h:commandButton value="#{msg['erreur.accueil']}" actionListener="#{form.accueil()}"/>
    </p>
    <hr/>
    <h:dataTable value="#{form.erreurs}" var="erreur" headerClass="erreursHeaders" columnClasses="erreurClasse,erreurMessage">
      <h:column>
        <f:facet name="header">
          <h:outputText value="#{msg['erreur.classe']}"/>
        </f:facet>
        <h:outputText value="#{erreur.classe}"/>
      </h:column>
      <h:column>
        <f:facet name="header">
          <h:outputText value="#{msg['erreur.message']}"/>
        </f:facet>
        <h:outputText value="#{erreur.message}"/>
      </h:column>
    </h:dataTable>
  </body>
</html>

Deze maakt gebruik van een <h:dataTable>-tag (regels 14-27) om de lijst met fouten weer te geven. Dit levert een pagina op die lijkt op de volgende:

Image

We gaan nu de verschillende fasen in de levenscyclus van de applicatie definiëren.

3.6.6. Interacties tussen pagina's en het model

3.6.6.1. Het weergeven van de startpagina

Als alles goed gaat, is de eerste pagina die wordt weergegeven [form1.xhtml]. Dit levert het volgende beeld op:

 

De pagina [form1.xhtml] ziet er als volgt uit:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <body>
    <h2><h:outputText value="#{msg['form1.titre']}"/></h2>
    <h:panelGrid columns="3">
      <h:panelGroup>
      <div align="center"><h3><h:outputText value="#{msg['form1.medecin']}"/></h3></div>
      </h:panelGroup>
      <h:panelGroup>
      <div align="center"><h3><h:outputText value="#{msg['form1.jour']}"/></h3></div>
      </h:panelGroup>
      <h:panelGroup/>
      <h:selectOneMenu value="#{form.idMedecin}">  
        <f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>  
      </h:selectOneMenu>              
      <h:inputText id="jour" value="#{form.jour}"  required="true" requiredMessage="#{msg['form1.jour.required']}" converterMessage="#{msg['form1.jour.erreur']}">
        <f:convertDateTime pattern="dd/MM/yyyy"/>
      </h:inputText>
      <h:message for="jour" styleClass="error"/>
    </h:panelGrid>
    <h:commandButton value="#{msg['form1.button.agenda']}" actionListener="#{form.getAgenda}"/>
  </body>
</html>

Deze pagina wordt aangedreven door het volgende sjabloon:


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

  // Application-bean
  @Inject
  private Application application;
  // sjabloon
  private Long idMedecin;
  private Date jour = new Date();
  
// lijst met artsen
  public List<Medecin> getMedecins() {
    return application.getMedecins();
  }
  // agenda
  public void getAgenda() {
    ...
}
  • Het veld op regel 9 leest en schrijft de waarde van de lijst op regel 18 van de pagina. Bij de eerste weergave van de pagina stelt deze de geselecteerde waarde in de keuzelijst vast. Bij de eerste weergave is idMedecin gelijk aan null, dus wordt de eerste arts geselecteerd;
  • de methode in de regels 13-15 genereert de opties voor de keuzelijst met artsen (regel 19 van de pagina). Elke gegenereerde optie heeft als label (itemLabel) de titel, achternaam en voornaam van de arts en als waarde (itemValue) de id van de arts,
  • het veld in regel 10 voorziet het invoerveld in regel 21 van de pagina van gegevens (lezen/schrijven). Bij de eerste weergave wordt dus de huidige datum weergegeven,
  • regels 17-19: de methode getAgenda verwerkt de klik op de knop [Agenda] op regel 26 van de pagina. Aangezien er geen navigatie is (er wordt altijd de pagina [index.html] opgevraagd), zal vaak het attribuut actionListener worden gebruikt in plaats van het attribuut action. In dit geval levert de methode die in het model wordt aangeroepen geen resultaat op.

Wanneer er op de knop [Agenda] wordt geklikt,

  • worden er waarden verzonden: de waarde die in de keuzelijst met artsen is geselecteerd, wordt opgeslagen in het veld idMedecin van het model en de gekozen dag in het veld ‘dag’,
  • wordt de methode getAgenda van het model aangeroepen.

De methode getAgenda is als volgt:


  // toepassings-bean
  @Inject
  private Application application;

  // sjabloon
  private Long idMedecin;
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private String form2Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Medecin medecin;
  private List<Erreur> erreurs;

  // agenda
  public void getAgenda() {
    try {
      // de arts opzoeken
      medecin = application.gethMedecins().get(idMedecin);
      // titel formulier 2
      form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
      // de agenda van de arts voor een bepaalde dag
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // formulier 2 weergeven
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // foutenoverzicht
      prepareVueErreur(th);
    }
  }

  // voorbereiding vueErreur
  private void prepareVueErreur(Throwable th) {
    // de foutenlijst wordt aangemaakt
    erreurs = new ArrayList<Erreur>();
    erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
    while (th.getCause() != null) {
      th = th.getCause();
      erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
    }
// het foutenoverzicht wordt weergegeven
    setForms(false, false, false, true);
}

Laten we nog eens bekijken wat de methode getAgenda moet weergeven:

  • regel 21: de geselecteerde arts wordt opgehaald uit het artsenregister dat is opgeslagen in de bean application. Hiervoor wordt de id gebruikt die is verzonden in idMedecin,
  • regel 23: de titel van de pagina [form2.xhtml] die zal worden weergegeven, wordt voorbereid. Dit bericht wordt uit het berichtenbestand gehaald, zodat het kan worden geïnternationaliseerd. Deze techniek is beschreven in paragraaf 2.8.5.7, pagina 135.
  • regel 25: de laag [métier] wordt aangeroepen om de agenda van de geselecteerde arts voor de gekozen dag te berekenen,
  • regel 27: [form2.xhtml] wordt weergegeven,
  • regel 28: als er een uitzondering optreedt, wordt er een foutenlijst samengesteld (regels 37-42) en wordt de pagina [erreur.xhtml] weergegeven (regel 44).

3.6.6.2. De agenda van een arts weergeven

De pagina [form2.xhtml] komt overeen met de volgende weergave:

De code van de pagina [form2.xhtml] is als volgt:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:c="http://java.sun.com/jsp/jstl/core">

  <body>
    <h2><h:outputText value="#{form.form2Titre}"/></h2>
    <h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
    <h:dataTable value="#{form.agendaMedecinJour.creneauxMedecinJour}" var="creneauMedecinJour" headerClass="reservationsHeaders" columnClasses="creneau,client,action">
      <h:column>  
        <f:facet name="header">  
          <h:outputText value="#{msg['form2.creneauHoraire']}"/> 
        </f:facet>  
        <h:outputText value="#{creneauMedecinJour.creneau.hdebut}:#{creneauMedecinJour.creneau.mdebut} - #{creneauMedecinJour.creneau.hfin}:#{creneauMedecinJour.creneau.mfin}" />  
      </h:column>  
      <h:column>  
        <f:facet name="header">  
          <h:outputText value="#{msg['form2.client']}"/>  
        </f:facet>  
        <c:if test="#{creneauMedecinJour.rv==null}">
          <h:outputText value=""/>
          <c:otherwise>
            <h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}"/>
          </c:otherwise>
        </c:if>
      </h:column>  
      <h:column>  
        <f:facet name="header"/>
        <h:commandLink action="#{form.action()}" value="#{creneauMedecinJour.rv==null ? msg['form2.reserver'] : msg['form2.supprimer']}">
          <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneau}"/>
        </h:commandLink>
      </h:column>  
    </h:dataTable>
  </body>
</html>

We herinneren ons dat de methode getAgenda twee velden in het model heeft geïnitialiseerd:


// sjabloon
  private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;

Deze twee velden vullen de pagina [form2.xhtml]:

  • regel 10: de titel van de pagina,
  • regel 12: de agenda van de arts wordt weergegeven via een <h:dataTable>-tag met drie kolommen,
  • regels 13-18: de eerste kolom toont de beschikbare tijdvakken,
  • regels 19-30: de tweede kolom toont de naam van de klant die het tijdvak eventueel heeft gereserveerd, of anders niets. Om deze keuze te maken, worden de tags uit de bibliotheek JSTL Core gebruikt, waarnaar in regel 7 wordt verwezen,
  • regels 30-35: de derde kolom toont de link [Réserver] als het tijdvak vrij is, en de link [Supprimer] als het tijdvak bezet is.

De links in de derde kolom zijn gekoppeld aan het volgende sjabloon:


// sjabloon
  private Long idCreneau;

  // actie op RV
  public void action() {
    ...
}
  • de methode action wordt aangeroepen wanneer de gebruiker op de link Reserveren / Verwijderen klikt (regel 32). Merk op dat hier het attribuut action is gebruikt. De methode waarnaar dit attribuut verwijst, zou de signatuur String action() moeten hebben, omdat de methode dan een navigatiesleutel moet retourneren. Maar hier is de signatuur void action(). Dit heeft geen fout veroorzaakt en we kunnen aannemen dat er in dit geval geen navigatie plaatsvindt. Dit was ook de bedoeling. Het gebruik van actionListener in plaats van action leidde tot een storing,
  • het veld idCreneau op regel 2 haalt de id op van het tijdslot van de link waarop is geklikt (regel 33 van de pagina).

3.6.6.3. Een afspraak verwijderen

Laten we eens kijken naar de code die het verwijderen van een afspraak afhandelt. Dit komt overeen met de volgende reeks weergaven:

De code die bij deze bewerking hoort, is de volgende:


// Application-bean
  @Inject
  private Application application;

  // sjabloon
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private AgendaMedecinJour agendaMedecinJour;
  private Long idCreneau;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;

  // actie op RV
  public void action() {
    // er wordt gezocht naar een tijdvak in de agenda
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // is er iets gevonden?
    if (!trouvé) {
      // dat is vreemd – form2 wordt opnieuw weergegeven
      setForms(false, true, false, false);
      return;
    }
    // gevonden
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // afhankelijk van de gewenste actie
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // reservering

  public void reserver() {
    ...
  }

  public void supprimer() {
    try {
      // een afspraak verwijderen
      application.getMetier().supprimerRv(creneauChoisi.getRv());
      // de agenda wordt bijgewerkt
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // formulier 2 weergeven
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // foutenoverzicht
      prepareVueErreur(th);
    }
  }
  • regel 16: wanneer de methode action wordt gestart, is de id van het geselecteerde tijdslot al in idCreneau (regel 11) geplaatst,
  • regels 18-26: er wordt geprobeerd het tijdvak op te halen aan de hand van de waarde in id (regel 21). Er wordt gezocht in de huidige agenda, agendaMedecinJour uit regel 10. Normaal gesproken zou deze gevonden moeten worden. Als dat niet het geval is, wordt er niets gedaan (regels 28-32),
  • regel 34: als het gezochte tijdvak is gevonden, wordt er een referentie opgehaald die wordt opgeslagen in regel 12,
  • regel 36: we kijken of het gekozen tijdvak al een afspraak bevatte. Zo ja, dan verwijderen we die (regel 39), zo niet, dan reserveren we er een (regel 37),
  • regel 51: de afspraak in het gekozen tijdvak wordt verwijderd. Dit wordt uitgevoerd door de laag [métier],
  • regel 53: we vragen de laag [métier] om de nieuwe agenda van de arts. Daarin zien we natuurlijk één afspraak minder. Maar aangezien de applicatie voor meerdere gebruikers is, kunnen we daar wijzigingen zien die door andere gebruikers zijn aangebracht,
  • regel 55: de pagina [form2.xhtml] wordt opnieuw weergegeven,
  • regel 58: aangezien de laag [métier] is aangeroepen, kunnen er uitzonderingen optreden. In dat geval wordt de uitzonderingsstack opgeslagen in de foutenlijst van regel 13 en weergegeven met behulp van de weergave [erreur.xhtml].

3.6.6.4. Afspraak maken

Het maken van een afspraak verloopt volgens de volgende volgorde:

Het sjabloon dat bij deze actie wordt gebruikt, is het volgende:


// sjabloon
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private String form3Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Medecin medecin;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;

  // actie op RV
  public void action() {
...
    // gevonden
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // afhankelijk van de gewenste actie
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // reservering

    public void reserver() {
    try {
      // formulierkop 3
      form3Titre = Messages.getMessage(null, "form3.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour),
                creneauChoisi.getCreneau().getHdebut(), creneauChoisi.getCreneau().getMdebut(), creneauChoisi.getCreneau().getHfin(), creneauChoisi.getCreneau().getMfin()}).getSummary();
      // klant geselecteerd in de keuzelijst
      idClient=null;
      // formulier 3 wordt weergegeven
      setForms(false, false, true, false);
    } catch (Throwable th) {
      // foutweergave
      prepareVueErreur(th);
    }
  }
  • regel 14: als het gekozen tijdvak nog geen afspraak heeft, is het een reservering,
  • regel 30: de titel van de pagina [form3.xhtml] wordt opgesteld met dezelfde techniek als die voor de titel van de pagina [form2.xhtml],
  • regel 34: in dit formulier staat een keuzelijst waarvan de waarde wordt aangevuld door idClient. We stellen de waarde van dit veld in op null om niemand te selecteren,
  • regel 36: we geven de pagina [form3.xhtml] weer,
  • regel 39: of de foutpagina als er een uitzondering is opgetreden.

De pagina [form3.xhtml] ziet er als volgt uit:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <body>
    <h2><h:outputText value="#{form.form3Titre}"/></h2>
    <h:panelGrid columns="2">
      <h:outputText value="#{msg['form3.client']}"/>
      <h:selectOneMenu value="#{form.idClient}">
        <f:selectItems value="#{form.clients}" var="client" itemLabel="#{client.titre} #{client.prenom} #{client.nom}" itemValue="#{client.id}"/>
      </h:selectOneMenu>
      <h:panelGroup>
        <h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
        <h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
      </h:panelGroup>
    </h:panelGrid>
  </body>
</html>

Deze pagina wordt gevoed door het volgende sjabloon:


// Application-bean
  @Inject
  private Application application;

  // sjabloon
  private Long idClient;

  // klantenlijst
  public List<Client> getClients() {
    return application.getClients();
  }
  • regel 6: het klantnummer vult het attribuut value van de klantenlijst in regel 12 van de pagina in. Het bepaalt welk item in de lijst is geselecteerd,
  • regels 9-11: de methode getClients vult de inhoud van de keuzelijst in (regel 13). De tekst (itemLabel) van elke optie is [Titre Prénom Nom] van de klant, en de bijbehorende waarde (itemValue) is de id van de klant. Het is dus deze waarde die wordt verzonden.

3.6.6.5. Bevestiging van een afspraak

De bevestiging van een afspraak verloopt volgens de volgende volgorde:

en komt overeen met het klikken op de knop [Valider]:


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

Het is dus de methode [Form].validerRv die deze gebeurtenis afhandelt. De code ervan is als volgt:


  // applicatie-bean
  @Inject
  private Application application;
  
  // sjabloon
  private Date jour = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean erreurRendered = false;
  private Long idCreneau;
  private Long idClient;
  private List<Erreur> erreurs;

  // afspraakvalidatie
  public void validerRv() {
    try {
      // we halen een instantie van het gekozen tijdvak op
      Creneau creneau = application.getMetier().getCreneauById(idCreneau);
      // de afspraak wordt toegevoegd
      application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
      // de agenda wordt bijgewerkt
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // form2 wordt weergegeven
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // foutenweergave
      prepareVueErreur(th);
    }
}
  • regel 12: voordat de methode validerRv wordt uitgevoerd, heeft het veld idClient de id van de door de gebruiker geselecteerde klant ontvangen,
  • regel 19: op basis van de ID van het tijdvak dat in een eerdere stap is opgeslagen (de bean heeft een sessiebereik), wordt de laag [métier] gevraagd om een verwijzing naar het tijdvak zelf,
  • regel 21: de laag [métier] wordt gevraagd een afspraak toe te voegen voor de gekozen dag (dag), het gekozen tijdvak (tijdvak) en de gekozen klant (idClient),
  • regel 23: de laag [métier] wordt gevraagd de agenda van de arts te vernieuwen. De toegevoegde afspraak wordt weergegeven, samen met alle wijzigingen die andere gebruikers van de applicatie mogelijk hebben aangebracht,
  • regel 25: de agenda [form2.xhtml] wordt opnieuw weergegeven,
  • regel 28: de foutpagina wordt weergegeven als er een fout optreedt.

3.6.6.6. Een afspraak annuleren

Dit komt overeen met de volgende reeks:

De knop [Annuler] op de pagina [form3.xhtml] is als volgt:


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

De methode [Form].annulerRv wordt dus aangeroepen:


  // afspraak annuleren
  public void annulerRv() {
    // formulier 2 weergeven
    setForms(false, true, false, false);
}

3.6.6.7. Terug naar de startpagina

Er is nog één actie te bekijken, namelijk die van de volgende reeks:

De code van de knop [Accueil] op de pagina [form2.xhtml] is als volgt:


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

De methode [Form].accueil is als volgt:


  public void accueil() {
    // de startpagina wordt weergegeven
    setForms(true, false, false, false);
}

3.7. Conclusion

We hebben de volgende applicatie gebouwd:

We hebben ons meer gericht op de functionaliteit van de applicatie dan op de gebruikerservaring. Deze zal worden verbeterd door gebruik te maken van de componentenbibliotheek PrimeFaces. We hebben een eenvoudige, maar niettemin representatieve applicatie gebouwd die een gelaagde Java-architectuur EE gebruikt met behulp van EJB. De applicatie kan op verschillende manieren worden verbeterd:

  • er is authenticatie nodig. Niet iedereen mag afspraken toevoegen of verwijderen,
  • men zou vooruit en achteruit door de agenda moeten kunnen bladeren bij het zoeken naar een dag met vrije tijdvakken,
  • het zou mogelijk moeten zijn om de lijst op te vragen van dagen waarop er vrije tijdvakken zijn voor een arts. Als deze arts bijvoorbeeld oogarts is, zijn zijn afspraken doorgaans al zes maanden van tevoren volgeboekt,
  • ...

3.8. Testen met Eclipse

3.8.1. De laag [DAO]

  • in [1] importeren we het project EJB uit de laag [DAO] en de bijbehorende client,
  • in [2] selecteert men het project EJB uit de laag [DAO] en voert men het uit in [3],
  • in [4], voer je het uit op een server,
  • in [5] wordt alleen de Glassfish-server voorgesteld, omdat dit de enige is met een container EJB,
  • in [6] is de module EJB geïmplementeerd,
  • in [7] worden de logbestanden weergegeven:
1
2
3
4
5
6
7
8
Infos: Hibernate Validator 4.2.0.Final

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

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

Dit zijn dezelfde logbestanden die we bij NetBeans hadden.

  • in [7A] [7B] voeren we de test JUnit van de klant uit,
  • in [8] is de test geslaagd,
  • in [9], de logbestanden van de console.

In [10] wordt de applicatie EJB ontladen.

3.8.2. De laag [métier]

  • in [1] importeren we de vier Maven-projecten uit de laag [métier],
  • in [2] selecteren we het bedrijfsproject en voeren we het uit in [3], op een Glassfish-server [4] [5],
  • in [6] is het bedrijfsproject geïmplementeerd op Glassfish,
  • en [7], bekijken we de logbestanden van Glassfish,
1
2
3
4
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: EJB5181:Portable JNDI names for EJB Metier: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]

Op regel 3 noteren we de draagbare naam van EJB [Metier] en plakken we deze in de console van deze EJB:


public class ClientRdvMedecinsMetier {

  // de naam van de externe interface van EJB [Metier]
  private static String IDaoRemoteName = "java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
  // huidige datum
private static Date jour = new Date();
  • in [8] voeren we de console-client uit,
  • in [9], de logbestanden.
  • in [10] wordt de bedrijfsapplicatie afgesloten;

3.8.3. De laag [web]

  • en [1], importeren we de drie Maven-projecten van de laag [web]. Het project met de extensie 'ear' is het bedrijfsproject dat op Glassfish moet worden geïmplementeerd,
  • in [2] voeren we het uit,
  • op de Glassfish-server [3],
  • in [4] is de bedrijfsapplicatie succesvol geïmplementeerd,
  • in [5], vragen we de URL van de applicatie op in de interne browser van Eclipse.