6. Приклад додатка-03: rdvmedecins-pf-ejb
Нагадаємо структуру прикладу додатка, розробленого для сервера Glassfish:
![]() |
Ми нічого не змінюємо в цій архітектурі, за винятком веб-шару, який тут буде реалізовано за допомогою JSF та Primefaces.
![]() |
6.1. Проєкт NetBeans
Вище наведено шари [métier] та [DAO] із прикладу 01 JSF / EJB / Glassfish. Ми використовуємо їх повторно.
![]() |
- [mv-rdvmedecins-ejb-dao-jpa]: проект EJB, що складається з шарів [DAO] та [JPA] з прикладу 01,
- [mv-rdvmedecins-ejb-metier]: проект EJB шару [métier] з прикладу 01,
- [mv-rdvmedecins-pf]: проєкт шару [web] / Primefaces — новий,
- [mv-rdvmedecins-app-ear]: корпоративний проєкт для розгортання додатка на сервері Glassfish — новий.
6.2. Корпоративний проєкт
Корпоративний проєкт призначений виключно для розгортання трьох модулів [mv-rdvmedecins-ejb-dao-jpa], [mv-rdvmedecins-ejb-metier], [mv-rdvmedecins-pf] на сервері Glassfish. Проєкт NetBeans має таку назву:
![]() |
Проєкт існує лише для цих трьох залежностей [1], визначених у файлі [pom.xml] наступним чином:
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<artifactId>mv-rdvmedecins-app</artifactId>
<groupId>istia.st</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>istia.st</groupId>
<artifactId>mv-rdvmedecins-app-ear</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>ear</packaging>
<name>mv-rdvmedecins-app-ear</name>
...
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
<type>ejb</type>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-metier</artifactId>
<version>${project.version}</version>
<type>ejb</type>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-pf</artifactId>
<version>${project.version}</version>
<type>war</type>
</dependency>
</dependencies>
</project>
- рядки 10–13: артефакт Maven корпоративного проєкту,
- рядки 18–37: три залежності проекту. Зверніть увагу на їхній тип (рядки 23, 29, 35).
Щоб запустити веб-додаток, потрібно запустити цей корпоративний проєкт.
6.3. Веб-проект Primefaces
Веб-проект Primefaces має такий вигляд:
![]() |
- у [1] — сторінки проекту. Сторінка [index.xhtml] є єдиною сторінкою проекту. Вона містить три фрагменти: [form1.xhtml], [form2.xhtml] та [erreur.xhtml]. Інші сторінки призначені лише для форматування.
- У [2] — Java-біни. Бін [Application] має область дії application, а бін [Form] — session. Клас [Erreur] інкапсулює помилку. Клас [MyDataModel] слугує шаблоном для тегу <dataTable> у Primefaces,
- у [3] — файли повідомлень для інтернаціоналізації,
- у [4] — залежності. Веб-проект має залежності від проекту EJB з шару [DAO], проекту EJB з шару [métier] та Primefaces для шару [web].
6.4. Конфігурація проекту
Конфігурація проєкту відповідає конфігурації проєктів Primefaces або 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.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
<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>
Зверніть увагу, що у рядку 30 вказано, що сторінка [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]: таблиця стилів додатка
.col1{
background-color: #ccccff
}
.col2{
background-color: #ffcccc
}
Бібліотека Primefaces має власні таблиці стилів. Наведена вище таблиця стилів використовується лише для сторінки, що відображається у разі виникнення винятку — сторінки, яка не підтримується додатком. У такому випадку відображається сторінка [exception.xhtml].
[messages_fr.properties]: файл повідомлень французькою мовою
# макет
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers - application propuls\u00e9e par PrimeFaces et JQuery
# виняток
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
form1.options=Options
form1.francais=Fran\u00e7ais
form1.anglais=Anglais
form1.rafraichir=Rafra\u00eechir
form1.precedent=Jour pr\u00e9c\u00e9dent
form1.suivant=Jour suivant
form1.agenda=Affiche l'agenda du m\u00e9decin choisi pour le jour choisi
form1.today=Aujourd'hui
# форма 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
form2.valider=Valider
form2.annuler=Annuler
form2.erreur=Erreur
form2.emtyMessage=Pas de cr\u00e9neaux entr\u00e9s dans la base
form2.suppression.confirmation=Etes-vous s\u00fbr(e) ?
form2.suppression.message=Suppression d'un rendez-vous
form2.supprimer.oui=Oui
form2.supprimer.non=Non
form2.erreurClient=Client [{0}] inconnu
form2.erreurClient_detail=Client {0} inconnu
form2.erreurAction=Action non autoris\u00e9e
form2.erreurAction_detail=Action non autoris\u00e9e
# помилка
erreur.titre=Une erreur s'est produite.
erreur.exceptions=Cha\u00eene des exceptions
erreur.type=Type de l'exception
erreur.message=Message associ\u00e9
erreur.accueil=Accueil
[messages_en.properties]: файл повідомлень англійською мовою
# макет
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university - Application powered by PrimeFaces and JQuery
# виняток
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
form1.options=Options
form1.francais=French
form1.anglais=English
form1.rafraichir=Refresh
form1.precedent=Previous Day
form1.suivant=Next day
form1.agenda=Show the doctor's diary for the chosen doctor and the chosen day
form1.today=Today
# форма 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
form2.valider=Submit
form2.annuler=Cancel
form2.erreur=Error
form2.emtyMessage=No Time periods in the database
form2.suppression.confirmation=Are-you sure ?
form2.suppression.message=Booking deletion
form2.supprimer.oui=Yes
form2.supprimer.non=No
form2.erreurClient=Unknown Client {0}
form2.erreurClient_detail=Unknown Client [{0}]
form2.erreurAction=Unauthorized action
form2.erreurAction_detail=Action non autoris\u00e9e
# помилка
erreur.titre=The following exceptions occurred
erreur.exceptions=Exceptions' chain
erreur.type=Exception type
erreur.message=Associated Message
erreur.accueil=Welcome
6.5. Шаблон сторінок [layout.xhtml]
![]() |
Шаблон [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:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{form.locale}">
<h:head>
<title>JSF</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: 10px;" bgcolor="#ffcccc">
<ui:include src="menu.xhtml"/>
</td>
<td>
<p:outputPanel id="contenu">
<ui:insert name="contenu">
<h2>Contenu</h2>
</ui:insert>
</p:outputPanel>
</td>
</tr>
<tr bgcolor="#ffcc66">
<td colspan="2">
<ui:include src="basdepage.xhtml"/>
</td>
</tr>
</table>
</h:form>
</h:body>
</f:view>
</html>
Єдина змінна частина цього шаблону — це область рядків 28–30. Ця область знаходиться в зоні з ідентифікатором :formulaire:contenu (рядок 27). Про це слід пам’ятати. Виклики AJAX, які оновлюють цю область, матимуть атрибут update=":formulaire:contenu". Крім того, форма починається з рядка 15. Отже, фрагмент, вставлений у рядки 28–30, вставляється в цю форму.
Цей шаблон має такий вигляд:
![]() |
Динамічна частина сторінки буде вставлена у зону, обведена рамкою вище.
6.6. Сторінка [index.xhtml]
![]() |
Проект завжди відображає одну й ту саму сторінку, а саме наступну сторінку [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:p="http://primefaces.org/ui"
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">
<ui:fragment rendered="#{form.form1Rendered}">
<ui:include src="form1.xhtml"/>
</ui:fragment>
<ui:fragment rendered="#{form.form2Rendered}">
<ui:include src="form2.xhtml"/>
</ui:fragment>
<ui:fragment rendered="#{form.erreurRendered}">
<ui:include src="erreur.xhtml"/>
</ui:fragment>
</ui:define>
</ui:composition>
</html>
- рядки 8–9: цей фрагмент XHTML буде вставлений у динамічну зону шаблону [layout.xhtml],
- сторінка складається з трьох підфрагментів:
- [form1.xhtml], рядки 10–12;
- [form2.xhtml], рядки 13–15;
- [erreur.xhtml], рядки 16–18.
Наявність цих фрагментів у [index.xhtml] контролюється булевими значеннями шаблону [Form.java], пов’язаного зі сторінкою. Отже, змінюючи ці значення, можна отримати різний вигляд сторінки.
Фрагмент [form1.xhtml] відображається наступним чином:
Фрагмент [form2.xhtml] відображається наступним чином:
![]() |
Фрагмент [erreur.xhtml] відображається так:
![]() |
6.7. Beans проекту
![]() |
Клас з пакета [utils] вже було представлено: клас [Messages] — це клас, що полегшує інтернаціоналізацію повідомлень додатка. Його було розглянуто в розділі 2.8.5.7.
6.7.1. Бін Application
Бін [Application.java] є біном області дії application. Нагадаємо, що цей тип біна слугує для зберігання даних, доступних лише для читання та доступних усім користувачам додатка. Цей бін має такий вигляд:
package beans;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import rdvmedecins.metier.service.IMetierLocal;
@Named(value = "application")
@ApplicationScoped
public class Application {
// бізнес-шар
@EJB
private IMetierLocal metier;
public Application() {
}
// гетери
public IMetierLocal getMetier() {
return metier;
}
}
- рядок 8: біну надано ім’я application,
- рядок 9: він має область дії «application»,
- рядки 13–14: посилання на локальний інтерфейс шару [métier] буде введено в нього контейнером EJB сервера додатка. Нагадаємо архітектуру додатка:
![]() |
Додатки JSF, EJB та [Metier] будуть виконуватися в одній і тій самій JVM (Java Virtual Machine). Отже, рівень [JSF] використовуватиме локальний інтерфейс EJB. Ось і все. Бін [Application] не містить нічого іншого. Щоб отримати доступ до шару [métier], інші біни будуть звертатися до нього через цей бін.
6.7.2. Бін [Erreur]
Клас [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;// методи отримання та встановлення
- рядок 9 — назва класу винятку, якщо було згенеровано виняток,
- рядок 10: повідомлення про помилку.
6.7.3. Бін [Form]
Його код такий:
package beans;
import java.io.IOException;
...
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// біан додатка
@Inject
private Application application;
// кеш сеансу
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 Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
// модель
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneauChoisi;
private Medecin medecin;
private Long idClient;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
private Boolean erreur = false;
private String identiteClient;
private String action;
private String msgErreurClient;
private Boolean erreurClient;
private String msgErreurAction;
private Boolean erreurAction;
private String locale = "fr";
@PostConstruct
private void init() {
// зберігаємо лікарів та клієнтів у кеші
try {
medecins = application.getMetier().getAllMedecins();
clients = application.getMetier().getAllClients();
} catch (Throwable th) {
// фіксуємо помилку
prepareVueErreur(th);
return;
}
// словники
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
hIdentitesClients.put(identite(c), c);
}
}
...
// відображення перегляду
private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean erreurRendered) {
this.form1Rendered = form1Rendered;
this.form2Rendered = form2Rendered;
this.erreurRendered = erreurRendered;
}
// підготовка 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(true, false, true);
}
// гетери та сеттери
...
}
- рядки 6–8: клас [Form] є біном з іменем «form» та областю дії «session». Нагадаємо, що в цьому випадку клас має бути серіалізованим,
- рядки 14–15: бін «form» має посилання на бін «application». Це посилання буде введено контейнером сервлетів, у якому виконується додаток (наявність анотації @Inject).
- рядки 17–44: шаблон сторінок [form1.xhtml, form2.xhtml, erreur.xhtml]. Відображення цих сторінок контролюється булевими значеннями в рядках 27–29. Зауважимо, що за замовчуванням відображається сторінка [form1.xhtml] (рядок 27),
- рядки 46–47: метод init виконується одразу після інстанціювання класу (наявність анотації @PostConstruct),
- рядки 50–51: до шару [métier] надсилається запит на отримання списку лікарів та клієнтів,
- рядки 59–65: якщо все пройшло успішно, створюються словники лікарів та клієнтів. Вони індексуються за номерами. Далі відображається сторінка [form1.xhtml] (рядок 27),
- рядок 54: у разі помилки формується шаблон сторінки [erreur.xhtml]. Цей шаблон — це список помилок із рядка 36,
- рядки 78–88: метод [prepareVueErreur] формує список помилок для відображення. Сторінка [index.xhtml] потім відображає фрагменти [form1.xhtml] та [erreur.xhtml] (рядок 87).
Сторінка [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:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<p:panel header="#{msg['erreur.titre']}" closable="true" >
<hr/>
<p:dataTable value="#{form.erreurs}" var="erreur">
<f:facet name="header">
<h:outputText value="#{msg['erreur.exceptions']}"/>
</f:facet>
<p:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.type']}"/>
</f:facet>
<h:outputText value="#{erreur.classe}"/>
</p:column>
<p:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.message']}"/>
</f:facet>
<h:outputText value="#{erreur.message}"/>
</p:column>
</p:dataTable>
</p:panel>
</body>
</html>
Вона використовує тег <p:dataTable> (рядки 12–28) для відображення списку помилок. У результаті отримуємо сторінку помилок, схожу на таку:
![]() |
Тепер ми визначимо різні етапи життєвого циклу додатка. Для кожної дії користувача ми розглянемо відповідні види та обробники подій.
6.8. Відображення головної сторінки
Якщо все йде як слід, першою відображеною сторінкою буде [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:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
...
</p:toolbarGroup>
</p:toolbar>
</html>
Панель інструментів, обведена на знімку екрана, — це компонент Primefaces Toolbar. Він визначений у рядках 8–14. Він містить дві групи компонентів, кожна з яких визначена тегом <toolbarGroup>, рядки 9–11 та 12–14. Одна з груп вирівняна ліворуч на панелі інструментів (рядок 9), інша — праворуч (рядок 12).
Розглянемо деякі компоненти лівої групи:
<p:toolbar>
<p:toolbarGroup align="left">
<h:outputText value="#{msg['form1.medecin']}"/>
<p:selectOneMenu value="#{form.idMedecin}" effect="fade">
<f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>
<p:ajax update=":formulaire:contenu" listener="#{form.hideAgenda}" />
</p:selectOneMenu>
<p:separator/>
<h:outputText value="#{msg['form1.jour']}"/>
<p:calendar id="calendrier" value="#{form.jour}" readOnlyInputText="true">
<p:ajax event="dateSelect" listener="#{form.hideAgenda}" update=":formulaire:contenu"/>
</p:calendar>
<p:separator/>
<p:commandButton id="resa-agenda" icon="ui-icon-check" actionListener="#{form.getAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-agenda" value="#{msg['form1.agenda']}"/>
...
</p:toolbarGroup>
...
- рядки 4–7: комбінований список лікарів, до якого додано ефект (effect="fade"),
- рядок 6: поведінка AJAX. Коли відбудеться зміна у комбінованому списку, буде виконано метод [Form].hideAgenda (listener="#{form.hideAgenda}") буде виконано, а динамічна область :form:content (update=":formulaire:contenu") буде оновлено,
- рядок 8: додає роздільник на панель інструментів,
- рядки 10–12: поле введення дати. Тут використовується календар Primefaces. Поле введення є лише для читання (readOnlyInputText="true"),
- рядок 11: поведінка AJAX. При зміні дати буде виконано метод [Form].hideAgenda, і динамічне поле :form:content буде оновлено,
- рядок 14: кнопка. При натисканні на неї виконується виклик AJAX до методу [Form].getAgenda (), після чого шаблон буде змінено, а відповідь сервера буде використана для оновлення динамічної області :form:content,
- рядок 15: тег <tooltip> дозволяє прив’язати підказку до компонента. Ідентифікатор цього компонента вказується атрибутом for тегу tooltip. Тут (for="resa-agenda") вказує на кнопку з рядка 14:
![]() | ![]() |
Ця сторінка використовує наступний шаблон:
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// кеш сеансу
private List<Medecin> medecins;
private List<Client> clients;
// модель
private Long idMedecin;
private Date jour = new Date();
// список лікарів
public List<Medecin> getMedecins() {
return medecins;
}
// список клієнтів
public List<Client> getClients() {
return clients;
}
// календар
public void getAgenda() {
...
}
- поле у рядку 12 забезпечує читання та запис значення зі списку у рядку 4 сторінки. Під час початкового відображення сторінки вона встановлює значення, вибране в комбінованому списку. Під час початкового відображення idMedecin дорівнює null, отже, буде вибрано першого лікаря,
- метод у рядках 16–18 генерує елементи комбінованого списку лікарів (рядок 5 сторінки). Кожен згенерований варіант матиме як мітку (itemLabel) прізвище, ім’я та по батькові лікаря, а як значення (itemValue) — ідентифікатор лікаря,
- поле в рядку 13 забезпечує читання/запис у поле введення в рядку 10 сторінки. Отже, при першому відображенні показується поточна дата,
- рядки 26–28: метод getAgenda обробляє натискання кнопки [Agenda] у рядку 14 сторінки. Він майже ідентичний тому, що був у версії JSF:
// компонент Application
@Inject
private Application application;
// кеш сеансу
private List<Medecin> medecins;
private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
// шаблон
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneauChoisi;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
private Boolean erreur = false;
public void getAgenda() {
try {
// отримання даних про лікаря
medecin = hMedecins.get(idMedecin);
// розклад лікаря на певний день
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// відображення форми 2
setForms(true, true, false);
} catch (Throwable th) {
// перегляд помилок
prepareVueErreur(th);
}
// наразі не обрано жодного часового проміжку
creneauChoisi = null;
}
Ми не будемо коментувати цей код. Це вже було зроблено.
6.9. Переглянути розклад лікаря
6.9.1. Загальний огляд розкладу
Це такий варіант використання:
![]() |
- у [1] вибираємо лікаря [1] та день [2], а потім запитуємо [3] розклад лікаря на обраний день,
- у [4] він з’являється під панеллю інструментів.
Код сторінки [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:p="http://primefaces.org/ui"
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>
<!-- контекстне меню -->
<p:contextMenu for="agenda">
...
</p:contextMenu>
<!-- розклад -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- стовпець розкладу -->
<p:column style="width: 100px">
...
</p:column>
<!-- стовпець клієнтів -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
<!-- підтвердження видалення RV -->
<p:confirmDialog id="confirmDialog" message="#{msg['form2.suppression.confirmation']}"
header="#{msg['form2.suppression.message']}" severity="alert" widgetVar="confirmation">
...
</p:confirmDialog>
<!-- повідомлення про помилку -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
...
</p:dialog>
<!-- обробка відповіді сервера -->
<script type="text/javascript">
...
}
</script>
</body>
</html>
- рядки 16–26: головним елементом сторінки є таблиця <dataTable>, яка відображає розклад прийому лікаря,
![]() |
- рядки 12–14: для додавання/видалення зустрічі ми будемо використовувати контекстне меню:
![]() |
- рядки 29–32: коли користувач захоче видалити зустріч, з’явиться вікно підтвердження:
![]() |
- рядки 35–37: для повідомлення про помилку буде використовуватися діалогове вікно:
![]() |
- рядки 40–43: нам знадобиться трохи JavaScript.
6.9.2. Таблиця зустрічей
Тут ми розглянемо модель таблиці даних, як це було описано в розділі 5.15 на сторінці 327.
Розглянемо головний елемент сторінки — таблицю, що відображає календар:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- колонка розкладу -->
<p:column style="width: 100px">
...
</p:column>
<!-- стовпець клієнтів -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
Результат виглядає так:
![]() |
Це таблиця з двома стовпцями (рядки 4–6 та 8–10), яка заповнюється з джерела [Form].getMyDataModel() (value="#{form.myDataModel}"). Одночасно можна вибрати лише один рядок (selectionMode="single"). При кожному виклику POST посилання на вибраний елемент присвоюється [Form].creneauChoisi (selection="#{form.creneauChoisi}").
Нагадаємо, що метод getAgenda ініціалізував таке поле в шаблоні:
// шаблон
private AgendaMedecinJour agendaMedecinJour;
Шаблон таблиці отримується шляхом виклику методу [Form].getMyDataModel (атрибут value тегу <dataTable>) наступного вигляду:
// шаблон dataTable
public MyDataModel getMyDataModel() {
return new MyDataModel(agendaMedecinJour.getCreneauxMedecinJour());
}
Розглянемо клас [MyDataModel], який слугує шаблоном для тегу <p:dataTable>:
package beans;
import javax.faces.model.ArrayDataModel;
import org.primefaces.model.SelectableDataModel;
import rdvmedecins.metier.entites.CreneauMedecinJour;
public class MyDataModel extends ArrayDataModel<CreneauMedecinJour> implements SelectableDataModel<CreneauMedecinJour> {
// виробники
public MyDataModel() {
}
public MyDataModel(CreneauMedecinJour[] creneauxMedecinJour) {
super(creneauxMedecinJour);
}
@Override
public Object getRowKey(CreneauMedecinJour creneauMedecinJour) {
return creneauMedecinJour.getCreneau().getId();
}
@Override
public CreneauMedecinJour getRowData(String rowKey) {
// список часових проміжків
CreneauMedecinJour[] creneauxMedecinJour = (CreneauMedecinJour[]) getWrappedData();
// ключ — ціле число типу long
long key = Long.parseLong(rowKey);
// шукається вибраний слот
for (CreneauMedecinJour creneauMedecinJour : creneauxMedecinJour) {
if (creneauMedecinJour.getCreneau().getId().longValue() == key) {
return creneauMedecinJour;
}
}
// нічого
return null;
}
}
- рядок 7: клас [MyDataModel] є шаблоном тегу <p:dataTable>. Цей клас призначений для встановлення зв’язку між елементом rowkey, що надсилається, та елементом, пов’язаним із цим рядком,
- рядок 7: клас реалізує інтерфейс [SelectableDataModel] за допомогою класу [ArrayDataModel]. Це означає, що параметром конструктора є масив. Саме цей масив заповнює тег <dataTable>. Тут кожен рядок масиву буде пов’язаний з елементом типу [CreneauMedecinJour],
- рядки 13–15: конструктор передає свій параметр своєму батьківському класу,
- рядки 18–20: кожен рядок масиву відповідає часовому проміжку й ідентифікується за ідентифікатором часового проміжку (рядок 19). Саме цей ідентифікатор надсилається на сервер,
- рядок 23: код, який буде виконано на стороні сервера, коли буде надіслано ідентифікатор часового проміжку. Мета цього методу — отримати посилання на об’єкт [CreneauMedecinJour], пов’язаний із цим ідентифікатором. Це посилання буде присвоєно цільовому значенню атрибута selection тегу <dataTable>:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
Отже, поле [Form].creneauChoisi міститиме посилання на об’єкт [CreneauMedecinJour], який потрібно додати або видалити.
6.9.3. Стовпець часових інтервалів
![]() |
Стовпець з часовими інтервалами отримується за допомогою такого коду:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- стовпець розкладу -->
<p:column style="width: 100px">
<f:facet name="header">
<h:outputText value="#{msg['form2.creneauHoraire']}"/>
</f:facet>
<div align="center">
<h:outputFormat value="{0,number,#00}:{1,number,#00} - {2,number,#00}:{3,number,#00}">
<f:param value="#{creneauMedecinJour.creneau.hdebut}" />
<f:param value="#{creneauMedecinJour.creneau.mdebut}" />
<f:param value="#{creneauMedecinJour.creneau.hfin}" />
<f:param value="#{creneauMedecinJour.creneau.mfin}" />
</h:outputFormat>
</div>
</p:column>
<!-- стовпець клієнтів -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
- рядки 5–7: заголовок стовпця,
- рядки 8–15: поточний елемент стовпця. Зверніть увагу на рядок 9, де використовується тег <h:outputFormat>, що дозволяє форматувати елементи, які потрібно відобразити. Параметр value вказує рядок символів, який потрібно відобразити. Запис {i,type,format} позначає параметр № i, тип цього параметра та його формат. Тут є 4 параметри, пронумеровані від 0 до 3, їхній тип — числовий, і вони будуть відображатися двома цифрами,
- рядки 10–13: чотири параметри, які очікує тег <h:outputFormat>.
6.9.4. Стовпець клієнтів
![]() |
Стовпець клієнтів отримується за допомогою такого коду:
<!-- календар -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- стовпець розкладу -->
...
<!-- стовпець клієнтів -->
<p:column style="width: 300px">
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
<ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
<h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}" />
</ui:fragment>
<ui:fragment rendered="#{creneauMedecinJour.rv==null and form.creneauChoisi!=null and form.creneauChoisi.creneau.id==creneauMedecinJour.creneau.id}">
...
</ui:fragment>
</p:column>
</p:dataTable>
- рядки 8–10: заголовок стовпця,
- рядки 11–13: поточний елемент, якщо для цього часового проміжку призначено зустріч. У цьому випадку відображаються прізвище, ім’я та по батькові клієнта, для якого призначено цю зустріч,
- рядки 14–16: ще один фрагмент, до якого ми ще повернемося.
6.10. Видалення зустрічі
Видалення зустрічі відбувається за такою послідовністю:
![]() |
![]() |
Відповідний вигляд цієї дії такий:
<!-- контекстне меню -->
<p:contextMenu for="agenda">
...
<p:menuitem value="#{msg['form2.supprimer']}" onclick="confirmation.show()"/>
</p:contextMenu>
<!-- календар -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
...
</p:dataTable>
<!-- підтвердження видалення RV -->
<p:confirmDialog id="confirmDialog" message="#{msg['form2.suppression.confirmation']}"
header="#{msg['form2.suppression.message']}" severity="alert" widgetVar="confirmation">
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
<f:setPropertyActionListener value="supprimer" target="#{form.action}"/>
</p:commandButton>
<p:commandButton value="#{msg['form2.supprimer.non']}" onclick="confirmation.hide()" type="button" />
</p:confirmDialog>
- рядки 2–5: контекстне меню, пов’язане з таблицею даних (атрибут for). Воно має два варіанти [1]:
![]() |
- рядок 4: опція [Supprimer] викликає діалогове вікно [2] з рядків 13–20,
- рядок 15: натискання на [Oui] запускає виконання [Form.action], що видалить зустріч. Зазвичай контекстне меню не повинно пропонувати опцію [Supprimer], якщо у вибраного елемента немає зустрічі, і не повинно пропонувати опцію [Réserver], якщо у вибраного елемента є зустріч. Нам не вдалося зробити контекстне меню настільки витонченим. Це вдається для першого вибраного елемента, але потім виявляється, що контекстне меню зберігає налаштування, встановлені для цього першого вибору. Тоді воно стає некоректним. Тому ми залишили обидві опції та вирішили надати користувачеві зворотний зв’язок, якщо він видаляє елемент без зустрічі,
- рядок 16: атрибут oncomplete, який дозволяє визначити код JavaScript, що має виконуватися після виконання виклику AJAX. Цей код буде таким:
<!-- повідомлення про помилку -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
<h:outputText value="#{form.msgErreur}" />
</p:dialog>
<!-- обробка відповіді сервера -->
<script type="text/javascript">
function handleRequest(xhr, status, args) {
// помилка?
if(args.erreur) {
dlgErreur.show();
}
}
</script>
- рядок 10: код JavaScript перевіряє, чи має словник args атрибут erreur. Якщо так, він викликає діалогове вікно з рядка 2 (атрибут widgetVar). У цьому діалоговому вікні відображається шаблон [Form].msgErreur.
Розглянемо код, що виконується для обробки видалення зустрічі:
<p:confirmDialog ...>
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
...>
<f:setPropertyActionListener value="supprimer" target="#{form.action}"/>
</p:commandButton>
...
</p:confirmDialog>
- рядок 2: буде виконано метод [Form].action,
- рядок 4: перед його виконанням поле action отримає значення «supprimer».
Метод [action] має такий вигляд:
// дія щодо RV
public void action() {
// залежно від бажаної дії
if (action.equals("supprimer")) {
supprimer();
}
...
}
public void supprimer() {
// чи потрібно щось робити?
Rv rv = creneauChoisi.getRv();
if (rv == null) {
signalerActionIncorrecte();
return;
}
try {
// видалення зустрічі
application.getMetier().supprimerRv(rv);
// оновлюємо календар
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// відображаємо form2
setForms(true, true, false);
} catch (Throwable th) {
// перегляд помилок
prepareVueErreur(th);
}
// очищення обраного часового проміжку
creneauChoisi = null;
}
- рядок 4: якщо дія — «видалити», виконується метод [supprimer],
- рядок 12: отримується зустріч із вибраного часового проміжку. Нагадаємо, що [creneauChoisi] було ініціалізовано за посиланням на вибраний елемент [CreneauMedecinJour],
- якщо ця зустріч існує, її видаляють (рядок 19), календар оновлюється (рядок 21), а потім знову відображається (рядок 23),
- якщо видалення не вдалося, відображається сторінка помилок (рядок 26),
- якщо у вибраного елемента немає зустрічі (рядок 13), то це означає, що користувач натиснув [Supprimer] на часовий проміжок, у якому немає зустрічі. Про цю помилку повідомляється:
![]() |
Метод [signalerActionIncorrecte] виглядає наступним чином:
// повідомлення про некоректну дію
private void signalerActionIncorrecte() {
// Скидання обраного часового проміжку
creneauChoisi = null;
// помилка
msgErreur = Messages.getMessage(null, "form2.erreurAction", null).getSummary();
RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
}
- рядок 4: знімається виділення,
- рядок 6: генерується інтернаціоналізоване повідомлення про помилку,
- рядок 7: додаємо до словника args виклику AJAX атрибут («помилка», true).
Повернемося до коду XHTML кнопки [Oui]:
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
- рядок 2: після виконання методу [Form].action виконується метод JavaScript handleRequest:
<!-- повідомлення про помилку -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
<h:outputText value="#{form.msgErreur}" />
</p:dialog>
<!-- обробка відповіді сервера -->
<script type="text/javascript">
function handleRequest(xhr, status, args) {
// помилка?
if(args.erreur) {
dlgErreur.show();
}
}
</script>
- рядок 10: перевіряється, чи має словник args атрибут із назвою «erreur». Якщо так, то відображається діалогове вікно з рядка 2,
- рядок 3: у ньому відображається повідомлення про помилку, сформоване за шаблоном.
6.11. Запис на прийом
Запис на прийом відповідає такій послідовності:
![]() |
Для цієї дії використовується такий вигляд:
<!-- контекстне меню -->
<p:contextMenu for="agenda">
<p:menuitem value="#{msg['form2.reserver']}" update=":formulaire:contenu" action="#{form.action}" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="reserver" target="#{form.action}"/>
</p:menuitem>
...
</p:contextMenu>
<!-- календар -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- стовпець розкладу -->
<p:column style="width: 100px">
...
</p:column>
<!-- стовпець клієнтів -->
<p:column style="width: 300px">
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
...
<ui:fragment rendered="#{creneauMedecinJour.rv==null and form.creneauChoisi!=null and form.creneauChoisi.creneau.id==creneauMedecinJour.creneau.id}">
<p:autoComplete completeMethod="#{form.completeClients}" value="#{form.identiteClient}" size="30"/>
<p:spacer width="50px"/>
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
<p:spacer width="50px"/>
<p:commandLink action="#{form.action()}" value="#{msg['form2.annuler']}" update=":formulaire:contenu">
<f:setPropertyActionListener value="annuler" target="#{form.action}"/>
</p:commandLink>
</ui:fragment>
</p:column>
</p:dataTable>
...
- рядки 21–31: відображають наступне:
![]() |
- рядок 21: відображення відбувається, якщо немає зустрічі, але вибір було зроблено, і ідентифікатор обраного часового проміжку відповідає ідентифікатору поточного елемента таблиці. Якщо не встановити цю умову, фрагмент відображатиметься для всіх часових проміжків,
- рядок 22: поле введення стане полем з підказками. Тут передбачається, що клієнтів може бути багато,
- рядки 24–26: посилання [Valider],
- рядки 28–30: посилання [Annuler].
Поле з автоматичним заповненням генерується за допомогою такого коду:
<p:autoComplete completeMethod="#{form.completeClients}" value="#{form.identiteClient}" size="30"/>
Метод [Form].completeClients відповідає за надання користувачеві пропозицій на основі символів, введених у поле введення:
![]() |
Пропозиції мають вигляд [Nom prénom titre]. Код методу [Form].completeClients такий:
// метод автозаповнення тексту
public List<String> completeClients(String query) {
List<String> identites = new ArrayList<String>();
// пошук відповідних клієнтів
for (Client c : clients) {
String identite = identite(c);
if (identite.toLowerCase().startsWith(query.toLowerCase())) {
identites.add(identite);
}
}
return identites;
}
private String identite(Client c) {
return c.getNom() + " " + c.getPrenom() + " " + c.getTitre();
}
- рядок 2: query — це рядок, введений користувачем,
- рядок 3: список пропозицій. Спочатку список порожній,
- рядки 5–10: формуються ідентифікатори клієнтів у форматі [Nom prénom titre]. Якщо ідентифікатор починається з query (рядок 7), він зберігається у списку пропозицій (рядок 8).
6.12. Підтвердження зустрічі
Підтвердження зустрічі відбувається за такою послідовністю:
![]() |
Код посилання [Valider] має такий вигляд:
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
Отже, цю подію оброблятиме метод [Form].action(). Тим часом модель [Form].action отримає рядок «valider». Код виглядає так:
// Bean «Application»
@Inject
private Application application;
// кеш сеансу
...
private Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
// модель
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
private AgendaMedecinJour agendaMedecinJour;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
private Boolean erreur = false;
private String identiteClient;
private String action;
private String msgErreur;
@PostConstruct
private void init() {
...
for (Client c : clients) {
hClients.put(c.getId(), c);
hIdentitesClients.put(identite(c), c);
}
}
// дія щодо RV
public void action() {
// залежно від бажаної дії
...
if (action.equals("valider")) {
validerResa();
}
}
// підтвердження зустрічі
public void validerResa() {
// підтвердження бронювання
try {
// чи існує клієнт?
Boolean erreur = !hIdentitesClients.containsKey(identiteClient);
if (erreur) {
msgErreur = Messages.getMessage(null, "form2.erreurClient", new Object[]{identiteClient}).getSummary();
RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
return;
}
// додаємо зустріч
application.getMetier().ajouterRv(jour, creneauChoisi.getCreneau(), hIdentitesClients.get(identiteClient));
// оновлюємо календар
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// відображається form2
setForms(true, true, false);
} catch (Throwable th) {
// перегляд помилок
prepareVueErreur(th);
}
// очищення обраного часового проміжку
creneauChoisi = null;
// очищення даних клієнта
identiteClient = null;
}
- рядки 33–35: через значення поля action буде виконано метод [validerResa],
- рядок 43: спочатку перевіряється, чи існує клієнт. Адже в полі автозаповнення користувач міг ввести дані вручну, не скориставшись запропонованими варіантами. Автозаповнення пов’язане з шаблоном [Form].identiteClient. Тому перевіряється, чи існує цей ідентифікатор у словнику identitesClients, створеному під час інстанціювання моделі (рядок 20). Цей словник пов’язує ідентифікатор клієнта типу [Nom prénom titre] із самим клієнтом (рядок 25),
- рядок 44: якщо клієнт не існує, браузеру повертається повідомлення про помилку,
- рядок 45: інтернаціоналізоване повідомлення про помилку,
- рядок 46: до словника args виклику AJAX додається атрибут («помилка», true). Виклик AJAX було визначено таким чином:
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
У рядку 3 вище видно, що посилання [Valider] має атрибут oncomplete. Саме цей атрибут забезпечить відображення повідомлення про помилку за допомогою техніки, з якою ми вже знайомі.
- рядок 50: до шару [métier] надсилається запит на додавання зустрічі для обраного дня (jour), у вибраний часовий проміжок (creneauChoisi.getCreneau()) та для вибраного клієнта (hIdentitesClients.get(identiteClient)),
- рядок 52: надсилається запит до шару [métier] на оновлення календаря лікаря. Ми побачимо доданий прийом, а також усі зміни, які могли внести інші користувачі додатка,
- рядок 54: повторно відображається календар [form2.xhtml],
- рядок 57: у разі виникнення помилки відображається сторінка помилки.
6.13. Скасування запису на прийом
Це відповідає такій послідовності:
![]() |
Кнопка [Annuler] на сторінці [form2.xhtml] виглядає так:
<p:commandLink action="#{form.action()}" value="#{msg['form2.annuler']}" update=":formulaire:contenu">
<f:setPropertyActionListener value="annuler" target="#{form.action}"/>
</p:commandLink>
Отже, викликається метод [Form].action:
// дія над RV
public void action() {
// залежно від бажаної дії
...
if (action.equals("annuler")) {
annulerRv();
}
}
// скасування запису на прийом
public void annulerRv() {
// відображається форма 2
setForms(true, true, false);
// очищення обраного часового проміжку
creneauChoisi = null;
// очищення даних клієнта
identiteClient = null;
}
6.14. Навігація в календарі
Панель інструментів дозволяє пересуватися по календарю:
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
Хоча на наведених вище скріншотах цього не видно, календар оновлюється з урахуванням зустрічей нового обраного дня.
Теги трьох відповідних кнопок у файлі [form1.xhtml] такі:
<p:toolbar>
<p:toolbarGroup align="left">
...
<h:outputText value="#{msg['form1.jour']}"/>
<p:calendar id="calendrier" value="#{form.jour}" readOnlyInputText="true">
<p:ajax event="dateSelect" listener="#{form.hideAgenda}" update=":formulaire:contenu"/>
</p:calendar>
<p:separator/>
<p:commandButton id="resa-agenda" icon="ui-icon-check" actionListener="#{form.getAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-agenda" value="#{msg['form1.agenda']}"/>
<p:commandButton id="resa-precedent" icon="ui-icon-seek-prev" actionListener="#{form.getPreviousAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-precedent" value="#{msg['form1.precedent']}"/>
<p:commandButton id="resa-suivant" icon="ui-icon-seek-next" actionListener="#{form.getNextAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-suivant" value="#{msg['form1.suivant']}"/>
<p:commandButton id="resa-today" icon="ui-icon-home" actionListener="#{form.today}" update=":formulaire:contenu"/>
<p:tooltip for="resa-today" value="#{msg['form1.today']}"/>
</p:toolbarGroup>
<p:toolbarGroup align="right">
...
</p:toolbarGroup>
</p:toolbar>
Методи [Form].getPreviousAgenda, [Form].getNextAgenda, [Form].today мають такий вигляд:
private Date jour = new Date();
public void getPreviousAgenda() {
// перехід до попереднього дня
Calendar cal = Calendar.getInstance();
cal.setTime(jour);
cal.add(Calendar.DAY_OF_YEAR, -1);
jour = cal.getTime();
// календар
if (form2Rendered) {
getAgenda();
}
}
public void getNextAgenda() {
// перехід до наступного дня
Calendar cal = Calendar.getInstance();
cal.setTime(jour);
cal.add(Calendar.DAY_OF_YEAR, 1);
jour = cal.getTime();
// календар
if (form2Rendered) {
getAgenda();
}
}
// календар на сьогодні
public void today() {
jour = new Date();
// календар
if (form2Rendered) {
getAgenda();
}
}
- рядок 1: день відображення календаря,
- рядок 5: використовується календар,
- рядок 6: який ініціалізується на поточний день календаря,
- рядок 7: віднімаємо один день від календаря,
- рядок 8: і переініціалізуємо його на день відображення календаря,
- рядок 11: повторно відображаємо календар, якщо він вже відображається. Адже користувач може користуватися панеллю інструментів, навіть якщо календар не відображається.
Інші методи працюють аналогічно.
6.15. Зміна мови відображення
Зміна мови здійснюється за допомогою кнопки меню на панелі інструментів:
![]() |
![]() |
Теги кнопки меню такі:
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
<p:menuButton value="#{msg['form1.options']}">
<p:menuitem id="menuitem-francais" value="#{msg['form1.francais']}" actionListener="#{form.setFrenchLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-anglais" value="#{msg['form1.anglais']}" actionListener="#{form.setEnglishLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
</p:menuButton>
</p:toolbarGroup>
</p:toolbar>
У шаблоні виконуються такі методи:
private String locale = "fr";
public void setFrenchLocale() {
locale = "fr";
// оновити сторінку
redirect();
}
public void setEnglishLocale() {
locale = "en";
// сторінка оновлюється
redirect();
}
private void redirect() {
// перенаправлення клієнта на сервлет
ExternalContext ctx = FacesContext.getCurrentInstance().getExternalContext();
try {
ctx.redirect(ctx.getRequestContextPath());
} catch (IOException ex) {
Logger.getLogger(Form.class.getName()).log(Level.SEVERE, null, ex);
}
}
Методи в рядках 3 і 9 просто ініціалізують поле locale з рядка 1, а потім перенаправляють браузер клієнта на ту саму сторінку. Перенаправлення — це відповідь, у якій сервер просить браузер завантажити іншу сторінку. Після цього браузер виконує GET на цю нову сторінку.
- рядок 17: [ExternalContext] — це клас JSF, який дозволяє отримати доступ до сервлету, що виконується,
- рядок 19: виконується перенаправлення. Параметром методу redirect є URL — URL-адреса сторінки, на яку має перенаправитися браузер клієнта. У цьому випадку ми хочемо перенаправитися на [/mv-rdvmedecins-pf], що є назвою нашого додатка:
![]() |
метод [getRequestContextPath] дозволяє отримати це ім’я. Отже, відбудеться завантаження головної сторінки [index.xhtml] нашого додатка. Ця сторінка пов’язана з моделлю [Form], що має область дії сесії. Ця модель керує трьома булевими значеннями, які визначають вигляд сторінки [index.xhtml]:
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
Оскільки шаблон має сесійну область дії, ці три булеві змінні зберегли свої значення. Отже, сторінка [index.xhtml] з’явиться у тому стані, в якому вона перебувала до перенаправлення. Ця сторінка оформлена за допомогою наступного шаблону facelet [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:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{form.locale}">
....
</f:view>
</html>
Тег у рядку 9 визначає мову відображення сторінки за допомогою атрибута locale. Отже, сторінка буде відображатися французькою або англійською мовою залежно від обставин. А тепер чому відбувається перенаправлення? Повернімося до тегів опцій зміни мови:
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
<p:menuButton value="#{msg['form1.options']}">
<p:menuitem id="menuitem-francais" value="#{msg['form1.francais']}" actionListener="#{form.setFrenchLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-anglais" value="#{msg['form1.anglais']}" actionListener="#{form.setEnglishLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
</p:menuButton>
</p:toolbarGroup>
</p:toolbar>
Спочатку вони були написані для оновлення поля з ідентифікатором форми за допомогою виклику AJAX (атрибут update у рядках 7 і 8). Однак під час тестування зміна мови не завжди працювала. Тому для вирішення цієї проблеми було застосовано перенаправлення. Можливо, можна було б також додати атрибут ajax='false' до тегів, щоб викликати перезавантаження сторінки. Це дозволило б уникнути перенаправлення.
6.16. Оновлення списків
Це відповідає такій дії:
![]() |
Тег, пов’язаний з опцією [Rafraîchir], такий:
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
Метод [Form].refresh виглядає так:
public void refresh() {
// оновлення списків
init();
}
Метод init — це метод, що виконується одразу після створення біна [Form]. Його метою є кешування даних з бази даних у моделі:
// біан Application
@Inject
private Application application;
// кеш сеансу
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 Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
...
@PostConstruct
private void init() {
// заносимо лікарів та клієнтів у кеш
try {
medecins = application.getMetier().getAllMedecins();
clients = application.getMetier().getAllClients();
} catch (Throwable th) {
...
}
...
// словники
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
hIdentitesClients.put(identite(c), c);
}
}
Метод init створює списки та словники, наведені в рядках 5–9. Недоліком цієї техніки є те, що ці елементи більше не враховують зміни в базі даних (додавання клієнта, лікаря тощо). Метод refresh примусово перебудовує ці списки та словники. Тому його слід використовувати щоразу, коли в базі даних відбуваються зміни, наприклад, додається новий клієнт.
6.17. Conclusion
Нагадаємо архітектуру додатка, який ми щойно створили:
![]() |
Ми значною мірою спиралися на вже створену версію JSF2:
- шари [métier], [DAO], [JPA] були збережені,
- компоненти [Application] та [Form] веб-шару були збережені, але до них було додано нові функції у зв’язку з розширенням можливостей користувацького інтерфейсу,
- користувацький інтерфейс було суттєво змінено. Зокрема, він став функціональнішим та зручнішим у використанні.
Перехід від JSF до Primefaces для створення веб-інтерфейсу вимагає певного досвіду, оскільки спочатку можна трохи розгубитися перед великою кількістю доступних компонентів і, зрештою, не знати, які саме використовувати. Тому слід зосередитися на бажаній ергономіці інтерфейсу.
6.18. Тестування в Eclipse
Як і в попередніх версіях прикладу додатка, ми покажемо, як протестувати цю версію 03 за допомогою Eclipse. Спочатку імпортуємо в Eclipse проекти Maven з прикладу 03 [1]:
![]() |
- [mv-rdvmedecins-ejb-dao-jpa]: шари [DAO] та [JPA],
- [mv-rdvmedecins-ejb-metier]: шар [métier],
- [mv-rdvmedecins-pf]: шар [web], реалізований за допомогою JSF та Primefaces,
- [mv-rdvmedecins-app]: батьківський проект корпоративного проекту [mv-rdvmedecins-app-ear]. Під час імпортування батьківського проекту дочірній проект автоматично імпортується,
- у [2] виконується корпоративний проект [mv-rdvmedecins-app-ear],
![]() |
- у [3] вибирається сервер Glassfish,
- у [4], на вкладці [Servers], додаток було розгорнуто. Він не запускається самостійно. Необхідно запустити її в браузері:
![]() |
















































