Skip to content

3. Beispielanwendung – 01: rdvmedecins-jsf2-ejb

Der folgende Text bezieht sich auf die folgenden Dokumente:

  • [ref7]: Einführung in Java EE 5 (Juni 2010) [http://tahe.developpez.com/java/javaee]. Dieses Dokument bietet einen Überblick über JSF 1 und die EJB3.
  • [ref8]: Java-Persistenz in der Praxis (Juni 2007) [http://tahe.developpez.com/java/jpa]. Dieses Dokument bietet einen Einblick in die Datenpersistenz mit JPA (Java Persistence API).
  • [ref9]: Erstellen eines Java-Webdienstes EE mit NetBeans und dem GlassFish-Server (Januar 2009) [http://tahe.developpez.com/java/webservice-jee]. Dieses Dokument befasst sich mit der Erstellung eines Webdienstes.

Die Beispielanwendung, die behandelt wird, stammt aus [ref9].

3.1. Die Anwendung

Ein IT-Dienstleistungsunternehmen [ISTIA-AGI] möchte einen Terminvereinbarungsdienst anbieten. Die erste Zielgruppe sind selbstständige Ärzte. Diese verfügen in der Regel nicht über ein Sekretariat. Kunden, die einen Termin vereinbaren möchten, rufen daher direkt beim Arzt an. Dieser wird dadurch im Laufe eines Tages häufig gestört, was seine Verfügbarkeit für seine Patienten einschränkt. Das Unternehmen [ISTIA-AGI] möchte ihnen einen Terminvereinbarungsdienst anbieten, der nach folgendem Prinzip funktioniert:

  • Ein Sekretariat übernimmt die Terminvergabe für eine große Anzahl von Ärzten. Dieses Sekretariat kann auf eine einzige Person reduziert werden. Deren Gehalt wird von allen Ärzten, die den Terminvermittlungsdienst nutzen, gemeinsam getragen.
  • Das Sekretariat und alle Ärzte sind mit dem Internet verbunden
  • Die Termine werden in einer zentralen Datenbank gespeichert, auf die das Sekretariat und die Ärzte über das Internet zugreifen können
  • Die Vergabe von RV erfolgt in der Regel durch das Sekretariat. Sie kann aber auch von den Ärzten selbst vorgenommen werden. Dies ist insbesondere dann der Fall, wenn der Arzt am Ende einer Konsultation seinem Patienten selbst einen neuen RV zuweist.

Die Architektur des Dienstes zur Vergabe von RV ist wie folgt:

Ärzte gewinnen an Effizienz, wenn sie sich nicht mehr um die RV kümmern müssen. Bei einer ausreichenden Anzahl von Ärzten fällt ihr Beitrag zu den Betriebskosten des Sekretariats gering aus.

Das Unternehmen [ISTIA-AGI] beschließt, die Anwendung in zwei Versionen zu entwickeln:

  • eine Version JSF / EJB3 / JPA EclipseLink / Glassfish-Server:
  • sowie eine Version JSF / Spring / JPA Hibernate / Tomcat-Server:

3.2. Funktionsweise der Anwendung

Wir nennen die Anwendung [RdvMedecins]. Im Folgenden zeigen wir Screenshots ihrer Funktionsweise.

Die Startseite der Anwendung sieht wie folgt aus:

Von dieser Startseite aus führt der Benutzer (Sekretariat, Arzt) eine Reihe von Aktionen durch. Diese stellen wir im Folgenden vor. Die Ansicht auf der linken Seite zeigt die Ansicht, von der aus der Benutzer eine Anfrage stellt, die Ansicht auf der rechten Seite die vom Server gesendete Antwort.

Schließlich kann auch eine Fehlerseite angezeigt werden:

3.3. Die Datenbank

Kommen wir zurück zur Architektur der zu entwickelnden Anwendung:

Die Datenbank, die wir [dbrdvmedecins2] nennen werden, ist eine MySQL5-Datenbank mit vier Tabellen:

  

3.3.1. Die Tabelle [MEDECINS]

Sie enthält Informationen zu den Ärzten, die von der Anwendung verwaltet werden. [RdvMedecins].

  • ID: Identifikationsnummer des Arztes – Primärschlüssel der Tabelle
  • VERSION: Nummer zur Identifizierung der Zeilenversion in der Tabelle. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
  • NOM: Name des Arztes
  • PRENOM: sein Vorname
  • TITRE: sein Titel (Fräulein, Frau, Herr)

3.3.2. Die Tabelle [CLIENTS]

Die Patienten der verschiedenen Ärzte sind in der Tabelle [CLIENTS] gespeichert:

  • ID: Nummer zur Identifizierung des Kunden – Primärschlüssel der Tabelle
  • VERSION: Nummer zur Identifizierung der Zeilenversion in der Tabelle. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
  • NOM: Name des Kunden
  • PRENOM: sein Vorname
  • TITRE: seine Anrede (Fräulein, Frau, Herr)

3.3.3. Die Tabelle [CRENEAUX]

Sie listet die Zeitfenster auf, in denen RV möglich sind:

  • ID: Nummer zur Identifizierung des Zeitfensters – Primärschlüssel der Tabelle (Zeile 8)
  • VERSION: Nummer zur Identifizierung der Version der Zeile in der Tabelle. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
  • ID_MEDECIN: Nummer zur Identifizierung des Arztes, zu dem dieses Zeitfenster gehört – Fremdschlüssel auf die Spalte MEDECINS (ID).
  • HDEBUT: Startzeit des Zeitfensters
  • MDEBUT: Startminute des Zeitfensters
  • HFIN: Endzeit des Zeitfensters
  • MFIN: Minuten des Zeitfensterschlusses

Die zweite Zeile der Tabelle [CRENEAUX] (siehe [1] oben) gibt beispielsweise an, dass der Termin Nr. 2 um 8:20 Uhr beginnt und um 8:40 Uhr endet und der Ärztin Nr. 1 (Frau Marie PELISSIER).

3.3.4. Die Tabelle [RV]

Sie listet die für jeden Arzt vergebenen RV auf:

  • ID: Nummer, die den RV eindeutig identifiziert – Primärschlüssel
  • JOUR: Tag des RV
  • ID_CRENEAU: Zeitfenster des RV – Fremdschlüssel auf das Feld [ID] der Tabelle [CRENEAUX] – legt sowohl das Zeitfenster als auch den betreffenden Arzt fest.
  • ID_CLIENT: Kundennummer, für die die Reservierung vorgenommen wird – Fremdschlüssel auf das Feld [ID] der Tabelle [CLIENTS]

Diese Tabelle verfügt über eine Eindeutigkeits für die Werte der verknüpften Spalten (JOUR, ID_CRENEAU):

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

Wenn eine Zeile der Tabelle [RV] den Wert (JOUR1, ID_CRENEAU1) für die Spalten (JOUR, ID_CRENEAU) aufweist, darf dieser Wert an keiner anderen Stelle vorkommen. Andernfalls würde dies bedeuten, dass zwei RV gleichzeitig für denselben Arzt erfasst wurden. Aus Sicht der Java-Programmierung löst der Treiber JDBC der Datenbank in diesem Fall einen SQLException aus.

Die Zeile mit dem Wert 3 für id (siehe [1] oben) bedeutet, dass am 23.08.2006 ein RV für den Terminblock Nr. 20 und den Kunden Nr. 4 gebucht wurde. Aus der Tabelle [CRENEAUX] geht hervor, dass der Termin Nr. 20 dem Zeitfenster 16:20 – 16:40 Uhr entspricht und der Ärztin Nr. 1 (Frau Marie PELISSIER) zugeordnet ist. Aus der Tabelle [CLIENTS] geht hervor, dass es sich bei Kunde Nr. 4 um Frau Brigitte BISTROU handelt.

3.3.5. Erstellung der Datenbank

Um die Tabellen anzulegen und zu füllen, kann das Skript [dbrdvmedecins2.sql] verwendet werden, das auf der Beispielseite zu finden ist. Mit [WampServer] (siehe Abschnitt 1.3.3) kann wie folgt vorgegangen werden:

  • In [1] klickt man auf das Symbol von [WampServer] und wählt die Option [PhpMyAdmin] [2],
  • in [3]; im sich öffnenden Fenster wählt man den Link [Bases de données] aus,
  • zu [2], man erstellt eine Datenbank mit dem Namen [4] und der Kodierung [5],
  • in [7] wurde die Datenbank erstellt. Man klickt auf den entsprechenden Link,
  • in [8], importiert man eine Datei SQL,
  • die man im Dateisystem über die Schaltfläche [9] auswählt,
  • in [11] wählt man das Skript SQL aus und führt es in [12] aus,
  • in [13] wurden die vier Tabellen der Datenbank angelegt. Man folgt einem der Links,
  • in [14], den Inhalt der Tabelle.

Im weiteren Verlauf werden wir nicht mehr auf diese Datenbank zurückkommen. Der Leser ist jedoch eingeladen, deren Entwicklung im Laufe der Programme zu verfolgen, insbesondere wenn es nicht funktioniert.

3.4. Die Schichten [DAO] und [JPA]

Kommen wir zurück zu der Architektur, die wir aufbauen müssen:

Wir werden vier Maven-Projekte erstellen:

  • ein Projekt für die Schichten [DAO] und [JPA],
  • ein Projekt für die Schicht [métier],
  • ein Projekt für die Schicht [web],
  • ein Unternehmensprojekt, das die drei vorangegangenen Projekte zusammenfasst.

Wir erstellen nun das Maven-Projekt für die Schichten [DAO] und [JPA].

Hinweis: Das Verständnis der Schichten [métier], [DAO] und [JPA] setzt Kenntnisse in Java EE voraus. Zu diesem Zweck kann man [ref7] lesen (siehe Absatz 3).

3.4.1. Das NetBeans-Projekt

Es handelt sich um Folgendes:

  • In [1] erstellt man ein Maven-Projekt vom Typ [EJB Module] [2],
  • in [3], man gibt dem Projekt einen Namen,
  • In [4] wählen wir als Server den Glassfish-Server aus,
  • in [5] das generierte Projekt.

3.4.2. Generierung der Schicht [JPA]

Kommen wir zurück zu der Architektur, die wir aufbauen müssen:

Mit NetBeans ist es möglich, die Schicht [JPA] und die Schicht [EJB], die den Zugriff auf die generierten Entitäten JPA steuert, automatisch zu generieren. Es ist interessant, diese Methoden der automatischen Generierung zu kennen, da der generierte Code wertvolle Hinweise darauf gibt, wie JPA-Entitäten oder der Code EJB, der diese verwendet, geschrieben werden sollten.

Im Folgenden beschreiben wir einige dieser Werkzeuge zur automatischen Generierung. Um den generierten Code zu verstehen, sind fundierte Kenntnisse über die Entitäten JPA, [ref8] sowie EJB und [ref7] erforderlich (siehe Abschnitt 3).

3.4.2.1. Einrichten einer NetBeans-Verbindung zur Datenbank

  • Starten Sie die Transaktionen SGBD und MySQL 5, damit die Transaktion BD verfügbar ist,
  • eine NetBeans-Verbindung zur Datenbank [dbrdvmedecins2] erstellen,
  • auf der Registerkarte „[Services] [1]“, im Zweig „[Databases] [2]“ den Treiber „JDBC MySQL [3]“ auswählen,
  • und wählen Sie anschließend die Option [4] „Connect Using“ aus, um eine Verbindung zu einer Datenbank MySQL herzustellen,
  • in [5] die erforderlichen Informationen eingeben. In [6] den Namen der Datenbank, in [7] den Datenbankbenutzer und dessen Passwort,
  • in [8] können die eingegebenen Daten überprüft werden,
  • in [9] die erwartete Meldung, wenn diese korrekt sind,
  • in [10] wird die Verbindung hergestellt. Dort sind die vier Tabellen der verbundenen Datenbank zu sehen.

3.4.2.2. Erstellung einer Persistenz-Einheit

Kehren wir zur Architektur zurück, die gerade aufgebaut wird:

Wir sind gerade dabei, die Schicht [JPA] aufzubauen. Die Konfiguration dieser Schicht erfolgt in einer Datei namens [persistence.xml], in der Persistenz-Einheiten definiert werden. Jede dieser Einheiten benötigt die folgenden Informationen:

  • die Zugangsdaten zur Datenbank (JDBC, URL, Benutzername, Passwort),
  • die Klassen, die als Abbilder der Datenbanktabellen dienen,
  • die verwendete Implementierung JPA. Tatsächlich handelt es sich bei JPA um eine Spezifikation, die von verschiedenen Produkten implementiert wird. Hier verwenden wir EclipseLink, die Standardimplementierung des GlassFish-Servers. Dadurch müssen wir GlassFish keine Bibliotheken einer anderen Implementierung hinzufügen.

NetBeans kann diese Persistenzdatei mithilfe eines Assistenten generieren.

  • Rechtsklick auf das Projekt und Auswahl der Option zum Anlegen einer Persistenz-Einheit [1],
  • in „[2]“, der neu angelegten Persistenz-Einheit einen Namen geben,
  • in [3] die Implementierung JPA EclipseLink (JPA 2.0) auswählen,
  • in [4] angeben, dass die Datenbanktransaktionen vom Container EJB des Glassfish-Servers verwaltet werden,
  • in [5] angeben, dass die Tabellen von BD bereits angelegt sind und daher nicht neu angelegt werden,
  • in [6]: Erstellen Sie eine neue Datenquelle für den Glassfish-Server,
  • in [7] den Namen JNDI (Java Naming Directory Interface) vergeben,
  • in [8], diesen Namen mit der im vorherigen Schritt erstellten Verbindung MySQL verknüpfen,
  • in [9], den Assistenten abschließen,
  • in [10], das neue Projekt,
  • in [11] wurde die Datei [persistence.xml] im Ordner [META-INF] generiert,
  • in [12] wurde ein Ordner [setup] erstellt,
  • in [13] wurden dem Maven-Projekt neue Abhängigkeiten hinzugefügt.

Die generierte Datei [META-INF/persistence.xml] lautet wie folgt:


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

Sie enthält die im Assistenten angegebenen Informationen:

  • Zeile 3: den Namen der Persistenz-Einheit,
  • Zeile 3: die Art der Transaktionen mit der Datenbank, hier JTA-Transaktionen (Java Transaction API), die vom Container EJB3 des Glassfish-Servers verwaltet werden,
  • Zeile 4: den Namen JNDI der Datenquelle.

Normalerweise findet man in dieser Datei den Typ der verwendeten Implementierung JPA. Im Assistenten haben wir EclipseLink angegeben. Da es sich hierbei um die vom GlassFish-Server standardmäßig verwendete Implementierung JPA handelt, wird sie in der Datei [persistence.xml] nicht erwähnt.

Auf der Registerkarte „[Design]“ erhält man einen Überblick über die Datei „[persistence.xml]“:

Um Protokolle von EclipseLink zu erhalten, verwenden wir die folgende Datei [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>
  • Zeile 4: Hier wird angegeben, dass die Implementierung JPA von EclipseLink verwendet wird,
  • Zeilen 7–9: enthalten die Konfigurationseigenschaften des Providers JPA, hier EclipseLink,
  • Zeile 8: Diese Eigenschaft ermöglicht die Protokollierung der Befehle SQL, die von EclipseLink ausgegeben werden.

Die erstellte Datei [glassfish-resources.xml] sieht wie folgt aus:


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

Diese Datei enthält die Informationen, die wir in den beiden zuvor verwendeten Assistenten eingegeben haben:

  • Zeilen 5–11: die Merkmale JDBC der Datenbank MySQL5 [dbrdvmedecins2],
  • Zeile 13: der Name JNDI der Datenquelle.

Diese Datei wird verwendet, um die Datenquelle JNDI [jdbc/dbrdvmedecins2] auf dem Glassfish-Server anzulegen. Dies ist eine rein serverseitige Angelegenheit. Bei einem anderen Server müsste man anders vorgehen, in der Regel über ein Verwaltungstool. Ein solches Tool gibt es auch für Glassfish.

Schließlich wurden dem Projekt Abhängigkeiten hinzugefügt. Die Datei [pom.xml] lautet wie folgt:


<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>
  • Zeilen 32–37: Eine Schicht [JPA] benötigt das Artefakt [javaee-api],
  • Zeilen 16, 22, 28: Die Artefakte, die von der hier verwendeten Implementierung JPA / EclipseLink benötigt werden.
  • Zeilen 18, 24, 30, 36: Alle Artefakte haben das Attribut provided. Zur Erinnerung: Das bedeutet, dass sie für die Kompilierung, nicht jedoch für die Ausführung erforderlich sind. Tatsächlich werden sie bei der Ausführung (provided) vom Glassfish-Server bereitgestellt,
  • Zeilen 41–48: Definieren ein neues Maven-Artefakt-Repository, in dem die Artefakte EclipseLink zu finden sind.

3.4.2.3. Generierung der Entitäten JPA

Die Entitäten JPA können über einen NetBeans-Assistenten generiert werden:

  • in [1] werden JPA-Entitäten aus einer Datenbank erstellt,
  • in [2] wählt man die zuvor erstellte Datenquelle [jdbc / dbrdvmedecins2] aus,
  • in [3] die Liste der Tabellen dieser Datenquelle,
  • in [4] werden alle Tabellen ausgewählt,
  • in [5] die ausgewählten Tabellen,
  • in [6] werden die den vier Tabellen zugeordneten Java-Klassen benannt,
  • sowie einen Paketnamen: [7],
  • in [8], JPA fasst Tabellenzeilen aus BD in Sammlungen zusammen. Wir wählen die Liste als Sammlung,
  • in [9], die vom Assistenten erstellten Java-Klassen.

3.4.2.4. Die generierten Entitäten JPA

Die Entität [Medecin] ist das Abbild der Tabelle [medecins]. Die Java-Klasse ist mit Annotationen übersät, die den Code auf den ersten Blick schwer lesbar machen. Behält man nur das bei, was für das Verständnis der Rolle der Entität wesentlich ist, erhält man den folgenden 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;

// Konstruktoren
....

   // Getter und Setter
....

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

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

  @Override
  public String toString() {
    ...
  }
  
}
  • In Zeile 4 macht die Annotation @Entity die Klasse [Medecin] zu einer Entität JPA, c.a.d. eine Klasse, die über die Entitäten BD, API und JPA mit der Tabelle c.a.d verknüpft ist,
  • Zeile 5: Der Name der Tabelle BD, die der Entität JPA zugeordnet ist. Jedes Feld der Tabelle entspricht einem Feld in der Java-Klasse,
  • Zeile 6: Die Klasse implementiert die Schnittstelle `Serializable`. Dies ist in Client-Server-Anwendungen erforderlich, in denen Entitäten zwischen Client und Server serialisiert werden.
  • Zeilen 10–11: Das Feld „id“ der Klasse [Medecin] entspricht dem Feld [ID] (Zeile 10) der Tabelle [medecins],
  • Zeilen 13–14: Das Feld „titel“ der Klasse [Medecin] entspricht dem Feld [TITRE] (Zeile 13) der Tabelle [medecins],
  • Zeilen 16–17: Das Feld „name“ der Klasse [Medecin] entspricht dem Feld [NOM] (Zeile 16) der Tabelle [medecins],
  • Zeilen 19–20: Das Feld „version“ der Klasse [Medecin] entspricht dem Feld [VERSION] (Zeile 19) der Tabelle [medecins]. Hier erkennt der Assistent nicht, dass es sich bei der Spalte tatsächlich um eine Versionsspalte handelt, die bei jeder Änderung der Zeile, zu der sie gehört, erhöht werden muss. Um ihr diese Rolle zuzuweisen, muss die Anmerkung @Version hinzugefügt werden. Dies werden wir in einem späteren Schritt tun,
  • Zeilen 22–23: Das Feld „prenom“ der Klasse [Medecin] entspricht dem Feld [PRENOM] der Tabelle [medecins],
  • Zeilen 10–11: Das Feld „id“ entspricht dem Primärschlüssel „[ID]“ der Tabelle. Die Annotationen in den Zeilen 8–9 verdeutlichen diesen Punkt,
  • Zeile 8: Die Anmerkung @Id gibt an, dass das mit dieser Anmerkung versehene Feld dem Primärschlüssel der Tabelle zugeordnet ist,
  • Zeile 9: Die Schicht [JPA] generiert den Primärschlüssel für die Zeilen, die sie in die Tabelle [Medecins] einfügt. Es gibt mehrere mögliche Strategien. Hier gibt die Strategie GenerationType.IDENTITY an, dass die Schicht JPA den Modus auto_increment der Tabelle MySQL verwenden wird,
  • Zeilen 25–26: Die Tabelle [creneaux] verfügt über einen Fremdschlüssel auf die Tabelle [medecins]. Ein Termin gehört zu einem Arzt. Umgekehrt sind einem Arzt mehrere Termine zugeordnet. Es handelt sich also um eine 1:n-Beziehung (ein Arzt zu mehreren Zeitfenstern), die durch die Annotation @OneToMany durch JPA (Zeile 25) qualifiziert wird. Das Feld in Zeile 26 enthält alle Termine des Arztes. Dies geschieht ohne Programmierung. Um Zeile 25 vollständig zu verstehen, müssen wir die Klasse [Creneau] vorstellen.

Diese lautet wie folgt:


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;

// Konstruktoren
...
// Getter und Setter
...

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

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

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

Wir gehen nur auf die neuen Anmerkungen ein:

  • Wir haben bereits erwähnt, dass die Tabelle [creneaux] einen Fremdschlüssel zur Tabelle [medecins] besitzt: Ein Termin ist einem Arzt zugeordnet. Einem Arzt können mehrere Termine zugeordnet sein. Es besteht eine Beziehung von der Tabelle [creneaux] zur Tabelle [medecins], die als „mehrere (Termine) zu einem (Arzt)“ definiert ist. Die Anmerkung @ManyToOne in Zeile 32 dient zur Qualifizierung des Fremdschlüssels,
  • die Zeile 31 mit der Anmerkung @JoinColumn präzisiert die Fremdschlüsselbeziehung: Die Spalte [ID_MEDECIN] der Tabelle [creneaux] ist ein Fremdschlüssel auf die Spalte [ID] der Tabelle [medecins],
  • Zeile 33: ein Verweis auf den Arzt, dem der Terminblock gehört. Auch dies lässt sich ohne Programmierung ermitteln.

Die Fremdschlüsselbeziehung zwischen der Entität [Creneau] und der Entität [Medecin] wird somit durch zwei Anmerkungen dargestellt:

  • in der Entität [Creneau]:

@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
private Medecin idMedecin;
  • in der Entität [Medecin]:

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

Beide Annotationen spiegeln dieselbe Beziehung wider: die des Fremdschlüssels von der Tabelle [creneaux] zur Tabelle [medecins]. Man sagt, dass sie zueinander invers sind. Nur die Beziehung @ManyToOne ist unverzichtbar. Sie kennzeichnet die Fremdschlüsselbeziehung eindeutig. Die Beziehung @OneToMany ist optional. Ist sie vorhanden, verweist sie lediglich auf die Beziehung @ManyToOne, mit der sie verknüpft ist. Dies ist die Bedeutung des Attributs mappedBy in Zeile 1 der Entität [Medecin]. Der Wert dieses Attributs ist der Name des Feldes der Entität [Creneau], das die Annotation @ManyToOne trägt, welche den Fremdschlüssel angibt. Ebenfalls in dieser Zeile 1 der Entität [Medecin] legt das Attribut cascade=CascadeType.ALL das Verhalten der Entität [Medecin] gegenüber der Entität [Creneau] fest:

  • Wenn eine neue Entität [Medecin] in die Datenbank eingefügt wird, müssen auch die Entitäten [Creneau] aus dem Feld der Zeile 2 eingefügt werden;
  • wenn ein Datensatz [Medecin] in der Datenbank geändert wird, müssen auch die Datensätze [Creneau] im Feld der Zeile 2 geändert werden,
  • wenn eine Entität [Medecin] aus der Datenbank gelöscht wird, müssen auch die Entitäten [Creneau] aus dem Feld in Zeile 2 gelöscht werden.

Den Code der beiden anderen Entitäten geben wir ohne besondere Anmerkungen an, da sie keine neuen Notationen einführen.

Die Entität [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;

// Konstruktoren
...
// Getter und Setter
...

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

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

  @Override
  public String toString() {
    ...
  }
  
}
  • Die Zeilen 24–25 spiegeln die Fremdschlüsselbeziehung zwischen der Tabelle [rv] und der Tabelle [clients] wider.

Die Entität [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;

   // Konstruktoren
...

   // Getter und Setter
...

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

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

  @Override
  public String toString() {
    ...
  }
  
}
  • Zeile 13 definiert das Feld „Tag“ vom Java-Typ Date. Es wird angegeben, dass in der Tabelle [rv] die Spalte [JOUR] (Zeile 12) vom Typ „Datum“ (ohne Uhrzeit) ist,
  • Zeilen 16–18: Definieren die Fremdschlüsselbeziehung der Tabelle [rv] zur Tabelle [creneaux],
  • Zeilen 20–22: definieren die Fremdschlüsselbeziehung der Tabelle [rv] zur Tabelle [clients].

Die automatische Generierung der Entitäten JPA liefert uns eine Arbeitsgrundlage. Manchmal reicht diese aus, manchmal nicht. Das ist hier der Fall:

  • muss die Annotation @Version zu den verschiedenen Versionsfeldern der Entitäten hinzugefügt werden,
  • es müssen Methoden toString geschrieben werden, die aussagekräftiger sind als die generierten,
  • die Entitäten [Medecin] und [Client] sind analog. Wir werden sie von einer Klasse [Personne] ableiten,
  • und die inversen Beziehungen @OneToMany der Beziehungen @ManyToOne entfernen. Sie sind nicht unbedingt erforderlich und führen zu Komplikationen bei der Programmierung;
  • wir entfernen die Validierung @NotNull für die Primärschlüssel. Wenn wir eine Entität JPA zusammen mit MySQL persistieren, hat die ursprüngliche Entität einen Primärschlüssel null. Erst nach der Speicherung in der Datenbank erhält der Primärschlüssel des gespeicherten Elements einen Wert.

Mit diesen Spezifikationen ergeben sich folgende Klassen:

Die Klasse „Person“ wird verwendet, um Ärzte und Kunden darzustellen:


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;
// Konstruktoren
...

// Getter und Setter
  ...

  @Override
  public String toString() {
    return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
  }
  
}
  • Zeile 8: Es ist zu beachten, dass die Klasse [Personne] selbst keine Entität (@Entity) ist. Sie wird die übergeordnete Klasse für Entitäten sein. Die Annotation @MappedSuperClass kennzeichnet diese Situation.

Die Entität [Client] kapselt die Zeilen der Tabelle [clients]. Sie leitet sich von der vorherigen Klasse [Personne] ab:


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;

// Konstruktoren
...

  @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());
  }
  
}
  • Zeile 6: Die Klasse [Client] ist eine JPA-Entität,
  • Zeile 7: Sie ist der Tabelle [clients] zugeordnet,
  • Zeile 8: Sie leitet sich von der Klasse [Personne] ab.

Die Entität [Medecin], die die Zeilen der Tabelle [medecins] kapselt, folgt demselben Muster:


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;

  // Konstruktoren
...

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

Die Entität [Creneau] kapselt die Zeilen der Tabelle [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;

  // Konstruktoren
  ...

  // Getter und Setter
  ...
 
  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    // TODO: Warnung – diese Methode funktioniert nicht, wenn die ID-Felder nicht gesetzt sind
    ...
  }

  @Override
  public String toString() {
    return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
  }
}
  • Die Zeilen 45–47 modellieren die „Viele-zu-Eins“-Beziehung, die zwischen der Tabelle [creneaux] und der Tabelle [medecins] der Datenbank besteht: Ein Arzt hat mehrere Termine, ein Termin gehört zu einem einzigen Arzt.

Die Entität [Rv] kapselt die Zeilen der Tabelle [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;

   // Konstruktoren
...

   // Getter und Setter
...

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

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

  @Override
  public String toString() {
    return String.format("Rv[%s, %s, %s]", id, creneau, client);
  }
}
  • Die Zeilen 29–31 modellieren die „Viele-zu-Eins“-Beziehung, die zwischen der Tabelle [rv] und der Tabelle [clients] (ein Kunde kann in mehreren Rv vorkommen) in der Datenbank, und die Zeilen 25–27 die „Viele-zu-Eins“-Beziehung, die zwischen der Tabelle [rv] und der Tabelle [creneaux] besteht (ein Termin kann in mehreren Rv vorkommen).

3.4.3. Die Ausnahmeklasse

Die Ausnahmeklasse [RdvMedecinsException] der Anwendung lautet wie folgt:


package rdvmedecins.exceptions;

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

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

  // private Felder
  private int code = 0;

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

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

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

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

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

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

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

  // Getter und Setter
  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • Zeile 7: Die Klasse leitet sich von der Klasse [RuntimeException] ab. Der Compiler erzwingt daher nicht, sie mit try/catch zu behandeln.
  • Zeile 6: Die Annotation @ApplicationException sorgt dafür, dass die Ausnahme nicht von einer Ausnahme vom Typ [EjbException] „abgefangen“ wird.

Um die Annotation @ApplicationException zu verstehen, kehren wir zur serverseitigen Architektur zurück:

Die Ausnahme vom Typ [RdvMedecinsException] wird von den Methoden des EJB der Schicht [DAO] innerhalb des Containers EJB3 ausgelöst und von diesem abgefangen. Ohne die Annotation @ApplicationException kapselt der Container EJB3 die aufgetretene Ausnahme in eine Ausnahme vom Typ [EjbException] ein und löst diese erneut aus. Möglicherweise möchte man diese Kapselung nicht und eine Ausnahme vom Typ [RdvMedecinsException] aus dem Container EJB3 herausgeben lassen. Dies ermöglicht die Annotation @ApplicationException. Darüber hinaus weist das Attribut (rollback=true) dieser Annotation den Container EJB3 an, dass, falls die Ausnahme vom Typ [RdvMedecinsException] innerhalb einer Methode auftritt, die im Rahmen einer Transaktion mit einem SGBD ausgeführt wird, diese rückgängig gemacht werden muss. Technisch gesehen wird dies als „rollback“ der Transaktion bezeichnet.

3.4.4. Die EJB der Schicht [DAO]

Die Java-Schnittstelle [IDao] der Schicht [DAO] lautet wie folgt:


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 {

  // Kundenliste
  public List<Client> getAllClients();
  // Liste der Ärzte
  public List<Medecin> getAllMedecins();
  // Liste der Terminfenster eines Arztes
  public List<Creneau> getAllCreneaux(Medecin medecin);
  // Liste der Termine eines Arztes an einem bestimmten Tag
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
  // einen Kunden anhand seiner ID suchen
  public Client getClientById(Long id);
  // einen Kunden anhand seiner ID suchen
  public Medecin getMedecinById(Long id);
  // einen Termin anhand seiner ID suchen
  public Rv getRvById(Long id);
  // einen Terminblock anhand seiner ID suchen
  public Creneau getCreneauById(Long id);
  // einen RV hinzufügen
  public Rv ajouterRv(Date jour, Creneau creneau, Client client);
  // einen Termin unter der ID RV löschen
  public void supprimerRv(Rv rv);
}

Diese Schnittstelle wurde nach der Ermittlung der Anforderungen der Schicht [web] erstellt:

  • Zeile 14: die Kundenliste. Diese benötigen wir, um die Dropdown-Liste der Kunden zu füllen,
  • Zeile 16: die Liste der Ärzte. Diese benötigen wir, um die Dropdown-Liste der Ärzte zu füllen,
  • Zeile 18: die Liste der Terminfenster eines Arztes. Diese wird benötigt, um den Terminkalender des Arztes für einen bestimmten Tag anzuzeigen,
  • Zeile 20: Die Liste der Termine eines Arztes für einen bestimmten Tag. In Kombination mit der vorherigen Methode können wir damit den Terminkalender des Arztes für einen bestimmten Tag mit den bereits reservierten Zeitfenstern anzeigen.
  • Zeile 22: Ermöglicht es, einen Kunden anhand seiner Nummer zu finden. Mit dieser Methode können wir einen Kunden anhand einer Auswahl aus der Dropdown-Liste der Kunden finden,
  • Zeile 24: dasselbe gilt für Ärzte,
  • Zeile 26: Sucht einen Termin anhand seiner Nummer. Kann beim Löschen eines Termins verwendet werden, um zuvor zu überprüfen, ob er tatsächlich existiert,
  • Zeile 28: Sucht einen Terminblock anhand seiner Nummer. Ermöglicht es, den Terminblock zu identifizieren, den ein Benutzer hinzufügen oder löschen möchte,
  • Zeile 30: zum Hinzufügen eines Termins,
  • Zeile 32: zum Löschen eines Termins.

Die lokale Schnittstelle [IDaoLocal] von EJB leitet lediglich die vorherige Schnittstelle [IDao] ab:


package rdvmedecins.dao;

import javax.ejb.Local;

@Local
public interface IDaoLocal extends IDao{

}

Das Gleiche gilt für die Remote-Schnittstelle [IDaoRemote]:


package rdvmedecins.dao;

import javax.ejb.Remote;

@Remote
public interface IDaoRemote extends IDao{

}

EJB und [DaoJpa] implementieren beide Schnittstellen, die lokale und die Remote-Schnittstelle:


package rdvmedecins.dao;

...

@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
  • Zeile 5 gibt an, dass die Remote-Instanz von EJB den Namen „rdvmedecins.dao“ trägt. Außerdem sorgt die Annotation @Singleton (Java EE6) dafür, dass nur eine Instanz von EJB erstellt wird. Die Annotation @Stateless (Java EE5) definiert ein EJB, das in mehreren Instanzen erstellt werden kann, um einen Pool von EJB zu versorgen,
  • Zeile 6 gibt an, dass alle Methoden des EJB innerhalb einer vom Container EJB3 verwalteten Transaktion ausgeführt werden,
  • Zeile 7 zeigt, dass EJB die lokalen und entfernten Schnittstellen implementiert und zudem serialisierbar ist.

Der vollständige Code von EJB lautet wie folgt:


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;

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

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

  // Liste der Terminfenster eines bestimmten Arztes
  // Arzt: der Arzt
  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);
    }
  }

  // Liste der Termine eines bestimmten Arztes an einem bestimmten Tag
  // Arzt: der Arzt
  // Tag: der Tag
  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);
    }
  }

  // Hinzufügen eines Termins
  // Tag: Tag des Termins
  // Zeitfenster: Zeitfenster des Termins
  // Kunde: Kunde, für den der Termin vereinbart wurde
  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);
    }
  }

  // Löschen eines Termins
  // Termin: der gelöschte Termin
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }

  // einen bestimmten Kunden abrufen
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // einen bestimmten Arzt abrufen
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // einen bestimmten Termin abrufen
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // einen bestimmten Terminblock abrufen
  public Creneau getCreneauById(Long id) {
    try {
      return (Creneau) em.find(Creneau.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
}
  • Zeile 22: Das Objekt EntityManager, das den Zugriff auf den Persistenzkontext verwaltet. Bei der Instanziierung der Klasse wird dieses Feld vom Container EJB mithilfe der Annotation @PersistenceContext in Zeile 21 initialisiert,
  • Zeile 27: Abfrage JPQL (Java Persistence Query Language), die alle Zeilen der Tabelle [clients] in Form einer Liste von Objekten [Client] zurückgibt,
  • Zeile 36: eine ähnliche Abfrage für Ärzte,
  • Zeile 46: Eine Abfrage JPQL, die eine Verknüpfung zwischen den Tabellen [creneaux] und [medecins] durchführt. Sie wird über die ID des Arztes parametrisiert,
  • Zeile 57: Eine Abfrage JPQL, die eine Verknüpfung zwischen den Tabellen [rv], [creneaux] und [medecins] herstellt und zwei Parameter hat: die ID des Arztes und den Tag des Termins,
  • Zeilen 69–73: Erstellung eines Termins und anschließende Speicherung in der Datenbank,
  • Zeile 83: Löschen eines Termins aus der Datenbank,
  • Zeile 92: Führt eine Abfrage nach select in der Datenbank durch, um einen bestimmten Kunden zu finden,
  • Zeile 101: dasselbe für einen Arzt,
  • Zeile 110: dasselbe für einen Termin,
  • Zeile 119: dasselbe für einen Terminblock,
  • Alle Operationen mit dem Persistenzkontext „em“ aus Zeile 22 können zu einem Problem mit der Datenbank führen. Daher sind sie alle von einem „try/catch“-Block umgeben. Die mögliche Ausnahme wird in die „hausinterne“ Ausnahme RdvMedecinsException gekapselt.

3.4.5. Implementierung des Treibers JDBC aus MySQL

In der folgenden Architektur:

EclipseLink benötigt den Treiber JDBC von MySQL. Dieser muss in den Bibliotheken des Glassfish-Servers im Ordner <glassfish>/domains/domain1/lib/ext installiert werden, wobei <glassfish> der Installationsordner des Glassfish-Servers ist. Man kann ihn wie folgt beziehen:

Der Ordner, in dem der Treiber JDBC von MySQL abgelegt werden muss, lautet <Domains-Ordner>[1]/domain1/lib/ext [2]. Dieser Treiber ist unter URL [http://www.mysql.fr/downloads/connector/j/] verfügbar. Nach der Installation muss der GlassFish-Server neu gestartet werden, damit er diese neue Bibliothek berücksichtigt.

3.4.6. Bereitstellung der EJB-Schicht der [DAO]

Kehren wir nun zu der bisher aufgebauten Architektur zurück:

Das Paket [web, métier, DAO, JPA] muss auf dem GlassFish-Server bereitgestellt werden. Das führen wir nun durch:

  • in [1] erstellen wir das Maven-Projekt,
  • in [2] führen wir es aus,
  • in [3] wurde es auf dem Glassfish-Server bereitgestellt (Registerkarte [Services])

Vielleicht möchte man sich die GlassFish-Protokolle ansehen:

Bei [1] sind die Glassfish-Protokolle auf der Registerkarte [Output / Glassfish Server 3+] verfügbar. Es handelt sich um folgende:

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.

Die mit [Config] und [Précis] gekennzeichneten Zeilen sind die Protokolle von EclipseLink, die mit [Infos] gekennzeichneten stammen von Glassfish.

  • Zeilen 1–12: EclipseLink verarbeitet die von ihm erkannten Entitäten JPA,
  • Zeilen 13–17: Informationen, die zeigen, dass die Verarbeitung der Entitäten JPA normal verlaufen ist,
  • Zeile 18: EclipseLink wird gemeldet,
  • Zeile 19: EclipseLink erkennt, dass es sich um SGBD MySQL handelt,
  • Zeilen 20–24: EclipseLink versucht, eine Verbindung zu BD herzustellen,
  • Zeilen 25–28: Dies ist ihm gelungen,
  • Zeilen 29–33: Er versucht, die Verbindung erneut herzustellen, diesmal unter Verwendung einer bestimmten Plattform MySQL (Zeile 30),
  • Zeilen 34–37: Auch hier erfolgreich,
  • Zeile 38: Bestätigung, dass die Persistenz-Einheit [dbrdvmedecins-PU] instanziiert werden konnte,
  • Zeile 39: Die portablen Namen der Remote- und lokalen Schnittstellen von EJB und [DaoJpa], wobei „portabel“ bedeutet, dass sie von allen Java-Anwendungsservern EE 6 erkannt werden,
  • Zeile 40: die Namen der Remote- und lokalen Schnittstellen von EJB und [DaoJpa], die spezifisch für GlassFish sind. Im folgenden Test werden wir den Namen „rdvmedecins.dao“ verwenden.

Die Zeilen 39 und 40 sind wichtig. Wenn man den Client für ein EJB auf Glassfish schreibt, muss man diese kennen.

3.4.7. Tests des EJB der Schicht [DAO]

Nachdem nun die EJB-Schicht der [DAO]-Schicht unserer Anwendung bereitgestellt wurde, können wir sie testen. Wir werden dies im Rahmen einer Client-Server-Anwendung tun:

Der Client wird die Remote-Schnittstelle des auf dem Glassfish-Server bereitgestellten EJB [DAO] testen.

Zunächst erstellen wir ein neues Maven-Projekt „ “:

  • In [1] erstellen wir ein neues Projekt,
  • unter [2,3] erstellen wir ein Maven-Projekt vom Typ [Java Application],
  • in [4] geben wir ihm einen Namen und legen es im selben Ordner wie das EJB und das [DAO] ab,
  • in [5], das generierte Projekt,
  • in [6] wurde eine Klasse [App.java] generiert. Diese werden wir löschen,
  • in [7] wurde ein Zweig [Source Packages] generiert. Auf diesen sind wir bisher noch nicht gestoßen. In diesen Zweig können wir Tests JUnit einfügen. Das werden wir tun. Die generierte Testklasse [AppTest] werden wir nicht beibehalten,
  • in [8], den Maven-Projektabhängigkeiten. Der Zweig [Dependencies] ist leer. Wir werden dort neue Abhängigkeiten hinzufügen müssen. Der Zweig [Test Dependencies] enthält die für die Tests erforderlichen Abhängigkeiten. Hier wird die Bibliothek des Frameworks JUnit 3.8 verwendet. Wir werden sie ändern müssen.

Das Projekt entwickelt sich wie folgt:

  • zu [1], dem Projekt, in dem die beiden generierten Klassen sowie die Abhängigkeit JUnit entfernt wurden.

Kommen wir zurück zur Client-Server-Architektur, die für den Test verwendet wird:

Der Client muss die von EJB und [DAO] bereitgestellte Remote-Schnittstelle kennen. Außerdem wird er mit EJB Entitäten vom Typ JPA austauschen. Daher benötigt er deren Definition. Damit das Testprojekt des EJB Zugriff auf diese Informationen hat, fügen wir das Projekt des EJB und des [DAO] als Abhängigkeit zum Projekt hinzu:

  • In [1] fügen wir eine Abhängigkeit zum Zweig [Test Dependencies] hinzu,
  • in [2] wählt man die Registerkarte [Open Projects],
  • in [3] wählt man das Maven-Projekt von EJB und [DAO] aus,
  • in [4] die hinzugefügte Abhängigkeit.

Kehren wir zur Client-Server-Architektur des Tests zurück:

Bei der Ausführung kommunizieren Client und Server über das Netzwerk TCP-IP. Wir werden diesen Datenaustausch nicht programmieren. Für jeden Anwendungsserver gibt es eine Bibliothek, die in die Abhängigkeiten des Clients integriert werden muss. Diejenige für Glassfish heißt [gf-client]. Wir fügen sie hinzu:

  • In [1] fügen wir eine Abhängigkeit hinzu,
  • bei [2] geben wir die Eigenschaften des gewünschten Artefakts an,
  • in [3] werden zahlreiche Abhängigkeiten hinzugefügt. Maven lädt diese herunter. Dies kann mehrere Minuten dauern. Anschließend werden sie im lokalen Maven-Repository gespeichert.

Nun können wir den Test JUnit erstellen:

  • in [2]: Wir klicken mit der rechten Maustaste auf [Test Packages], um einen neuen Test JUnit zu erstellen,
  • in [3], man gibt der Testklasse einen Namen sowie ein Paket für diese an: [4],
  • in [5], das Framework JUnit und 4.x wird ausgewählt,
  • in [6] die generierte Testklasse,
  • in [7], die neuen Abhängigkeiten des Maven-Projekts.

Die Datei [pom.xml] sieht dann wie folgt aus:


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

Zu beachten:

  • Zeilen 32–51: die Projektabhängigkeiten,
  • Zeilen 13–26: Es wurden zwei Maven-Repositorys definiert, eines für EclipseLink (Zeilen 14–19), das andere für JUnit4 (Zeilen 20–25).

Die Testklasse lautet wie folgt:


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 {

  // Schicht [dao] getestet
  private static IDaoRemote dao;
  // heutiges Datum
  Date jour = new Date();

  @BeforeClass
  public static void init() throws NamingException {
    // Initialisierung der Umgebung JNDI
    InitialContext initialContext = new InitialContext();
    // Instanziierung der DAO-Schicht
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }

  @Test
  public void test1() {
    // Anzeige der Kunden
    List<Client> clients =dao.getAllClients();
    display("Liste des clients :", clients);
    // Anzeige der Ärzte
    List<Medecin> medecins =dao.getAllMedecins();
    display("Liste des médecins :", medecins);
    // Anzeige der Termine eines Arztes
    Medecin medecin = medecins.get(0);
    List<Creneau> creneaux = dao.getAllCreneaux(medecin);
    display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
    // Liste der Termine eines Arztes an einem bestimmten Tag
    display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // einen Termin (RV) hinzufügen
    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));
    // einen Termin RV im selben Terminfenster desselben Tages hinzufügen
    // sollte eine Ausnahme auslösen
    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();
      }
      // Der Fehler wird vermerkt
      erreur=true;
    }
    // Es wird überprüft, ob ein Fehler aufgetreten ist
    Assert.assertTrue(erreur);
    // Liste der RV
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // ein RV löschen
    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));
  }

  // Hilfsfunktion – zeigt die Elemente einer Sammlung an
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • Zeilen 23–29: Die mit @BeforeClass markierte Methode wird vor allen anderen ausgeführt. Hier wird eine Referenz auf die Remote-Schnittstelle von EJB [DaoJpa] erstellt. Wir erinnern uns, dass wir ihr den Namen JNDI „rdvmedecins.dao“ gegeben hatten,
  • Zeilen 34–35: zeigen die Kundenliste an,
  • Zeilen 37–38: zeigen die Liste der Ärzte an,
  • Zeilen 40–42: zeigen die Zeitfenster des ersten Arztes an,
  • Zeile 44: Zeigt die Termine des ersten Arztes für den Tag aus Zeile 21 an,
  • Zeilen 46–51: fügen dem ersten Arzt einen Termin für sein Zeitfenster Nr. 2 und den Tag aus Zeile 21 hinzu,
  • Zeile 52: Zeigt zur Überprüfung die Termine des ersten Arztes für den Tag aus Zeile 21 an. Es muss mindestens einen geben, nämlich den gerade hinzugefügten,
  • Zeilen 55–70: Wir fügen denselben Termin erneut hinzu. Da die Tabelle [RV] eine Eindeutigkeitsbeschränkung hat, muss dieser Eintrag eine Ausnahme auslösen. Dies überprüfen wir in Zeile 70,
  • Zeile 72: Zur Überprüfung werden die Termine des ersten Arztes für den Tag in Zeile 21 angezeigt. Der Termin, den wir hinzufügen wollten, darf dort nicht vorhanden sein,
  • Zeilen 74–76: Der einzige Termin, der hinzugefügt wurde, wird gelöscht,
  • Zeile 77: Zur Überprüfung werden die Termine des ersten Arztes für den Tag aus Zeile 21 angezeigt. Der soeben gelöschte Termin darf dort nicht mehr erscheinen.

Dieser Test ist ein Scheintest JUnit. Er enthält nur eine Assertion (Zeile 70). Es handelt sich um einen visuellen Test mit den damit verbundenen Mängeln.

Wenn alles in Ordnung ist, sollten die Tests erfolgreich sein:

  • in [1], man erstellt das Testprojekt,
  • in [2] wird der Test ausgeführt,
  • in [3] wurde der Test erfolgreich bestanden.

Schauen wir uns die Testausgaben genauer an:

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]

Der Leser wird gebeten, diese Protokolle zusammen mit dem Code zu lesen, der sie erzeugt hat. Wir wollen uns die Ausnahme genauer ansehen, die beim Hinzufügen eines bereits vorhandenen Termins aufgetreten ist (Zeilen 41–49). Der Ausnahmestapel ist in den Zeilen 42–48 wiedergegeben. Er ist unerwartet. Kehren wir zum Code der Methode zum Hinzufügen eines Termins zurück:


  // Hinzufügen eines Termins
  // Tag: Tag des Termins
  // Zeitfenster: Zeitfenster des Termins
  // Kunde: Kunde, für den der Termin vereinbart wurde
  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);
    }
}

Sehen wir uns die GlassFish-Protokolle beim Hinzufügen der beiden Termine an:

...
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
...
  • Zeile 2: vor dem ersten persist,
  • Zeile 3: nach dem ersten „persist“,
  • Zeile 4: der Befehl INSERT, der ausgeführt wird. Es ist zu beachten, dass dies nicht gleichzeitig mit der Operation persist geschieht. Wäre dies der Fall, wäre dieser Log-Eintrag vor Zeile 2 erschienen. Die Operation INSERT findet dann normalerweise am Ende der Transaktion statt, in der die Methode ausgeführt wird,
  • Zeile 6: EclipseLink fragt bei MySQL nach dem zuletzt verwendeten Primärschlüssel. Es erhält den Primärschlüssel des hinzugefügten Termins. Dieser Wert wird in das Feld „id“ der persistierten Entität [Rv] geschrieben,
  • Zeilen 7–8: Die Abfrage SELECT, die die Termine des Arztes anzeigt,
  • Zeilen 9–10: die Bildschirmanzeigen der zweiten Abfrage persist,
  • Zeilen 11–12: der Befehl INSERT, der ausgeführt wird. Er muss eine Ausnahme auslösen. Diese erscheint in den Zeilen 15–16 und ist eindeutig. Sie wird ursprünglich vom Treiber JDBC von MySQL wegen Verstoßes gegen die Eindeutigkeitsbedingung der Termine ausgelöst. Daraus lässt sich ableiten, dass diese Ausnahmen in den Protokollen des Tests JUnit zu sehen sein müssten. Dies ist jedoch nicht der Fall:
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

Zur Erinnerung: Die Client-Server-Architektur des Tests sieht wie folgt aus:

Wenn der EJB [DAO] eine Ausnahme auslöst, muss diese serialisiert werden, um den Client zu erreichen. Wahrscheinlich ist genau dieser Vorgang aus einem mir unbekannten Grund fehlgeschlagen. Da unsere vollständige Anwendung nicht im Client-Server-Modus laufen wird, können wir dieses Problem ignorieren.

Da nun das EJB der Schicht [DAO] betriebsbereit ist, können wir zum EJB der Schicht [métier] übergehen.

3.5. Die Schicht [métier]

Kehren wir zur Architektur der derzeit im Aufbau befindlichen Anwendung zurück:

Wir werden ein neues Maven-Projekt für die Schichten EJB und [métier] erstellen. Wie oben zu sehen ist, wird es eine Abhängigkeit zu dem Maven-Projekt haben, das für die Schichten [DAO] und [JPA] erstellt wurde.

3.5.1. Das NetBeans-Projekt

Wir erstellen ein neues Maven-Projekt vom Typ EJB. Dazu müssen Sie lediglich die bereits angewandte und auf Seite 174 beschriebene Vorgehensweise befolgen.

  • in [1], dem Maven-Projekt der Schicht [métier],
  • in [2], fügt man eine Abhängigkeit hinzu,
  • in [3] wählt man das Maven-Projekt der Schichten [DAO] und [JPA] aus,
  • in [4] wählt man den Umfang [provided] aus. Zur Erinnerung: Das bedeutet, dass dieser für die Kompilierung, nicht jedoch für die Ausführung des Projekts benötigt wird. Tatsächlich wird dasEJB der Schicht [métier] wird zusammen mit dem EJB der Schichten [DAO] und [JPA] auf dem GlassFish-Server bereitgestellt. Wenn es also ausgeführt wird, ist die Schicht EJB der Schichten [DAO] und [JPA] bereits vorhanden,
  • in [6], dem neuen Projekt mit seiner Abhängigkeit.

Sehen wir uns nun den Quellcode der Schicht [métier] an:

EJB [Metier] wird die folgende Schnittstelle [IMetier] haben:


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-Ebene
    // Kundenliste
    public List<Client> getAllClients();

    // Ärzteverzeichnis
    public List<Medecin> getAllMedecins();

    // Liste der Terminfenster eines Arztes
    public List<Creneau> getAllCreneaux(Medecin medecin);

    // Liste der Termine eines Arztes an einem bestimmten Tag
    public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);

    // einen Kunden anhand seiner ID suchen
    public Client getClientById(Long id);

    // einen Kunden anhand seiner ID suchen
    public Medecin getMedecinById(Long id);

    // einen Termin anhand seiner ID suchen
    public Rv getRvById(Long id);

    // Ein Zeitfenster anhand seiner ID suchen
    public Creneau getCreneauById(Long id);

    // einen RV hinzufügen
    public Rv ajouterRv(Date jour, Creneau creneau, Client client);

    // einen Termin unter der ID RV löschen
    public void supprimerRv(Rv rv);
    
    // Beruf
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);

}

Um diese Schnittstelle zu verstehen, muss man sich die Architektur des Projekts vor Augen führen:

Wir haben die Schnittstelle der Schicht [DAO] definiert (Abschnitt 3.4.4) und angegeben, dass diese den Anforderungen der Schicht [web], also den Benutzeranforderungen, entspricht. Die Schicht [web] kommuniziert mit der Schicht [DAO] ausschließlich über die Schicht [métier]. Dies erklärt, warum in der Schicht [métier] alle Methoden der Schicht [DAO] zu finden sind. Diese Methoden leiten die Anfrage der Schicht [web] lediglich an die Schicht [DAO] weiter. Mehr nicht.

Bei der Analyse der Anwendung zeigt sich ein Bedarf: Es soll möglich sein, auf einer Webseite den Terminkalender eines Arztes für einen bestimmten Tag anzuzeigen, um die belegten und freien Termine des Tages zu erkennen. Dies ist typischerweise der Fall, wenn die Sekretärin eine Anfrage am Telefon entgegennimmt. Man bittet sie um einen Termin für einen bestimmten Tag bei einem bestimmten Arzt. Um diesem Bedarf gerecht zu werden, bietet die Schicht [métier] die Methode in Zeile 46 an.


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

Man könnte sich fragen, wo diese Methode platziert werden soll:

  • Man könnte sie in der Schicht [DAO] platzieren. Diese Methode entspricht jedoch nicht wirklich einem Bedarf an Datenzugriff, sondern eher einem geschäftlichen Bedarf,
  • Man könnte sie in die Ebene [web] verschieben. Das wäre jedoch keine gute Idee. Denn wenn man die Ebene [web] in die Ebene [Swing] umbenennt, geht die Methode verloren, obwohl der Bedarf weiterhin besteht.

Die Methode erhält als Parameter den Arzt und den Tag, für den der Terminplan abgerufen werden soll. Sie gibt ein Objekt vom Typ [AgendaMedecinJour] zurück, das den Terminplan des Arztes für den jeweiligen Tag darstellt:


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;
    // Felder
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;

    // Hersteller
    public AgendaMedecinJour() {

    }

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

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

    // Getter und Setter
...
  
}
  • Zeile 12: der Arzt, zu dem der Terminkalender gehört,
  • Zeile 13: der Tag des Terminkalenders,
  • Zeile 14: die Terminfenster des Arztes für diesen Tag.
  • Die Klasse verfügt über Konstruktoren (Zeilen 17, 21) sowie eine angepasste Methode toString (Zeile 27).

Die Klasse [CreneauMedecinJour] (Zeile 14) lautet wie folgt:


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;
    // Felder
    private Creneau creneau;
    private Rv rv;

    // Konstruktoren
    public CreneauMedecinJour() {

    }

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

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

    // Getter und Setter

  ...
}
  • Zeile 12: ein Termin des Arztes,
  • Zeile 13: der zugehörige Termin, null, wenn der Termin frei ist.

So sehen wir, dass das Feld creneauxMedecinJour in Zeile 14 der Klasse [AgendaMedecinJour] es uns ermöglicht, alle Zeitfenster des Arztes mit der Angabe „belegt“ oder „frei“ für jedes einzelne davon abzurufen. Das war das Ziel der neuen Methode [getAgendaMedecinJour] der Schnittstelle [IMetier].

Unser EJB [Metier] wird über eine lokale und eine entfernte Schnittstelle verfügen, die lediglich von der Hauptschnittstelle [IMetier] abgeleitet sind:


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{

}

Das EJB EJB implementiert diese Schnittstellen wie folgt:


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-Schicht
  @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) {
    // Liste der Terminfenster des Arztes
    List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
    // Liste der Termine dieses Arztes für denselben Tag
    List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
    // Es wird ein Dictionary aus den gebuchten Terminen erstellt
    Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
    for (Rv resa : reservations) {
      hReservations.put(resa.getCreneau().getId(), resa);
    }
    // Der Terminkalender für den gewünschten Tag wird erstellt
    AgendaMedecinJour agenda = new AgendaMedecinJour();
    // der Arzt
    agenda.setMedecin(medecin);
    // der Tag
    agenda.setJour(jour);
    // die Buchungszeitfenster
    CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
    agenda.setCreneauxMedecinJour(creneauxMedecinJour);
    // Füllen der Terminfenster
    for (int i = 0; i < creneauxHoraires.size(); i++) {
      // Zeile i im Terminkalender
      creneauxMedecinJour[i] = new CreneauMedecinJour();
      // ID des Termins
      creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
      // Ist das Zeitfenster frei oder reserviert?
      if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
        // Der Termin ist belegt – die Reservierung wird vermerkt
        Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
        creneauxMedecinJour[i].setRv(resa);
      }
    }
    // Das Ergebnis wird zurückgegeben
    return agenda;
  }
}
  • Zeile 22: Die Klasse [Metier] ist ein Singleton der Klasse EJB,
  • Zeile 23: Jede Methode von EJB wird innerhalb einer Transaktion ausgeführt. Das bedeutet, dass die Transaktion zu Beginn der Methode in der Schicht [métier] startet. Diese ruft Methoden der Schicht [DAO] auf. Diese werden innerhalb derselben Transaktion ausgeführt,
  • Zeile 24: EJB implementiert seine lokalen und entfernten Schnittstellen und ist zudem serialisierbar,
  • Zeile 27: Eine Referenz auf die Klasse EJB aus der Schicht [DAO],
  • Zeile 29: Diese wird vom Container EJB des Glassfish-Servers mithilfe der Annotation @EJB injiziert. Wenn also die Methoden der Klasse [Metier] ausgeführt werden, ist die Referenz auf EJB der Schicht [DAO] bereits initialisiert,
  • Zeilen 33–81: Diese Referenz wird verwendet, um den Aufruf an die Schicht [métier] an die Schicht [DAO] zu delegieren,
  • Zeile 84: Die Methode getAgendaMedecinJour, mit der der Terminkalender eines Arztes für einen bestimmten Tag abgerufen werden kann. Wir überlassen es dem Leser, den Kommentaren zu folgen.

3.5.2. Bereitstellung der Schicht [métier]

Die Schicht [métier] ist von der Schicht [DAO] abhängig. Jede Schicht wurde mit einem EJB implementiert. Um die EJB und die [métier] zu testen, müssen wir beide EJB bereitstellen. Dazu benötigen wir ein Unternehmensprojekt.

  • [1]: Wir erstellen ein neues Projekt,
  • vom Typ „Maven“ ([2]) und „Unternehmensanwendung“ ([3]),
  • und geben ihm den Namen [4]. Das Suffix ear wird automatisch hinzugefügt,
  • in [5], man wählt den Glassfish-Server und Java EE 6,
  • zu [6]. Eine Unternehmensanwendung enthält Module, in der Regel EJB-Module und Webmodule. Hier wird die Unternehmensanwendung die Module der beiden von uns erstellten EJB enthalten. Da diese Module bereits vorhanden sind, aktivieren wir die Kontrollkästchen nicht,
  • In [7,8] wurden zwei Projekte angelegt. [8] ist das Unternehmensprojekt, das wir verwenden werden. [7] ist ein Projekt, dessen Zweck mir unbekannt ist. Ich musste es nicht verwenden und da ich mich nicht eingehend mit Maven beschäftigt habe, weiß ich nicht, wozu es dienen könnte. Deshalb werden wir es ignorieren.

Nachdem das Unternehmensprojekt nun angelegt ist, können wir seine Module definieren.

  • In [1] erstellen wir eine neue Abhängigkeit,
  • in [2] wählen wir das Projekt EJB [DAO] aus,
  • in [3] wird angegeben, dass es sich um ein EJB handelt. Der Typ darf nicht leer gelassen werden, da in diesem Fall der Typ jar verwendet wird und dieser Typ hier nicht geeignet ist,
  • bei [4] wird der Gültigkeitsbereich [compile] verwendet,
  • in [5] das Projekt mit seiner neuen Abhängigkeit,
  • bei [6, 7, 8] fangen wir von vorne an, um das EJB aus der Ebene [métier] hinzuzufügen,
  • zu [9], die beiden Abhängigkeiten
  • in [10], das Projekt wird erstellt,
  • in [11] wird es ausgeführt,
  • in [12], auf der Registerkarte [Services] sieht man, dass das Projekt auf dem Glassfish-Server bereitgestellt wurde. Das bedeutet, dass die beiden EJBs nun auf dem Server vorhanden sind.

In den Protokollen des Glassfish-Servers finden sich Informationen zur Bereitstellung der beiden EJB:

  • sowie [1] auf der Registerkarte „Protokolle“ von GlassFish.

Dort finden sich die folgenden Protokolle:

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]
  • Zeilen 1–5: Die Entitäten JPA wurden erkannt,
  • Zeile 7: zeigt an, dass die Erstellung der Persistenz-Einheit [dbrdvmedecins2-PU] erfolgreich war und die Verbindung zur zugehörigen Datenbank hergestellt wurde,
  • Zeile 8: Die portablen Namen der Remote- und lokalen Schnittstellen von EJB, [DaoJpa] und portable bedeuten, dass sie von allen Anwendungsservern erkannt werden,
  • Zeile 9: dasselbe, jedoch mit den GlassFish-eigenen Namen,
  • Zeilen 10–11: dasselbe für EJB und [Metier].

Wir behalten den portablen Namen der Remote-Schnittstelle von EJB [Metier] bei:

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

Wir werden sie bei den Tests der Schicht [métier] benötigen.

3.5.3. Test der Schicht [métier]

Genau wie bei der Schicht [DAO] werden wir die Schicht [métier] im Rahmen einer Client-Server-Anwendung testen:

Der Client wird die Remote-Schnittstelle der auf dem Glassfish-Server bereitgestellten Schicht EJB [Metier] testen.

Zunächst erstellen wir ein neues Maven-Projekt. Dazu folgen wir dem Vorgehen, das bereits zur Erstellung des Testprojekts für die Schicht [dao] verwendet wurde (siehe Abschnitt 3.4.7), mit Ausnahme der Erstellung des Tests JUnit. Das so erstellte Projekt sieht wie folgt aus

  • in [1], das erstellte Projekt mit seinen Abhängigkeiten: gegenüber dem EJB der Ebene [dao], gegenüber dem EJB der Ebene [métier], der Bibliothek [gf-client].

Zu diesem Zeitpunkt sieht die Datei [pom.xml] des Projekts wie folgt aus:


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

Stellen Sie sicher, dass die in den Zeilen 17–33 beschriebenen Abhängigkeiten vorhanden sind. Der Test besteht aus einer einfachen Konsolenklasse:

Der Code der Klasse [ClientRdvMedecinsMetier] lautet wie folgt:


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 {

  // Name der Remote-Schnittstelle von 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";
  // heutiges Datum
  private static Date jour = new Date();

  public static void main(String[] args) {
    try {
      // Kontext JNDI des Glassfish-Servers
      InitialContext initialContext = new InitialContext();
      // Referenz auf die Remote-Ebene [metier]
      IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
      // Kundenanzeige
      List<Client> clients = metier.getAllClients();
      display("Liste des clients :", clients);
      // Anzeige der Ärzte
      List<Medecin> medecins = metier.getAllMedecins();
      display("Liste des médecins :", medecins);
      // Anzeige der Termine eines Arztes
      Medecin medecin = medecins.get(0);
      List<Creneau> creneaux = metier.getAllCreneaux(medecin);
      display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
      // Liste der Termine eines Arztes an einem bestimmten Tag
      display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // Kalenderansicht
      AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // einen Termin hinzufügen RV
      Rv rv = null;
      Creneau creneau = creneaux.get(2);
      Client client = clients.get(0);
      System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
      rv = metier.ajouterRv(jour, creneau, client);
      System.out.println("Rv ajouté");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // Kalender anzeigen
      agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // einen Termin löschen RV
      System.out.println("Suppression du Rv ajouté");
      metier.supprimerRv(rv);
      System.out.println("Rv supprimé");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // Kalender anzeigen
      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();
      }
    }
  }

  // Hilfsfunktion – zeigt die Elemente einer Sammlung an
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • Zeile 18: Der portable Name der Remote-Schnittstelle des EJBs [Metier] wurde aus den GlassFish-Protokollen übernommen,
  • Zeilen 24–27: Es wird eine Referenz auf die Remote-Schnittstelle des EJBs [Metier] abgerufen,
  • Zeilen 29–30: zeigen die Clients an,
  • Zeilen 32–33: zeigen die Ärzte an,
  • Zeilen 35–37: Zeigen die Termine eines Arztes an,
  • Zeile 39: Zeigt die Termine eines Arztes an einem bestimmten Tag an,
  • Zeilen 41–42: den Terminkalender desselben Arztes für denselben Tag,
  • Zeilen 44–49: Fügen einen Termin hinzu,
  • Zeile 50: Die Termine des Arztes werden angezeigt. Es muss ein weiterer Termin vorhanden sein,
  • Zeilen 52–53: Der Terminkalender des Arztes wird angezeigt. Der hinzugefügte Termin muss zu sehen sein,
  • Zeilen 55–57: Der soeben hinzugefügte Termin wird gelöscht,
  • Zeile 58: Dies muss sich in der Liste der Termine des Arztes widerspiegeln,
  • Zeilen 60–61: und in seinem Terminkalender.

Wir führen den Test durch:

 

Die folgenden Bildschirm en werden angezeigt:


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]]
  • Zeile 37: Der Terminkalender von Frau PELISSIER am 23. Mai 2012. Es ist kein Termin reserviert,
  • Zeile 39: Hinzufügen eines Termins,
  • Zeile 42: der neue Terminkalender von Frau PELISSIER. Ein Termin ist nun für Herrn MARTIN reserviert,
  • Zeile 44: Der Termin wurde gelöscht,
  • Zeile 46: Der Kalender von Frau PELISSIER zeigt, dass kein Terminblock reserviert ist.

Wir gehen nun davon aus, dass die Schichten [DAO] und [métier] betriebsbereit sind. Nun müssen wir noch die Schicht [web] mit dem Framework JSF erstellen. Dazu werden wir das zu Beginn dieses Dokuments erworbene Wissen nutzen.

3.6. Die Schicht [web]

Kehren wir zur Architektur zurück, die wir gerade aufbauen:

Wir werden nun die letzte Schicht erstellen, nämlich die Schicht [web].

3.6.1. Das NetBeans-Projekt

Wir erstellen ein Maven-Projekt:

  • In [1] erstellen wir ein neues Projekt,
  • in [2, 3] ein Maven-Projekt vom Typ [Web Application],
  • in [4], geben wir ihm einen Namen,
  • in [5] wählen wir den Glassfish-Server und Java EE 6 Web,
  • in [6], das so erstellte Projekt,
  • in [7], das Projekt, nachdem die Seite [index.jsp] und das Paket in [Source Packages] entfernt wurden,
  • in [8, 9]: In den Projekteigenschaften fügen wir ein Framework hinzu,
  • in [10] wählt man „Java Server Faces“ aus,
  • in [11] die Konfiguration von Java Server Faces. Die Standardwerte werden beibehalten. Es ist zu beachten, dass JSF 2 verwendet wird,
  • in [12] wird das Projekt dann an zwei Stellen geändert: Es wird eine Datei [web.xml] sowie eine Seite [index.html] generiert.

Die Datei [web.xml] sieht wie folgt aus:


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

Diese Datei ist uns bereits begegnet.

  • Zeilen 7–11: Definieren das Servlet, das alle an die Anwendung gerichteten Anfragen verarbeitet. Es handelt sich um das Servlet von JSF,
  • Zeilen 12–15: Definieren die von diesem Servlet verarbeiteten URL. Dabei handelt es sich um die URL der Form /faces/*,
  • Zeilen 21–23: definieren die Seite [index.xhtml] als Startseite.

Diese Seite lautet wie folgt:


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

Wir sind ihr bereits begegnet. Wir können dieses Projekt ausführen:

  • in [1] ausführen, das Projekt ausführen und erhalten im Browser das Ergebnis [2].

Wir stellen nun das vollständige Projekt vor, um anschließend die verschiedenen Elemente im Detail zu erläutern.

  • in [1], die Seiten XHTML des Projekts,
  • in [2], die Java-Codes,
  • in [3] die Sprachdateien, da die Anwendung internationalisiert ist,
  • in [4] die Projektabhängigkeiten.

3.6.2. Die Projektabhängigkeiten

Kommen wir zurück zur Projektarchitektur:

Die Schicht JSF stützt sich auf die Schichten [métier], [DAO] und [JPA]. Diese drei Schichten sind in den beiden von uns erstellten Maven-Projekten gekapselt, was die Abhängigkeiten des Projekts [4] erklärt. Schauen wir uns einfach einmal an, wie diese Abhängigkeiten hinzugefügt werden:

  • In [1] geben wir „ejb“ an, um zu kennzeichnen, dass die Abhängigkeit auf das Projekt EJB verweist,
  • in [2] geben wir „[provided]“ ein. Das Webprojekt wird nämlich gleichzeitig mit den beiden Projekten EJB bereitgestellt. Daher müssen die JAR-Dateien von EJB nicht eingebunden werden.

3.6.3. Die Projektkonfiguration

Die Projektkonfiguration entspricht der der Projekte JSF, die wir zu Beginn dieses Dokuments behandelt haben. Wir listen die Konfigurationsdateien auf, ohne sie erneut zu erläutern.

 

[web.xml]: Konfiguriert die Webanwendung.


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

Beachten Sie in Zeile 26, dass die Seite [index.xhtml] die Startseite der Anwendung ist.

[faces-config.xml]: Konfiguriert die Anwendung 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]: leer, aber für die Anmerkung @Named erforderlich


<?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]: das Stylesheet der Anwendung


.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]: Die Datei mit den Meldungen auf Französisch


# Layout
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers
layout.entete.langue1=Fran\u00e7ais
layout.entete.langue2=Anglais
# Ausnahme
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
# Formular 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
# Formular 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
# Formular 3
form3.titre=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,Zahl,#00} – {6,Zahl,#00}:{7,Zahl,#00}
form3.titre_detail=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,Zahl,#00} – {6,Zahl,#00}:{7,Zahl,#00}
form3.client=Client
form3.valider=Valider
form3.annuler=Annuler
# Fehler
erreur.titre=Une erreur s'est produite.
erreur.message=Message d'erreur
erreur.accueil=Page d'accueil
erreur.classe=Cause

[messages_en.properties]: die englische Meldungsdatei


# Layout
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university
layout.entete.langue1=French
layout.entete.langue2=English
# Ausnahme
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
# Formular 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
# Formular 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
# Formular 3
form3.titre=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,Zahl,#00} – {6,Zahl,#00}:{7,Zahl,#00}
form3.titre_detail=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,Zahl,#00} - {6,Zahl,#00}:{7,Zahl,#00}
form3.client=Client
form3.valider=Submit
form3.annuler=Cancel
# Fehler
erreur.titre=An error occurred
erreur.message=Error message
erreur.accueil=Welcome Page
erreur.classe=Cause

3.6.4. Die Ansichten des Projekts

Zur Erinnerung: So funktioniert die Anwendung. Die Startseite sieht wie folgt aus:

 

Von dieser ersten Seite aus führt der Benutzer (Sekretariat, Arzt) eine Reihe von Aktionen durch. Diese stellen wir im Folgenden vor. Die Ansicht auf der linken Seite zeigt die Ansicht, von der aus der Benutzer eine Anfrage stellt, die Ansicht auf der rechten Seite die vom Server gesendete Antwort.

Schließlich kann auch eine Fehlerseite angezeigt werden:

Diese verschiedenen Ansichten werden mit den folgenden Seiten des Webprojekts aufgerufen:

  • In [1] sorgen die Seiten [basdepage, entete, layout] für die Formatierung aller Ansichten,
  • in [2] die von [layout.xhtml] erzeugte Ansicht.

Hier kam die Facelets-Technologie zum Einsatz. Diese wurde in Abschnitt 2.11 beschrieben. Wir beschränken uns darauf, den Code der Seiten XHTML anzugeben, die für das Layout verwendet werden:

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

Beachten Sie in den Zeilen 10–12 die beiden Links zum Ändern der Sprache der Anwendung.

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

Diese Seite ist die Vorlage (Template) für die Seite [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>

Die Zeilen 8–21 definieren den Bereich namens „Inhalt“ (Zeile 8) in [layout.xhtml] (Zeile 7). Dies ist der zentrale Bereich der Ansichten:

 

Die Seite [index.xhtml] ist die einzige Seite der Anwendung. Es findet daher keine Navigation zwischen den Seiten statt. Sie zeigt eine der vier Seiten [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml] an. Diese Anzeige wird durch vier boolesche Werte [form1Rendered, form2Rendered, form3Rendered, erreurRendered] des Form-Beans gesteuert, die wir im Folgenden beschreiben werden.

3.6.5. Die Beans des Projekts

Die Klassen des Pakets [utils] wurden bereits vorgestellt:

  • Die Klasse [ChangeLocale] ist für den Sprachwechsel zuständig. Sie wurde bereits behandelt (Abschnitt 2.4.4).
  • Die Klasse [Messages] ist eine Klasse, die die Internationalisierung der Meldungen einer Anwendung erleichtert. Sie wurde in Abschnitt 2.8.5.7 behandelt.

3.6.5.1. Das Application-Bean

Das Bean [Application] sieht wie folgt aus:


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{

  // Geschäftsschicht
  @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>();
  // Fehler
  private List<Erreur> erreurs = new ArrayList<Erreur>();
  private Boolean erreur = false;

  public Application() {
  }

  @PostConstruct
  public void init() {
    // Ärzte und Kunden werden zwischengespeichert
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // Der Fehler wird protokolliert
      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;
    }
    // Überprüfung der Listen
    if (medecins.size() == 0) {
      // Der Fehler wird notiert
      erreur = true;
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // Der Fehler wird notiert
      erreur = true;
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // Fehler?
    if (erreur) {
      return;
    }

    // die Wörterbücher
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
  }

  // Getter und Setter
  ...
}
  • Zeilen 15–16: Die Klasse [Application] ist ein Bean mit Anwendungsbereich. Sie wird einmalig zu Beginn des Lebenszyklus der Anwendung JSF angelegt und ist für alle Anfragen aller Benutzer zugänglich. In der Regel werden dort schreibgeschützte Daten abgelegt. Hier werden wir die Liste der Ärzte und die Liste der Kunden ablegen. Wir gehen also davon aus, dass sich diese nicht oft ändern. Die Seiten XHTML greifen über den Anwendungsnamen darauf zu,
  • Zeilen 20–21: Eine Referenz auf die lokale Schnittstelle von EJB [Metier] wird vom GlassFish-Container EJB eingefügt. Erinnern wir uns an die Architektur der Anwendung:

Die Anwendung JSF sowie die EJB und [Metier] werden in derselben JVM (Java Virtual Machine) ausgeführt. Daher wird die Schicht [JSF] die lokale Schnittstelle von EJB nutzen. Hier nutzt die Anwendungs-Bean die EJB und [Metier]. Selbst wenn dies nicht der Fall wäre, wäre es normal, dort einen Verweis auf die Schicht [métier] zu finden. Es handelt sich nämlich um eine Information, die von allen Anfragen aller Benutzer gemeinsam genutzt werden kann, also um eine Datenangabe mit dem Geltungsbereich Application.

  • Zeilen 34–35: Die Methode „init“ wird unmittelbar nach der Instanziierung der Klasse [Application] ausgeführt (vorhandene Annotation @PostConstruct),
  • Zeilen 36–73: Die Methode erstellt die folgenden Elemente: die Liste der Ärzte in Zeile 23, die Liste der Kunden in Zeile 24, ein nach ihrer ID indiziertes Wörterbuch der Ärzte in Zeile 25 und dasselbe für die Kunden in Zeile 26. Es können Fehler auftreten. Diese werden in der Liste in Zeile 28 protokolliert.

Die Klasse [Erreur] lautet wie folgt:


package beans;

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

  // Konstruktor
  public Erreur(String classe, String message){
    this.setClasse(classe);
    this.message=message;
  }
  
  // Getter und Setter
...  
}
  • Zeile 9: der Name einer Ausnahmeklasse, falls eine Ausnahme ausgelöst wurde,
  • Zeile 10: eine Fehlermeldung.

3.6.5.2. Die Bean [Form]

Ihr Code lautet wie folgt:


package beans;

...

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

  public Form() {
  }

  // Anwendungs-Bean
  @Inject
  private Application application;

  // Modell
  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() {
    // Ist die Initialisierung erfolgreich verlaufen?
    if (application.getErreur()) {
      // Die Fehlerliste wird abgerufen
      erreurs = application.getErreurs();
      // Die Fehleransicht wird angezeigt
      setForms(false, false, false, true);
    }
  }

  // Ansicht anzeigen
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.form3Rendered = form3Rendered;
    this.erreurRendered = erreurRendered;
  }
.................................................
}
  • Zeilen 5–7: Die Klasse [Form] ist ein Bean mit dem Namen „form“ und dem Gültigkeitsbereich „session“. Es sei daran erinnert, dass die Klasse in diesem Fall serialisierbar sein muss.
  • Zeilen 13–14: Die Form-Bean verfügt über eine Referenz auf die Application-Bean. Diese wird vom Servlet-Container, in dem die Anwendung ausgeführt wird, injiziert (Vorhandensein der Annotation @Inject).
  • Zeilen 17–31: Die Seitenvorlagen [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. Die Anzeige dieser Seiten wird durch die booleschen Werte in den Zeilen 19–22 gesteuert. Es ist zu beachten, dass standardmäßig die Seite [form1.xhtml] gerendert wird,
  • Zeilen 33–34: Die Methode „init“ wird unmittelbar nach der Instanziierung der Klasse ausgeführt (vorhandene Annotation @PostConstruct),
  • Zeilen 35–41: Die Methode `init` wird verwendet, um festzustellen, welche Seite zuerst angezeigt werden soll: normalerweise die Seite [form1.xhtml] (Zeile 19), es sei denn, die Initialisierung der Anwendung ist fehlgeschlagen (Zeile 36); in diesem Fall wird die Seite [erreur.xhtml] angezeigt (Zeile 40).

Die Seite [erreur.xhtml] lautet wie folgt:


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

Sie verwendet ein <h:dataTable>-Tag (Zeilen 14–27), um die Fehlerliste anzuzeigen. Das Ergebnis ist eine Seite, die in etwa wie folgt aussieht:

Image

Wir werden nun die verschiedenen Phasen des Lebenszyklus der Anwendung definieren.

3.6.6. Interaktionen zwischen Seiten und Modell

3.6.6.1. Anzeige der Startseite

Wenn alles gut geht, wird als erste Seite [form1.xhtml] angezeigt. Das ergibt folgende Ansicht:

 

Die Seite [form1.xhtml] sieht wie folgt aus:


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

Diese Seite wird durch die folgende Vorlage generiert:


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

  // Anwendungs-Bean
  @Inject
  private Application application;
  // Vorlage
  private Long idMedecin;
  private Date jour = new Date();
  
// Ärzteverzeichnis
  public List<Medecin> getMedecins() {
    return application.getMedecins();
  }
  // Terminkalender
  public void getAgenda() {
    ...
}
  • Das Feld in Zeile 9 überträgt den Wert aus der Liste in Zeile 18 der Seite und ermöglicht Lese- und Schreibzugriffe darauf. Bei der ersten Anzeige der Seite wird der in der Kombinationsliste ausgewählte Wert festgelegt. Bei der ersten Anzeige ist idMedecin gleich null, daher wird der erste Arzt ausgewählt.
  • die Methode in den Zeilen 13–15 generiert die Elemente des Ärzte-Kombinationsfelds (Zeile 19 der Seite). Jede generierte Option hat als Bezeichnung (itemLabel) den Titel, den Nachnamen und den Vornamen des Arztes und als Wert (itemValue) die ID des Arztes,
  • das Feld in Zeile 10 versorgt das Eingabefeld in Zeile 21 der Seite mit Lese- und Schreibzugriff. Bei der ersten Anzeige wird daher das aktuelle Datum angezeigt,
  • Zeilen 17–19: Die Methode getAgenda verarbeitet den Klick auf die Schaltfläche [Agenda] in Zeile 26 der Seite. Da keine Navigation stattfindet (es wird immer die Seite [index.html] aufgerufen), wird häufig das Attribut actionListener anstelle des Attributs action verwendet. In diesem Fall liefert die im Modell aufgerufene Methode kein Ergebnis.

Wenn auf die Schaltfläche [Agenda] geklickt wird,

  • Es werden Werte gebucht: Der in der Combobox „Ärzte“ ausgewählte Wert wird im Feld idMedecin der Vorlage gespeichert, und der im Feld „Tag“ ausgewählte Tag
  • wird die Methode getAgenda des Modells aufgerufen.

Die Methode getAgenda lautet wie folgt:


  // Anwendungs-Bean
  @Inject
  private Application application;

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

  // Terminkalender
  public void getAgenda() {
    try {
      // Arzt abrufen
      medecin = application.gethMedecins().get(idMedecin);
      // Formularüberschrift 2
      form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
      // Der Terminkalender des Arztes für einen bestimmten Tag
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // Formular 2 anzeigen
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // Fehlerübersicht
      prepareVueErreur(th);
    }
  }

  // Vorbereitung vueErreur
  private void prepareVueErreur(Throwable th) {
    // Die Fehlerliste wird erstellt
    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()));
    }
// Die Fehleransicht wird angezeigt
    setForms(false, false, false, true);
}

Zur Erinnerung: Was muss die Methode getAgenda anzeigen?

  • Zeile 21: Der ausgewählte Arzt wird aus dem Ärzteverzeichnis abgerufen, das in der Bean application gespeichert wurde. Dazu wird seine ID verwendet, die in idMedecin übermittelt wurde,
  • Zeile 23: Der Titel der Seite [form2.xhtml], die angezeigt werden soll, wird vorbereitet. Diese Meldung wird aus der Meldungsdatei entnommen, damit sie internationalisiert werden kann. Diese Technik wurde in Abschnitt 2.8.5.7 auf Seite 135 beschrieben.
  • Zeile 25: Die Schicht [métier] wird aufgerufen, um den Terminkalender des ausgewählten Arztes für den ausgewählten Tag zu berechnen,
  • Zeile 27: [form2.xhtml] wird angezeigt,
  • Zeile 28: Tritt eine Ausnahme auf, wird eine Fehlerliste erstellt (Zeilen 37–42) und die Seite [erreur.xhtml] angezeigt (Zeile 44).

3.6.6.2. Terminplan eines Arztes anzeigen

Die Seite [form2.xhtml] entspricht der folgenden Ansicht:

Der Code der Seite [form2.xhtml] lautet wie folgt:


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

Zur Erinnerung: Die Methode getAgenda hat zwei Felder im Modell initialisiert:


// Vorlage
  private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;

Diese beiden Felder werden in die Seite [form2.xhtml] übernommen:

  • Zeile 10: der Titel der Seite,
  • Zeile 12: Der Terminkalender des Arztes wird durch ein <h:dataTable>-Tag in drei Spalten angezeigt,
  • Zeilen 13–18: In der ersten Spalte werden die Zeitfenster angezeigt,
  • Zeilen 19–30: In der zweiten Spalte wird der Name des Kunden angezeigt, der den Termin möglicherweise gebucht hat, andernfalls steht dort nichts. Für diese Auswahl werden die Tags aus der in Zeile 7 referenzierten Bibliothek „JSTL Core“ verwendet,
  • Zeilen 30–35: In der dritten Spalte wird der Link „[Réserver]“ angezeigt, wenn der Zeitblock frei ist, und der Link „[Supprimer]“, wenn der Zeitblock belegt ist.

Die Links in der dritten Spalte sind mit der folgenden Vorlage verknüpft:


// Vorlage
  private Long idCreneau;

  // Aktion auf RV
  public void action() {
    ...
}
  • Die Methode action wird aufgerufen, wenn der Benutzer auf den Link „Buchen / Löschen“ klickt (Zeile 32). Es ist zu beachten, dass hier das Attribut action verwendet wurde. Die Methode, auf die dieses Attribut verweist, sollte die Signatur String action() haben, da die Methode dann einen Navigationsschlüssel zurückgeben muss. Hier lautet sie jedoch void action(). Dies hat keinen Fehler verursacht, und man kann davon ausgehen, dass in diesem Fall keine Navigation stattfindet. Das war auch beabsichtigt. Die Verwendung von actionListener anstelle von action führte zu einer Fehlfunktion,
  • das Feld „idCreneau“ in Zeile 2 ruft die ID des Zeitfensters des angeklickten Links ab (Zeile 33 der Seite).

3.6.6.3. Löschen eines Termins

Sehen wir uns den Code an, der das Löschen eines Termins verwaltet. Dies entspricht der folgenden Abfolge von Ansichten:

Der für diesen Vorgang relevante Code lautet wie folgt:


// Anwendungs-Bean
  @Inject
  private Application application;

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

  // Aktion auf RV
  public void action() {
    // Es wird nach einem Termin im Kalender gesucht
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // Wurde etwas gefunden?
    if (!trouvé) {
      // Das ist seltsam – form2 wird erneut angezeigt
      setForms(false, true, false, false);
      return;
    }
    // Es wurde gefunden
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // je nach gewünschter Aktion
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // Reservierung

  public void reserver() {
    ...
  }

  public void supprimer() {
    try {
      // Termin löschen
      application.getMetier().supprimerRv(creneauChoisi.getRv());
      // Der Kalender wird aktualisiert
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // Formular 2 wird angezeigt
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // Fehleransicht
      prepareVueErreur(th);
    }
  }
  • Zeile 16: Beim Start der Methode action wurde die ID des ausgewählten Zeitfensters in idCreneau (Zeile 11) übermittelt,
  • Zeilen 18–26: Es wird versucht, das Zeitfenster anhand seines id (Zeile 21) abzurufen. Man sucht ihn im aktuellen Kalender, agendaMedecinJour aus Zeile 10. Normalerweise sollte man ihn finden. Ist dies nicht der Fall, wird nichts unternommen (Zeilen 28–32),
  • Zeile 34: Wenn der gesuchte Termin gefunden wurde, wird eine Referenz abgerufen und in Zeile 12 gespeichert,
  • Zeile 36: Es wird geprüft, ob der ausgewählte Zeitblock bereits einen Termin enthielt. Wenn ja, wird dieser gelöscht (Zeile 39), andernfalls wird ein Termin reserviert (Zeile 37),
  • Zeile 51: Der Termin im ausgewählten Zeitfenster wird gelöscht. Diese Aufgabe übernimmt die Ebene [métier],
  • Zeile 53: Die Ebene [métier] wird um den neuen Terminkalender des Arztes gebeten. Dort ist natürlich ein Termin weniger zu sehen. Da es sich jedoch um eine Mehrbenutzeranwendung handelt, sind dort möglicherweise Änderungen zu sehen, die von anderen Benutzern vorgenommen wurden,
  • Zeile 55: Die Seite [form2.xhtml] wird erneut angezeigt,
  • Zeile 58: Da die Schicht [métier] aufgerufen wurde, können Ausnahmen auftreten. In diesem Fall wird der Ausnahmestapel in der Fehlerliste in Zeile 13 gespeichert und mithilfe der Ansicht [erreur.xhtml] angezeigt.

3.6.6.4. Terminvereinbarung

Die Terminvereinbarung erfolgt nach folgendem Ablauf:

Das für diese Aktion verwendete Modell lautet wie folgt:


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

  // Aktion auf RV
  public void action() {
...
    // Es wurde gefunden
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // je nach gewünschter Aktion
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // Reservierung

    public void reserver() {
    try {
      // Formularüberschrift 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();
      // im Dropdown-Menü ausgewählter Kunde
      idClient=null;
      // Formular 3 wird angezeigt
      setForms(false, false, true, false);
    } catch (Throwable th) {
      // Fehleransicht
      prepareVueErreur(th);
    }
  }
  • Zeile 14: Wenn der ausgewählte Terminblock noch keinen Termin enthält, handelt es sich um eine Reservierung,
  • Zeile 30: Der Titel der Seite [form3.xhtml] wird mit derselben Technik vorbereitet wie der Titel der Seite [form2.xhtml],
  • Zeile 34: In diesem Formular gibt es ein Kombinationsfeld, dessen Wert von idClient bereitgestellt wird. Der Wert dieses Feldes wird auf null gesetzt, um niemanden auszuwählen,
  • Zeile 36: Die Seite [form3.xhtml] wird angezeigt,
  • Zeile 39: oder die Fehlerseite, falls eine Ausnahme aufgetreten ist.

Die Seite [form3.xhtml] sieht wie folgt aus:


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

Diese Seite wird durch die folgende Vorlage generiert:


// Anwendungs-Bean
  @Inject
  private Application application;

  // Vorlage
  private Long idClient;

  // Kundenliste
  public List<Client> getClients() {
    return application.getClients();
  }
  • Zeile 6: Die Kundennummer füllt das Attribut value des Kunden-Dropdown-Menüs in Zeile 12 der Seite. Sie legt das ausgewählte Element des Dropdown-Menüs fest,
  • Zeilen 9–11: Die Methode getClients füllt den Inhalt des Dropdown-Menüs (Zeile 13). Die Bezeichnung (itemLabel) jeder Option ist [Titre Prénom Nom] des Kunden, und der zugehörige Wert (itemValue) ist die ID des Kunden. Dieser Wert wird also übermittelt.

3.6.6.5. Bestätigung eines Termins

Die Bestätigung eines Termins erfolgt nach folgender Abfolge:

und entspricht dem Klick auf die Schaltfläche [Valider]:


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

Dieses Ereignis wird also von der Methode [Form].validerRv verarbeitet. Ihr Code lautet wie folgt:


  // Anwendungs-Bean
  @Inject
  private Application application;
  
  // Vorlage
  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;

  // Terminvalidierung
  public void validerRv() {
    try {
      // Rufen einer Instanz des ausgewählten Zeitfensters ab
      Creneau creneau = application.getMetier().getCreneauById(idCreneau);
      // Der Termin wird hinzugefügt
      application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
      // Der Kalender wird aktualisiert
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // form2 wird angezeigt
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // Fehleranzeige
      prepareVueErreur(th);
    }
}
  • Zeile 12: Bevor die Methode validerRv ausgeführt wird, hat das Feld idClient die ID des vom Benutzer ausgewählten Kunden erhalten,
  • Zeile 19: Anhand der in einem vorherigen Schritt gespeicherten ID des Zeitfensters (das Bean hat Sitzungsgültigkeit) wird von der Ebene [métier] eine Referenz auf das Zeitfenster selbst angefordert,
  • Zeile 21: Die Schicht [métier] wird aufgefordert, einen Termin für den ausgewählten Tag (jour), das ausgewählte Zeitfenster (creneau) und den ausgewählten Kunden (idClient) hinzuzufügen,
  • Zeile 23: Die Ebene [métier] wird aufgefordert, den Terminkalender des Arztes zu aktualisieren. Der hinzugefügte Termin sowie alle Änderungen, die andere Nutzer der Anwendung möglicherweise vorgenommen haben, werden angezeigt,
  • Zeile 25: Der Terminkalender [form2.xhtml] wird erneut angezeigt,
  • Zeile 28: Bei Auftreten eines Fehlers wird die Fehlerseite angezeigt.

3.6.6.6. Stornierung eines Termins

Dies entspricht der folgenden Abfolge:

Die Schaltfläche [Annuler] auf der Seite [form3.xhtml] lautet wie folgt:


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

Die Methode [Form].annulerRv wird daher aufgerufen:


  // Terminabsage
  public void annulerRv() {
    // Formular 2 wird angezeigt
    setForms(false, true, false, false);
}

3.6.6.7. Zurück zur Startseite

Es bleibt noch eine Aktion zu betrachten, nämlich die der folgenden Sequenz:

Der Code der Schaltfläche [Accueil] auf der Seite [form2.xhtml] lautet wie folgt:


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

Die Methode [Form].accueil lautet wie folgt:


  public void accueil() {
    // Die Startseite wird angezeigt
    setForms(true, false, false, false);
}

3.7. Fazit

Wir haben die folgende Anwendung erstellt:

Wir haben uns mehr auf die Funktionalitäten der Anwendung als auf deren Erscheinungsbild für den Benutzer konzentriert. Letzteres wird durch die Verwendung der Komponentenbibliothek PrimeFaces verbessert. Wir haben eine einfache, aber dennoch repräsentative Anwendung einer mehrschichtigen Java-Architektur (EE) unter Verwendung von EJB erstellt. Die Anwendung lässt sich auf verschiedene Weise verbessern:

  • Eine Authentifizierung ist erforderlich. Nicht jeder ist berechtigt, Termine hinzuzufügen oder zu löschen,
  • man sollte den Kalender vor- und zurückblättern können, wenn man nach einem Tag mit freien Terminfenstern sucht,
  • es sollte möglich sein, eine Liste der Tage abzufragen, an denen für einen Arzt freie Termine verfügbar sind. Denn wenn es sich um einen Augenarzt handelt, sind seine Termine in der Regel bereits sechs Monate im Voraus ausgebucht,
  • ...

3.8. Tests mit Eclipse

3.8.1. Die Schicht [DAO]

  • In [1] wird das Projekt EJB aus der Ebene [DAO] sowie dessen Mandant
  • in [2] wählt man das Projekt EJB aus der Ebene [DAO] aus und führt es in [3] aus,
  • in [4] wird es auf einem Server ausgeführt,
  • in [5] wird nur der Glassfish-Server vorgeschlagen, da dieser als einziger über einen Container EJB verfügt,
  • bei [6] wurde das Modul EJB bereitgestellt,
  • In [7] werden die Protokolle angezeigt:
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.

Das sind dieselben, die wir mit NetBeans hatten.

  • In [7A] und [7B] wird der Test JUnit des Kunden ausgeführt,
  • in [8], der Test ist erfolgreich,
  • in [9], die Konsolenprotokolle.

In [10] wird die Anwendung EJB entladen.

3.8.2. Die Schicht [métier]

  • In [1] werden die vier Maven-Projekte aus der Schicht [métier] importiert,
  • in [2] wählt man das Unternehmensprojekt aus und führt es in [3] auf einem Glassfish-Server [4] [5] aus,
  • in [6] wurde das Unternehmensprojekt auf Glassfish bereitgestellt,
  • in [7], man sieht sich die Glassfish-Protokolle an,
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]

Zeile 3: Wir notieren uns den portablen Namen von EJB und [Metier] und fügen ihn in die Client-Konsole dieses EJB ein:


public class ClientRdvMedecinsMetier {

  // Name der Remote-Schnittstelle von 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";
  // heutiges Datum
private static Date jour = new Date();
  • Auf dem [8] führen wir den Konsolen-Client aus,
  • in [9] die Protokolle.
  • in [10] wird die Unternehmensanwendung entladen;

3.8.3. Die Schicht [web]

  • in [1] werden die drei Maven-Projekte aus der Schicht [web] importiert. Das Projekt mit der Endung „ear“ ist das Unternehmensprojekt, das auf Glassfish bereitgestellt werden muss,
  • in [2] wird es ausgeführt,
  • auf dem GlassFish-Server [3],
  • in [4] wurde die Unternehmensanwendung erfolgreich bereitgestellt,
  • in [5], die Anwendung wird im internen Eclipse-Browser aufgerufen.