Skip to content

9. Приклад додатка 05: rdvmedecins-pfm-ejb

Нагадаємо структуру прикладу додатка 01 JSF / EJB, розробленого для сервера Glassfish:

Ми не змінюємо цю архітектуру, за винятком веб-шару, який тут буде реалізовано за допомогою JSF, Primefaces та Primefaces mobile. Цільовим браузером буде мобільний браузер.

Ми розробили два додатки на базі Glassfish:

  • додаток 01 використовував JSF / EJB і мав досить простий інтерфейс,
  • додаток 03 використовував PF / EJB і мав багатофункціональний інтерфейс.

Через невелику кількість компонентів, доступних у Primefaces Mobile, ми повернемося до простого інтерфейсу додатка 01. Крім того, екранні форми повинні адаптуватися до зменшеного розміру екранів мобільних пристроїв.

9.1. Візуалізації

Для наочності наводимо кілька знімків екрана додатка, що працює в симуляторі iPhone 4:

  • на [1] — головна сторінка. Зазначимо, що цього разу довелося вказати ім’я машини (можна також вказати її адресу IP), оскільки на машині localhost нічого не відображалося,
  • на [2] — список лікарів,
  • на [3] — бажану дату прийому,
  • у [4] — кнопка для перегляду розкладу на день,
  • у [5] — новий вигляд, що відображає часові проміжки лікаря,
  • в [6] — набір кнопок для навігації по календарю,
  • в [7] — повідомлення з нагадуванням про лікаря та день,
  • у [8] — часовий проміжок для бронювання. Робимо це,
  • у [9] — вікно вибору клієнта,
  • у [10] — повідомлення з нагадуванням про лікаря, день та часовий проміжок, на які призначено зустріч,
  • в [11] — список клієнтів,
  • в [12] — кнопка підтвердження,
  • у [13] після підтвердження ми повертаємося до календаря,
  • у [14] — зайнятий часовий проміжок тепер зарезервовано. Тепер ми скасуємо бронювання,
  • у [15] ми залишаємося на тому самому екрані,
  • але в [16] зустріч було скасовано,

На головній сторінці можна змінити мову: [17], [18], [19]:

Нарешті, передбачено перегляд помилок:

9.2. Проєкт NetBeans

Проект NetBeans виглядає так:

  
  • [mv-rdvmedecins-ejb-dao-jpa]: проект EJB, що складається з шарів [DAO] та [JPA] з прикладу 01,
  • [mv-rdvmedecins-ejb-metier]: проект EJB шару [métier] з прикладу 01,
  • [mv-rdvmedecins-pfmobile]: проект шару [web] / Primefaces mobile — новий,
  • [mv-rdvmedecins-pfmobile-app-ear]: корпоративний проєкт для розгортання додатка на сервері Glassfish — новий.

9.3. Корпоративний проєкт

Корпоративний проєкт призначений виключно для розгортання трьох модулів [mv-rdvmedecins-ejb-dao-jpa], [mv-rdvmedecins-ejb-metier], [mv-rdvmedecins-pfmobile] на сервері 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>
...

  <groupId>istia.st</groupId>
  <artifactId>mv-rdvmedecins-pfmobile-app-ear</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>ear</packaging>

  <name>mv-rdvmedecins-pfmobile-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-pfmobile</artifactId>
            <version>${project.version}</version>
            <type>war</type>
        </dependency>
    </dependencies>
</project>
  • рядки 6–9: артефакт Maven корпоративного проєкту,
  • рядки 14–33: три залежності проекту. Зверніть увагу на їхній тип (рядки 19, 25, 31).

Щоб запустити веб-додаток, потрібно запустити цей корпоративний проєкт.

9.4. Веб-проект Primefaces mobile

Веб-проект Primefaces mobile виглядає так:

  • у [1] — сторінки проекту. Сторінка [index.xhtml] є єдиною сторінкою проекту. Вона містить п’ять представлень: [vue1.xhtml], [vue2.xhtml], [vue3.xhtml], [vueErreurs.xhtml] та [config.xhtml],
  • а в [2] — Java-біни. Бін [Application] має область дії application, а бін [Form] — session. Клас [Erreur] інкапсулює помилку,
  • у [3] — файли повідомлень для інтернаціоналізації,
  • а в [4] — залежності. Веб-проект має залежності від проекту EJB з шару [DAO], проекту EJB на рівні [métier] та Primefaces mobile на рівні [web].

9.5. Конфігурація проєкту

Конфігурація проєкту відповідає конфігурації проєктів 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.PROJECT_STAGE</param-name>
    <param-value>Development</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>
    <default-render-kit-id>PRIMEFACES_MOBILE</default-render-kit-id>
  </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>

[messages_fr.properties]: файл повідомлень французькою мовою


# сторінка
page.titre=Les M\u00e9decins Associ\u00e9s
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy

# вигляд1
vue1.header=Les M\u00e9decins Associ\u00e9s - R\u00e9servations

# форма 1
form1.titre=R\u00e9servations
form1.medecin=M\u00e9decin
form1.jour=Jour (jj/mm/aaaa)
form1.date.requise=La date est n\u00e9cessaire
form1.date.invalide=La date est invalide
form1.date.invalide_detail=La date est invalide
form1.agenda=Agenda
form1.options=Options

# форма 2
form2.titre={0} {1} {2}<br/>{3}
form2.titre_detail={0} {1} {2}<br/>{3}
form2.retour=Retour
form2.supprimer=Supprimer
form2.reserver=R\u00e9server
form2.precedent=Jour pr\u00e9c\u00e9dent
form2.suivant=Jour suivant
form2.today=Aujourd'hui

# форма 3
form3.client=Client
form3.valider=Valider
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.retour=Retour

# помилка
erreur.titre=Une erreur s'est produite.

# конфігурація
config.retour=Retour
config.titre=Configuration
config.langue=Langue
config.langue.francais=Fran\u00e7ais
config.langue.anglais=Anglais
config.valider=Valider

#виняток
exception.titre=Application indisponible. Veuillez recommencer ult\u00e9rieurement.

[messages_en.properties]: файл повідомлень англійською мовою


# сторінка
page.titre=The Associated Doctors
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy

# вигляд1
vue1.header=The Associated Doctors - Reservations

# форма 1
form1.titre=Reservations
form1.medecin=Doctor
form1.jour=day (dd/mm/yyyy)
form1.date.requise=The date is necessary
form1.date.invalide=Invalid date
form1.date.invalide_detail=invalid date
form1.agenda=Diary
form1.options=Options

# форма 2
form2.titre={0} {1} {2}<br/>{3}
form2.titre_detail={0} {1} {2}<br/>{3}
form2.retour=Back
form2.supprimer=Delete
form2.reserver=Reserve
form2.precedent=Previous Day
form2.suivant=Next Day
form2.today=Today

# форма 3
form3.client=Patient
form3.valider=Validate
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.retour=Back

# помилка
erreur.titre=Some error happened

# конфігурація
config.retour=Back
config.titre=Configuration
config.langue=Language
config.langue.francais=French
config.langue.anglais=English
config.valider=Validate

#виняток
exception.titre=Application not available. Please try again later.

9.6. Сторінка [index.xhtml]

Проєкт завжди відображає одну й ту саму сторінку, а саме наступну сторінку [index.xhtml]:


<f:view xmlns="http://www.w3.org/1999/xhtml"
        xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html"
        xmlns:ui="http://java.sun.com/jsf/facelets"
        xmlns:p="http://primefaces.org/ui"
        xmlns:pm="http://primefaces.org/mobile"
        contentType="text/html"
        locale="#{form.locale}">

  <pm:page title="#{msg['page.titre']}">
    <pm:view id="vue1">
      <ui:fragment rendered="#{form.form1Rendered}">
        <ui:include src="vue1.xhtml"/>
      </ui:fragment>
      <ui:fragment rendered="#{form.erreurInit}">
        <ui:include src="vueErreurs.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="vue2">
      <ui:fragment rendered="#{form.form2Rendered}">
        <ui:include src="vue2.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="vue3">
      <ui:fragment rendered="#{form.form3Rendered}">
        <ui:include src="vue3.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="vueErreurs">
      <ui:fragment rendered="#{form.erreurRendered}">
        <ui:include src="vueErreurs.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="config">
      <ui:include src="config.xhtml"/>
    </pm:view>
  </pm:page>    
</f:view>
  • рядок 8: сторінка інтернаціоналізована (атрибут locale),
  • рядок 10: сторінка містить п’ять переглядів: vue1 у рядку 11, vue2 у рядку 19, vue3 у рядку 24, vueErreurs у рядку 29, config у рядку 34. У певний момент видно лише один із цих переглядів. Під час запуску додатка відображається перегляд vue1. Тут ми зіткнулися з такою проблемою: якщо ініціалізація додатка пройшла успішно, має відображатися [vue1.xhtml], інакше має відображатися [vueErreurs.xhtml]. Ми вирішили цю проблему, забезпечивши, щоб вміст виду vue1 керувався моделлю, яка присвоює значення булевим змінним [Form].form1rendered (рядок 12) та [Form].erreurInit (рядок 15), щоб зафіксувати вміст vue1 (рядок 11),

У симуляторі вигляд [vue1.xhtml] має рендеринг [1], вигляд [vue2.xhtml] — рендеринг [2], а вигляд [vue3.xhtml] — вивід [3]:

вигляд [vueErreurs.xhtml], відображення [4], вигляд [config.xhtml], відображення [5]:

9.7. Компоненти проекту

Клас з пакета [utils] вже було представлено: клас [Messages] — це клас, що полегшує інтернаціоналізацію повідомлень додатка. Його було розглянуто в розділі 2.8.5.7.

9.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 сервера додатка. Нагадаємо архітектуру додатка:

Додатки PFM, EJB та [Metier] будуть виконуватися в одній і тій самій JVM (Java Virtual Machine). Отже, рівень [PFM] використовуватиме локальний інтерфейс EJB. Ось і все. Бін [Application] не містить нічого іншого. Щоб отримати доступ до шару [métier], інші біни будуть звертатися до нього через цей бін.

9.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: повідомлення про помилку.

9.7.3. Бін [Form]

Його код такий:


package beans;

...

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

  public Form() {
  }
  // біан додатка
  @Inject
  private Application application;
  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 Long idMedecin;
  private Date jour = new Date();
  private String strJour;
  private Boolean form1Rendered;
  private Boolean form2Rendered;
  private Boolean form3Rendered;
  private Boolean erreurRendered;
  private String form2Titre;
  private String form3Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Long idCreneauChoisi;
  private Medecin medecin;
  private Long idClient;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;
  private Boolean erreurInit = false;
  private String action;
  private String locale = "fr";
  private String msgErreurDate = "";
  private SimpleDateFormat dateFormatter;
  private Boolean erreurDate;
  
  @PostConstruct
  private void init() {
    System.out.println("init");
    // спочатку помилок немає
    erreurInit = false;
    // форматування дат
    dateFormatter = new SimpleDateFormat(Messages.getMessage(null, "format.date", null).getSummary());
    dateFormatter.setLenient(false);
    // поточний день
    strJour = dateFormatter.format(jour);
    // отримуємо бізнес-шар
    metier = application.getMetier();
    // заносимо лікарів та клієнтів у кеш
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // фіксуємо помилку
      erreurInit = true;
      prepareVueErreur(th);
      return;
    }
    // перевірка списків
    if (medecins.size() == 0) {
      // фіксуємо помилку
      erreurInit = true;
      erreurs = new ArrayList<Erreur>();
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // фіксуємо помилку
      erreurInit = true;
      erreurs = new ArrayList<Erreur>();
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // помилка?
    if (erreurInit) {
      // відображається перелік помилок
      setForms(false, false, false, true);
      return;
    }

    // словники
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
    // вигляд 1
    setForms(true, false, false, false);
  }

  // відображення вікна
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.form3Rendered = form3Rendered;
    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(false, false, false, true);
  }

  // гетери та сеттери
  ..
}
  • рядки 5–7: клас [Form] є біном з іменем «form» та областю дії «session». Нагадаємо, що в цьому випадку клас повинен бути серіалізованим,
  • рядки 12–13: бін «form» має посилання на бін «application». Це посилання буде введено контейнером сервлетів, у якому виконується додаток (наявність анотації @Inject).
  • рядки 24–27: керують відображенням переглядів vue1 (рядок 24), vue2 (рядок 25), vue3 (рядок 26), vueErreurs (рядок 27),
  • рядки 43–44: метод init виконується одразу після створення екземпляра класу (наявність анотації @PostConstruct),
  • рядки 49–50: керують форматом дат. PrimeFaces Mobile не надає календаря. Крім того, валідатори JSF не можна використовувати на сторінці PFM. Тому нам доведеться вручну керувати введенням дати в календарі,
  • рядок 49: визначає формат дати. Цей формат береться з інтернаціоналізованих файлів:

format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
  • рядок 50: вказується, що дати не повинні мати формат «laxiste». Якщо цього не зробити, таке введення, як 32/12/2011, яке є неправильним, розглядається як дійсна дата 01/01/2012,
  • рядок 54: отримуємо посилання на шар [métier] від bean [Application],
  • рядки 57–58: запитується у шару [métier] список лікарів та клієнтів,
  • рядки 66–91: якщо все пройшло успішно, формуються словники лікарів та клієнтів. Вони індексуються за номерами. Далі відображається вид [vue1.xhtml] (рядок 93),
  • рядок 59: у разі помилки формується шаблон сторінки [vueErreurs.xhtml]. Цей шаблон — це список помилок із рядка 35,
  • рядки 105–115: метод [prepareVueErreur] формує список помилок для відображення. Сторінка [index.xhtml] потім відображає представлення [vueErreurs.xhtml] (рядок 114).

Сторінка [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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <!-- Вигляд «Помилки» -->
  <pm:header title="#{msg['page.titre']}" swatch="b">
    <f:facet name="left">
      <p:button icon="home" value=" " href="#vue1?reverse=true" />
    </f:facet>
  </pm:header>
  <pm:content>
    <div align="center">
      <h1><h:outputText value="#{msg['erreur.titre']}" style="color: blue"/></h1>
    </div>

    <p:dataList value="#{form.erreurs}" var="erreur">
      <b>#{erreur.classe}</b> : <i>#{erreur.message}</i>
    </p:dataList>
  </pm:content>
</html>

Вона використовує тег <p:dataList> (рядки 21–23) для відображення списку помилок. Кнопка в рядку 13 дозволяє повернутися до подання vue1.

  • кнопка [1] створюється рядком 13. Атрибут icon визначає піктограму кнопки. Кнопка повертає до подання vue1 (атрибут href),
  • заголовок [2] формується рядками 11–15,
  • заголовок [3] генерується рядком 18,
  • текст [4] генерується за шаблоном #{erreur.classe} у рядку 22,
  • текст [5] генерується за шаблоном #{erreur.message} з рядка 22.

Тепер ми визначимо різні етапи життєвого циклу додатка. Для кожної дії користувача ми розглянемо відповідні подання та обробники подій, що в них відбуваються.

9.8. Відображення головної сторінки

Якщо все йде як слід, першим відображається екран [vue1.xhtml]. Це дає такий вигляд:

Код екрану [vue1.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <!-- Вигляд 1 -->
  <pm:header title="#{msg['page.titre']}" swatch="b">
    <f:facet name="left">
      <p:button icon="gear" value=" "  href="#config" />
    </f:facet>
  </pm:header>
  <pm:content>
    <h:form id="form1">
      <div align="center">
        <h1><h:outputText value="#{msg['form1.titre']}" style="color: blue"/></h1>
      </div>
      <pm:field>
        <h:outputLabel value="#{msg['form1.medecin']}" for="choixMedecin"/>
        <h:selectOneMenu id="choixMedecin" value="#{form.idMedecin}">  
          <f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>  
        </h:selectOneMenu>              
      </pm:field>
      <pm:field>
        <h:outputLabel value="#{msg['form1.jour']}" for="jour"/>
        <p:inputText id="jour" value="#{form.strJour}"/>
        <ui:fragment rendered="#{form.erreurDate}">
          <p:spacer width="50px"/>
          <h:outputText id="msgErreurDate" value="#{form.msgErreurDate}" style="color: red"/>
        </ui:fragment>
      </pm:field>
      <p:commandButton value="#{msg['form1.agenda']}" update=":form1, :vue2, :vueErreurs" action="#{form.getAgenda}" />
    </h:form>
  </pm:content>
</html>
  • рядки 11–15: формують заголовок [1],
  • рядок 13: створює кнопку [2]. При натисканні на неї відображається вікно налаштувань (атрибут href),
  • рядок 19: створює заголовок [3],
  • рядки 21–26: створюють комбінований список лікарів [4],
  • рядки 27–34: створюють поле введення дати [5]. Це поле введення реалізовано за допомогою тегу <p:inputText> без валідатора. Перевірка дати відбуватиметься на стороні сервера. Якщо дата неправильна, сервер розмістить повідомлення про помилку, яке відображається у рядках 30–34,
  • рядок 35: кнопка, що підтверджує форму. Вона оновлює три області: form1 (форма у vue1), vue2 та vueErreurs. Адже якщо дата недійсна, оновлювати потрібно саме form1. Якщо дата правильна, оновлювати потрібно vue2. Нарешті, якщо трапляється виняток (наприклад, збій підключення до бази даних), має відображатися vueErreurs. Може виникнути спокуса замінити form1 на vue1 (щоб оновити весь вигляд). У цьому випадку додаток дасть збій.

Цей вигляд базується на такій моделі:


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

  public Form() {
  }

  // Bean «Application»
  @Inject
  private Application application;
  private IMetierLocal metier;
  // кеш сеансу
  private List<Medecin> medecins;
  private List<Client> clients;
  // модель
  private Long idMedecin;
  private Date jour = new Date();
  private String strJour;
  private Boolean form1Rendered;
  private Boolean form2Rendered;
  private Boolean form3Rendered;
  private Boolean erreurRendered;
  private String msgErreurDate = "";
  private Boolean erreurDate;
  
    // список лікарів
  public List<Medecin> getMedecins() {
    return medecins;
  }

  // календар
  public String getAgenda() {
...
    }
  }
  • поле в рядку 16 забезпечує читання та запис значення списку в рядку 23 сторінки. Під час початкового відображення сторінки воно встановлює значення, вибране в комбінованому списку,
  • метод у рядках 27–29 генерує елементи комбінованого списку лікарів (рядок 24 подання). Кожен згенерований варіант матиме як мітку (itemLabel) титул, прізвище та ім’я лікаря, а як значення (itemValue) — ідентифікатор лікаря,
  • поле в рядку 18 забезпечує читання/запис у поле введення в рядку 29 сторінки,
  • рядки 32–34: метод getAgenda обробляє натискання кнопки [Agenda] у рядку 35 сторінки. Його код такий:

// календар
  public String getAgenda() {
    try {
      // перевірка дати
      jour = dateFormatter.parse(strJour);
      // помилок немає
      erreurDate=false;
      msgErreurDate = "";
      // створюється календар
      return getAgenda(jour);
    } catch (ParseException ex) {
      // повідомлення про помилку
      erreurDate=true;
      msgErreurDate = Messages.getMessage(null, "form1.date.invalide", null).getSummary();
      // вид1      
      setForms(true, false, false, false);
      return "pm:vue1";
    }
  }
  • метод спочатку перевіряє правильність дати, введеної користувачем,
  • рядок 5: дата має вигляд parsée відповідно до формату дати, ініціалізованого методом init під час інстанціювання моделі,
  • рядок 11: якщо виникає виняток, встановлюється помилка (рядок 13), формується інтернаціоналізоване повідомлення про помилку (рядок 14), готується представлення vue1 (рядок 16) і відображається представлення vue1 (рядок 17),
  • рядок 10: якщо дата правильна, виконується наступний метод:

  // календар
  public String getAgenda(Date jour) {
    // наразі не обрано жодного часового проміжку
    creneauChoisi = null;
    try {
      // отримано інформацію про обраного лікаря
      medecin = hMedecins.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 = metier.getAgendaMedecinJour(medecin, jour);
      // відображення виду 2
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      System.out.println(th);
      // перегляд помилок
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
}

Тут ми бачимо код, який вже неодноразово зустрічався. У рядку 9 формується інтернаціоналізоване повідомлення для подання vue2:


form2.titre={0} {1} {2}<br/>{3}
form2.titre_detail={0} {1} {2}<br/>{3}

Зверніть увагу, що ми вставили XHTML у повідомлення. Воно відобразиться наступним чином:

9.9. Відображення розкладу лікаря

Розклад лікаря відображається за допомогою подання [vue2.xhtml]:

Код подання [vue2.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <!-- Вигляд 2 -->
  <pm:header title="#{msg['page.titre']}" swatch="b"/>
  <pm:content>
    <h:form id="form2">
      <div align="center">
        <pm:buttonGroup orientation="horizontal">
          <p:commandButton inline="true" icon="back" value=" " action="#{form.showVue1}" update=":vue1"/>
          <p:commandButton inline="true" icon="minus" value=" " action="#{form.getPreviousAgenda}" update=":form2"/>
          <p:commandButton inline="true" icon="home" value=" " action="#{form.getTodayAgenda}" update=":form2"/>
          <p:commandButton inline="true" icon="plus" value=" " action="#{form.getNextAgenda}" update=":form2"/>
        </pm:buttonGroup>
        <h3><h:outputText value="#{form.form2Titre}" style="color: blue" escape="false"/></h3>
      </div>

      <p:dataList id="creneaux" type="inset" value="#{form.agendaMedecinJour.creneauxMedecinJour}" var="creneauMedecinJour">
        <p:column>
          <div align="center">
            <h2>
              <h:outputFormat value="{0,number,#00}h:{1,number,#00} - {2,number,#00}h:{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>
              <ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
                <br/>
                <h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}" style="color: blue"/>
              </ui:fragment>
            </h2>
          </div>
          <div align="center">
            <ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.supprimer']}" icon="minus" update=":form2, :vue3, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
            <ui:fragment rendered="#{creneauMedecinJour.rv==null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.reserver']}" icon="plus" update=":form2, :vue3, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
          </div>
        </p:column>
      </p:dataList>
    </h:form>
  </pm:content>
</html>
  • рядок 11: формує заголовок [1],
  • рядки 15–20: формують групу кнопок [2],
  • рядок 21: генерує заголовок [3]. Зверніть увагу на значення атрибута escape. Саме воно дозволяє інтерпретувати код XHTML, який ми помістили в form2Titre,
  • рядок 24: відображення часових інтервалів здійснюється за допомогою dataList,
  • рядки 28–33: формують заголовок часового проміжку [4],
  • рядки 34–37: відображають фрагмент, якщо у часовому проміжку є зустріч,
  • рядок 36: відображає ім’я клієнта, який записався на зустріч,
  • рядки 41–45: відображають кнопку [Supprimer], якщо є зустріч,
  • рядки 46–50: відображають кнопку [Réserver], якщо зустрічі немає.

Цей вигляд в основному формується за допомогою такої моделі:


private AgendaMedecinJour agendaMedecinJour;

яка заповнює поле dataList у рядку 24. Це поле було сформовано методом getAgenda під час переходу з подання vue1 до подання vue2.

9.10. Видалення зустрічі

Видалення зустрічі відповідає такій послідовності:

Погляд, до якого відноситься ця дія, є таким:


<ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.supprimer']}" icon="minus" update=":form2, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
  • рядок 2: кнопка [Supprimer] пов’язана з методом [Form].action (атрибут action),
  • рядок 3: ідентифікатор сегмента, на якому знаходиться курсор, буде передано до моделі [Form].idCreneauChoisi,
  • рядок 2: виклик AJAX оновить поля form2 (форма «view2») та вигляд vueErreurs. Фактично маємо два випадки: якщо все пройде успішно, буде знову відображено вигляд vue2, інакше — вигляд vueErreurs.

Метод [action] виглядає наступним чином:


  // дія над RV
  public String action() {
    // шукаємо вільний час у календарі
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneauChoisi) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // знайдено?
    if (!trouvé) {
      // Дивно — знову відображаємо form2
      setForms(false, true, false, false);
      return "pm:vue2";
    } else {
      creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    }
    // знайдено
    // відповідно до бажаної дії
    if (creneauChoisi.getRv() == null) {
      return reserver();
    } else {
      return supprimer();
    }
  }

  // бронювання
  public String reserver() {
 ...
  }

  public String supprimer() {
    try {
      // видалення зустрічі
      metier.supprimerRv(creneauChoisi.getRv());
      // оновлюємо календар
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // відображається form2
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      // перегляд помилок
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
}
  • рядки 3–12: шукаємо часовий проміжок, ідентифікатор якого отримано (рядок 7),
  • якщо його не знайдено, що є аномалією, знову відображається vue2 (рядки 16–17),
  • рядок 19: якщо його знайдено, запам'ятовується відповідний об'єкт [CreneauMedecinJour]. Саме він надає доступ до зустрічі, яку потрібно видалити,
  • рядок 26: його видаляємо,
  • рядки 35–48: метод «видалити» повертає вікно vue2, якщо видалення пройшло успішно (рядки 42–43), або вікно vueErreurs, якщо виникла проблема (рядки 46–47).

9.11. Запис на прийом

Запис на прийом відбувається за такою послідовністю:

Перехід відбувається з перегляду vue2 до перегляду vue3. Код, пов’язаний із цією дією, є таким:


<ui:fragment rendered="#{creneauMedecinJour.rv==null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.reserver']}" icon="plus" update=":vue3, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
  • рядок 2: кнопка [Réserver] пов’язана з методом [Form].action (атрибут action), тобто таким самим, як і для кнопки [Supprimer]. Виклик AJAX оновлює подання vue3 та vueErreurs залежно від того, чи виникли помилки під час обробки виклику.
  • рядок 3: як і для кнопки [Supprimer], ідентифікатор часового проміжку передається до моделі.

Шаблон, який обробляє цю дію, має такий вигляд:


// дія над RV
  public String action() {
    ...
    // залежно від бажаної дії
    if (creneauChoisi.getRv() == null) {
      return reserver();
    } else {
      return supprimer();
    }
  }

  // бронювання
  public String 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();
      // відображається форма 3
      setForms(false, false, true, false);
      return "pm:vue3";
    } catch (Throwable th) {
      // перегляд помилок
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
  }
  • рядки 2–10: метод action визначає посилання creneauChoisi на об’єкт [CreneauMedecinJour], який є предметом бронювання, а потім викликає метод reserver,
  • рядок 16: формується інтернаціоналізоване повідомлення. Воно має такий вигляд:
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}

Це буде заголовок подання vue3. Як і у випадку з поданням vue2, цей заголовок містить XML. Він також містить параметри, відформатовані для відображення розкладу часового проміжку,

  • рядки 19–20: відображається вікно vue3,
  • рядки 23–24: якщо виникли проблеми, відображається вікно vueErreurs.

9.12. Підтвердження зустрічі

Підтвердження зустрічі відбувається за такою послідовністю:

У [1] — вигляд vue3, а в [2] — вигляд vue2 після додавання зустрічі.

Код vue3 у [vue3.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <!-- Вигляд 3 -->
  <pm:header title="#{msg['page.titre']}" swatch="b"/>
  <pm:content>
    <h:form id="form3">
      <p:commandButton inline="true" value=" " icon="back" action="#{form.showVue2}" update=":vue2"/>
      <div align="center">
        <h3><h:outputText value="#{form.form3Titre}" style="color: blue" escape="false"/></h3>
      </div>
      <pm:field>
        <h:outputLabel value="#{msg['form3.client']}" for="choixClient"/>
        <h:selectOneMenu id="choixClient" value="#{form.idClient}">
          <f:selectItems value="#{form.clients}" var="client" itemLabel="#{client.titre} #{client.prenom} #{client.nom}" itemValue="#{client.id}"/>
        </h:selectOneMenu>
      </pm:field>
      <div align="center">
        <p:commandButton inline="true" value="#{msg['form3.valider']}" action="#{form.validerRv}" update=":vue2, :vueErreurs" icon="check"/>
      </div>
    </h:form>
  </pm:content>
</html>
  • рядок 16: формує заголовок виду [3]. Зверніть увагу на значення атрибута escape, яке дозволяє інтерпретувати символи XHTML у заголовку,
  • рядки 18–23: формують комбінований список клієнтів [4],
  • рядок 25: створює кнопку [Valider] [5]. З цією кнопкою пов’язаний метод [Form].validerRv:

// підтвердження зустрічі
  public String validerRv() {
    try {
      // отримуємо екземпляр обраного часового проміжку
      Creneau creneau = metier.getCreneauById(idCreneauChoisi);
      // додаємо зустріч
      metier.ajouterRv(jour, creneau, hClients.get(idClient));
      // оновлюємо календар
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // відображаємо form2
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      // перегляд помилок
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
  }

Цей код вже зустрічався у версії 01. Зазначимо лише відображення переглядів:

  • погляд vue2 (рядки 11–12), якщо все пройшло успішно,
  • погляд vueErreurs (рядки 15–16) — у протилежному випадку.

9.13. Скасування запису на прийом

Це відповідає такій послідовності:

Кнопка [1] у вікні [vue3.xhtml] виглядає так:


      <p:commandButton inline="true" value=" " icon="back" action="#{form.showVue2}" update=":vue2"/>

Отже, викликається метод [Form].showVue2. Він просто відображає вид2:


  public String showVue2() {
    // вигляд 2
    setForms(false, true, false, false);
    return "pm:vue2?reverse=true";
}

9.14. Навігація в календарі

У «vue2» за допомогою кнопок можна пересуватися по календарю:

Попередній день:

Наступний день:

Сьогодні:

Хоча на наведених вище скріншотах цього не видно, календар оновлюється і відображає зустрічі нового обраного дня.

Теги трьох відповідних кнопок у [vue2.xhtml] такі:


<pm:buttonGroup orientation="horizontal">
          <p:commandButton inline="true" icon="back" value=" " action="#{form.showVue1}" update=":vue1"/>
          <p:commandButton inline="true" icon="minus" value=" " action="#{form.getPreviousAgenda}" update=":form2"/>
          <p:commandButton inline="true" icon="home" value=" " action="#{form.getTodayAgenda}" update=":form2"/>
          <p:commandButton inline="true" icon="plus" value=" " action="#{form.getNextAgenda}" update=":form2"/>
        </pm:buttonGroup>

Методи [Form].getPreviousAgenda, [Form].getNextAgenda, [Form].today були розглянуті в прикладі 03.

9.15. Зміна мови відображення

Зміна мови здійснюється за допомогою кнопки на головній сторінці:

Код кнопки такий:


      <p:button icon="gear" value=" "  href="#config" />

Отже, він переводить у вигляд конфігурації [2]. Вигляд [config.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <!-- Вигляд 1 -->
  <pm:header title="#{msg['page.titre']}" swatch="b">
    <f:facet name="left">
      <p:button icon="back" value=" " href="#vue1?reverse=true" />
    </f:facet>
  </pm:header>
  <pm:content>
    <h:form id="frmConfig">
      <div align="center">
        <h3><h:outputText value="#{msg['config.titre']}" style="color: blue"/></h3>
      </div>
      <pm:field>
        <h:outputLabel value="#{msg['config.langue']}" for="langue"/>
        <h:selectOneRadio id="langue" value="#{form.locale}">
          <f:selectItem itemLabel="#{msg['config.langue.francais']}" itemValue="fr"/>
          <f:selectItem itemLabel="#{msg['config.langue.anglais']}" itemValue="en" />
        </h:selectOneRadio>
      </pm:field>
      <p:commandButton value="#{msg['config.valider']}" action="#{form.configurer}" update=":vue1"/>
    </h:form>
  </pm:content>
</html>
  • рядок 11: відображає [3],
  • рядок 13: відображає кнопку [4]. Ця кнопка дозволяє повернутися до подання vue1,
  • рядок 17: форма подання,
  • рядок 19: відображає заголовок подання [5],
  • рядки 21–27: відображають перемикачі. Значення (itemValue) вибраного перемикача буде передано до моделі [Form].locale (атрибут value у рядку 23),
  • рядок 28: відображає кнопку [Valider]. Виклик AJAX оновлює подання vue1 (атрибут update). Викликаний метод — [Form].configurer:

public String configurer(){
    // після налаштування — повторно відображається перегляд 1
    redirect();
    return null;
  }
  
  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);
    }
  }

Метод «configurer» (рядок 1) просто перенаправляє браузер мобільного пристрою на сторінку URL додатка. Отже, завантажиться сторінка [index.xhtml]:


<f:view xmlns="http://www.w3.org/1999/xhtml"
        xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html"
        xmlns:ui="http://java.sun.com/jsf/facelets"
        xmlns:p="http://primefaces.org/ui"
        xmlns:pm="http://primefaces.org/mobile"
        contentType="text/html"
        locale="#{form.locale}">

  <pm:page title="#{msg['page.titre']}">
    <pm:view id="vue1">
      ...
    </pm:view>
    ...
  </pm:page>    
</f:view>
  • рядок 8: вікно використовуватиме мову, яку щойно було змінено (атрибут locale), і відображатиме vue1 (рядок 11).

9.16. Conclusion

Нагадаємо архітектуру додатка, який ми щойно створили:

Перехід на мобільний інтерфейс вимагав переписання сторінок XHTML. Натомість модель майже не змінилася. Нижчі шари [métier], [DAO], [JPA] взагалі не зазнали змін.

9.17. Тестування в Eclipse

Як і в попередніх версіях прикладу програми, ми покажемо, як протестувати цю версію 05 за допомогою Eclipse. Спочатку імпортуємо в Eclipse проекти Maven з прикладу 05 [1]:

  • [mv-rdvmedecins-ejb-dao-jpa]: шари [DAO] та [JPA],
  • [mv-rdvmedecins-ejb-metier]: шар [métier],
  • [mv-rdvmedecins-pfmobile]: шар [web], реалізований Primefaces mobile,
  • [mv-rdvmedecins-pfmobile-app]: батьківський проект корпоративного проекту [mv-rdvmedecins-pfmobile-app-ear]. Під час імпортування батьківського проекту дочірній проект автоматично імпортується,
  • у [2] виконується корпоративний проект [mv-rdvmedecins-pfmobile-app-ear],
  • у [3] вибирається сервер Glassfish,
  • у [4], на вкладці [Servers], додаток було розгорнуто. Він не запускається самостійно. Необхідно запустити її URL [http://localhost:8080/mv-rdvmedecins-pfmobile/] у браузері або мобільному симуляторі [5]: