3. Приклад додатка – 01: rdvmedecins-jsf2-ejb
Наступний текст посилається на такі документи:
- [ref7]: Вступ до Java EE 5 (червень 2010 р.) [Вступ до Java EE з використанням IDE NetBeans та сервера додатків GlassFish (2012)]. Цей документ допоможе ознайомитися з JSF 1 та EJB3.
- [ref8]: Практичний підхід до персистентності в Java (червень 2007 р.) [Персистентність у Java 5 на практиці (2007)]. Цей документ дозволяє ознайомитися з персистентністю даних за допомогою JPA (Java Persistence API).
- [ref9]: Створення веб-сервісу на Java EE за допомогою NetBeans та сервера GlassFish (січень 2009 р.) [Створення веб-сервісу Java EE за допомогою IDE NetBeans 6.5 та сервера Java EE GlassFish (2009)]. У цьому документі розглядається процес створення веб-сервісу.
Приклад програми, який буде розглянуто, взято з [ref9].
3.1. L'application
Компанія з надання ІТ-послуг [ISTIA-AGI] бажає запропонувати сервіс запису на прийом. Першим цільовим ринком є лікарі, які працюють самостійно. Зазвичай вони не мають секретаріату. Клієнти, які бажають записатися на прийом, телефонують безпосередньо до лікаря. Це часто відволікає його протягом дня, що зменшує його доступність для пацієнтів. Компанія [ISTIA-AGI] хоче запропонувати їм послугу з запису на прийом, яка працює за таким принципом:
- секретаріат забезпечує прийом RV для великої кількості лікарів. Цей секретаріат може складатися лише з однієї особи. Її заробітна плата розподіляється між усіма лікарями, які користуються послугою RV.
- секретаріат та всі лікарі підключені до Інтернету
- записи RV зберігаються в централізованій базі даних, доступ до якої через Інтернет мають секретаріат та лікарі
- Призначення RV зазвичай здійснює секретаріат. Це також можуть робити самі лікарі. Зокрема, це відбувається тоді, коли наприкінці прийому лікар самостійно призначає пацієнту новий RV.
Архітектура сервісу для призначення RV є такою:
![]() |
Лікарі стають ефективнішими, якщо їм більше не доводиться займатися управлінням RV. Якщо їх буде достатньо багато, їхній внесок у витрати на функціонування секретаріату буде незначним.
Компанія [ISTIA-AGI] вирішила розробити додаток у двох версіях:
- версія JSF / EJB3 / JPA EclipseLink / сервер Glassfish:
![]() |
- а також версію JSF / Spring / JPA Hibernate / сервер Tomcat:
![]() |
3.2. Робота додатка
Ми назвемо додаток [RdvMedecins]. Нижче наведено знімки екрана, що ілюструють його роботу.
Головна сторінка додатка виглядає так:
![]() |
З цієї першої сторінки користувач (секретаріат, лікар) виконуватиме певні дії. Нижче ми їх наводимо. Лівий екран показує вікно, з якого користувач надсилає запит, правий — відповідь, надіслану сервером.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Нарешті, також може з’явитися сторінка з помилками:
![]() |
3.3. База даних
Повернемося до архітектури додатка, який ми будемо створювати:
![]() |
База даних, яку ми назвемо [dbrdvmedecins2] , — це база даних MySQL5 із чотирма таблицями:
![]() |
3.3.1. Таблиця [MEDECINS]
Вона містить інформацію про лікарів, які обліковуються в додатку [RdvMedecins].
![]() | ![]() |
- ID: номер, що ідентифікує лікаря — первинний ключ таблиці
- VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 щоразу, коли до рядка вносяться зміни.
- NOM: прізвище лікаря
- PRENOM: його ім’я
- TITRE: його/її титул (пані, пані, пан)
3.3.2. Таблиця [CLIENTS]
Пацієнти різних лікарів заносяться до таблиці [CLIENTS]:
![]() | ![]() |
- ID: номер, що ідентифікує клієнта — первинний ключ таблиці
- VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 кожного разу, коли до рядка вносяться зміни.
- NOM: ім’я клієнта
- PRENOM: його ім’я
- TITRE: титул (пані, пані, пан)
3.3.3. Таблиця [CRENEAUX]
У ній перелічено часові проміжки, у яких можливі RV:
![]() |
![]() |
- ID: номер, що ідентифікує часовий проміжок — первинний ключ таблиці (рядок 8)
- VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 щоразу, коли до рядка вносяться зміни.
- ID_MEDECIN: номер, що ідентифікує лікаря, якому належить цей часовий проміжок — зовнішній ключ у стовпці MEDECINS (ID).
- HDEBUT: час початку часового проміжку
- MDEBUT: хвилини початку часового проміжку
- HFIN: година закінчення часового проміжку
- MFIN: хвилини закінчення інтервалу
Другий рядок таблиці [CRENEAUX] (див. [1] вище) вказує, наприклад, що слот № 2 починається о 8:20 і закінчується о 8:40 та належить лікарю № 1 (пані Марі PELISSIER).
3.3.4. Таблиця [RV]
У ній наведено перелік RV, призначених кожному лікарю:
![]() |
- ID: номер, що однозначно ідентифікує RV — первинний ключ
- JOUR: день RV
- ID_CRENEAU: часовий проміжок для RV — зовнішній ключ до поля [ID] у таблиці [CRENEAUX] — визначає як часовий проміжок, так і відповідного лікаря.
- ID_CLIENT: номер клієнта, для якого зроблено бронювання — зовнішній ключ до поля [ID] таблиці [CLIENTS]
Ця таблиця має обмеження унікальності ( ) для значень з’єднаних стовпців (JOUR, ID_CRENEAU):
Якщо рядок таблиці [RV] має значення (JOUR1, ID_CRENEAU1) для стовпців (JOUR, ID_CRENEAU), це значення не може зустрічатися більше ніде. Інакше це означало б, що одночасно було зареєстровано два записи RV для одного й того самого лікаря. З точки зору програмування на Java драйвер JDBC бази даних запускає SQLException, коли трапляється такий випадок.
Рядок id, що дорівнює 3 (див. [1] вище), означає, що 23.08.2006 було заброньовано RV для слоту № 20 та клієнта № 4. З таблиці [CRENEAUX] випливає, що слот № 20 відповідає часовому проміжку 16:20–16:40 і належить лікарю № 1 (пані Марі PELISSIER). З таблиці [CLIENTS] випливає, що клієнт № 4 — це пані Бріжит BISTROU.
3.3.5. Створення бази даних
Для створення таблиць та їх заповнення можна використати скрипт [dbrdvmedecins2.sql], який можна знайти на сайті прикладів. За допомогою [WampServer] (див. параграф 1.3.3) можна діяти наступним чином:
![]() |
- у [1] натисніть на піктограму [WampServer] і виберіть опцію [PhpMyAdmin] [2],
- на [3], у вікні, що відкрилося, виберіть посилання [Bases de données],
![]() |
- на [2], створюємо базу даних, якій дали ім’я [4] та кодування [5],
- у [7] база даних створена. Клацаємо на її посилання,
![]() |
- у [8] імпортуємо файл SQL,
- який вибираємо у файловій системі за допомогою кнопки [9],
![]() |
- у [11] вибираємо скрипт SQL, а в [12] запускаємо його,
- у [13] було створено чотири таблиці бази даних. Переходимо за одним із посилань,
![]() |
- у [14] — вміст таблиці.
Надалі ми більше не повертатимемося до цієї бази даних. Але читачеві пропонується стежити за її розвитком у міру виконання програм, особливо коли щось не працює.
3.4. Таблиці [DAO] та [JPA]
Повернімося до архітектури, яку ми маємо побудувати:
![]() |
Ми створимо чотири проекти Maven:
- проект для шарів [DAO] та [JPA],
- проект для шару [métier],
- проект для шару [web],
- корпоративний проєкт, який об’єднає три попередні проєкти.
Зараз ми створюємо проект Maven для шарів [DAO] та [JPA].
Примітка: для розуміння шарів [métier], [DAO], [JPA] необхідні знання Java EE. Для цього можна ознайомитися з [ref7] (див. параграф 3).
3.4.1. Проєкт NetBeans
Він виглядає так:
![]() |
- у [1] створюється проект Maven типу [EJB Module] [2],
- у [3] надаємо проекту ім’я,
![]() |
- у [4] вибираємо сервер Glassfish,
- у [5] — згенерований проєкт.
3.4.2. Генерація шару [JPA]
Повернемося до архітектури, яку нам потрібно побудувати:
![]() |
За допомогою NetBeans можна автоматично згенерувати шар [JPA] та шар [EJB], який контролює доступ до згенерованих сутностей JPA. Цікаво ознайомитися з цими методами автоматичного генерування, оскільки згенерований код дає цінні підказки щодо того, як писати сутності JPA або код EJB, який їх використовує.
Тепер ми опишемо деякі з цих інструментів автоматичного генерування. Щоб зрозуміти згенерований код, потрібно мати хороші знання про сутності JPA, [ref8] та EJB, [ref7] (див. параграф 3).
3.4.2.1. Створення з’єднання NetBeans із базою даних
- запустіть SGBD та MySQL 5, щоб BD став доступним,
- створіть з’єднання NetBeans із базою даних [dbrdvmedecins2],
![]() |
- на вкладці [Services] [1], у гілці [Databases] [2] вибрати драйвер JDBC MySQL [3],
- потім виберіть опцію [4] «Connect Using», що дозволяє створити з’єднання з базою даних MySQL,
- у вікні [5] введіть необхідну інформацію. У вікні [6] вкажіть назву бази даних, у вікні [7] — ім’я користувача бази даних та його пароль,
- у [8] можна перевірити введені дані,
- у [9] — очікуване повідомлення, якщо дані правильні,
![]() |
- у [10] — з’єднання встановлено. Тут відображаються чотири таблиці підключеної бази даних.
3.4.2.2. Створення модуля збереження даних
Повернемося до архітектури, що зараз будується:
![]() |
Ми зараз будуємо рівень [JPA]. Його конфігурація здійснюється у файлі [persistence.xml], у якому визначаються одиниці збереження. Кожна з них потребує такої інформації:
- параметри JDBC доступу до бази даних (URL, ім’я користувача, пароль),
- класи, які будуть відображати таблиці бази даних,
- використовувана реалізація JPA. Адже JPA — це специфікація, реалізована різними продуктами. Тут ми будемо використовувати EclipseLink — реалізацію за замовчуванням, яку використовує сервер Glassfish. Це дозволяє нам уникнути додавання до Glassfish бібліотек іншої реалізації.
NetBeans може згенерувати цей файл персистентності за допомогою майстра.
![]() |
- клацніть правою кнопкою миші на проекті та виберіть створення одиниці персистентності [1],
- у [2], надайте ім’я створюваному модулю збереження даних,
- у [3] вибрати реалізацію JPA EclipseLink (JPA 2.0),
- у [4] вказати, що транзакції з базою даних будуть оброблятися контейнером EJB сервера Glassfish,
- у [5] вказати, що таблиці з BD вже створені, а тому їх не створювати,
![]() |
- у [6] створити нове джерело даних для сервера Glassfish,
- у [7] присвоїти ім’я JNDI (Java Naming Directory Interface),
- в [8], пов’язати це ім’я з підключенням MySQL, створеним на попередньому етапі,
![]() |
- в [9], завершити роботу майстра,
- в [10] — новий проєкт,
- у [11], у папці [META-INF] було створено файл [persistence.xml],
- у [12], було створено папку [setup],
- у [13] до проекту Maven було додано нові залежності.
Створений файл [META-INF/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">
<jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties/>
</persistence-unit>
</persistence>
Він містить інформацію, вказану в майстрі:
- рядок 3: назва модуля збереження даних,
- рядок 3: тип транзакцій із базою даних, у даному випадку транзакції JTA (Java Transaction API), що керуються контейнером EJB3 сервера Glassfish,
- рядок 4: ім’я джерела даних JNDI.
Зазвичай у цьому файлі вказано тип реалізації JPA, що використовується. У майстрі ми вказали EclipseLink. Оскільки це реалізація JPA, яка використовується за замовчуванням сервером Glassfish, вона не згадується у файлі [persistence.xml].
На вкладці [Design] можна отримати загальний огляд файлу [persistence.xml]:
![]() |
Щоб отримати журнали EclipseLink, ми використаємо такий файл [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>
- рядок 4: вказується, що використовується реалізація JPA від EclipseLink,
- рядки 7–9: містять параметри конфігурації провайдера JPA, у даному випадку EclipseLink,
- рядок 8: ця властивість дозволяє реєструвати в журналі команди SQL, які буде генерувати EclipseLink.
Створений файл [glassfish-resources.xml] має такий вигляд:
<?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>
Цей файл містить інформацію, яку ми вказали у двох майстрах, що використовувалися раніше:
- рядки 5–11: характеристики JDBC бази даних MySQL5 [dbrdvmedecins2],
- рядок 13: назва JNDI джерела даних.
Цей файл буде використано для створення джерела даних JNDI [jdbc/dbrdvmedecins2] на сервері Glassfish. Це повністю власний процес для цього сервера. Для іншого сервера потрібно діяти інакше, зазвичай за допомогою інструменту адміністрування. Такий інструмент також існує для Glassfish.
Нарешті, до проєкту було додано залежності. Файл [pom.xml] має такий вигляд:
<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>
- у рядках 32–37 шар [JPA] потребує артефакту [javaee-api],
- рядки 16, 22, 28 — артефакти, необхідні для реалізації JPA / EclipseLink, що використовується тут.
- рядки 18, 24, 30, 36: усі артефакти мають атрибут provided. Нагадаємо, що це означає, що вони необхідні для компіляції, але не для виконання. Адже під час виконання вони надаються (provided) сервером Glassfish,
- рядки 41–48: визначають нове сховище артефактів Maven, у якому можна знайти артефакти EclipseLink.
3.4.2.3. Генерація сутностей JPA
Ентітети JPA можна згенерувати за допомогою майстра NetBeans:
![]() |
- у [1] створюються сутності JPA на основі бази даних,
- у [2] вибирається джерело даних [jdbc / dbrdvmedecins2], створене раніше,
- у [3] — список таблиць цього джерела даних,
- у [4] — вибираємо їх усі,
![]() |
- у [5] — вибрані таблиці,
- у [6] присвоюємо імена класам Java, пов’язаним із цими чотирма таблицями,
- а також ім’я пакета [7],
- у [8], JPA об’єднує рядки таблиць з BD у колекції. Ми обираємо список як колекцію,
![]() |
- у [9] — класи Java, створені майстром.
3.4.2.4. Згенеровані сутності JPA
Об’єкт [Medecin] є відображенням таблиці [medecins]. Клас Java переповнений анотаціями, що на перший погляд ускладнюють читання коду. Якщо залишити лише те, що є необхідним для розуміння ролі об’єкта, отримаємо такий код:
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;
// конструктори
....
// методи getter та setter
....
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- у рядку 4 анотація @Entity робить клас [Medecin] сутністю JPA, c.a.d. клас, пов’язаний із таблицею BD через API та JPA,
- у рядку 5 вказано назву таблиці BD, пов’язаної з сутністю JPA. Кожне поле таблиці відповідає полю в класі Java,
- рядок 6: клас реалізує інтерфейс Serializable. Це необхідно в клієнт-серверних додатках, де сутності серіалізуються між клієнтом і сервером.
- рядки 10–11: поле id класу [Medecin] відповідає полю [ID] (рядок 10) таблиці [medecins],
- рядки 13–14: поле «title» класу [Medecin] відповідає полю [TITRE] (рядок 13) таблиці [medecins],
- рядки 16–17: поле «nom» класу [Medecin] відповідає полю [NOM] (рядок 16) таблиці [medecins],
- рядки 19–20: поле «версія» класу [Medecin] відповідає полю [VERSION] (рядок 19) таблиці [medecins]. У цьому випадку майстер не розпізнає, що цей стовпець насправді є стовпцем версії, значення якого має збільшуватися при кожній зміні рядка, до якого він належить. Щоб надати йому цю роль, потрібно додати анотацію @Version. Ми зробимо це на наступному кроці,
- рядки 22–23: поле prenom класу [Medecin] відповідає полю [PRENOM] таблиці [medecins],
- рядки 10–11: поле id відповідає первинному ключу [ID] таблиці. Анотації в рядках 8–9 уточнюють цей момент,
- рядок 8: анотація @Id вказує, що анотоване поле пов’язане з первинним ключем таблиці,
- рядок 9: шар [JPA] згенерує первинний ключ для рядків, які він вставит у таблицю [Medecins]. Існує кілька можливих стратегій. У цьому випадку стратегія GenerationType.IDENTITY вказує, що рівень JPA використовуватиме режим auto_increment таблиці MySQL,
- рядки 25–26: таблиця [creneaux] має зовнішній ключ до таблиці [medecins]. Один час прийому належить одному лікарю. І навпаки, один лікар має кілька часів прийому, пов’язаних із ним. Отже, маємо відношення «один (лікар) до декількох (часових слотів)», яке кваліфікується анотацією @OneToMany через JPA (рядок 25). Поле рядка 26 міститиме всі часові слоти лікаря. Це відбувається без програмування. Щоб повністю зрозуміти рядок 25, нам потрібно представити клас [Creneau].
Він має такий вигляд:
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;
// конструктори
...
// методи getter та setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
Ми коментуємо лише нові примітки:
- ми вже зазначали, що таблиця [creneaux] має зовнішній ключ до таблиці [medecins]: кожен час прийому пов’язаний з лікарем. З одним лікарем може бути пов’язано кілька часів прийому. Існує зв’язок із таблиці [creneaux] до таблиці [medecins], який кваліфікується як «багато (часових проміжків) до одного (лікаря)». Анотація @ManyToOne у рядку 32 використовується для визначення зовнішнього ключа,
- а рядок 31 з анотацією @JoinColumn уточнює зв’язок зовнішнього ключа: стовпець [ID_MEDECIN] таблиці [creneaux] є зовнішнім ключем для стовпця [ID] таблиці [medecins],
- рядок 33: посилання на лікаря, якому належить часовий проміжок. Його також можна отримати без програмування.
Отже, зв’язок зовнішнього ключа між сутністю [Creneau] та сутністю [Medecin] реалізується двома анотаціями:
- в об’єкті [Creneau]:
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
- в об’єкті [Medecin]:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
Обидві анотації відображають один і той самий зв’язок: зв’язок зовнішнього ключа таблиці [creneaux] із таблицею [medecins]. Кажуть, що вони є взаємно оберненими. Необхідним є лише зв’язок @ManyToOne. Він однозначно визначає зв’язок як зв’язок із зовнішнім ключем. Зв’язок @OneToMany є необов’язковим. Якщо він присутній, то лише посилається на зв’язок @ManyToOne, з яким він пов’язаний. Саме це означає атрибут mappedBy у рядку 1 сутності [Medecin]. Значенням цього атрибута є назва поля сутності [Creneau], яке має анотацію @ManyToOne, що визначає зовнішній ключ. Також у цьому ж рядку 1 сутності [Medecin] атрибут cascade=CascadeType.ALL визначає поведінку сутності [Medecin] щодо сутності [Creneau]:
- якщо в базу даних вставляється новий об’єкт [Medecin], то об’єкти [Creneau] з поля рядка 2 також мають бути вставлені,
- якщо в базі даних змінюється об’єкт [Medecin], то об’єкти [Creneau] у полі рядка 2 також мають бути змінені,
- якщо з бази видалити об’єкт [Medecin], то об’єкти [Creneau] у полі рядка 2 також мають бути видалені.
Код двох інших об’єктів наводимо без особливих коментарів, оскільки вони не містять нових позначень.
Елемент [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;
// конструктори
...
// методи getter та setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- рядки 24–25 відображають зв’язок із зовнішнім ключем між таблицею [rv] та таблицею [clients].
Об’єкт [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;
// конструктори
...
// методи getter та setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- у рядку 13 вказано тип поля «день» у таблиці Java Date. Зазначено, що в таблиці [rv] стовпець [JOUR] (рядок 12) має тип «дата» (без часу),
- рядки 16–18: визначають зв’язок із зовнішнім ключем, який таблиця [rv] має з таблицею [creneaux],
- рядки 20–22: визначають зв’язок із зовнішнім ключем, який має таблиця [rv] до таблиці [clients].
Автоматичне створення сутностей JPA дозволяє нам отримати робочу базу даних. Іноді цього достатньо, іноді — ні. Саме такий випадок маємо тут:
- необхідно додати анотацію @Version до різних полів version сутностей,
- необхідно написати методи toString, які будуть більш зрозумілими, ніж згенеровані,
- ентітети [Medecin] та [Client] є аналогічними. Ми зробимо їх похідними від класу [Personne],
- видалимо зворотні зв’язки @OneToMany, що є зворотними до зв’язків @ManyToOne. Вони не є обов’язковими та ускладнюють програмування,
- видаляємо перевірку @NotNull для первинних ключів. Коли ми зберігаємо суть JPA разом із MySQL, початкова суть має первинний ключ null. Лише після збереження в базі даних первинний ключ збереженого елемента набуває значення.
З урахуванням цих специфікацій класи мають такий вигляд:
Клас «Personne» використовується для представлення лікарів та клієнтів:
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;
// конструктори
...
// методи getter та setter
...
@Override
public String toString() {
return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
}
}
- рядок 8: слід зауважити, що клас [Personne] сам по собі не є сутністю (@Entity). Він буде батьківським класом для сутностей. Анотація @MappedSuperClass позначає цю ситуацію.
Ентітет [Client] інкапсулює рядки таблиці [clients]. Він походить від попереднього класу [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;
// конструктори
...
@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());
}
}
- рядок 6: клас [Client] є сутністю JPA,
- рядок 7: він пов’язаний із таблицею [clients],
- рядок 8: вона походить від класу [Personne].
Ентітет [Medecin], який інкапсулює рядки таблиці [medecins], побудований за тим самим зразком:
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;
// конструктори
...
@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());
}
}
Об’єкт [Creneau] інкапсулює рядки таблиці [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;
// конструктори
...
// методи getter та setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
// TODO: Попередження — цей метод не працюватиме, якщо поля id не задані
...
}
@Override
public String toString() {
return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
}
}
- рядки 45–47 моделюють відношення «багато до одного», яке існує між таблицею [creneaux] та таблицею [medecins] у базі даних: один лікар має кілька часових слотів, а один часовий слот належить лише одному лікарю.
Ентітет [Rv] інкапсулює рядки таблиці [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;
// конструктори
...
// методи getter та setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Rv[%s, %s, %s]", id, creneau, client);
}
}
- рядки 29–31 моделюють відношення «багато до одного», що існує між таблицею [rv] та таблицею [clients] (один клієнт може фігурувати в декількох Rv) у базі даних, а рядки 25–27 — відношення «багато до одного», яке існує між таблицею [rv] та таблицею [creneaux] (один часовий проміжок може фігурувати в декількох Rv).
3.4.3. Клас винятків
![]() |
Клас винятків [RdvMedecinsException] додатка має такий вигляд:
package rdvmedecins.exceptions;
import java.io.Serializable;
import javax.ejb.ApplicationException;
@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException implements Serializable{
// приватні поля
private int code = 0;
// конструктори
public RdvMedecinsException() {
super();
}
public RdvMedecinsException(String message) {
super(message);
}
public RdvMedecinsException(String message, Throwable cause) {
super(message, cause);
}
public RdvMedecinsException(Throwable cause) {
super(cause);
}
public RdvMedecinsException(String message, int code) {
super(message);
setCode(code);
}
public RdvMedecinsException(Throwable cause, int code) {
super(cause);
setCode(code);
}
public RdvMedecinsException(String message, Throwable cause, int code) {
super(message, cause);
setCode(code);
}
// методи getter та setter
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- рядок 7: клас походить від класу [RuntimeException]. Отже, компілятор не вимагає обробляти його за допомогою try / catch.
- рядок 6: анотація @ApplicationException гарантує, що виняток не буде «поглинутий» винятком типу [EjbException].
Щоб зрозуміти анотацію @ApplicationException, повернімося до архітектури, що використовується на стороні сервера:
![]() |
Виняток типу [RdvMedecinsException] буде згенеровано методами EJB шару [DAO] всередині контейнера EJB3 і перехоплено ним. Без анотації @ApplicationException контейнер EJB3 інкапсулює виникле виключення у виключення типу [EjbException] і перекидає його. Можливо, ви не бажаєте такого інкапсулювання і хочете, щоб з контейнера EJB3 виходило виключення типу [RdvMedecinsException]. Саме це дозволяє зробити анотація @ApplicationException. Крім того, атрибут (rollback=true) цієї анотації вказує контейнеру EJB3, що якщо виняток типу [RdvMedecinsException] виникає всередині методу, що виконується в рамках транзакції з SGBD, останню слід скасувати. З технічної точки зору це називається виконанням rollback транзакції.
3.4.4. EJB шару [DAO]
![]() |
![]() |
Java-інтерфейс [IDao] шару [DAO] має такий вигляд:
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 {
// список клієнтів
public List<Client> getAllClients();
// список лікарів
public List<Medecin> getAllMedecins();
// список часових слотів лікаря
public List<Creneau> getAllCreneaux(Medecin medecin);
// список прийомів лікаря на певний день
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// пошук клієнта за його ідентифікаційним номером
public Client getClientById(Long id);
// пошук клієнта за його ідентифікатором
public Medecin getMedecinById(Long id);
// пошук прийому за його ідентифікатором
public Rv getRvById(Long id);
// знайти часовий проміжок за його ідентифікатором
public Creneau getCreneauById(Long id);
// додати RV
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// видалити RV
public void supprimerRv(Rv rv);
}
Цей інтерфейс було створено після визначення потреб рівня [web]:
- рядок 14: список клієнтів. Він знадобиться нам для заповнення випадаючого списку клієнтів,
- рядок 16: список лікарів. Він знадобиться нам для заповнення випадаючого списку лікарів,
- рядок 18: список часових проміжків лікаря. Він знадобиться для відображення розкладу лікаря на певний день,
- рядок 20: список прийомів лікаря на певний день. У поєднанні з попереднім методом це дозволить нам відобразити розклад лікаря на певний день із вже заброньованими часовими проміжками,
- рядок 22: дозволяє знайти клієнта за його номером. Цей метод дозволить нам знайти клієнта за вибором із випадаючого списку клієнтів,
- рядок 24: те саме для лікарів,
- рядок 26: знаходить зустріч за її номером. Може використовуватися під час видалення зустрічі, щоб попередньо перевірити, чи вона дійсно існує,
- рядок 28: пошук часового проміжку за його номером. Дозволяє ідентифікувати проміжок, який користувач хоче додати або видалити,
- рядок 30: для додавання зустрічі,
- рядок 32: для видалення зустрічі.
Локальний інтерфейс [IDaoLocal], що є похідним від EJB, просто успадковує попередній інтерфейс [IDao]:
package rdvmedecins.dao;
import javax.ejb.Local;
@Local
public interface IDaoLocal extends IDao{
}
Те саме стосується віддаленого інтерфейсу [IDaoRemote]:
package rdvmedecins.dao;
import javax.ejb.Remote;
@Remote
public interface IDaoRemote extends IDao{
}
EJB [DaoJpa] реалізує обидва інтерфейси — локальний та віддалений:
package rdvmedecins.dao;
...
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
- у рядку 5 вказано, що віддалений EJB має ім’я «rdvmedecins.dao». Крім того, анотація @Singleton (Java EE6) гарантує, що буде створено лише один екземпляр EJB. Анотація @Stateless (Java EE5) визначає EJB, який може бути створений у вигляді декількох екземплярів для наповнення пулу EJB,
- у рядку 6 вказано, що всі методи класу EJB виконуються в рамках транзакції, яку керує контейнер EJB3,
- у рядку 7 показано, що EJB реалізує локальний та віддалений інтерфейси, а також є серіалізованим.
Повний код EJB виглядає так:
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;
// перелік клієнтів
public List<Client> getAllClients() {
try {
return em.createQuery("select rc from Client rc").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 1);
}
}
// список лікарів
public List<Medecin> getAllMedecins() {
try {
return em.createQuery("select rm from Medecin rm").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 2);
}
}
// перелік часових проміжків для певного лікаря
// лікар: лікар
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);
}
}
// список прийомів певного лікаря в певний день
// лікар: лікар
// день: день
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);
}
}
// додавання прийому
// день: день прийому
// інтервал: часовий інтервал прийому
// клієнт: клієнт, для якого призначено зустріч
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);
}
}
// видалення зустрічі
// запис: скасований запис
public void supprimerRv(Rv rv) {
try {
em.remove(em.merge(rv));
} catch (Throwable th) {
throw new RdvMedecinsException(th, 5);
}
}
// отримати даних клієнта
public Client getClientById(Long id) {
try {
return (Client) em.find(Client.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// отримати даних лікаря
public Medecin getMedecinById(Long id) {
try {
return (Medecin) em.find(Medecin.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// отримати певний запис на прийом
public Rv getRvById(Long id) {
try {
return (Rv) em.find(Rv.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// отримати певний час прийому
public Creneau getCreneauById(Long id) {
try {
return (Creneau) em.find(Creneau.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
}
- рядок 22: об’єкт EntityManager, який керує доступом до контексту персистентності. Під час інстанціювання класу це поле буде ініціалізовано контейнером EJB завдяки анотації @PersistenceContext у рядку 21,
- рядок 27: запит JPQL (Java Persistence Query Language), який повертає всі рядки таблиці [clients] у вигляді списку об’єктів [Client],
- рядок 36: аналогічний запит для лікарів,
- рядок 46: запит JPQL, що виконує з'єднання таблиць [creneaux] та [medecins]. Він параметризується за ідентифікатором лікаря,
- рядок 57: запит JPQL, що виконує з'єднання таблиць [rv], [creneaux] та [medecins] і має два параметри: ідентифікатор лікаря та день прийому,
- рядки 69–73: створення запису про прийом та його збереження в базі даних,
- рядок 83: видалення запису про прийом із бази даних,
- рядок 92: виконання запиту select до бази даних для пошуку певного клієнта,
- рядок 101: те саме для лікаря,
- рядок 110: те саме для прийому,
- рядок 119: те саме для часового проміжку,
- усі операції з контекстом збереження em із рядка 22 можуть зіткнутися з проблемою в базі даних. Тому всі вони обгорнуті блоком try / catch. Можливе виключення інкапсульовано у «власне» виключення RdvMedecinsException.
3.4.5. Впровадження драйвера JDBC на основі MySQL
У наведеній нижче архітектурі:
![]() |
EclipseLink потребує драйвера JDBC від MySQL. Його потрібно встановити в бібліотеки сервера Glassfish у папці <glassfish>/domains/domain1/lib/ext, де <glassfish> — це папка, в якій встановлено сервер Glassfish. Його можна отримати таким чином:
![]() |
Папка, куди слід помістити драйвер JDBC з MySQL, — це <Domains folder>[1]/domain1/lib/ext [2]. Цей драйвер доступний у URL [http://www.mysql.fr/downloads/connector/j/]. Після його встановлення необхідно перезапустити сервер Glassfish, щоб він врахував цю нову бібліотеку.
3.4.6. Розгортання EJB на рівні [DAO]
Повернемося до архітектури, створеної на даний момент:
![]() |
Набір [web, métier, DAO, JPA] потрібно розгорнути на сервері Glassfish. Зробимо це:
![]() |
- у [1], збираємо проект Maven,
- у [2] запускаємо його,
- у [3] — він розгорнуто на сервері Glassfish (вкладка [Services])
Можна з цікавістю переглянути журнали Glassfish:
![]() |
У [1] журнали Glassfish доступні на вкладці [Output / Glassfish Server 3+]. Вони мають такий вигляд:
Рядки, позначені як [Config] та [Précis], є логами EclipseLink, а ті, що позначені як [Infos], походять із Glassfish.
- рядки 1–12: EclipseLink обробляє виявлені ним сутності JPA,
- рядки 13–17: інформація, що свідчить про те, що обробка сутностей JPA пройшла нормально,
- рядок 18: EclipseLink повідомляє про себе,
- рядок 19: EclipseLink розпізнає, що має справу з SGBD MySQL,
- рядки 20–24: EclipseLink намагається встановити з’єднання з BD,
- рядки 25–28: йому це вдалося,
- рядки 29–33: він намагається підключитися знову, цього разу використовуючи конкретно платформу MySQL (рядок 30),
- рядки 34–37: тут також все пройшло успішно,
- рядок 38: підтвердження того, що модуль збереження даних [dbrdvmedecins-PU] вдалося ініціалізувати,
- рядок 39: переносні імена віддаленого та локального інтерфейсів EJB [DaoJpa], де «переносні» означає, що вони розпізнаються всіма серверами Java-додатків EE 6,
- рядок 40: імена віддаленого та локального інтерфейсів EJB [DaoJpa], специфічні для Glassfish. У майбутньому тесті ми будемо використовувати ім’я «rdvmedecins.dao».
Рядки 39 і 40 є важливими. При написанні клієнта для EJB на Glassfish необхідно знати їх.
3.4.7. Тестування EJB на рівні [DAO]
Тепер, коли EJB з шару [DAO] нашого додатка розгорнуто, ми можемо його протестувати. Ми зробимо це в рамках клієнт-серверного додатка:
![]() |
Клієнт протестує віддалений інтерфейс EJB [DAO], розгорнутий на сервері Glassfish.
Почнемо зі створення нового проекту у Maven:
![]() |
- У [1] ми створюємо новий проєкт,
- у [2,3] створюємо проект Maven типу [Java Application],
- у [4] ми надаємо йому ім’я та розміщуємо його в тій самій папці, що й EJB та [DAO],
![]() |
- у [5], у згенерованому проєкті,
- у [6] було згенеровано клас [App.java]. Його видалимо,
- у [7] було згенеровано гілку [Source Packages]. Ми ще не стикалися з нею. У цю гілку можна додати тести JUnit. Ми це зробимо. Ми не будемо зберігати згенерований тестовий клас [AppTest],
- у [8] — залежності проекту Maven. Гілка [Dependencies] порожня. Нам доведеться додати туди нові залежності. Гілка [Test Dependencies] містить залежності, необхідні для тестування. Тут використовується бібліотека фреймворку JUnit 3.8. Нам доведеться її змінити.
Проєкт розвивається наступним чином:
![]() |
- до [1] — проекту, з якого було видалено два згенеровані класи, а також залежність JUnit.
Повернемося до архітектури «клієнт/сервер», яка буде використовуватися для тестування:
![]() |
Клієнту потрібно знати віддалений інтерфейс, який надає EJB [DAO]. Крім того, він буде обмінюватися з EJB сутностями JPA. Тому йому потрібне їхнє визначення. Щоб проект тестування EJB мав доступ до цієї інформації, ми додамо проект EJB [DAO] як залежність до проекту:
![]() |
- у [1] додаємо залежність від гілки [Test Dependencies],
- у [2] вибираємо вкладку [Open Projects],
- у [3] вибираємо проект Maven з EJB [DAO],
![]() |
- у [4] додано залежність.
Повернемося до архітектури «клієнт/сервер» тесту:
![]() |
Під час виконання клієнт і сервер спілкуються через мережу TCP-IP. Ми не будемо програмувати ці обміни. Для кожного сервера додатків існує бібліотека, яку потрібно інтегрувати в залежності клієнта. Бібліотека для Glassfish називається [gf-client]. Додаємо її:
![]() |
- у [1] додаємо залежність,
- у [2] вказуємо характеристики потрібного артефакту,
- у [3] додається велика кількість залежностей. Maven завантажить їх. Це може зайняти кілька хвилин. Потім вони зберігаються у локальному сховищі Maven.
Тепер ми можемо створити тест JUnit:
![]() |
- у [2] клацніть правою кнопкою миші на [Test Packages], щоб створити новий тест JUnit,
![]() |
- в [3], присвоїти ім’я класу тесту та вказати пакет для нього [4],
- в [5], обираємо фреймворк JUnit 4.x,
- у [6] — згенерований тестовий клас,
- у [7] — нові залежності проекту Maven.
Файл [pom.xml] має такий вигляд:
<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>
Зверніть увагу:
- у рядках 32–51 — залежності проекту,
- рядки 13–26 — визначено два репозиторії Maven: один для EclipseLink (рядки 14–19), інший — для JUnit4 (рядки 20–25).
Тестовий клас матиме такий вигляд:
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 {
// тестований шар [dao]
private static IDaoRemote dao;
// поточна дата
Date jour = new Date();
@BeforeClass
public static void init() throws NamingException {
// ініціалізація середовища JNDI
InitialContext initialContext = new InitialContext();
// інстанціювання шару DAO
dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
}
@Test
public void test1() {
// відображення клієнтів
List<Client> clients =dao.getAllClients();
display("Liste des clients :", clients);
// відображення лікарів
List<Medecin> medecins =dao.getAllMedecins();
display("Liste des médecins :", medecins);
// відображення вільних слотів лікаря
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = dao.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// список прийомів лікаря на певний день
display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// додати 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));
// додати RV у той самий час того самого дня
// повинно викликати виняток
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();
}
// фіксується помилка
erreur=true;
}
// перевіряємо, чи сталася помилка
Assert.assertTrue(erreur);
// перелік RV
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// видалити 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));
}
// допоміжна функція — відображає елементи колекції
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- рядки 23–29: метод, позначений тегом @BeforeClass, виконується раніше за всі інші. Тут створюється посилання на віддалений інтерфейс EJB [DaoJpa]. Нагадаємо, що ми надали йому ім’я JNDI «rdvmedecins.dao»,
- рядки 34–35: відображають список клієнтів,
- рядки 37–38: відображають список лікарів,
- рядки 40–42: відображають часові проміжки першого лікаря,
- рядок 44: відображає зустрічі першого лікаря на день, вказаний у рядку 21,
- рядки 46–51: додають прийом до першого лікаря у його часовий проміжок № 2 та на день, зазначений у рядку 21,
- рядок 52: виводить для перевірки зустрічі першого лікаря на день, вказаний у рядку 21. Їх має бути принаймні одна — та, яку щойно додали,
- рядки 55–70: додаємо той самий прийом. Оскільки таблиця [RV] має обмеження на унікальність, це додавання має спричинити виняток. Перевіряємо це в рядку 70,
- рядок 72: для перевірки відображаються прийоми першого лікаря на день, зазначений у рядку 21. Той, який ми хотіли додати, не повинен там бути,
- рядки 74–76: видаляємо єдиний запис про прийом, який було додано,
- рядок 77: для перевірки відображаються зустрічі першого лікаря на день, вказаний у рядку 21. Та, яку щойно видалили, не повинна там бути.
Цей тест є фіктивним тестом JUnit. У ньому міститься лише одне твердження (рядок 70). Це візуальний тест із відповідними недоліками.
Якщо все гаразд, тести мають пройти успішно:
![]() |
- у [1], створюємо тестовий проект,
- у [2] виконується тест,
- у [3] — тест пройшов успішно.
Давайте детальніше розглянемо результати тесту:
Читачеві пропонується ознайомитися з цими логами разом із кодом, який їх згенерував. Зупинимося на винятковій ситуації, що виникла під час додавання вже існуючої зустрічі, рядки 41–49. Стек винятків наведено в рядках 42–48. Він є несподіваним. Повернімося до коду методу додавання зустрічі:
// додавання Rv
// день: день зустрічі
// інтервал: часовий інтервал зустрічі
// клієнт: клієнт, для якого призначено зустріч
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);
}
}
Подивимося на логи Glassfish під час додавання двох зустрічей:
- рядок 2: перед першим persist,
- рядок 3: після першого persist,
- рядок 4: команда INSERT, яка буде виконана. Слід зауважити, що вона не відбувається одночасно з операцією persist. Якби це було так, цей запис у журналі з’явився б перед рядком 2. Операція INSERT, як правило, відбувається наприкінці транзакції, в якій виконується метод,
- рядок 6: EclipseLink запитує у MySQL, який останній первинний ключ було використано. Він отримає первинний ключ доданого прийому. Це значення буде занесено в поле id збереженої сутності [Rv],
- рядки 7–8: запит SELECT, який відобразить зустрічі лікаря,
- рядки 9–10: екранні виводи другого запиту persist,
- рядки 11–12: команда INSERT, яка буде виконана. Вона має викликати виняток. Цей виняток з’являється у рядках 15–16 і є зрозумілим. Спочатку його запускає драйвер JDBC з MySQL через порушення обмеження унікальності зустрічей. Звідси випливає, що ці винятки мали б бути помітні в журналах тесту JUnit. Однак це не так:
Нагадаємо архітектуру «клієнт/сервер» тесту:
![]() |
Коли EJB [DAO] генерує виняток, його потрібно серіалізувати, щоб він дійшов до клієнта. Ймовірно, саме ця операція завершилася невдало з причини, яку я не зрозумів. Оскільки наш повний додаток не працюватиме в режимі «клієнт/сервер», ми можемо проігнорувати цю проблему.
Тепер, коли EJB на рівні [DAO] працює, можна перейти до EJB на рівні [métier].
3.5. Рівень [métier]
Повернемося до архітектури додатка, що зараз розробляється:
![]() |
Ми створимо новий проект Maven для EJB та [métier]. Як видно вище, він матиме залежність від проекту Maven, який було створено для шарів [DAO] та [JPA].
3.5.1. Проєкт NetBeans
Ми створюємо новий проект Maven типу EJB. Для цього достатньо дотримуватися процедури, яка вже використовувалася та описана на сторінці 174.
![]() |
- у [1], проект Maven шару [métier],
- у [2] додаємо залежність,
- у [3] вибираємо проект Maven для шарів [DAO] та [JPA],
- у [4] вибираємо сферу дії [provided]. Нагадаємо, що це означає, що вона потрібна для компіляції, але не для виконання проєкту. Дійсно,EJB з шару [métier] буде розгорнуто на сервері Glassfish разом із EJB з шарів [DAO] та [JPA]. Отже, коли він запуститься, EJB з рівнів [DAO] та [JPA] вже буде присутній,
![]() |
- у [6] — це новий проєкт із його залежністю.
Тепер розглянемо вихідний код шару [métier]:
![]() |
EJB [Metier] матиме такий інтерфейс [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 {
// рівень DAO
// список клієнтів
public List<Client> getAllClients();
// список лікарів
public List<Medecin> getAllMedecins();
// список часових проміжків лікаря
public List<Creneau> getAllCreneaux(Medecin medecin);
// список прийомів лікаря на певний день
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// знайти клієнта за його ідентифікаційним номером
public Client getClientById(Long id);
// пошук клієнта за його ідентифікатором
public Medecin getMedecinById(Long id);
// пошук прийому за його ідентифікатором
public Rv getRvById(Long id);
// знайти часовий проміжок за його ідентифікатором
public Creneau getCreneauById(Long id);
// додати RV
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// видалити RV
public void supprimerRv(Rv rv);
// професія
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
}
Щоб зрозуміти цей інтерфейс, слід згадати архітектуру проекту:
![]() |
Ми визначили інтерфейс рівня [DAO] (розділ 3.4.4) і зазначили, що він задовольняє потреби рівня [web], а саме потреби користувачів. Рівень [web] взаємодіє з рівнем [DAO] виключно через рівень [métier]. Це пояснює, чому в шарі [métier] містяться всі методи шару [DAO]. Ці методи лише делегуватимуть запит шару [web] до шару [DAO]. І нічого більше.
Під час аналізу додатка виникла потреба: мати можливість відображати на веб-сторінці розклад прийомів лікаря на певний день, щоб дізнатися, які часові проміжки на цей день зайняті, а які вільні. Типовим прикладом є ситуація, коли секретарка відповідає на запит по телефону. Її просять призначити зустріч на певний день з певним лікарем. Щоб задовольнити цю потребу, рівень [métier] надає метод у рядку 46.
// професія
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
Можна задатися питанням, куди розмістити цей метод:
- можна було б розмістити його в шарі [DAO]. Однак цей метод насправді не відповідає потребі в доступі до даних, а скоріше — бізнес-потребі,
- можна було б розмістити його в шарі [web]. Це було б поганою ідеєю. Адже якщо змінити шар [web] на шар [Swing], ми втратимо цей метод, хоча потреба в ньому все ще залишається.
Метод отримує як параметри ім’я лікаря та день, для якого потрібно отримати календар записів. Він повертає об’єкт [AgendaMedecinJour], який представляє календар лікаря та дня:
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;
// поля
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// конструктори
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// методи getter та setter
...
}
- рядок 12: лікар, якому належить розклад,
- рядок 13: день розкладу,
- рядок 14: часові проміжки лікаря на цей день.
- клас містить конструктори (рядки 17, 21), а також відповідний метод toString (рядок 27).
Клас [CreneauMedecinJour] (рядок 14) має такий вигляд:
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;
// поля
private Creneau creneau;
private Rv rv;
// конструктори
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);
}
// геттери та сеттери
...
}
- рядок 12: часовий проміжок лікаря,
- рядок 13: відповідний прийом, null, якщо час вільний.
Таким чином, поле creneauxMedecinJour у рядку 14 класу [AgendaMedecinJour] дозволяє отримати всі часові слоти лікаря з позначкою «зайнято» або «вільно» для кожного з них. Саме в цьому полягала мета нового методу [getAgendaMedecinJour] інтерфейсу [IMetier].
Наш EJB [Metier] матиме локальний та віддалений інтерфейси, які просто успадковуватимуть головний інтерфейс [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 та [Metier] реалізують ці інтерфейси наступним чином:
package rdvmedecins.metier.service;
import java.io.Serializable;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import javax.ejb.EJB;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import rdvmedecins.dao.IDaoLocal;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.entites.CreneauMedecinJour;
@Singleton
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote, Serializable {
// DAO-шар
@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) {
// список часових слотів лікаря
List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
// список бронювань цього ж лікаря на цей же день
List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
// створюємо словник на основі записаних прийомів
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// створюється календар на запитуваний день
AgendaMedecinJour agenda = new AgendaMedecinJour();
// лікар
agenda.setMedecin(medecin);
// день
agenda.setJour(jour);
// інтервали бронювання
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// заповнення слотів для бронювання
for (int i = 0; i < creneauxHoraires.size(); i++) {
// рядок i розкладу
creneauxMedecinJour[i] = new CreneauMedecinJour();
// ідентифікатор слота
creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
// чи вільний цей час або він заброньований?
if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
// інтервал зайнятий — фіксуємо бронювання
Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
creneauxMedecinJour[i].setRv(resa);
}
}
// повертається результат
return agenda;
}
}
- у рядку 22 клас [Metier] є одиничним екземпляром класу EJB,
- рядок 23, кожен метод класу EJB виконується в рамках транзакції. Це означає, що транзакція починається на початку методу в шарі [métier]. Цей шар викликатиме методи шару [DAO]. Вони виконуватимуться в межах тієї самої транзакції,
- у рядку 24 EJB реалізує свої локальний та віддалений інтерфейси, а також є серіалізованим,
- рядок 27: посилання на EJB з шару [DAO],
- рядок 29: це буде введено контейнером EJB сервера Glassfish завдяки анотації @EJB. Отже, під час виконання методів класу [Metier] посилання на EJB з шару [DAO] було ініціалізовано,
- рядки 33–81: це посилання використовується для делегування виклику, зробленого до шару [métier], шару [DAO],
- рядок 84: метод getAgendaMedecinJour, який дозволяє отримати розклад прийомів лікаря на певний день. Пропонуємо читачеві самостійно ознайомитися з коментарями.
3.5.2. Розгортання шару [métier]
Шар [métier] залежить від шару [DAO]. Кожен шар було реалізовано за допомогою EJB. Щоб протестувати EJB та [métier], нам потрібно розгорнути обидва EJB. Для цього нам потрібен корпоративний проєкт.
![]() |
- [1], створюємо новий проєкт,
- типу Maven [2] та «Корпоративний додаток» [3],
- даємо йому ім’я [4]. Суфікс ear буде додано автоматично,
![]() |
- до [5], вибираємо сервер Glassfish та Java EE 6,
- в [6]; корпоративний додаток містить модулі, як правило, модулі EJB та веб-модулі. Тут корпоративний додаток міститиме модулі обох EJB, які ми створили. Оскільки ці модулі вже існують, ми не ставимо галочки,
- у [7,8] було створено два проекти. [8] — це корпоративний проект, який ми будемо використовувати. [7] — це проект, призначення якого мені невідоме. Мені не доводилося ним користуватися, і, оскільки я не вивчав Maven докладно, я не знаю, для чого він може знадобитися. Тому ми його проігноруємо.
Тепер, коли корпоративний проєкт створено, ми можемо визначити його модулі.
![]() |
- у [1] створюємо нову залежність,
- у [2] вибираємо проект EJB [DAO],
- у [3] вказуємо, що це EJB. Не залишайте тип порожнім, оскільки в цьому випадку буде використано тип jar, а цей тип тут не підходить,
- у [4] використовується область дії [compile],
- у [5] — проект із новою залежністю,
![]() |
- у [6, 7, 8] — повторюємо процедуру, щоб додати EJB з шару [métier],
- у [9], обидві залежності,
- в [10], збираємо проєкт,
![]() |
- у [11] — запускаємо його,
- у [12], на вкладці [Services] видно, що проект було розгорнуто на сервері Glassfish. Це означає, що обидва EJB тепер знаходяться на сервері.
У журналах сервера Glassfish міститься інформація про розгортання обох EJB:
![]() |
- та [1] на вкладці «Журнали» Glassfish.
Там містяться такі журнали:
- рядки 1–5: сутності JPA було розпізнано,
- рядок 7: вказує, що створення модуля збереження даних [dbrdvmedecins2-PU] пройшло успішно і що з’єднання з відповідною базою даних було встановлено,
- рядок 8: переносні імена віддаленого та локального інтерфейсів EJB, [DaoJpa] та portable означають, що вони розпізнаються всіма серверами додатків,
- рядок 9: те саме, але з власними іменами Glassfish,
- рядки 10–11: те саме для EJB та [Metier].
Ми запам’ятаємо переносиме ім’я віддаленого інтерфейсу для EJB та [Metier]:
java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote
Воно знадобиться нам під час тестування рівня [métier].
3.5.3. Тестування шару [métier]
Як і у випадку з шаром [DAO], ми протестуємо шар [métier] у рамках клієнт-серверної програми:
![]() |
Клієнт буде тестувати віддалений інтерфейс EJB [Metier], розгорнутий на сервері Glassfish.
Почнемо зі створення нового проєкту Maven. Для цього дотримуємося процедури, використаної для створення тестового проєкту шару [dao] (див. розділ 3.4.7), за винятком створення тесту JUnit. Створений таким чином проєкт має такий вигляд
![]() |
- у [1], створений проект із його залежностями: від EJB у шарі [dao], від EJB у шарі [métier], бібліотеки [gf-client].
На цьому етапі файл [pom.xml] проекту має такий вигляд:
<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>
Переконаємося, що всі залежності, описані в рядках 17–33, присутні. Тестом буде простий консольний клас:
![]() |
Код класу [ClientRdvMedecinsMetier] має такий вигляд:
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 {
// назва віддаленого інтерфейсу 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";
// поточна дата
private static Date jour = new Date();
public static void main(String[] args) {
try {
// контекст JNDI сервера Glassfish
InitialContext initialContext = new InitialContext();
// посилання на віддалений рівень [metier]
IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
// відображення клієнтів
List<Client> clients = metier.getAllClients();
display("Liste des clients :", clients);
// перегляд лікарів
List<Medecin> medecins = metier.getAllMedecins();
display("Liste des médecins :", medecins);
// перегляд вільних слотів лікаря
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = metier.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// список прийомів лікаря на певний день
display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// перегляд календаря
AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// додати 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));
// перегляд календаря
agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// видалити 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));
// перегляд календаря
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();
}
}
}
// допоміжна функція — відображає елементи колекції
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- рядок 18: портативна назва віддаленого інтерфейсу EJB [Metier] була взята з логів GlassFish,
- рядки 24–27: отримуємо посилання на віддалений інтерфейс EJB [Metier],
- рядки 29–30: відображають клієнтів,
- рядки 32–33: відображають лікарів,
- рядки 35–37: відображають часові проміжки лікаря,
- рядок 39: відображає зустрічі лікаря на певний день,
- рядки 41–42: розклад цього ж лікаря на цей самий день,
- рядки 44–49: додавання зустрічі,
- рядок 50: відображаються зустрічі лікаря. Їх має бути на одну більше,
- рядки 52–53: відображення розкладу лікаря. Доданий прийом має бути видимим,
- рядки 55–57: видаляємо щойно доданий прийом,
- рядок 58: це має відобразитися у списку прийомів лікаря,
- рядки 60–61: і в його календарі.
Виконуємо тест:
![]() | ![]() |
Отримані екранні зображення такі:
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]]
- рядок 37: календар пані PELISSIER, 23 травня 2012 року. Жодного часового проміжку не зарезервовано,
- рядок 39: додано зустріч,
- рядок 42: новий календар пані PELISSIER. Тепер один проміжок часу зарезервовано для пана MARTIN,
- рядок 44: зустріч було скасовано,
- рядок 46: календар пані PELISSIER показує, що жоден час не зарезервовано.
Тепер ми вважаємо, що шари [DAO] та [métier] є працездатними. Нам залишається написати шар [web] за допомогою фреймворку JSF. Для цього ми використаємо знання, отримані на початку цього документа.
3.6. Рівень [web]
Повернімося до архітектури, яка зараз перебуває на стадії розробки:
![]() |
Ми побудуємо останній шар — шар [web].
3.6.1. Проєкт NetBeans
Ми створюємо проект Maven:
![]() |
- у [1] створюємо новий проєкт,
- у [2, 3] — проект Maven типу [Web Application],
- у [4], надаємо йому ім’я,
![]() |
- у [5] вибираємо сервер Glassfish та Java EE 6 Web,
- у [6] — так створений проект,
- у [7] — проект після видалення сторінки [index.jsp] та пакета, що міститься в [Source Packages],
![]() |
- в [8, 9], у властивостях проекту додаємо фреймворк,
- у [10] вибираємо Java Server Faces,
![]() |
- у [11] — конфігурація Java Server Faces. Залишаємо значення за замовчуванням. Звертаємо увагу, що використовується саме JSF 2,
- у [12] проект змінюється у двох місцях: генерується файл [web.xml], а також сторінка [index.html].
Файл [web.xml] має такий вигляд:
<?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>
Ми вже зустрічали цей файл.
- рядки 7–11: визначають сервлет, який оброблятиме всі запити, що надходять до додатка. Це сервлет JSF,
- рядки 12–15: визначають запити URL, які обробляє цей сервлет. Це запити URL у формі /faces/*,
- рядки 21–23: визначають сторінку [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">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
Hello from Facelets
</h:body>
</html>
Ми вже з нею стикалися. Ми можемо запустити цей проект:
![]() |
- у [1], запускаємо проект і отримуємо результат [2] у браузері.
Тепер ми представимо повний проект, щоб потім детально розглянути його різні елементи.
![]() |
- в [1], сторінки XHTML проекту,
- у [2] — коди Java,
- у [3] — файли повідомлень, оскільки додаток є інтернаціоналізованим,
![]() |
- у [4] — залежності проекту.
3.6.2. Залежності проекту
Повернемося до архітектури проєкту:
![]() |
Рівень JSF базується на рівнях [métier], [DAO] та [JPA]. Ці три шари інкапсульовані у двох проектах Maven, які ми створили, що пояснює залежності проекту [4]. Давайте просто покажемо, як додаються ці залежності:
![]() |
- у [1] вкажемо ejb, щоб позначити, що залежність стосується проекту EJB,
- у [2] вкажемо [provided]. Адже веб-проект буде розгорнуто одночасно з обома проектами EJB. Тому йому не потрібно включати JAR-файли з EJB.
3.6.3. Конфігурація проєкту
Конфігурація проєкту відповідає конфігурації проєктів JSF, яку ми розглянули на початку цього документа. Ми наводимо перелік конфігураційних файлів без додаткових пояснень.
![]() | ![]() |
[web.xml]: налаштовує веб-додаток.
<?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>
Зверніть увагу, що у рядку 26 вказано, що сторінка [index.xhtml] є головною сторінкою додатка.
[faces-config.xml]: налаштовує додаток 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]: порожня, але необхідна для анотації @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]: таблиця стилів додатка
.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]: файл повідомлень французькою мовою
# макет
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers
layout.entete.langue1=Fran\u00e7ais
layout.entete.langue2=Anglais
# виняток
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
# форма 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
# форма 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
# форма 3
form3.titre=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,число,#00} - {6,число,#00}:{7,число,#00}
form3.titre_detail=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,число,#00} - {6,число,#00}:{7,число,#00}
form3.client=Client
form3.valider=Valider
form3.annuler=Annuler
# помилка
erreur.titre=Une erreur s'est produite.
erreur.message=Message d'erreur
erreur.accueil=Page d'accueil
erreur.classe=Cause
[messages_en.properties]: файл повідомлень англійською мовою
# макет
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university
layout.entete.langue1=French
layout.entete.langue2=English
# виняток
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
# форма 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
# форма 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
# форма 3
form3.titre=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,число,#00} - {6,число,#00}:{7,число,#00}
form3.titre_detail=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,число,#00} - {6,число,#00}:{7,число,#00}
form3.client=Client
form3.valider=Submit
form3.annuler=Cancel
# помилка
erreur.titre=An error occurred
erreur.message=Error message
erreur.accueil=Welcome Page
erreur.classe=Cause
3.6.4. Види проекту
Нагадаємо, як працює додаток. Головна сторінка виглядає так:
![]() |
З цієї першої сторінки користувач (секретаріат, лікар) виконуватиме певні дії. Ми наводимо їх нижче. На екрані ліворуч показано, звідки користувач надсилає запит, а на екрані праворуч — відповідь, надіслану сервером.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Нарешті, також може з’явитися сторінка з помилками:
![]() |
Ці різні види відображаються на таких сторінках веб-проєкту:
![]() |
- у [1] сторінки [basdepage, entete, layout] забезпечують форматування всіх виглядів,
- на [2] — вигляд, створений на [layout.xhtml].
Тут було використано технологію facelets. Вона була описана в розділі 2.11. Ми обмежимося наведенням коду сторінок XHTML, що використовуються для верстки:
[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>
Зверніть увагу на рядки 10–12, де містяться два посилання для зміни мови додатка.
[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>
Ця сторінка є шаблоном (template) сторінки [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>
Рядки 8–21 визначають область під назвою «вміст» (рядок 8) у [layout.xhtml] (рядок 7). Це центральна область переглядів:
![]() |
Сторінка [index.xhtml] є єдиною сторінкою додатка. Отже, навігації між сторінками не буде. Вона відображає одну з чотирьох сторінок [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. Це відображення керується чотирма булевими значеннями [form1Rendered, form2Rendered, form3Rendered, erreurRendered] з біна, який ми опишемо трохи пізніше.
3.6.5. Bean-и проекту
![]() |
Класи пакета [utils] вже були представлені:
- клас [ChangeLocale] — це клас, що забезпечує зміну мови. Він уже розглядався (розділ 2.4.4).
- клас [Messages] — це клас, що полегшує інтернаціоналізацію повідомлень додатка. Він розглядався у розділі 2.8.5.7.
3.6.5.1. Бін Application
Bean [Application] має такий вигляд:
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{
// бізнес-шар
@EJB
private IMetierLocal metier;
// кеш
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>();
// помилки
private List<Erreur> erreurs = new ArrayList<Erreur>();
private Boolean erreur = false;
public Application() {
}
@PostConstruct
public void init() {
// лікарі та клієнти зберігаються у кеші
try {
medecins = metier.getAllMedecins();
clients = metier.getAllClients();
} catch (Throwable th) {
// фіксуємо помилку
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;
}
// перевірка списків
if (medecins.size() == 0) {
// помічаємо помилку
erreur = true;
erreurs.add(new Erreur("", "La liste des médecins est vide"));
}
if (clients.size() == 0) {
// помічаємо помилку
erreur = true;
erreurs.add(new Erreur("", "La liste des clients est vide"));
}
// помилка?
if (erreur) {
return;
}
// словники
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
}
}
// гетери та сеттери
...
}
- рядки 15–16: клас [Application] є біном з областю дії «Додаток». Він створюється один раз на початку життєвого циклу додатка JSF і доступний для всіх запитів усіх користувачів. Зазвичай туди розміщують дані, призначені лише для читання. Тут ми розмістимо список лікарів та список клієнтів. Отже, ми виходимо з припущення, що ці дані не змінюються часто. Сторінки XHTML отримують доступ до них через ім’я «application»,
- рядки 20–21: посилання на локальний інтерфейс EJB [Metier] буде вставлено контейнером EJB з Glassfish. Нагадаємо архітектуру додатка:
![]() |
Додатки JSF, EJB та [Metier] будуть виконуватися в одній і тій самій JVM (Java Virtual Machine). Отже, рівень [JSF] використовуватиме локальний інтерфейс EJB. У цьому випадку bean додатка використовує EJB та [Metier]. Навіть якби це не було так, було б цілком природно знайти там посилання на рівень [métier]. Адже це інформація, яка може бути спільною для всіх запитів усіх користувачів, а отже, дані з областю дії Application.
- рядки 34–35: метод init виконується одразу після інстанціювання класу [Application] (наявність анотації @PostConstruct),
- У рядках 36–73 метод створює такі елементи: список лікарів у рядку 23, список клієнтів у рядку 24, словник лікарів, індексований за їхніми ідентифікаторами, у рядку 25, а також аналогічний словник клієнтів у рядку 26. Можуть виникнути помилки. Вони фіксуються у списку в рядку 28.
Клас [Erreur] має такий вигляд:
package beans;
public class Erreur {
public Erreur() {
}
// поле
private String classe;
private String message;
// конструктор
public Erreur(String classe, String message){
this.setClasse(classe);
this.message=message;
}
// методи getter та setter
...
}
- у рядку 9 — назва класу винятку, якщо було згенеровано виняток,
- рядок 10: повідомлення про помилку.
3.6.5.2. Бін [Form]
Його код такий:
package beans;
...
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// біни Application
@Inject
private Application application;
// модель
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() {
// Чи пройшла ініціалізація успішно?
if (application.getErreur()) {
// отримано список помилок
erreurs = application.getErreurs();
// відображення переліку помилок
setForms(false, false, false, true);
}
}
// відображення списку
private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
this.form1Rendered = form1Rendered;
this.form2Rendered = form2Rendered;
this.form3Rendered = form3Rendered;
this.erreurRendered = erreurRendered;
}
.................................................
}
- рядки 5–7: клас [Form] є біном з іменем «form» та областю дії «session». Нагадаємо, що в цьому випадку клас повинен бути серіалізованим.
- рядки 13–14: бін form має посилання на бін application. Це посилання буде введено контейнером сервлетів, у якому виконується додаток (наявність анотації @Inject).
- рядки 17–31: шаблон сторінок [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. Відображення цих сторінок контролюється булевими значеннями в рядках 19–22. Зауважимо, що за замовчуванням відображається сторінка [form1.xhtml],
- рядки 33–34: метод init виконується одразу після інстанціювання класу (наявність анотації @PostConstruct),
- рядки 35–41: метод init використовується для визначення, яку сторінку слід відобразити першою: зазвичай це сторінка [form1.xhtml] (рядок 19), за винятком випадків, коли ініціалізація додатка пройшла з помилкою (рядок 36); у такому разі буде відображено сторінку [erreur.xhtml] (рядок 40).
Сторінка [erreur.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['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>
На ній використовується тег <h:dataTable> (рядки 14–27) для відображення списку помилок. У результаті отримуємо сторінку, схожу на таку:

Тепер ми визначимо різні етапи життєвого циклу додатка.
3.6.6. Взаємодія між сторінками та моделлю
3.6.6.1. Відображення головної сторінки
Якщо все гаразд, першою відображеною сторінкою буде [form1.xhtml]. У результаті ми отримаємо такий вигляд:
![]() |
Сторінка [form1.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['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>
Ця сторінка базується на такому шаблоні:
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
// Bean «Application»
@Inject
private Application application;
// модель
private Long idMedecin;
private Date jour = new Date();
// список лікарів
public List<Medecin> getMedecins() {
return application.getMedecins();
}
// календар
public void getAgenda() {
...
}
- поле в рядку 9 забезпечує читання та запис значення зі списку в рядку 18 сторінки. Під час початкового відображення сторінки вона встановлює значення, вибране в комбінованому списку. Під час початкового відображення idMedecin дорівнює null, отже, буде вибрано першого лікаря,
- метод у рядках 13–15 генерує елементи комбінованого списку лікарів (рядок 19 сторінки). Кожен згенерований варіант матиме як мітку (itemLabel) прізвище, ім’я та по батькові лікаря, а як значення (itemValue) — ідентифікатор лікаря,
- поле в рядку 10 забезпечує читання/запис у поле введення в рядку 21 сторінки. Отже, при першому відображенні показується поточна дата,
- рядки 17–19: метод getAgenda обробляє натискання кнопки [Agenda] у рядку 26 сторінки. Оскільки навігації немає (завжди запитується сторінка [index.html]), часто замість атрибута action використовується атрибут actionListener. У цьому випадку метод, що викликається в шаблоні, не повертає жодного результату.
Коли відбувається натискання кнопки [Agenda],
- відправляються такі значення: значення, вибране у комбінованому списку лікарів, записується у поле idMedecin моделі, а день, вибраний у полі «день»,
- викликається метод getAgenda моделі.
Метод getAgenda виглядає наступним чином:
// Bean-додаток
@Inject
private Application application;
// шаблон
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;
// календар
public void getAgenda() {
try {
// пошук лікаря
medecin = application.gethMedecins().get(idMedecin);
// заголовок форми 2
form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
// розклад лікаря на певний день
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// відображення форми 2
setForms(false, true, false, false);
} catch (Throwable th) {
// перегляд помилок
prepareVueErreur(th);
}
}
// підготовка vueErreur
private void prepareVueErreur(Throwable th) {
// створюється список помилок
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()));
}
// відображається перегляд помилок
setForms(false, false, false, true);
}
Нагадаємо, що має відображати метод getAgenda:
![]() |
- рядок 21: отримуємо лікаря, обраного зі словника лікарів, який було збережено в біні application. Для цього використовуємо його ідентифікатор, який було відправлено в idMedecin,
- рядок 23: готується заголовок сторінки [form2.xhtml], яка буде відображена. Це повідомлення береться з файлу повідомлень, щоб забезпечити його інтернаціоналізацію. Цей метод описано в розділі 2.8.5.7, на сторінці 135.
- рядок 25: викликається шар [métier] для розрахунку розкладу обраного лікаря на обраний день,
- рядок 27: відображається [form2.xhtml],
- рядок 28: якщо виникає виняток, формується список помилок (рядки 37–42) і відображається сторінка [erreur.xhtml] (рядок 44).
3.6.6.2. Відображення розкладу прийому лікаря
Сторінка [form2.xhtml] відповідає такому вигляду:
![]() |
Код сторінки [form2.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"
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>
Нагадаємо, що метод getAgenda ініціалізував два поля в моделі:
// шаблон
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
Ці два поля заповнюють сторінку [form2.xhtml]:
- рядок 10 — заголовок сторінки,
- рядок 12: розклад лікаря відображається за допомогою тегу <h:dataTable> у трьох стовпцях,
- рядки 13–18: у першому стовпці відображаються часові проміжки,
- рядки 19–30: у другому стовпці відображається ім’я клієнта, який, можливо, забронював цей часовий проміжок, або нічого, якщо бронювання відсутнє. Для цього використовуються теги з бібліотеки JSTL Core, на яку є посилання в рядку 7,
- рядки 30–35: у третьому стовпці відображається посилання [Réserver], якщо часовий проміжок вільний, або посилання [Supprimer], якщо він зайнятий.
Посилання у третьому стовпці пов’язані з таким шаблоном:
// шаблон
private Long idCreneau;
// дія над RV
public void action() {
...
}
- метод action викликається, коли користувач натискає на посилання «Забронювати / Видалити» (рядок 32). Зверніть увагу, що тут використано атрибут action. Метод, на який вказує цей атрибут, повинен мати сигнатуру String action(), оскільки метод має повертати ключ навігації. Однак тут вона має сигнатуру void action(). Це не спричинило помилки, і можна припустити, що в цьому випадку навігація не відбувається. Саме цього й було потрібно. Встановлення actionListener замість action спричиняло збій у роботі,
- поле idCreneau у рядку 2 отримає ідентифікатор часового проміжку посилання, на яке натиснули (рядок 33 сторінки).
3.6.6.3. Видалення зустрічі
Розглянемо код, який обробляє видалення зустрічі. Це відповідає такій послідовності переглядів:
![]() |
Код, що відповідає цій операції, такий:
// біан «Application»
@Inject
private Application application;
// шаблон
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;
// дія щодо RV
public void action() {
// шукаємо вільний час у календарі
int i = 0;
Boolean trouvé = false;
while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
trouvé = true;
} else {
i++;
}
}
// знайдено?
if (!trouvé) {
// дивно — знову відображаємо form2
setForms(false, true, false, false);
return;
}
// знайдено
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// відповідно до бажаної дії
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// бронювання
public void reserver() {
...
}
public void supprimer() {
try {
// видалення зустрічі
application.getMetier().supprimerRv(creneauChoisi.getRv());
// оновлюємо календар
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// відображення form2
setForms(false, true, false, false);
} catch (Throwable th) {
// перегляд помилок
prepareVueErreur(th);
}
}
- рядок 16: коли запускається метод action, ідентифікатор вибраного часового проміжку був переданий у idCreneau (рядок 11),
- рядки 18–26: здійснюється пошук часового проміжку за його id (рядок 21). Його шукають у поточному календарі, agendaMedecinJour із рядка 10. Зазвичай його слід знайти. Якщо це не так, ніяких дій не виконується (рядки 28–32),
- рядок 34: якщо знайдено потрібний часовий проміжок, отримуємо його ідентифікатор і зберігаємо його в рядку 12,
- рядок 36: перевіряємо, чи був у вибраному часовому проміжку призначений зустріч. Якщо так, її видаляємо (рядок 39), інакше резервуємо зустріч (рядок 37),
- рядок 51: зустріч у вибраному проміжку часу видаляється. Цю роботу виконує шар [métier],
- рядок 53: у шару [métier] запитується новий календар лікаря. Звісно, там буде на один прийом менше. Але оскільки додаток є багатокористувацьким, там можна побачити зміни, внесені іншими користувачами,
- рядок 55: знову відображається сторінка [form2.xhtml],
- рядок 58: оскільки було зроблено запит до шару [métier], можуть виникнути винятки. У цьому випадку стек винятків записується у список помилок у рядку 13 і відображається за допомогою подання [erreur.xhtml].
3.6.6.4. Запис на прийом
Призначення зустрічі відбувається за такою послідовністю:
![]() |
Шаблон, що використовується в цій дії, є таким:
// шаблон
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;
// дія над RV
public void action() {
...
// знайдено
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// відповідно до бажаної дії
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// бронювання
public void reserver() {
try {
// заголовок форми 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();
// клієнт, обраний у списку
idClient=null;
// відображається форма 3
setForms(false, false, true, false);
} catch (Throwable th) {
// перегляд помилок
prepareVueErreur(th);
}
}
- рядок 14: якщо у вибраному часовому проміжку немає зустрічі, то це бронювання,
- рядок 30: готується заголовок сторінки [form3.xhtml] за тією ж схемою, що й для заголовка сторінки [form2.xhtml],
- рядок 34: у цій формі є комбінований список, значення якого надходить із idClient. Встановлюємо значення цього поля на null, щоб не вибирати нікого,
- рядок 36: відображаємо сторінку [form3.xhtml],
- рядок 39: або сторінку помилок, якщо стався виняток.
Сторінка [form3.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="#{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>
Ця сторінка формується за допомогою такого шаблону:
// Bean «Application»
@Inject
private Application application;
// шаблон
private Long idClient;
// список клієнтів
public List<Client> getClients() {
return application.getClients();
}
- рядок 6: номер клієнта заповнює атрибут value у списку клієнтів у рядку 12 сторінки. Він фіксує вибраний елемент списку,
- рядки 9–11: метод getClients заповнює вміст комбінованого списку (рядок 13). Назва (itemLabel) кожного варіанту — це [Titre Prénom Nom] клієнта, а відповідне значення (itemValue) — це ідентифікатор клієнта. Отже, саме це значення буде відправлено.
3.6.6.5. Підтвердження зустрічі
Підтвердження зустрічі відбувається за такою послідовністю:
![]() |
і відповідає натисканню кнопки [Valider]:
<h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
Отже, цю подію оброблятиме метод [Form].validerRv. Його код такий:
// біан додатка
@Inject
private Application application;
// модель
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;
// перевірка Rv
public void validerRv() {
try {
// отримання екземпляра обраного часового проміжку
Creneau creneau = application.getMetier().getCreneauById(idCreneau);
// додаємо зустріч
application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
// оновлюємо календар
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// відображається form2
setForms(false, true, false, false);
} catch (Throwable th) {
// перегляд помилок
prepareVueErreur(th);
}
}
- рядок 12: перед тим, як виконається метод validerRv, поле idClient отримало ідентифікатор клієнта, обраного користувачем,
- рядок 19: на основі ідентифікатора часового проміжку, збереженого на попередньому етапі (біан має сесійну область дії), до шару [métier] надсилається запит на отримання посилання на сам часовий проміжок,
- рядок 21: до шару [métier] надсилається запит на додавання зустрічі для обраного дня (day), обраного часового проміжку (slot) та обраного клієнта (idClient),
- рядок 23: надсилається запит до шару [métier] на оновлення календаря лікаря. Буде відображено доданий прийом, а також усі зміни, які могли внести інші користувачі додатка,
- рядок 25: повторно відображається календар [form2.xhtml],
- рядок 28: у разі виникнення помилки відображається сторінка помилки.
3.6.6.6. Скасування запису на прийом
Це відповідає такій послідовності:
![]() |
Кнопка [Annuler] на сторінці [form3.xhtml] виглядає так:
<h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
Отже, викликається метод [Form].annulerRv:
// скасування зустрічі
public void annulerRv() {
// відображається форма form2
setForms(false, true, false, false);
}
3.6.6.7. Повернутися на головну сторінку
Залишилося розглянути ще одну дію — з наступної послідовності:
![]() |
Код кнопки [Accueil] на сторінці [form2.xhtml] такий:
<h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
Метод [Form].accueil виглядає так:
public void accueil() {
// відображення головної сторінки
setForms(true, false, false, false);
}
3.7. Conclusion
Ми створили такий додаток:
![]() |
Ми зосередилися на функціональних можливостях додатка, а не на його зовнішньому вигляді для користувача. Останній буде вдосконалено за допомогою бібліотеки компонентів PrimeFaces. Ми створили простий, але типовий приклад багатошарової архітектури Java EE з використанням EJB. Додаток можна вдосконалити різними способами:
- необхідна аутентифікація. Не всі мають право додавати/видаляти зустрічі,
- потрібно мати можливість прокручувати календар вперед і назад під час пошуку дня з вільними часовими проміжками,
- повинна бути можливість отримати список днів, коли у лікаря є вільні часові проміжки. Адже, якщо це офтальмолог, його прийоми зазвичай записуються за шість місяців наперед,
- ...
3.8. Тестування за допомогою Eclipse
3.8.1. Рівень [DAO]
![]() |
- у [1] імпортується проект EJB із шару [DAO] та його клієнт,
- у [2] вибираємо проект EJB з шару [DAO] і запускаємо його у [3],
- у [4] запускаємо його на сервері,
![]() |
- у [5] пропонується лише сервер Glassfish, оскільки це єдиний сервер, що має контейнер EJB,
- у [6] модуль EJB було розгорнуто,
![]() |
- у [7] відображаються журнали:
Це ті самі, що були в NetBeans.
![]() |
- у [7A] [7B] виконується тест JUnit клієнта,
![]() |
- у [8] тест пройшов успішно,
- у [9] — журнали консолі.
![]() |
У [10] виконується вивантаження додатка EJB.
3.8.2. Шар [métier]
![]() |
- у [1] імпортуємо чотири проекти Maven з шару [métier],
- у [2] вибирається корпоративний проєкт і запускається у [3] на сервері Glassfish [4] [5],
![]() |
- у [6] проект підприємства було розгорнуто на Glassfish,
![]() |
- до [7], переглядаємо журнали Glassfish,
У рядку 3 ми записуємо ім'я порталу EJB [Metier] і вставляємо його в консоль клієнта цього EJB:
public class ClientRdvMedecinsMetier {
// ім'я віддаленого інтерфейсу 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";
// поточна дата
private static Date jour = new Date();
![]() |
- у [8] запускаємо консольний клієнт,
- у [9] — його журнали.
![]() |
- у [10] виконується вивантаження корпоративного додатка;
3.8.3. Рівень [web]
![]() |
- в [1] імпортуються три проекти Maven з шару [web]. Проект із суфіксом ear — це корпоративний проект, який потрібно розгорнути на Glassfish,
- у [2] його запускають,
![]() |
- на сервері Glassfish [3],
- у файлі [4] корпоративний додаток успішно розгорнуто,
![]() |
- у [5], запитуємо URL додатка у внутрішньому браузері Eclipse.


























































































































