3. Przykładowa aplikacja – 01: rdvmedecins-jsf2-ejb
Poniższy tekst odnosi się do następujących dokumentów:
- [ref7]: Wprowadzenie do języka Java EE 5 (czerwiec 2010) [http://tahe.developpez.com/java/javaee]. Niniejszy dokument pozwala zapoznać się z JSF 1 oraz EJB3.
- [ref8]: Trwałość danych w Javie w praktyce (czerwiec 2007) [http://tahe.developpez.com/java/jpa]. Niniejszy dokument pozwala zapoznać się z trwałością danych za pomocą JPA (Java Persistence API).
- [ref9]: Tworzenie serwisu internetowego w Javie EE przy użyciu NetBeans i serwera GlassFish (styczeń 2009) [http://tahe.developpez.com/java/webservice-jee]. Niniejszy dokument omawia proces tworzenia serwisu internetowego.
Przykładowa aplikacja, która zostanie omówiona, pochodzi z [ref9].
3.1. L'application
Firma świadcząca usługi informatyczne [ISTIA-AGI] pragnie zaoferować usługę umawiania wizyt. Pierwszym docelowym rynkiem są lekarze prowadzący prywatną praktykę. Zazwyczaj nie dysponują oni sekretariatem. Klienci pragnący umówić się na wizytę dzwonią zatem bezpośrednio do lekarza. W ten sposób lekarz jest często niepokojony w ciągu dnia, co ogranicza jego dostępność dla pacjentów. Firma [ISTIA-AGI] pragnie zaoferować im usługę umawiania wizyt działającą na następującej zasadzie:
- sekretariat zajmuje się umawianiem wizyt dla dużej liczby lekarzy. Sekretariat ten może składać się z jednej osoby. Jej wynagrodzenie jest dzielone między wszystkich lekarzy korzystających z tej usługi.
- Sekretariat i wszyscy lekarze mają dostęp do Internetu
- rezerwacje RV są zapisywane w scentralizowanej bazie danych, dostępnej przez Internet zarówno dla sekretariatu, jak i dla lekarzy
- Rejestracja RV odbywa się zazwyczaj przez sekretariat. Może być również przeprowadzona przez samych lekarzy. Dzieje się tak zwłaszcza wtedy, gdy pod koniec wizyty lekarz samodzielnie ustala dla swojego pacjenta nowy RV.
Struktura usługi generowania kodu RV jest następująca:
![]() |
Lekarze zyskują na wydajności, jeśli nie muszą już zajmować się zarządzaniem numerami RV. Jeśli jest ich wystarczająco dużo, ich wkład w koszty funkcjonowania sekretariatu będzie niewielki.
Firma [ISTIA-AGI] postanawia stworzyć aplikację w dwóch wersjach:
- wersja JSF / EJB3 / JPA EclipseLink / serwer Glassfish:
![]() |
- oraz wersję JSF / Spring / JPA Hibernate / serwer Tomcat:
![]() |
3.2. Działanie aplikacji
Aplikację nazwiemy [RdvMedecins]. Poniżej przedstawiamy zrzuty ekranu ilustrujące jej działanie.
Strona główna aplikacji wygląda następująco:
![]() |
Z tej pierwszej strony użytkownik (sekretariat, lekarz) będzie mógł wykonać szereg czynności. Przedstawiamy je poniżej. Widok po lewej stronie przedstawia ekran, z którego użytkownik składa wniosek, a widok po prawej stronie – odpowiedź wysłaną przez serwer.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Wreszcie można również uzyskać stronę z komunikatami o błędach:
![]() |
3.3. Baza danych
Wróćmy do architektury tworzonej aplikacji:
![]() |
Baza danych, którą nazwiemy [dbrdvmedecins2] , to baza danych MySQL5 zawierająca cztery tabele:
![]() |
3.3.1. Tabela [MEDECINS]
Zawiera informacje o lekarzach obsługiwanych przez aplikację [RdvMedecins].
![]() | ![]() |
- ID: numer identyfikacyjny lekarza – klucz główny tabeli
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- NOM: nazwisko lekarza
- PRENOM: jego imię
- TITRE: tytuł (panna, pani, pan)
3.3.2. Tabela [CLIENTS]
Pacjenci poszczególnych lekarzy są zapisani w tabeli [CLIENTS]:
![]() | ![]() |
- ID: numer identyfikacyjny pacjenta – klucz główny tabeli
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- NOM: nazwisko klienta
- PRENOM: imię klienta
- TITRE: tytuł (panna, pani, pan)
3.3.3. Tabela [CRENEAUX]
Zawiera listę przedziałów czasowych, w których możliwe są RV:
![]() |
![]() |
- ID: numer identyfikujący przedział czasowy – klucz główny tabeli (wiersz 8)
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- ID_MEDECIN: numer identyfikujący lekarza, do którego należy ten przedział czasowy – klucz obcy w kolumnie MEDECINS (ID).
- HDEBUT: godzina rozpoczęcia terminu
- MDEBUT: minuty początku przedziału czasowego
- HFIN: godzina zakończenia przedziału czasowego
- MFIN: minuty zakończenia przedziału czasowego
Drugi wiersz tabeli [CRENEAUX] (por. [1] powyżej) wskazuje na przykład, że przedział nr 2 rozpoczyna się o godz. 8:20 i kończy o godz. 8:40 oraz należy do lekarza nr 1 (pani Marie PELISSIER).
3.3.4. Tabela [RV]
Zawiera listę RV przypisanych każdemu lekarzowi:
![]() |
- ID: numer jednoznacznie identyfikujący RV – klucz główny
- JOUR: dzień, w którym wystawiono RV
- ID_CRENEAU: przedział czasowy dla RV – klucz obcy w polu [ID] tabeli [CRENEAUX] – określa zarówno przedział czasowy, jak i danego lekarza.
- ID_CLIENT: numer klienta, dla którego dokonano rezerwacji – klucz obcy w polu [ID] tabeli [CLIENTS]
Ta tabela posiada , która wymusza unikalność wartości połączonych kolumn (JOUR, ID_CRENEAU):
Jeśli wiersz w tabeli [RV] ma wartość (JOUR1, ID_CRENEAU1) w kolumnach (JOUR, ID_CRENEAU), to ta wartość nie może występować nigdzie indziej. W przeciwnym razie oznaczałoby to, że dwa rekordy o wartości RV zostały zarejestrowane w tym samym czasie dla tego samego lekarza. Z punktu widzenia programowania w Javie sterownik bazy danych o wartości JDBC uruchamia sterownik o wartości SQLException, gdy wystąpi taka sytuacja.
Wiersz id równy 3 (por. [1] powyżej) oznacza, że rezerwacja RV została dokonana na przedział czasowy nr 20 i klienta nr 4 w dniu 23.08.2006 r. Z tabeli [CRENEAUX] wynika, że slot nr 20 odpowiada przedziałowi czasowemu 16:20–16:40 i należy do lekarza nr 1 (pani Marie PELISSIER). Z tabeli [CLIENTS] wynika, że klient nr 4 to panna Brigitte BISTROU.
3.3.5. Tworzenie bazy danych
Aby utworzyć tabele i wypełnić je, można skorzystać ze skryptu [dbrdvmedecins2.sql], który znajduje się na stronie z przykładami. W połączeniu ze skryptem [WampServer] (patrz punkt 1.3.3) można postępować w następujący sposób:
![]() |
- w [1] należy kliknąć ikonę [WampServer] i wybrać opcję [PhpMyAdmin] [2],
- na [3], w oknie, które się otworzyło, wybieramy link [Bases de données],
![]() |
- do [2], tworzymy bazę danych o nazwie [4] i kodowaniu [5],
- w [7] baza została utworzona. Klikamy na jej link,
![]() |
- w [8] importujemy plik SQL,
- który wybieramy w systemie plików za pomocą przycisku [9],
![]() |
- w pliku [11] wybieramy skrypt SQL, a w pliku [12] uruchamiamy go,
- w [13] utworzono cztery tabele bazy danych. Klikamy jeden z linków,
![]() |
- w [14], zawartość tabeli.
W dalszej części nie będziemy już wracać do tej bazy danych. Zachęcamy jednak czytelnika do śledzenia jej rozwoju w miarę postępów w programach, zwłaszcza gdy coś nie działa.
3.4. Warstwy [DAO] i [JPA]
Wróćmy do architektury, którą musimy zbudować:
![]() |
Zbudujemy cztery projekty Maven:
- projekt dotyczący warstw [DAO] i [JPA],
- projekt dla warstwy [métier],
- projekt dotyczący warstwy [web],
- projekt korporacyjny, który połączy trzy poprzednie projekty.
Teraz tworzymy projekt Maven dla warstw [DAO] i [JPA].
Uwaga: zrozumienie warstw [métier], [DAO], [JPA] wymaga znajomości języka Java EE. W tym celu można zapoznać się z dokumentem [ref7] (patrz akapit 3).
3.4.1. Projekt NetBeans
Wygląda to następująco:
![]() |
- w [1] tworzy się projekt Maven typu [EJB Module] [2],
- w [3] nadajemy nazwę projektowi,
![]() |
- w [4] wybieramy serwer Glassfish,
- w [5] – wygenerowany projekt.
3.4.2. Generowanie warstwy [JPA]
Wróćmy do architektury, którą musimy zbudować:
![]() |
Za pomocą NetBeans można automatycznie wygenerować warstwę [JPA] oraz warstwę [EJB], która kontroluje dostęp do wygenerowanych encji JPA. Warto zapoznać się z tymi metodami automatycznego generowania, ponieważ wygenerowany kod dostarcza cennych wskazówek dotyczących sposobu pisania encji JPA lub kodu EJB, który z nich korzysta.
Poniżej opisujemy niektóre z tych narzędzi do automatycznego generowania. Aby zrozumieć wygenerowany kod, należy posiadać solidną wiedzę na temat encji JPA, [ref8] oraz EJB i [ref7] (patrz akapit 3).
3.4.2.1. Tworzenie połączenia NetBeans z bazą danych
- uruchomić SGBD i MySQL 5, aby BD stało się dostępne,
- utworzyć połączenie NetBeans z bazą danych [dbrdvmedecins2],
![]() |
- w zakładce [Services] [1], w gałęzi [Databases] [2], należy wybrać sterownik JDBC MySQL [3],
- a następnie wybrać opcję [4] „Connect Using”, umożliwiającą nawiązanie połączenia z bazą danych MySQL,
- w polu [5] należy podać wymagane informacje. W polu [6] należy wpisać nazwę bazy danych, w polu [7] – nazwę użytkownika bazy danych i hasło,
- w [8] można sprawdzić podane dane,
- w [9] wyświetla się oczekiwany komunikat, jeśli dane są poprawne,
![]() |
- w [10] nawiązano połączenie. Widoczne są cztery tabele podłączonej bazy danych.
3.4.2.2. Tworzenie jednostki trwałości
Wróćmy do tworzonej architektury:
![]() |
Tworzymy właśnie warstwę [JPA]. Jej konfiguracja odbywa się w pliku [persistence.xml], w którym definiuje się jednostki trwałości. Każda z nich wymaga następujących informacji:
- charakterystyki JDBC dostępu do bazy danych (URL, nazwa użytkownika, hasło),
- klasy, które będą odzwierciedlać tabele bazy danych,
- wykorzystywaną implementację JPA. JPA jest bowiem specyfikacją realizowaną przez różne produkty. W tym przypadku wykorzystamy EclipseLink, czyli domyślną implementację stosowaną przez serwer GlassFish. Dzięki temu nie musimy dołączać do GlassFish bibliotek innej implementacji.
NetBeans może wygenerować ten plik trwałości za pomocą kreatora.
![]() |
- kliknij prawym przyciskiem myszy na projekt i wybierz opcję utworzenia jednostki trwałości [1],
- w [2] nadać nazwę tworzonej jednostce trwałości,
- w [3], wybrać implementację JPA EclipseLink (JPA 2.0),
- w [4] należy wskazać, że transakcje z bazą danych będą zarządzane przez kontener EJB serwera Glassfish,
- w pliku [5] należy zaznaczyć, że tabele z pliku BD zostały już utworzone i w związku z tym nie należy ich tworzyć,
![]() |
- w [6] należy utworzyć nowe źródło danych dla serwera Glassfish,
- w [7] nadać nazwę JNDI (Java Naming Directory Interface),
- w [8], powiązać tę nazwę z połączeniem MySQL utworzonym w poprzednim kroku,
![]() |
- na [9], zakończyć pracę kreatora,
- w [10], nowy projekt,
- w [11], plik [persistence.xml] został wygenerowany w folderze [META-INF],
- w [12], wygenerowano folder [setup],
- w [13] do projektu Maven dodano nowe zależności.
Wygenerowany plik [META-INF/persistence.xml] ma następującą treść:
<?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>
Zawiera on informacje podane w kreatorze:
- wiersz 3: nazwa jednostki trwałości,
- wiersz 3: typ transakcji z bazą danych, w tym przypadku transakcje JTA (Java Transaction API) zarządzane przez kontener EJB3 serwera Glassfish,
- wiersz 4: nazwa źródła danych JNDI.
Zazwyczaj w tym pliku podany jest typ używanej implementacji: JPA. W kreatorze wskazaliśmy EclipseLink. Ponieważ jest to implementacja JPA używana domyślnie przez serwer Glassfish, nie jest ona wymieniona w pliku [persistence.xml].
W zakładce [Design] można uzyskać ogólny przegląd pliku [persistence.xml]:
![]() |
Aby uzyskać logi z pliku EclipseLink, wykorzystamy następujący plik [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>
- wiersz 4: wskazujemy, że używamy implementacji JPA dla EclipseLink,
- wiersze 7–9: zawierają właściwości konfiguracyjne dostawcy JPA, w tym przypadku EclipseLink,
- wiersz 8: ta właściwość umożliwia rejestrowanie zleceń SQL, które wygeneruje EclipseLink.
Utworzony plik [glassfish-resources.xml] ma następującą treść:
<?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>
Plik ten zawiera informacje, które podaliśmy w dwóch poprzednio użytych kreatorach:
- wiersze 5–11: charakterystyka JDBC bazy danych MySQL5 [dbrdvmedecins2],
- wiersz 13: nazwa źródła danych JNDI.
Plik ten zostanie wykorzystany do utworzenia źródła danych JNDI [jdbc/dbrdvmedecins2] na serwerze Glassfish. Jest to rozwiązanie całkowicie specyficzne dla tego serwera. W przypadku innego serwera należałoby postępować inaczej, zazwyczaj za pomocą narzędzia administracyjnego. Takie narzędzie istnieje również dla Glassfish.
Na koniec do projektu dodano zależności. Plik [pom.xml] ma następującą treść:
<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>
- w wierszach 32–37 warstwa [JPA] wymaga artefaktu [javaee-api],
- w wierszach 16, 22, 28 wymieniono artefakty wymagane przez wykorzystaną tutaj implementację JPA / EclipseLink.
- wiersze 18, 24, 30, 36: wszystkie artefakty mają atrybut provided. Przypominamy, że oznacza to, iż są one niezbędne do kompilacji, ale nie do wykonania. W trakcie wykonywania są one bowiem dostarczane (provided) przez serwer Glassfish,
- wiersze 41–48: definiują nowe repozytorium artefaktów Maven, w którym można znaleźć artefakty o atrybucie EclipseLink.
3.4.2.3. Generowanie encji JPA
Entities JPA można wygenerować za pomocą kreatora w NetBeans:
![]() |
- w [1] tworzy się encje JPA na podstawie bazy danych,
- w [2] wybiera się źródło danych [jdbc / dbrdvmedecins2] utworzone wcześniej,
- w [3] wyświetla się lista tabel tego źródła danych,
- w [4] wybiera się je wszystkie,
![]() |
- w [5] – wybrane tabele,
- w [6] nadajemy nazwy klasom Java powiązanym z czterema tabelami,
- a także nadajemy nazwę pakietu w [7],
- w [8], JPA grupuje wiersze tabel z BD w kolekcjach. Wybieramy listę jako kolekcję,
![]() |
- w [9], klasy Java utworzone przez kreatora.
3.4.2.4. Wygenerowane encje JPA
Entyteta [Medecin] jest odwzorowaniem tabeli [medecins]. Klasa Java jest przepełniona adnotacjami, które na pierwszy rzut oka utrudniają czytanie kodu. Jeśli zachowamy tylko to, co jest niezbędne do zrozumienia roli tej entytety, otrzymamy następujący kod:
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;
// konstruktory
....
// metody pobierające i ustawiające
....
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- w wierszu 4 adnotacja @Entity sprawia, że klasa [Medecin] staje się encją JPA, c.a.d. klasa powiązana z tabelą BD poprzez API i JPA,
- w wierszu 5 podano nazwę tabeli BD powiązanej z encją JPA. Każde pole tabeli odpowiada polu w klasie Java,
- w wierszu 6 klasa implementuje interfejs Serializable. Jest to konieczne w aplikacjach typu klient/serwer, gdzie encje są serializowane między klientem a serwerem.
- wiersze 10–11: pole id klasy [Medecin] odpowiada polu [ID] (wiersz 10) tabeli [medecins],
- wiersze 13–14: pole „tytuł” klasy [Medecin] odpowiada polu [TITRE] (wiersz 13) w tabeli [medecins],
- wiersze 16–17: pole „nazwa” klasy [Medecin] odpowiada polu [NOM] (wiersz 16) w tabeli [medecins],
- wiersze 19–20: pole „wersja” klasy [Medecin] odpowiada polu [VERSION] (wiersz 19) w tabeli [medecins]. W tym przypadku kreator nie rozpoznaje, że kolumna ta jest w rzeczywistości kolumną wersji, której wartość powinna być zwiększana przy każdej modyfikacji wiersza, do którego należy. Aby nadać jej tę rolę, należy dodać adnotację @Version. Zrobimy to w kolejnym kroku,
- wiersze 22–23: pole „prenom” klasy [Medecin] odpowiada polu [PRENOM] w tabeli [medecins],
- wiersze 10–11: pole „id” odpowiada kluczowi pierwotnemu [ID] tabeli. Adnotacje w wierszach 8–9 wyjaśniają tę kwestię,
- wiersz 8: adnotacja @Id wskazuje, że pole z adnotacją jest powiązane z kluczem głównym tabeli,
- wiersz 9: warstwa [JPA] wygeneruje klucz główny dla wierszy, które wstawi do tabeli [Medecins]. Istnieje kilka możliwych strategii. W tym przypadku strategia GenerationType.IDENTITY wskazuje, że warstwa JPA będzie korzystać z trybu auto_increment z tabeli MySQL,
- wiersze 25–26: tabela [creneaux] posiada klucz obcy do tabeli [medecins]. Jeden termin należy do jednego lekarza. I odwrotnie, jeden lekarz ma przypisanych kilka terminów. Mamy zatem relację „jeden (lekarz) do wielu (terminów)”, relację określoną przez adnotację @OneToMany przez JPA (wiersz 25). Pole w wierszu 26 będzie zawierało wszystkie terminy tego lekarza. Odbywa się to bez konieczności programowania. Aby w pełni zrozumieć wiersz 25, musimy przedstawić klasę [Creneau].
Wygląda ona następująco:
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;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
Komentujemy tylko nowe uwagi:
- wspomnieliśmy, że tabela [creneaux] posiada klucz obcy do tabeli [medecins]: jeden termin jest powiązany z jednym lekarzem. Z tym samym lekarzem może być powiązanych kilka terminów. Mamy relację z tabeli [creneaux] do tabeli [medecins], która jest określona jako relacja typu „wiele (terminów)” do „jednego (lekarza)”. Adnotacja @ManyToOne w wierszu 32 służy do określenia klucza obcego,
- a wiersz 31 z adnotacją @JoinColumn określa relację klucza obcego: kolumna [ID_MEDECIN] w tabeli [creneaux] jest kluczem obcym w kolumnie [ID] w tabeli [medecins],
- wiersz 33: odniesienie do lekarza będącego właścicielem terminu. Uzyskuje się to również bez konieczności programowania.
Powiązanie klucza obcego między encją [Creneau] a encją [Medecin] jest zatem odzwierciedlone przez dwa adnotacje:
- w encji [Creneau]:
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
- w encji [Medecin]:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
Oba adnotacje odzwierciedlają tę samą relację: relację klucza obcego z tabeli [creneaux] do tabeli [medecins]. Mówi się, że są one odwrotne względem siebie. Niezbędna jest jedynie relacja @ManyToOne. Jednoznacznie określa ona relację klucza obcego. Relacja @OneToMany jest opcjonalna. Jeśli występuje, ogranicza się jedynie do odwołania się do relacji @ManyToOne, z którą jest powiązana. Takie jest znaczenie atrybutu mappedBy w wierszu 1 encji [Medecin]. Wartością tego atrybutu jest nazwa pola w encji [Creneau], które posiada adnotację @ManyToOne określającą klucz obcy. Również w tym samym wierszu 1 encji [Medecin] atrybut cascade=CascadeType.ALL określa zachowanie encji [Medecin] względem encji [Creneau]:
- jeśli do bazy zostanie wstawiony nowy element [Medecin], wówczas elementy [Creneau] z pola w wierszu 2 również muszą zostać wstawione,
- jeśli zmodyfikuje się entytę [Medecin] w bazie, wówczas entytę [Creneau] z pola w wierszu 2 również należy zmodyfikować,
- jeśli usunie się element [Medecin] z bazy, wówczas elementy [Creneau] z pola w wierszu 2 również muszą zostać usunięte.
Podajemy kod dwóch pozostałych jednostek bez szczególnych komentarzy, ponieważ nie wprowadzają one nowych oznaczeń.
Jednostka [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;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- wiersze 24–25 odzwierciedlają relację klucza obcego między tabelą [rv] a tabelą [clients].
Entyteta [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;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- wiersz 13 określa pole „dzień” typu Java w tabeli Date. Wskazuje się, że w tabeli [rv] kolumna [JOUR] (wiersz 12) jest typu data (bez godziny),
- wiersze 16–18: definiują relację klucza obcego między tabelą [rv] a tabelą [creneaux],
- wiersze 20–22: określają relację klucza obcego między tabelą [rv] a tabelą [clients].
Automatyczne wygenerowanie encji JPA pozwala nam uzyskać bazę roboczą. Czasami jest to wystarczające, a czasami nie. Tak jest w tym przypadku:
- należy dodać adnotację @Version do poszczególnych pól wersji encji,
- należy napisać metody toString bardziej jednoznaczne niż te wygenerowane,
- entytety [Medecin] i [Client] są analogiczne. Spowodujemy, że będą one pochodzić od klasy [Personne],
- usuniemy relacje @OneToMany będące odwrotnością relacji @ManyToOne. Nie są one niezbędne i powodują komplikacje podczas programowania,
- usuwamy walidację @NotNull dotyczącą kluczy głównych. Gdy zapisujemy encję JPA wraz z MySQL, encja ta ma początkowo klucz główny null. Dopiero po zapisaniu w bazie danych klucz podstawowy zapisanego elementu otrzymuje wartość.
Zgodnie z tymi specyfikacjami poszczególne klasy przedstawiają się następująco:
Klasa Personne służy do reprezentowania lekarzy i klientów:
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;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public String toString() {
return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
}
}
- wiersz 8: należy zauważyć, że klasa [Personne] sama w sobie nie jest encją (@Entity). Będzie ona klasą nadrzędną dla encji. Adnotacja @MappedSuperClass wskazuje na tę sytuację.
Entyteta [Client] zawiera wiersze tabeli [clients]. Wywodzi się ona z poprzedniej klasy [Personne]:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// konstruktory
...
@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());
}
}
- wiersz 6: klasa [Client] jest encją JPA,
- wiersz 7: jest ona powiązana z tabelą [clients],
- wiersz 8: wywodzi się z klasy [Personne].
Entyteta [Medecin], która hermetyzuje wiersze tabeli [medecins], jest zbudowana według tego samego wzorca:
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;
// konstruktory
...
@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());
}
}
Entyteta [Creneau] zawiera wiersze tabeli [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;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
// TODO: Ostrzeżenie – ta metoda nie zadziała, jeśli pola id nie są ustawione
...
}
@Override
public String toString() {
return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
}
}
- wiersze 45–47 modelują relację „wiele do jednego”, która istnieje między tabelą [creneaux] a tabelą [medecins] w bazie danych: jeden lekarz ma wiele terminów, a jeden termin należy do jednego lekarza.
Entyteta [Rv] zawiera wiersze tabeli [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;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Rv[%s, %s, %s]", id, creneau, client);
}
}
- wiersze 29–31 modelują relację „wiele do jednego”, która istnieje między tabelą [rv] a tabelą [clients] (jeden klient może występować w wielu rezerwacjach) w bazie danych, a wiersze 25–27 modelują relację „wiele do jednego” między tabelą [rv] a tabelą [creneaux] (jeden termin może występować w wielu rezerwacjach).
3.4.3. Klasa wyjątków
![]() |
Klasa wyjątków [RdvMedecinsException] aplikacji ma następujący wygląd:
package rdvmedecins.exceptions;
import java.io.Serializable;
import javax.ejb.ApplicationException;
@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException implements Serializable{
// pola prywatne
private int code = 0;
// konstruktory
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);
}
// metody pobierające i ustawiające
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- wiersz 7: klasa ta wywodzi się z klasy [RuntimeException]. Kompilator nie wymusza zatem obsługi tej klasy za pomocą instrukcji try / catch.
- wiersz 6: adnotacja @ApplicationException sprawia, że wyjątek nie zostanie „przejęty” przez wyjątek typu [EjbException].
Aby zrozumieć adnotację @ApplicationException, wróćmy do architektury stosowanej po stronie serwera:
![]() |
Wyjątek typu [RdvMedecinsException] zostanie wygenerowany przez metody klasy EJB warstwy [DAO] wewnątrz kontenera EJB3 i przechwycony przez ten kontener. Bez adnotacji @ApplicationException kontener EJB3 hermetyzuje wystąpiony wyjątek w wyjątku typu [EjbException] i ponownie go rzuca. Można nie chcieć tego enkapsulowania i pozwolić, aby z kontenera EJB3 wyszedł wyjątek typu [RdvMedecinsException]. Umożliwia to adnotacja @ApplicationException. Ponadto atrybut (rollback=true) tej adnotacji informuje kontener EJB3, że jeśli wyjątek typu [RdvMedecinsException] wystąpi wewnątrz metody wykonywanej w ramach transakcji z SGBD, należy ją cofnąć. W terminologii technicznej nazywa się to wykonaniem operacji rollback na transakcji.
3.4.4. acja EJB warstwy [DAO]
![]() |
![]() |
Interfejs Java [IDao] warstwy [DAO] ma następujący wygląd:
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 {
// lista klientów
public List<Client> getAllClients();
// lista lekarzy
public List<Medecin> getAllMedecins();
// lista terminów wizyt u lekarza
public List<Creneau> getAllCreneaux(Medecin medecin);
// lista wizyt u lekarza w danym dniu
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// znalezienie klienta na podstawie jego identyfikatora
public Client getClientById(Long id);
// znalezienie klienta na podstawie jego identyfikatora
public Medecin getMedecinById(Long id);
// znalezienie wizyty na podstawie jej identyfikatora
public Rv getRvById(Long id);
// znalezienie przedziału czasowego na podstawie identyfikatora
public Creneau getCreneauById(Long id);
// dodaj RV
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// usunąć RV
public void supprimerRv(Rv rv);
}
Interfejs ten został opracowany po zidentyfikowaniu wymagań warstwy [web]:
- wiersz 14: lista klientów. Będzie nam ona potrzebna do zasilania listy rozwijanej klientów,
- wiersz 16: lista lekarzy. Będzie nam potrzebna do zasilania listy rozwijanej lekarzy,
- wiersz 18: lista terminów wizyt u lekarza. Będzie nam ona potrzebna do wyświetlenia terminarza lekarza na dany dzień,
- wiersz 20: lista wizyt lekarza na dany dzień. W połączeniu z poprzednią metodą pozwoli nam to wyświetlić kalendarz lekarza na dany dzień wraz z już zarezerwowanymi terminami,
- wiersz 22: umożliwia wyszukanie klienta na podstawie jego numeru. Metoda ta pozwoli nam znaleźć klienta na podstawie wyboru z listy rozwijanej klientów,
- wiersz 24: to samo dotyczy lekarzy,
- wiersz 26: wyszukuje wizytę na podstawie jej numeru. Można z niej skorzystać podczas usuwania wizyty, aby wcześniej sprawdzić, czy rzeczywiście istnieje,
- wiersz 28: wyszukuje przedział czasowy na podstawie jego numeru. Pozwala zidentyfikować przedział, który użytkownik chce dodać lub usunąć,
- wiersz 30: służy do dodania wizyty,
- wiersz 32: służy do usunięcia wizyty.
Interfejs lokalny [IDaoLocal] modułu EJB jest po prostu pochodną poprzedniego interfejsu [IDao]:
package rdvmedecins.dao;
import javax.ejb.Local;
@Local
public interface IDaoLocal extends IDao{
}
To samo dotyczy interfejsu zdalnego [IDaoRemote]:
package rdvmedecins.dao;
import javax.ejb.Remote;
@Remote
public interface IDaoRemote extends IDao{
}
EJB [DaoJpa] implementuje oba interfejsy, lokalny i zdalny:
package rdvmedecins.dao;
...
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
- Wiersz 5 wskazuje, że interfejs zdalny EJB nosi nazwę „rdvmedecins.dao”. Ponadto adnotacja @Singleton (Java EE6) powoduje, że zostanie utworzona tylko jedna instancja EJB. Adnotacja @Stateless (Java EE5) definiuje obiekt EJB, który może być utworzony w wielu instancjach w celu zasilenia puli obiektów EJB,
- w wierszu 6 wskazano, że wszystkie metody klasy EJB są wykonywane w ramach transakcji zarządzanej przez kontener EJB3,
- wiersz 7 pokazuje, że klasa EJB implementuje interfejsy lokalny i zdalny oraz jest również serializowalna.
Pełny kod klasy EJB wygląda następująco:
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;
// lista klientów
public List<Client> getAllClients() {
try {
return em.createQuery("select rc from Client rc").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 1);
}
}
// lista lekarzy
public List<Medecin> getAllMedecins() {
try {
return em.createQuery("select rm from Medecin rm").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 2);
}
}
// lista terminów wizyt u danego lekarza
// lekarz: dany lekarz
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);
}
}
// lista wizyt u danego lekarza w danym dniu
// lekarz: dany lekarz
// dzień: dany dzień
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);
}
}
// dodanie wizyty
// dzień: dzień wizyty
// przedział czasowy: przedział czasowy wizyty
// pacjent: pacjent, dla którego umówiono wizytę
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);
}
}
// usunięcie terminu spotkania
// wizyta: usunięta wizyta
public void supprimerRv(Rv rv) {
try {
em.remove(em.merge(rv));
} catch (Throwable th) {
throw new RdvMedecinsException(th, 5);
}
}
// pobranie określonego klienta
public Client getClientById(Long id) {
try {
return (Client) em.find(Client.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// pobranie danych określonego lekarza
public Medecin getMedecinById(Long id) {
try {
return (Medecin) em.find(Medecin.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// pobranie określonej wizyty
public Rv getRvById(Long id) {
try {
return (Rv) em.find(Rv.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// pobierz określony termin
public Creneau getCreneauById(Long id) {
try {
return (Creneau) em.find(Creneau.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
}
- wiersz 22: obiekt EntityManager, który zarządza dostępem do kontekstu trwałości. Podczas instancjonowania klasy pole to zostanie zainicjowane przez kontener EJB dzięki adnotacji @PersistenceContext w wierszu 21,
- wiersz 27: zapytanie JPQL (Java Persistence Query Language), które zwraca wszystkie wiersze tabeli [clients] w postaci listy obiektów [Client],
- wiersz 36: analogiczne zapytanie dotyczące lekarzy,
- wiersz 46: zapytanie JPQL wykonujące połączenie tabel [creneaux] i [medecins]. Jest ono parametryzowane na podstawie identyfikatora lekarza,
- wiersz 57: zapytanie JPQL wykonujące połączenie tabel [rv], [creneaux] i [medecins] oraz posiadające dwa parametry: identyfikator lekarza i dzień wizyty,
- wiersze 69–73: utworzenie wizyty, a następnie zapisanie jej w bazie danych,
- wiersz 83: usunięcie wizyty z bazy danych,
- wiersz 92: wykonuje zapytanie select w bazie danych w celu znalezienia określonego klienta,
- wiersz 101: to samo dla lekarza,
- wiersz 110: to samo dla wizyty,
- wiersz 119: to samo dla przedziału czasowego,
- wszystkie operacje z kontekstem trwałości „em” z linii 22 mogą napotkać problem z bazą danych. Dlatego wszystkie są otoczone blokiem try / catch. Ewentualny wyjątek jest zamknięty w „własnym” wyjątku RdvMedecinsException.
3.4.5. Wdrożenie sterownika JDBC na podstawie MySQL
W poniższej architekturze:
![]() |
EclipseLink wymaga sterownika JDBC pochodzącego z MySQL. Należy go zainstalować w bibliotekach serwera Glassfish w folderze <glassfish>/domains/domain1/lib/ext, gdzie <glassfish> to folder instalacyjny serwera Glassfish. Można go uzyskać w następujący sposób:
![]() |
Katalog, w którym należy umieścić sterownik JDBC z pakietu MySQL, to <folder domen>[1]/domain1/lib/ext [2]. Ten sterownik jest dostępny w URL [http://www.mysql.fr/downloads/connector/j/]. Po jego zainstalowaniu należy ponownie uruchomić serwer Glassfish, aby uwzględnił tę nową bibliotekę.
3.4.6. Wdrożenie warstwy EJB z warstwy [DAO]
Wróćmy do architektury zbudowanej do tej pory:
![]() |
Zestaw [web, métier, DAO, JPA] należy wdrożyć na serwerze Glassfish. Wykonujemy to w następujący sposób:
![]() |
- w [1] kompilujemy projekt Maven,
- w [2] uruchamiamy go,
- w [3] został on wdrożony na serwerze Glassfish (zakładka [Services])
Można z ciekawością przejrzeć logi Glassfish:
![]() |
W przypadku [1] logi serwera Glassfish są dostępne w zakładce [Output / Glassfish Server 3+]. Są to następujące logi:
Wiersze oznaczone jako [Config] i [Précis] to logi z EclipseLink, natomiast te oznaczone jako [Infos] pochodzą z Glassfish.
- wiersze 1–12: EclipseLink przetwarza wykryte przez siebie encje JPA,
- wiersze 13–17: informacje wskazujące, że przetwarzanie encji JPA przebiegło prawidłowo,
- wiersz 18: EclipseLink zgłasza się,
- wiersz 19: EclipseLink rozpoznaje, że ma do czynienia z SGBD MySQL,
- wiersze 20–24: EclipseLink próbuje połączyć się z BD,
- wiersze 25–28: połączenie się powiodło,
- wiersze 29–33: próbuje ponownie nawiązać połączenie, tym razem korzystając konkretnie z platformy MySQL (wiersz 30),
- wiersze 34–37: również zakończyło się sukcesem,
- wiersz 38: potwierdzenie, że udało się utworzyć instancję jednostki trwałości [dbrdvmedecins-PU],
- wiersz 39: przenośne nazwy interfejsów zdalnego i lokalnego dla EJB i [DaoJpa], przy czym „przenośne” oznacza rozpoznawane przez wszystkie serwery aplikacji Java EE 6,
- wiersz 40: nazwy zdalnego i lokalnego interfejsu dla EJB [DaoJpa], specyficzne dla Glassfish. W nadchodzącym teście użyjemy nazwy „rdvmedecins.dao”.
Wiersze 39 i 40 są istotne. Podczas pisania klienta dla EJB na Glassfish należy je znać.
3.4.7. Testy EJB warstwy [DAO]
Teraz, gdy warstwa [DAO] naszej aplikacji została wdrożona, możemy ją przetestować. Zrobimy to w ramach aplikacji klient-serwer:
![]() |
Klient przetestuje zdalny interfejs komponentu EJB [DAO] wdrożonego na serwerze Glassfish.
Zaczynamy od utworzenia nowego projektu Maven o nazwie „ ”:
![]() |
- W [1] tworzymy nowy projekt,
- w [2,3] tworzymy projekt Maven typu [Java Application],
- w [4] nadajemy mu nazwę i umieszczamy go w tym samym folderze co EJB i [DAO],
![]() |
- w [5], w wygenerowanym projekcie,
- w pliku [6] wygenerowano klasę [App.java]. Usuniemy ją,
- w [7] wygenerowano gałąź [Source Packages]. Jeszcze się z nią nie spotkaliśmy. W tej gałęzi można umieścić testy JUnit. Zrobimy to. Nie zachowamy wygenerowanej klasy testowej [AppTest],
- w [8] znajdują się zależności projektu Maven. Gałąź [Dependencies] jest pusta. Będziemy musieli umieścić w niej nowe zależności. Gałąź [Test Dependencies] zawiera zależności niezbędne do testów. W tym przypadku wykorzystywana jest biblioteka frameworka JUnit 3.8. Będziemy musieli ją zmienić.
Projekt rozwija się w następujący sposób:
![]() |
- do [1] – projektu, w którym usunięto dwie wygenerowane klasy oraz zależność JUnit.
Wróćmy do architektury klient-serwer, która posłuży do testów:
![]() |
Klient musi znać interfejs zdalny oferowany przez EJB i [DAO]. Ponadto będzie wymieniać z EJB encje JPA. Potrzebuje zatem definicji tych elementów. Aby projekt testowy EJB miał dostęp do tych informacji, dodamy projekt EJB i [DAO] jako zależności do projektu:
![]() |
- w projekcie [1] dodajemy zależność od gałęzi [Test Dependencies],
- w [2] wybieramy kartę [Open Projects],
- w [3] wybieramy projekt Maven z EJB [DAO],
![]() |
- w [4] dodano zależność.
Wróćmy do architektury klient-serwer testu:
![]() |
Podczas działania klient i serwer komunikują się za pośrednictwem sieci TCP-IP. Nie będziemy programować tej komunikacji. Dla każdego serwera aplikacji istnieje biblioteka, którą należy zintegrować z zależnościami klienta. Ta dla Glassfish nosi nazwę [gf-client]. Dodajemy ją:
![]() |
- w [1] dodajemy zależność,
- w [2] podajemy specyfikację pożądanego artefaktu,
- w pliku [3] dodaje się bardzo wiele zależności. Maven je pobierze. Może to potrwać kilka minut. Następnie są one zapisywane w lokalnym repozytorium Mavena.
Teraz możemy utworzyć test JUnit:
![]() |
- w [2] klikamy prawym przyciskiem myszy na [Test Packages], aby utworzyć nowy test JUnit,
![]() |
- w [3], nadajemy nazwę klasie testowej oraz pakietowi dla niej [4],
- w [5] wybieramy framework JUnit 4.x,
- w [6] – wygenerowaną klasę testową,
- w [7] – nowe zależności projektu Maven.
Plik [pom.xml] ma wówczas następującą treść:
<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>
Warto zwrócić uwagę na:
- w wierszach 32–51 – zależności projektu,
- wiersze 13–26: zdefiniowano dwa repozytoria Maven, jedno dla EclipseLink (wiersze 14–19), drugie dla JUnit4 (wiersze 20–25).
Klasa testowa będzie wyglądać następująco:
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 {
// warstwa [dao] przetestowana
private static IDaoRemote dao;
// dzisiejsza data
Date jour = new Date();
@BeforeClass
public static void init() throws NamingException {
// inicjalizacja środowiska JNDI
InitialContext initialContext = new InitialContext();
// instancjonowanie warstwy DAO
dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
}
@Test
public void test1() {
// wyświetlanie klientów
List<Client> clients =dao.getAllClients();
display("Liste des clients :", clients);
// wyświetlanie lekarzy
List<Medecin> medecins =dao.getAllMedecins();
display("Liste des médecins :", medecins);
// wyświetlanie terminów wizyt u lekarza
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = dao.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// lista wizyt u lekarza w danym dniu
display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// dodaj RV
Rv rv = null;
Creneau creneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
rv = dao.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// dodaj RV w tym samym terminie tego samego dnia
// powinno spowodować wyjątek
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();
}
// odnotowuje się błąd
erreur=true;
}
// sprawdza się, czy wystąpił błąd
Assert.assertTrue(erreur);
// lista RV
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// usuń element RV
System.out.println("Suppression du Rv ajouté");
dao.supprimerRv(rv);
System.out.println("Rv supprimé");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
}
// metoda pomocnicza – wyświetla elementy kolekcji
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- wiersze 23–29: metoda oznaczona tagiem @BeforeClass jest wykonywana przed wszystkimi innymi. W tym miejscu tworzymy odwołanie do zdalnego interfejsu EJB [DaoJpa]. Przypomnijmy, że nadaliśmy jej nazwę JNDI „rdvmedecins.dao”,
- wiersze 34–35: wyświetlają listę klientów,
- wiersze 37–38: wyświetlają listę lekarzy,
- wiersze 40–42: wyświetlają terminy pierwszego lekarza,
- wiersz 44: wyświetla wizyty u pierwszego lekarza w dniu z wiersza 21,
- wiersze 46–51: dodają wizytę u pierwszego lekarza w jego przedziale czasowym nr 2 oraz w dniu z wiersza 21,
- wiersz 52: wyświetla w celach weryfikacyjnych wizyty pierwszego lekarza w dniu z wiersza 21. Musi być co najmniej jedna – ta, którą właśnie dodaliśmy,
- wiersze 55–70: dodajemy tę samą wizytę. Ponieważ tabela [RV] ma ograniczenie unikalności, dodanie to musi spowodować wyjątek. Sprawdzamy to w wierszu 70,
- wiersz 72: wyświetla w celu weryfikacji wizyty pierwszego lekarza w dniu z wiersza 21. Wizyta, którą chcieliśmy dodać, nie powinna się tam znajdować,
- wiersze 74–76: usuwamy jedyną wizytę, która została dodana,
- wiersz 77: wyświetla w celu weryfikacji wizyty pierwszego lekarza na dzień z wiersza 21. Ta, którą właśnie usunięto, nie powinna się tam znajdować.
Ten test jest fałszywym testem JUnit. Zawiera on tylko jedną asercję (wiersz 70). Jest to test wizualny z typowymi dla niego niedoskonałościami.
Jeśli wszystko jest w porządku, testy powinny zakończyć się powodzeniem:
![]() |
- w [1], tworzymy projekt testowy,
- w [2] uruchamia się test,
- w [3] test zakończył się powodzeniem.
Przyjrzyjmy się bliżej wynikom testu:
Zachęcamy czytelnika do zapoznania się z tymi logami równolegle z kodem, który je wygenerował. Zajmiemy się wyjątkiem, który wystąpił podczas dodawania już istniejącego terminu, w wierszach 41–49. Stos wyjątków jest przedstawiony w wierszach 42–48. Jest to nieoczekiwane. Wróćmy do kodu metody dodawania terminu:
// dodanie terminu spotkania
// dzień: dzień terminu spotkania
// przedział czasowy: przedział czasowy spotkania
// klient: klient, dla którego umówiono wizytę
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);
}
}
Przyjrzyjmy się logom Glassfish podczas dodawania obu spotkań:
- wiersz 2: przed pierwszym persist,
- wiersz 3: po pierwszym persist,
- wiersz 4: polecenie INSERT, które zostanie wykonane. Należy zauważyć, że nie odbywa się to w tym samym czasie co operacja persist. Gdyby tak było, ten wpis pojawiłby się przed wierszem 2. Operacja INSERT odbywa się zatem zazwyczaj na końcu transakcji, w ramach której wykonywana jest metoda,
- wiersz 6: EclipseLink pyta MySQL o ostatni użyty klucz pierwotny. Otrzyma klucz pierwotny dodanej wizyty. Wartość ta zostanie zapisana w polu id trwałej encji [Rv],
- wiersze 7–8: zapytanie SELECT, które wyświetli terminy wizyt lekarza,
- wiersze 9–10: wyświetlanie na ekranie drugiego zapytania persist,
- wiersze 11–12: polecenie INSERT, które zostanie wykonane. Powinno ono wywołać wyjątek. Wyjątek ten pojawia się w wierszach 15–16 i jest jasno widoczny. Został on początkowo wywołany przez sterownik JDBC z MySQL z powodu naruszenia ograniczenia unikalności wizyt. Wynika z tego, że wyjątki te powinny być widoczne w logach testu JUnit. Jednak tak nie jest:
Przypomnijmy architekturę klient-serwer tego testu:
![]() |
Gdy test EJB lub [DAO] generuje wyjątek, musi on zostać zserializowany, aby dotrzeć do klienta. Prawdopodobnie właśnie ta operacja zakończyła się niepowodzeniem z przyczyny, której nie zrozumiałem. Ponieważ nasza kompletna aplikacja nie będzie działać w modelu klient-serwer, możemy zignorować ten problem.
Teraz, gdy moduł EJB warstwy [DAO] działa poprawnie, możemy przejść do modułu EJB warstwy [métier].
3.5. Warstwa [métier]
Wróćmy do architektury tworzonej obecnie aplikacji:
![]() |
Stworzymy nowy projekt Maven dla warstw EJB i [métier]. Jak widać powyżej, będzie on zależny od projektu Maven, który został stworzony dla warstw [DAO] i [JPA].
3.5.1. Projekt NetBeans
Tworzymy nowy projekt Maven typu EJB. W tym celu wystarczy postępować zgodnie z procedurą, z której już korzystaliśmy i która została opisana na stronie 174.
![]() |
- w [1], projekt Maven warstwy [métier],
- w [2] dodajemy zależność,
- w [3] wybiera się projekt Maven warstw [DAO] i [JPA],
- w [4] wybieramy zakres [provided]. Przypominamy, że oznacza to, iż jest on potrzebny do kompilacji, ale nie do uruchomienia projektu. W rzeczywistościEJB z warstwy [métier] zostanie wdrożony na serwerze Glassfish wraz z EJB z warstw [DAO] i [JPA]. Zatem w momencie jego uruchomienia warstwa EJB, pochodząca z warstw [DAO] i [JPA], będzie już obecna,
![]() |
- w [6], nowym projekcie wraz z jego zależnością.
Przedstawmy teraz kod źródłowy warstwy [métier]:
![]() |
EJB [Metier] będzie posiadał następujący interfejs [IMetier]:
package rdvmedecins.metier.service;
import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
public interface IMetier {
// warstwa DAO
// lista klientów
public List<Client> getAllClients();
// lista lekarzy
public List<Medecin> getAllMedecins();
// lista przedziałów czasowych lekarza
public List<Creneau> getAllCreneaux(Medecin medecin);
// lista wizyt u lekarza w danym dniu
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// znalezienie klienta na podstawie jego identyfikatora
public Client getClientById(Long id);
// znalezienie klienta na podstawie jego identyfikatora
public Medecin getMedecinById(Long id);
// znalezienie wizyty na podstawie jej identyfikatora
public Rv getRvById(Long id);
// znalezienie przedziału czasowego na podstawie identyfikatora
public Creneau getCreneauById(Long id);
// dodaj RV
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// usunięcie RV
public void supprimerRv(Rv rv);
// zawód
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
}
Aby zrozumieć ten interfejs, należy przypomnieć sobie architekturę projektu:
![]() |
Zdefiniowaliśmy interfejs warstwy [DAO] (punkt 3.4.4) i wskazaliśmy, że odpowiada on na potrzeby warstwy [web], czyli na potrzeby użytkownika. Warstwa [web] komunikuje się z warstwą [DAO] wyłącznie za pośrednictwem warstwy [métier]. To wyjaśnia, dlaczego w warstwie [métier] znajdują się wszystkie metody warstwy [DAO]. Metody te będą jedynie przekazywać żądanie z warstwy [web] do warstwy [DAO]. To wszystko.
Podczas analizy aplikacji pojawiła się potrzeba: możliwość wyświetlenia na stronie internetowej terminarza lekarza na dany dzień, aby sprawdzić, które terminy są zajęte, a które wolne. Jest to typowa sytuacja, gdy sekretarka odpowiada na zapytanie telefoniczne. Pacjent prosi o umówienie wizyty na dany dzień u danego lekarza. Aby zaspokoić tę potrzebę, warstwa [métier] udostępnia metodę z linii 46.
// zawód
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
Można się zastanawiać, gdzie umieścić tę metodę:
- można by ją umieścić w warstwie [DAO]. Jednak metoda ta nie odpowiada tak naprawdę na potrzebę dostępu do danych, a raczej na potrzebę biznesową,
- można by ją umieścić w warstwie [web]. Byłby to jednak zły pomysł. Gdyby bowiem zmienić warstwę [web] na warstwę [Swing], utracilibyśmy tę metodę, podczas gdy zapotrzebowanie na nią nadal istnieje.
Metoda ta przyjmuje jako parametry nazwisko lekarza oraz dzień, dla którego potrzebny jest kalendarz rezerwacji. Zwraca obiekt [AgendaMedecinJour], który reprezentuje kalendarz lekarza na dany dzień:
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;
// pola
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// konstruktorzy
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());
}
// metody pobierające i ustawiające
...
}
- wiersz 12: lekarz, którego dotyczy kalendarz,
- wiersz 13: dzień, którego dotyczy harmonogram,
- wiersz 14: przedziały czasowe lekarza na ten dzień.
- klasa zawiera konstruktory (wiersze 17, 21) oraz dostosowaną metodę toString (wiersz 27).
Klasa [CreneauMedecinJour] (wiersz 14) ma następującą postać:
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;
// pola
private Creneau creneau;
private Rv rv;
// konstruktorów
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);
}
// metody pobierające i ustawiające
...
}
- wiersz 12: termin wizyty u lekarza,
- wiersz 13: powiązana wizyta, null, jeśli termin jest wolny.
Widać zatem, że pole creneauxMedecinJour w wierszu 14 klasy [AgendaMedecinJour] pozwala nam uzyskać wszystkie przedziały czasowe lekarza wraz z informacją „zajęty” lub „wolny” dla każdego z nich. Taki był cel nowej metody [getAgendaMedecinJour] interfejsu [IMetier].
Nasz komponent EJB [Metier] będzie posiadał interfejs lokalny oraz interfejs zdalny, które będą po prostu pochodnymi głównego interfejsu [IMetier]:
package rdvmedecins.metier.service;
import javax.ejb.Local;
@Local
public interface IMetierLocal extends IMetier{
}
package rdvmedecins.metier.service;
import javax.ejb.Remote;
@Remote
public interface IMetierRemote extends IMetier{
}
EJB i [Metier] implementują te interfejsy w następujący sposób:
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 {
// warstwa DAO
@EJB
private IDaoLocal dao;
public Metier() {
}
@Override
public List<Client> getAllClients() {
return dao.getAllClients();
}
@Override
public List<Medecin> getAllMedecins() {
return dao.getAllMedecins();
}
@Override
public List<Creneau> getAllCreneaux(Medecin medecin) {
return dao.getAllCreneaux(medecin);
}
@Override
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
return dao.getRvMedecinJour(medecin, jour);
}
@Override
public Client getClientById(Long id) {
return dao.getClientById(id);
}
@Override
public Medecin getMedecinById(Long id) {
return dao.getMedecinById(id);
}
@Override
public Rv getRvById(Long id) {
return dao.getRvById(id);
}
@Override
public Creneau getCreneauById(Long id) {
return dao.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return dao.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(Rv rv) {
dao.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour) {
// lista terminów wizyt u lekarza
List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
// lista rezerwacji tego samego lekarza na ten sam dzień
List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
// tworzymy słownik na podstawie zarezerwowanych wizyt
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// tworzy się kalendarz na wybrany dzień
AgendaMedecinJour agenda = new AgendaMedecinJour();
// lekarz
agenda.setMedecin(medecin);
// dzień
agenda.setJour(jour);
// przedziały czasowe rezerwacji
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// wypełnianie przedziałów czasowych rezerwacji
for (int i = 0; i < creneauxHoraires.size(); i++) {
// wiersz w kalendarzu
creneauxMedecinJour[i] = new CreneauMedecinJour();
// identyfikator terminu
creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
// czy przedział jest wolny czy zarezerwowany?
if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
// przedział jest zajęty – odnotowuje się rezerwację
Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
creneauxMedecinJour[i].setRv(resa);
}
}
// zwracamy wynik
return agenda;
}
}
- w wierszu 22 klasa [Metier] jest singletonem klasy EJB,
- w wierszu 23 każda metoda klasy EJB jest wykonywana w ramach transakcji. Oznacza to, że transakcja rozpoczyna się na początku metody w warstwie [métier]. Ta z kolei wywoła metody z warstwy [DAO]. Będą one realizowane w ramach tej samej transakcji,
- w wierszu 24 warstwa EJB implementuje swoje interfejsy lokalne i zdalne, a ponadto jest serializowalna,
- wiersz 27: odwołanie do klasy EJB z warstwy [DAO],
- wiersz 29: zostanie ona wstrzyknięta przez kontener EJB serwera Glassfish dzięki adnotacji @EJB. Zatem podczas wykonywania metod klasy [Metier] odwołanie do klasy EJB z warstwy [DAO] zostało zainicjowane,
- wiersze 33–81: odwołanie to służy do przekazania warstwie [DAO] wywołania skierowanego do warstwy [métier],
- wiersz 84: metoda getAgendaMedecinJour, która pozwala uzyskać harmonogram wizyt lekarza na dany dzień. Pozostawiamy czytelnikowi śledzenie komentarzy.
3.5.2. Wdrożenie warstwy [métier]
Warstwa [métier] jest zależna od warstwy [DAO]. Każda warstwa została zaimplementowana przy użyciu EJB. Aby przetestować warstwę EJB oraz [métier], musimy wdrożyć obie warstwy EJB. W tym celu potrzebujemy projektu korporacyjnego.
![]() |
- [1], tworzymy nowy projekt,
- typu Maven [2] oraz Aplikacja korporacyjna [3],
- nadajemy mu nazwę [4]. Sufiks ear zostanie dodany automatycznie,
![]() |
- w [5] wybieramy serwer Glassfish i wersję Javy EE 6,
- na [6]; aplikacja korporacyjna zawiera moduły, zazwyczaj moduły EJB i moduły internetowe. W tym przypadku aplikacja korporacyjna będzie zawierała moduły obu EJB, które stworzyliśmy. Ponieważ moduły te już istnieją, nie zaznaczamy pól wyboru,
- w [7,8] utworzono dwa projekty. [8] to projekt korporacyjny, z którego będziemy korzystać. [7] to projekt, którego przeznaczenia nie znam. Nie musiałem z niego korzystać, a ponieważ nie zagłębiałem się w Maven, nie wiem, do czego może służyć. Zignorujemy go więc.
Teraz, gdy projekt firmowy został utworzony, możemy zdefiniować jego moduły.
![]() |
- w [1] tworzymy nową zależność,
- w pliku [2] wybieramy projekt EJB [DAO],
- w [3] deklaruje się, że jest to EJB. Nie należy pozostawiać typu pustego, ponieważ w takim przypadku zostanie użyty typ jar, a ten typ nie jest tutaj odpowiedni,
- w przypadku [4] stosuje się zakres [compile],
- w [5] projekt wraz z nową zależnością,
![]() |
- w [6, 7, 8] zaczynamy od nowa, aby dodać EJB z warstwy [métier],
- do [9], obie zależności,
- w [10], kompilujemy projekt,
![]() |
- w [11] uruchamia się go,
- w [12], w zakładce [Services] widać, że projekt został wdrożony na serwerze Glassfish. Oznacza to, że oba komponenty EJB znajdują się teraz na serwerze.
W logach serwera Glassfish znajdują się informacje dotyczące wdrożenia obu EJB:
![]() |
- w [1], w zakładce dzienników serwera Glassfish.
Znajdują się tam następujące logi:
- wiersze 1–5: rozpoznano encje JPA,
- wiersz 7: wskazuje, że utworzenie jednostki trwałości [dbrdvmedecins2-PU] zakończyło się powodzeniem i nawiązano połączenie z powiązaną bazą danych,
- wiersz 8: przenośne nazwy interfejsów zdalnego i lokalnego dla EJB, [DaoJpa] oraz portable oznaczają, że są one rozpoznawane przez wszystkie serwery aplikacji,
- wiersz 9: to samo, ale z nazwami zastrzeżonymi dla Glassfish,
- wiersze 10–11: to samo dotyczy EJB i [Metier].
Zatrzymamy przenośną nazwę zdalnego interfejsu dla EJB i [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
Będziemy jej potrzebować podczas testowania warstwy [métier].
3.5.3. Testowanie warstwy [métier]
Podobnie jak w przypadku warstwy [DAO], przetestujemy warstwę [métier] w ramach aplikacji klient-serwer:
![]() |
Klient przetestuje zdalny interfejs warstwy EJB [Metier] wdrożonej na serwerze Glassfish.
Zaczynamy od utworzenia nowego projektu Maven. W tym celu postępujemy zgodnie z procedurą zastosowaną przy tworzeniu projektu testowego warstwy [dao] (patrz punkt 3.4.7), z wyłączeniem tworzenia testu JUnit. Utworzony w ten sposób projekt wygląda następująco
![]() |
- w [1], utworzony projekt wraz z jego zależnościami: względem projektu EJB z warstwy [dao], względem projektu EJB z warstwy [métier], biblioteki [gf-client].
W tym momencie plik [pom.xml] projektu wygląda następująco:
<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>
Należy upewnić się, że wszystkie zależności opisane w wierszach 17–33 są obecne. Testem będzie prosta klasa konsolowa:
![]() |
Kod klasy [ClientRdvMedecinsMetier] wygląda następująco:
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 {
// nazwa zdalnego interfejsu 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";
// dzisiejsza data
private static Date jour = new Date();
public static void main(String[] args) {
try {
// kontekst serwera Glassfish JNDI
InitialContext initialContext = new InitialContext();
// odwołanie do warstwy zdalnej [metier]
IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
// wyświetlanie klientów
List<Client> clients = metier.getAllClients();
display("Liste des clients :", clients);
// wyświetlanie lekarzy
List<Medecin> medecins = metier.getAllMedecins();
display("Liste des médecins :", medecins);
// wyświetlanie terminów wizyt u lekarza
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = metier.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// lista wizyt danego lekarza w danym dniu
display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// wyświetlanie kalendarza
AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// dodaj 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));
// wyświetlanie kalendarza
agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// usunięcie 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));
// wyświetlanie kalendarza
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();
}
}
}
// metoda pomocnicza – wyświetla elementy kolekcji
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- wiersz 18: przenośna nazwa zdalnego interfejsu EJB [Metier] została pobrana z logów Glassfish,
- wiersze 24–27: uzyskano odwołanie do zdalnego interfejsu EJB [Metier],
- wiersze 29–30: wyświetlają klientów,
- wiersze 32–33: wyświetlają lekarzy,
- wiersze 35–37: wyświetlają terminy wizyt u lekarza,
- wiersz 39: wyświetla wizyty danego lekarza w danym dniu,
- wiersze 41–42: kalendarz tego samego lekarza na ten sam dzień,
- wiersze 44–49: dodaje się wizytę,
- wiersz 50: wyświetla się listę wizyt u lekarza. Powinna pojawić się tam jedna dodatkowa wizyta,
- wiersze 52–53: wyświetla się kalendarz lekarza. Powinna być widoczna dodana wizyta,
- wiersze 55–57: usuwa się właśnie dodaną wizytę,
- wiersz 58: zmiana ta musi znaleźć odzwierciedlenie na liście wizyt lekarza,
- wiersze 60–61: oraz w jego kalendarzu.
Przeprowadzamy test:
![]() | ![]() |
Otrzymane ekrany ( ) przedstawiają się następująco:
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]]
- wiersz 37: kalendarz pani PELISSIER z dnia 23 maja 2012 r. Żaden termin nie jest zarezerwowany,
- wiersz 39: dodanie spotkania,
- wiersz 42: nowy kalendarz pani PELISSIER. Jeden termin jest teraz zarezerwowany dla pana MARTIN,
- wiersz 44: spotkanie zostało usunięte,
- wiersz 46: kalendarz pani PELISSIER wskazuje, że żaden termin nie jest zarezerwowany.
Uznajemy zatem, że warstwy [DAO] i [métier] są gotowe do działania. Pozostaje nam jeszcze napisać warstwę [web] przy użyciu frameworka JSF. W tym celu wykorzystamy wiedzę zdobytą na początku niniejszego dokumentu.
3.6. Warstwa [web]
Wróćmy do architektury, nad którą obecnie pracujemy:
![]() |
Zbudujemy ostatnią warstwę, czyli warstwę [web].
3.6.1. Projekt NetBeans
Tworzymy projekt Maven:
![]() |
- w [1] tworzymy nowy projekt,
- w [2, 3] tworzymy projekt Maven typu [Web Application],
- w [4] nadajemy mu nazwę,
![]() |
- w [5] wybieramy serwer Glassfish i Java EE 6 Web,
- w [6], tak utworzony projekt,
- w [7], projekt po usunięciu strony [index.jsp] oraz pakietu znajdującego się w [Source Packages],
![]() |
- w [8, 9], w właściwościach projektu dodajemy framework,
- w [10] wybiera się Java Server Faces,
![]() |
- w pliku [11] konfigurację Java Server Faces. Pozostawiamy wartości domyślne. Zwracamy uwagę, że używany jest plik JSF 2,
- w [12] projekt zostaje następnie zmodyfikowany w dwóch punktach: generowany jest plik [web.xml] oraz strona [index.html].
Plik [web.xml] ma następującą treść:
<?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>
Ten plik już znamy.
- wiersze 7–11: definiują serwlet, który będzie przetwarzał wszystkie żądania kierowane do aplikacji. Jest to serwlet JSF,
- wiersze 12–15: definiują URL przetwarzane przez ten serwlet. Są to URL o postaci /faces/*,
- wiersze 21–23: definiują stronę [index.xhtml] jako stronę główną.
Strona ta wygląda następująco:
<?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>
Już ją znamy. Możemy uruchomić ten projekt:
![]() |
- w [1], uruchamiamy projekt i otrzymujemy wynik [2] w przeglądarce.
Przedstawiamy teraz cały projekt, a następnie omówimy szczegółowo jego poszczególne elementy.
![]() |
- na [1], strony projektu XHTML,
- w [2] – kody Java,
- w [3] – pliki komunikatów, ponieważ aplikacja jest zinternacjonalizowana,
![]() |
- w pliku [4] – zależności projektu.
3.6.2. Zależności projektu
Wróćmy do architektury projektu:
![]() |
Warstwa JSF opiera się na warstwach [métier], [DAO] oraz [JPA]. Te trzy warstwy są zawarte w dwóch projektach Maven, które stworzyliśmy, co wyjaśnia zależności projektu [4]. Pokażmy po prostu, w jaki sposób dodaje się te zależności:
![]() |
- w projekcie [1] wprowadzimy „ejb”, aby wskazać, że zależność dotyczy projektu EJB,
- w [2] wpiszemy [provided]. Projekt internetowy zostanie bowiem wdrożony jednocześnie z dwoma projektami EJB. Nie ma więc potrzeby dołączania plików JAR z projektu EJB.
3.6.3. Konfiguracja projektu
Konfiguracja projektu jest taka sama jak w przypadku projektów JSF, które omówiliśmy na początku niniejszego dokumentu. Wymieniamy pliki konfiguracyjne bez ponownego ich wyjaśniania.
![]() | ![]() |
[web.xml]: konfiguruje aplikację internetową.
<?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>
Warto zwrócić uwagę, że w wierszu 26 strona [index.xhtml] jest stroną główną aplikacji.
[faces-config.xml]: konfiguruje aplikację 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]: pusta, ale niezbędna dla adnotacji @Named
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>
[styles.css]: arkusz stylów aplikacji
.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]: plik komunikatów w języku francuskim
# układ
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers
layout.entete.langue1=Fran\u00e7ais
layout.entete.langue2=Anglais
# wyjątek
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
# formularz 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
# formularz 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
# formularz 3
form3.titre=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,liczba,#00} - {6,liczba,#00}:{7,liczba,#00}
form3.titre_detail=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,liczba,#00} - {6,liczba,#00}:{7,liczba,#00}
form3.client=Client
form3.valider=Valider
form3.annuler=Annuler
# błąd
erreur.titre=Une erreur s'est produite.
erreur.message=Message d'erreur
erreur.accueil=Page d'accueil
erreur.classe=Cause
[messages_en.properties]: plik komunikatów w języku angielskim
# układ
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university
layout.entete.langue1=French
layout.entete.langue2=English
# wyjątek
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
# formularz 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
# formularz 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
# formularz 3
form3.titre=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.titre_detail=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,liczba,#00} - {6,liczba,#00}:{7,liczba,#00}
form3.client=Client
form3.valider=Submit
form3.annuler=Cancel
# błąd
erreur.titre=An error occurred
erreur.message=Error message
erreur.accueil=Welcome Page
erreur.classe=Cause
3.6.4. Widoki projektu
Przypomnijmy sobie, jak działa aplikacja. Strona główna wygląda następująco:
![]() |
Z tej pierwszej strony użytkownik (sekretariat, lekarz) będzie wykonywał szereg czynności. Przedstawiamy je poniżej. Widok po lewej stronie przedstawia ekran, z którego użytkownik składa wniosek, a widok po prawej stronie – odpowiedź wysłaną przez serwer.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Wreszcie można również uzyskać stronę z komunikatami o błędach:
![]() |
Te różne widoki uzyskuje się za pomocą następujących stron projektu internetowego:
![]() |
- w [1] strony [basdepage, entete, layout] zapewniają formatowanie wszystkich widoków,
- w [2] widok generowany jest przez stronę [layout.xhtml].
W tym przypadku wykorzystano technologię faceletów. Została ona opisana w punkcie 2.11. Ograniczamy się do podania kodu stron XHTML wykorzystywanych do formatowania:
[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>
Warto zwrócić uwagę na wiersze 10–12, gdzie znajdują się dwa linki służące do zmiany języka aplikacji.
[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>
Ta strona jest szablonem (template) strony [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>
Wiersze 8–21 definiują obszar o nazwie „treść” (wiersz 8) w pliku [layout.xhtml] (wiersz 7). Jest to centralny obszar widoków:
![]() |
Strona [index.xhtml] jest jedyną stroną aplikacji. Nie będzie więc żadnej nawigacji między stronami. Wyświetla ona jedną z czterech stron [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. Wyświetlanie to jest kontrolowane przez cztery zmienne logiczne [form1Rendered, form2Rendered, form3Rendered, erreurRendered] z bean formularza, które opiszemy wkrótce.
3.6.5. Beany projektu
![]() |
Klasy z pakietu [utils] zostały już przedstawione:
- klasa [ChangeLocale] odpowiada za zmianę języka. Została ona już omówiona (punkt 2.4.4).
- klasa [Messages] to klasa ułatwiająca internacjonalizację komunikatów aplikacji. Została ona omówiona w punkcie 2.8.5.7.
3.6.5.1. Bean Application
Bean [Application] ma następującą postać:
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{
// warstwa biznesowa
@EJB
private IMetierLocal metier;
// pamięć podręczna
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>();
// błędy
private List<Erreur> erreurs = new ArrayList<Erreur>();
private Boolean erreur = false;
public Application() {
}
@PostConstruct
public void init() {
// lekarze i klienci są zapisywani w pamięci podręcznej
try {
medecins = metier.getAllMedecins();
clients = metier.getAllClients();
} catch (Throwable th) {
// odnotowuje się błąd
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;
}
// weryfikacja list
if (medecins.size() == 0) {
// odnotowuje się błąd
erreur = true;
erreurs.add(new Erreur("", "La liste des médecins est vide"));
}
if (clients.size() == 0) {
// odnotowuje się błąd
erreur = true;
erreurs.add(new Erreur("", "La liste des clients est vide"));
}
// błąd?
if (erreur) {
return;
}
// słowniki
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
}
}
// metody pobierające i ustawiające
...
}
- wiersze 15–16: klasa [Application] jest beanem o zasięgu aplikacji. Jest tworzona jednorazowo na początku cyklu życia aplikacji JSF i jest dostępna dla wszystkich żądań wszystkich użytkowników. Zazwyczaj umieszcza się w niej dane tylko do odczytu. W tym przypadku umieścimy w nim listę lekarzy oraz listę klientów. Zakładamy zatem, że listy te nie ulegają częstym zmianom. Strony XHTML mają do nich dostęp poprzez nazwę aplikacji,
- wiersze 20–21: odniesienie do lokalnego interfejsu EJB [Metier] zostanie wstrzyknięte przez kontener EJB z Glassfish. Przypomnijmy sobie architekturę aplikacji:
![]() |
Aplikacje JSF oraz EJB i [Metier] będą działać w tej samej maszynie wirtualnej Java (JVM). Zatem warstwa [JSF] będzie korzystać z lokalnego interfejsu warstwy EJB. W tym przypadku bean aplikacji korzysta z warstw EJB i [Metier]. Nawet gdyby tak nie było, normalne byłoby znalezienie tam odwołania na warstwie [métier]. Jest to bowiem informacja, która może być wspólna dla wszystkich zapytań wszystkich użytkowników, a zatem dane o zasięgu Application.
- wiersze 34–35: metoda init jest wykonywana zaraz po instancjonowaniu klasy [Application] (obecność adnotacji @PostConstruct),
- W wierszach 36–73 metoda tworzy następujące elementy: listę lekarzy w wierszu 23, listę klientów w wierszu 24, słownik lekarzy indeksowany według ich identyfikatorów w wierszu 25 oraz analogiczny słownik klientów w wierszu 26. Mogą wystąpić błędy. Są one rejestrowane na liście w wierszu 28.
Klasa [Erreur] ma następującą postać:
package beans;
public class Erreur {
public Erreur() {
}
// pole
private String classe;
private String message;
// konstruktor
public Erreur(String classe, String message){
this.setClasse(classe);
this.message=message;
}
// metody pobierające i ustawiające
...
}
- w wierszu 9 – nazwa klasy wyjątku, jeśli został zgłoszony wyjątek,
- wiersz 10: komunikat o błędzie.
3.6.5.2. Bean [Form]
Jego kod wygląda następująco:
package beans;
...
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// bean aplikacji
@Inject
private Application application;
// model
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private String form3Titre;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneau;
private Medecin medecin;
private Client client;
private Long idClient;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
@PostConstruct
private void init() {
// czy inicjalizacja przebiegła pomyślnie?
if (application.getErreur()) {
// pobieramy listę błędów
erreurs = application.getErreurs();
// wyświetlany jest widok błędów
setForms(false, false, false, true);
}
}
// wyświetlanie widoku
private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
this.form1Rendered = form1Rendered;
this.form2Rendered = form2Rendered;
this.form3Rendered = form3Rendered;
this.erreurRendered = erreurRendered;
}
.................................................
}
- wiersze 5–7: klasa [Form] jest beanem o nazwie „form” i zasięgu sesji. Należy pamiętać, że w takim przypadku klasa musi być serializowalna.
- wiersze 13–14: bean „form” posiada odwołanie do beana „application”. Odwołanie to zostanie wstrzyknięte przez kontener serwletów, w którym działa aplikacja (obecność adnotacji @Inject).
- wiersze 17–31: szablon stron [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. Wyświetlanie tych stron jest kontrolowane przez wartości logiczne w wierszach 19–22. Należy zauważyć, że domyślnie wyświetlana jest strona [form1.xhtml],
- wiersze 33–34: metoda init jest wykonywana zaraz po instancjonowaniu klasy (obecność adnotacji @PostConstruct),
- wiersze 35–41: metoda init służy do ustalenia, która strona ma zostać wyświetlona jako pierwsza: zazwyczaj jest to strona [form1.xhtml] (wiersz 19), chyba że inicjalizacja aplikacji przebiegła nieprawidłowo (wiersz 36), w którym to przypadku wyświetlana jest strona [erreur.xhtml] (wiersz 40).
Strona [erreur.xhtml] wygląda następująco:
<?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>
Wykorzystuje ona tag <h:dataTable> (wiersze 14–27) do wyświetlenia listy błędów. W rezultacie powstaje strona podobna do poniższej:

Teraz zdefiniujemy różne fazy cyklu życia aplikacji.
3.6.6. Interakcje między stronami a modelem
3.6.6.1. Wyświetlanie strony głównej
Jeśli wszystko przebiegnie pomyślnie, pierwszą wyświetlaną stroną jest [form1.xhtml]. Wygląda to następująco:
![]() |
Strona [form1.xhtml] wygląda następująco:
<?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>
Ta strona jest oparta na następującym szablonie:
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
// Bean aplikacji
@Inject
private Application application;
// szablon
private Long idMedecin;
private Date jour = new Date();
// lista lekarzy
public List<Medecin> getMedecins() {
return application.getMedecins();
}
// kalendarz
public void getAgenda() {
...
}
- Pole w wierszu 9 odczytuje i zapisuje wartość z listy w wierszu 18 strony. Przy pierwszym wyświetleniu strony ustala ona wartość wybraną w polu kombi. Przy pierwszym wyświetleniu idMedecin jest równe null, więc wybrany zostanie pierwszy lekarz,
- metoda z wierszy 13–15 generuje elementy listy rozwijanej lekarzy (wiersz 19 strony). Każda wygenerowana opcja będzie miała jako etykietę (itemLabel) tytuł, nazwisko i imię lekarza, a jako wartość (itemValue) identyfikator lekarza,
- pole w wierszu 10 zasilają w trybie odczytu/zapisu pole wprowadzania danych w wierszu 21 strony. Przy pierwszym wyświetleniu widoczna jest zatem bieżąca data,
- wiersze 17–19: metoda getAgenda obsługuje kliknięcie przycisku [Agenda] w wierszu 26 strony. Ponieważ nie ma nawigacji (zawsze wywoływana jest strona [index.html]), często zamiast atrybutu action używa się atrybutu actionListener. W takim przypadku metoda wywołana w szablonie nie zwraca żadnego wyniku.
Po kliknięciu przycisku [Agenda]
- przesyłane są wartości: wartość wybrana z listy rozwijanej lekarzy jest zapisywana w polu idMedecin szablonu, a dzień wybrany w polu „dzień”,
- wywoływana jest metoda getAgenda szablonu.
Metoda getAgenda wygląda następująco:
// bean aplikacji
@Inject
private Application application;
// szablon
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;
// kalendarz
public void getAgenda() {
try {
// wyszukiwanie lekarza
medecin = application.gethMedecins().get(idMedecin);
// nazwa formularza 2
form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
// kalendarz lekarza na dany dzień
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// wyświetlanie formularza 2
setForms(false, true, false, false);
} catch (Throwable th) {
// podgląd błędów
prepareVueErreur(th);
}
}
// przygotowanie vueErreur
private void prepareVueErreur(Throwable th) {
// tworzy się listę błędów
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()));
}
// wyświetlany jest widok błędów
setForms(false, false, false, true);
}
Przypomnijmy, co powinna wyświetlać metoda getAgenda:
![]() |
- wiersz 21: pobieramy lekarza wybranego ze słownika lekarzy, który został zapisany w bean application. W tym celu wykorzystujemy jego identyfikator, który został przesłany w idMedecin,
- wiersz 23: przygotowuje się tytuł strony [form2.xhtml], która ma zostać wyświetlona. Komunikat ten jest pobierany z pliku komunikatów, aby umożliwić jego internacjonalizację. Technika ta została opisana w paragrafie 2.8.5.7, strona 135.
- wiersz 25: wywoływana jest warstwa [métier] w celu obliczenia harmonogramu wybranego lekarza na wybrany dzień,
- wiersz 27: wyświetlany jest plik [form2.xhtml],
- wiersz 28: w przypadku wystąpienia wyjątku tworzona jest lista błędów (wiersze 37–42), a następnie wyświetlana jest strona [erreur.xhtml] (wiersz 44).
3.6.6.2. Wyświetlenie harmonogramu lekarza
Strona [form2.xhtml] odpowiada następującemu widokowi:
![]() |
Kod strony [form2.xhtml] wygląda następująco:
<?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>
Przypomnijmy, że metoda getAgenda zainicjowała dwa pola w modelu:
// szablon
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
Te dwa pola zasilają stronę [form2.xhtml]:
- wiersz 10 – tytuł strony,
- wiersz 12: harmonogram lekarza jest wyświetlany za pomocą znacznika <h:dataTable> w trzech kolumnach,
- wiersze 13–18: w pierwszej kolumnie wyświetlane są terminy,
- wiersze 19–30: w drugiej kolumnie wyświetlane jest imię i nazwisko klienta, który ewentualnie zarezerwował dany przedział czasowy, a w przeciwnym razie pozostaje puste miejsce. Aby dokonać tego wyboru, wykorzystuje się tagi z biblioteki JSTL Core, do której odwołuje się wiersz 7,
- wiersze 30–35: trzecia kolumna wyświetla link [Réserver], jeśli przedział czasowy jest wolny, oraz link [Supprimer], jeśli przedział czasowy jest zajęty.
Linki w trzeciej kolumnie są powiązane z następującym szablonem:
// szablon
private Long idCreneau;
// działanie na RV
public void action() {
...
}
- metoda action jest wywoływana, gdy użytkownik kliknie link „Zarezerwuj / Usuń” (wiersz 32). Należy zauważyć, że użyto tutaj atrybutu action. Metoda, do której odwołuje się ten atrybut, powinna mieć sygnaturę String action(), ponieważ musi ona zwracać klucz nawigacyjny. Jednak w tym przypadku ma ona sygnaturę void action(). Nie spowodowało to błędu i można założyć, że w tym przypadku nie ma nawigacji. Tak właśnie miało być. Zastąpienie action na actionListener powodowało nieprawidłowe działanie,
- pole idCreneau w wierszu 2 pobierze identyfikator przedziału czasowego linku, który został kliknięty (wiersz 33 strony).
3.6.6.3. Usuwanie terminu
Przyjrzyjmy się kodowi obsługującemu usuwanie terminu. Odpowiada to następującej sekwencji widoków:
![]() |
Kod związany z tą operacją wygląda następująco:
// komponent Application
@Inject
private Application application;
// szablon
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;
// działanie na RV
public void action() {
// szukamy wolnego terminu w kalendarzu
int i = 0;
Boolean trouvé = false;
while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
trouvé = true;
} else {
i++;
}
}
// czy coś znaleziono?
if (!trouvé) {
// to dziwne – ponownie wyświetlamy form2
setForms(false, true, false, false);
return;
}
// znaleziono
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// zgodnie z wybraną czynnością
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// rezerwacja
public void reserver() {
...
}
public void supprimer() {
try {
// usunięcie spotkania
application.getMetier().supprimerRv(creneauChoisi.getRv());
// aktualizujemy kalendarz
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// wyświetlono form2
setForms(false, true, false, false);
} catch (Throwable th) {
// widok błędów
prepareVueErreur(th);
}
}
- wiersz 16: gdy uruchamia się metoda action, identyfikator wybranego przedziału czasowego został zapisany w idCreneau (wiersz 11),
- wiersze 18–26: próbuje się pobrać przedział czasowy na podstawie jego identyfikatora z pliku id (wiersz 21). Szukamy go w bieżącym kalendarzu, agendaMedecinJour z wiersza 10. Zazwyczaj powinniśmy go znaleźć. Jeśli tak nie jest, nie podejmujemy żadnych działań (wiersze 28–32),
- wiersz 34: jeśli znaleziono poszukiwany przedział czasowy, pobieramy jego identyfikator i zapisujemy go w wierszu 12,
- wiersz 36: sprawdzamy, czy w wybranym przedziale czasowym było już spotkanie. Jeśli tak, usuwamy je (wiersz 39), w przeciwnym razie rezerwujemy spotkanie (wiersz 37),
- wiersz 51: wizyta w wybranym przedziale czasowym zostaje usunięta. Zadanie to wykonuje warstwa [métier],
- wiersz 53: żądamy od warstwy [métier] nowego kalendarza lekarza. Oczywiście zobaczymy w nim o jedną wizytę mniej. Ponieważ jednak aplikacja jest wieloosobowa, możemy dostrzec zmiany wprowadzone przez innych użytkowników,
- wiersz 55: ponownie wyświetlamy stronę [form2.xhtml],
- wiersz 58: ponieważ wywołano warstwę [métier], mogą wystąpić wyjątki. W takim przypadku zapisujemy stos wyjątków na liście błędów w wierszu 13 i wyświetlamy je za pomocą widoku [erreur.xhtml].
3.6.6.4. Umówienie wizyty
Umówienie wizyty przebiega zgodnie z następującą sekwencją:
![]() |
Szablon wykorzystywany w tej operacji to:
// szablon
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;
// operacja na RV
public void action() {
...
// znaleziono
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// zgodnie z wybraną akcją
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// rezerwacja
public void reserver() {
try {
// nazwa formularza 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();
// klient wybrany z listy rozwijanej
idClient=null;
// wyświetlany jest formularz 3
setForms(false, false, true, false);
} catch (Throwable th) {
// widok błędów
prepareVueErreur(th);
}
}
- wiersz 14: jeśli w wybranym przedziale czasowym nie ma żadnej wizyty, to jest to rezerwacja,
- wiersz 30: przygotowuje się tytuł strony [form3.xhtml] przy użyciu tej samej techniki, co w przypadku tytułu strony [form2.xhtml],
- wiersz 34: w tym formularzu znajduje się lista rozwijana, której wartość jest pobierana z idClient. Ustawiamy wartość tego pola na null, aby nie wybierać nikogo,
- wiersz 36: wyświetla się strona [form3.xhtml],
- wiersz 39: lub stronę błędów, jeśli wystąpił wyjątek.
Strona [form3.xhtml] wygląda następująco:
<?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>
Ta strona jest generowana na podstawie następującego szablonu:
// bean aplikacji
@Inject
private Application application;
// szablon
private Long idClient;
// lista klientów
public List<Client> getClients() {
return application.getClients();
}
- wiersz 6: numer klienta zasilają atrybut value w polu rozwijanym klientów w wierszu 12 strony. Określa on wybrany element tego pola rozwijanego,
- wiersze 9–11: metoda getClients wypełnia zawartość listy rozwijanej (wiersz 13). Nazwa (itemLabel) każdej opcji to [Titre Prénom Nom] klienta, a powiązana wartość (itemValue) to identyfikator klienta. To właśnie ta wartość zostanie przesłana.
3.6.6.5. Potwierdzenie terminu spotkania
Potwierdzenie terminu spotkania przebiega zgodnie z następującą sekwencją:
![]() |
i odpowiada kliknięciu przycisku [Valider]:
<h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
Zatem zdarzeniem tym zajmie się metoda [Form].validerRv. Jej kod wygląda następująco:
// bean aplikacji
@Inject
private Application application;
// szablon
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;
// weryfikacja terminu spotkania
public void validerRv() {
try {
// pobieramy instancję wybranego przedziału czasowego
Creneau creneau = application.getMetier().getCreneauById(idCreneau);
// dodajemy spotkanie
application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
// aktualizujemy kalendarz
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// wyświetla się form2
setForms(false, true, false, false);
} catch (Throwable th) {
// wyświetlanie błędów
prepareVueErreur(th);
}
}
- wiersz 12: zanim metoda validerRv zostanie wykonana, pole idClient otrzymało identyfikator klienta wybranego przez użytkownika,
- wiersz 19: na podstawie identyfikatora przedziału czasowego zapisanego w poprzednim etapie (bean ma zasięg sesji) zwracamy się do warstwy [métier] o odwołanie do samego przedziału czasowego,
- wiersz 21: zwracamy się do warstwy [métier] o dodanie wizyty na wybrany dzień (dzień), wybrany przedział czasowy (przedział) oraz wybranego klienta (idClient),
- wiersz 23: wysyłamy żądanie do warstwy [métier] o odświeżenie kalendarza lekarza. Zobaczymy dodaną wizytę oraz wszystkie zmiany, które mogli wprowadzić inni użytkownicy aplikacji,
- wiersz 25: ponownie wyświetla się kalendarz [form2.xhtml],
- wiersz 28: wyświetla stronę błędu, jeśli wystąpi błąd.
3.6.6.6. Anulowanie wizyty
Odpowiada to następującej sekwencji:
![]() |
Przycisk [Annuler] na stronie [form3.xhtml] wygląda następująco:
<h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
Wywoływana jest zatem metoda [Form].annulerRv:
// anulowanie terminu
public void annulerRv() {
// wyświetla się formularz 2
setForms(false, true, false, false);
}
3.6.6.7. Powrót do strony głównej
Pozostała jeszcze jedna akcja do omówienia, dotycząca następującej sekwencji:
![]() |
Kod przycisku [Accueil] na stronie [form2.xhtml] jest następujący:
<h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
Metoda [Form].accueil wygląda następująco:
public void accueil() {
// wyświetla stronę główną
setForms(true, false, false, false);
}
3.7. Conclusion
Stworzyliśmy następującą aplikację:
![]() |
Skupiliśmy się bardziej na funkcjonalnościach aplikacji niż na jej wyglądzie dla użytkownika. Wygląd ten zostanie ulepszony dzięki wykorzystaniu biblioteki komponentów PrimeFaces. Stworzyliśmy prostą, ale reprezentatywną aplikację opartą na warstwowej architekturze Java EE z wykorzystaniem EJB. Aplikację można ulepszyć na różne sposoby:
- konieczne jest uwierzytelnianie. Nie każdy ma uprawnienia do dodawania/usuwania spotkań,
- powinna istnieć możliwość przewijania kalendarza do przodu i do tyłu podczas wyszukiwania dnia z wolnymi terminami,
- powinna istnieć możliwość wyświetlenia listy dni, w których są wolne terminy u danego lekarza. Jeśli bowiem jest to okulista, jego wizyty są zazwyczaj rezerwowane z sześciomiesięcznym wyprzedzeniem,
- ...
3.8. Testy w Eclipse
3.8.1. Warstwa [DAO]
![]() |
- w [1] importuje się projekt EJB z warstwy [DAO] wraz z jego klientem,
- w [2] wybiera się projekt EJB z warstwy [DAO] i uruchamia się go w [3],
- w [4] uruchamia się go na serwerze,
![]() |
- w [5], proponowany jest wyłącznie serwer Glassfish, ponieważ jest to jedyny serwer posiadający kontener EJB,
- w przypadku [6] wdrożono moduł EJB,
![]() |
- w [7] wyświetlane są logi:
Są to te same logi, które mieliśmy w NetBeans.
![]() |
- w plikach [7A] i [7B] przeprowadzamy test klienta JUnit,
![]() |
- w [8] test zakończył się powodzeniem,
- w [9] – logi konsoli.
![]() |
W pliku [10] wyładowuje się aplikację EJB.
3.8.2. Warstwa [métier]
![]() |
- w [1] importuje się cztery projekty Maven z warstwy [métier],
- w [2] wybiera się projekt korporacyjny i uruchamia go w [3] na serwerze Glassfish [4] [5],
![]() |
- w [6] projekt został wdrożony na serwerze Glassfish,
![]() |
- w [7], przeglądamy logi Glassfish,
W wierszu 3 odnotowujemy nazwę przenośną EJB [Metier] i wklejamy ją do konsoli klienta tego EJB:
public class ClientRdvMedecinsMetier {
// nazwa zdalnego interfejsu 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";
// dzisiejsza data
private static Date jour = new Date();
![]() |
- w [8] uruchamiamy klienta konsoli,
- w pliku [9] przeglądamy jego logi.
![]() |
- w pliku [10] zapisuje się dane z aplikacji korporacyjnej;
3.8.3. Warstwa [web]
![]() |
- w [1] importuje się trzy projekty Maven z warstwy [web]. Ten z rozszerzeniem „ear” to projekt korporacyjny, który należy wdrożyć na Glassfish,
- w warstwie [2] uruchamia się go,
![]() |
- na serwerze Glassfish [3],
- w pliku [4] widać, że aplikacja korporacyjna została pomyślnie wdrożona,
![]() |
- w [5], wywołujemy URL aplikacji w wewnętrznej przeglądarce Eclipse.


























































































































