Skip to content

6. Aplikacja przykładowa-03: rdvmedecins-pf-ejb

Przypomnijmy strukturę przykładowej aplikacji opracowanej dla serwera Glassfish:

Nie wprowadzamy żadnych zmian w tej architekturze, z wyjątkiem warstwy internetowej, która zostanie tutaj zrealizowana przy użyciu JSF i Primefaces.

6.1. Projekt NetBeans

Powyżej warstwy [métier] i [DAO] pochodzą z przykładu 01 JSF / EJB / Glassfish. Wykorzystujemy je ponownie.

  
  • [mv-rdvmedecins-ejb-dao-jpa]: projekt EJB warstw [DAO] i [JPA] z przykładu 01,
  • [mv-rdvmedecins-ejb-metier]: projekt EJB warstwy [métier] z przykładu 01,
  • [mv-rdvmedecins-pf]: projekt warstwy [web] / Primefaces – nowy,
  • [mv-rdvmedecins-app-ear]: projekt korporacyjny służący do wdrożenia aplikacji na serwerze Glassfish – nowy.

6.2. Projekt korporacyjny

Projekt korporacyjny służy wyłącznie do wdrożenia trzech modułów [mv-rdvmedecins-ejb-dao-jpa], [mv-rdvmedecins-ejb-metier], [mv-rdvmedecins-pf] na serwerze Glassfish. Projekt NetBeans ma następującą nazwę:

Projekt ten istnieje wyłącznie w celu obsługi tych trzech zależności [1], zdefiniowanych w pliku [pom.xml] w następujący sposób:


<?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>
  • wiersze 10–13: artefakt Maven projektu korporacyjnego,
  • wiersze 18–37: trzy zależności projektu. Należy zwrócić uwagę na ich typ (wiersze 23, 29, 35).

Aby uruchomić aplikację internetową, należy uruchomić ten projekt korporacyjny.

6.3. Projekt internetowy Primefaces

Projekt internetowy Primefaces wygląda następująco:

  • w [1] znajdują się strony projektu. Strona [index.xhtml] jest jedyną stroną projektu. Zawiera trzy fragmenty: [form1.xhtml], [form2.xhtml] oraz [erreur.xhtml]. Pozostałe strony służą wyłącznie do formatowania.
  • W pliku [2] znajdują się komponenty Java. Komponent [Application] o zakresie application, a komponent [Form] o zakresie session. Klasa [Erreur] hermetyzuje błąd. Klasa [MyDataModel] służy jako szablon dla tagu <dataTable> w Primefaces,
  • w [3] – pliki komunikatów do internacjonalizacji,
  • w [4] znajdują się pliki komunikatów na potrzeby internacjonalizacji, w [4] znajdują się zależności. Projekt internetowy jest zależny od projektu EJB z warstwy [DAO], projekt EJB z warstwy [métier] oraz Primefaces dla warstwy [web].

6.4. Konfiguracja projektu

Konfiguracja projektu jest taka sama jak w przypadku projektów Primefaces lub JSF, które już omówiliśmy. Wymieniamy pliki konfiguracyjne bez ponownego ich wyjaśniania.

 

[web.xml]: konfiguruje aplikację internetową.


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
  <context-param>
    <param-name>javax.faces.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>

Warto zwrócić uwagę, że w wierszu 30 strona [index.xhtml] jest stroną główną aplikacji.

[faces-config.xml]: konfiguruje aplikację JSF


<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
    <message-bundle>messages</message-bundle>
  </application>
</faces-config>

[beans.xml]: pusta, ale niezbędna do adnotacji @Named


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

[styles.css]: arkusz stylów aplikacji


.col1{
   background-color: #ccccff
}

.col2{
   background-color: #ffcccc
}

Biblioteka Primefaces zawiera własne arkusze stylów. Powyższy arkusz stylów jest używany wyłącznie do wyświetlania strony w przypadku wystąpienia wyjątku – strony, która nie jest obsługiwana przez aplikację. Wówczas wyświetlana jest strona [exception.xhtml].

[messages_fr.properties]: plik komunikatów w języku francuskim


# układ
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers - application propuls\u00e9e par PrimeFaces et JQuery

# wyjątek
exception.header=L'exception suivante s'est produite
exception.httpCode=Code HTTP de l'erreur
exception.message=Message de l'exception
exception.requestUri=Url demand\u00e9e lors de l'erreur
exception.servletName=Nom de la servlet demand\u00e9e lorsque l'erreur s'est produite

# formularz 1
form1.titre=R\u00e9servations
form1.medecin=M\u00e9decin
form1.jour=Jour
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

# formularz 2
form2.titre=Agenda de {0} {1} {2} le {3}
form2.titre_detail=Agenda de {0} {1} {2} le {3}
form2.creneauHoraire=Cr\u00e9neau horaire
form2.client=Client
form2.accueil=Accueil
form2.supprimer=Supprimer
form2.reserver=R\u00e9server
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

# błąd
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]: plik komunikatów w języku angielskim


# układ
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university - Application powered by PrimeFaces and JQuery

# wyjątek
exception.header=The following exceptions occurred
exception.httpCode=Error HTTP code
exception.message=Exception message
exception.requestUri=Url targeted when error occurred
exception.servletName=Servlet targeted's name when error occurred

# formularz 1
form1.titre=Reservations
form1.medecin=Doctor
form1.jour=Date
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

# formularz 2
form2.titre={0} {1} {2}'' diary on {3}
form2.titre_detail={0} {1} {2}'' diary on {3}
form2.creneauHoraire=Time Period
form2.client=Client
form2.accueil=Welcome Page
form2.supprimer=Delete
form2.reserver=Reserve
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

# błąd
erreur.titre=The following exceptions occurred
erreur.exceptions=Exceptions' chain
erreur.type=Exception type
erreur.message=Associated Message
erreur.accueil=Welcome

6.5. Szablon stron [layout.xhtml]

Szablon [layout.xhtml] wygląda następująco:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns: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>

Jedyną zmienną częścią tego szablonu jest obszar w wierszach 28–30. Obszar ten znajduje się w strefie o identyfikatorze :formularz:treść (wiersz 27). Należy o tym pamiętać. Wywołania AJAX, które aktualizują ten obszar, będą miały atrybut update=":formularz:treść". Ponadto formularz zaczyna się w wierszu 15. Zatem fragment wstawiony w wierszach 28–30 zostanie umieszczony w tym formularzu.

Wygląd wynikający z tego szablonu jest następujący:

Dynamiczna część strony zostanie umieszczona w obszarze zaznaczonym ramką powyżej.

6.6. Strona [index.xhtml]

Projekt zawsze wyświetla tę samą stronę, a mianowicie następującą stronę [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>
  • wiersze 8–9: ten fragment XHTML zostanie wstawiony do dynamicznego obszaru szablonu [layout.xhtml],
  • Strona zawiera trzy podfragmenty:
  • [form1.xhtml], wiersze 10–12;
  • [form2.xhtml], wiersze 13–15;
  • [erreur.xhtml], wiersze 16–18.

Obecność tych fragmentów w pliku [index.xhtml] jest kontrolowana przez wartości logiczne zawarte w szablonie [Form.java] powiązanym z tą stroną. Zmiana tych wartości powoduje zatem zmianę wyglądu wyświetlanej strony.

Fragment [form1.xhtml] ma następujący wygląd:

Fragment [form2.xhtml] wygląda następująco:

Fragment [erreur.xhtml] wyświetla się następująco:

6.7. Beany projektu

Klasa z pakietu [utils] została już przedstawiona: klasa [Messages] ułatwia internacjonalizację komunikatów aplikacji. Została ona omówiona w punkcie 2.8.5.7.

6.7.1. Bean Application

Bean [Application.java] jest beanem o zasięgu application. Przypomnijmy, że ten typ beana służy do przechowywania danych tylko do odczytu, dostępnych dla wszystkich użytkowników aplikacji. Bean ten wygląda następująco:


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 {

  // warstwa biznesowa
  @EJB
  private IMetierLocal metier;

  public Application() {
  }
  
  // metody pobierające

  public IMetierLocal getMetier() {
    return metier;
  }
  
}
  • wiersz 8: nadajemy beanowi nazwę „application”,
  • wiersz 9: ma zasięg aplikacji,
  • wiersze 13–14: odniesienie do lokalnego interfejsu warstwy [métier] zostanie mu wstrzyknięte przez kontener EJB serwera aplikacji. Przypomnijmy sobie architekturę aplikacji:

Aplikacje JSF, EJB i [Metier] będą uruchamiane w tej samej maszynie wirtualnej Java (JVM). Zatem warstwa [JSF] będzie korzystać z lokalnego interfejsu warstwy EJB. To wszystko. Bean [Application] nie zawiera niczego innego. Aby uzyskać dostęp do warstwy [métier], pozostałe beany będą ją pobierać z tego beana.

6.7.2. Bean [Erreur]

Klasa [Erreur] ma następującą postać:

  1. package beans;
    
    
    
        public class Erreur {
        
    
    
    
          public Erreur() {
        
      }
    
    
    
    
          // pole
        
    
          private String classe;
        
    
          private String message;
        
    
    
    
          // konstruktor
        
    
          public Erreur(String classe, String message){
        
    
            this.setClasse(classe);
        
    
            this.message=message;
        
      }
    
    
    
    
          // metody pobierające i ustawiające
        
    ...  
    
    }
    
  • wiersz 9 – nazwa klasy wyjątku, jeśli wyjątek został zgłoszony,
  • wiersz 10: komunikat o błędzie.

6.7.3. Bean [Form]

Jego kod wygląda następująco:


package beans;

import java.io.IOException;
...

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

  public Form() {
  }
  
// bean aplikacji
  @Inject
  private Application application;

  // pamięć podręczna sesji
  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>();

  // model
  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() {
    // buforowanie lekarzy i klientów
    try {
      medecins = application.getMetier().getAllMedecins();
      clients = application.getMetier().getAllClients();
    } catch (Throwable th) {
      // rejestrujemy błąd
      prepareVueErreur(th);
      return;
    }

    // słowniki
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
      hIdentitesClients.put(identite(c), c);
    }
  }

  ...

  // wyświetlanie widoku
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.erreurRendered = erreurRendered;
  }

  // przygotowanie vueErreur
  private void prepareVueErreur(Throwable th) {
    // tworzy się listę błędów
    erreurs = new ArrayList<Erreur>();
    erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
    while (th.getCause() != null) {
      th = th.getCause();
      erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
    }
// wyświetlany jest widok błędów
    setForms(true, false, true);
  }

  // metody pobierające i ustawiające
  ...
}
  • wiersze 6–8: klasa [Form] jest beanem o nazwie „form” i zasięgu sesji. Przypominamy, że w takim przypadku klasa musi być serializowalna,
  • wiersze 14–15: bean „form” posiada odwołanie do beana „application”. Odwołanie to zostanie wstrzyknięte przez kontener serwletów, w którym działa aplikacja (obecność adnotacji @Inject).
  • wiersze 17–44: szablon stron [form1.xhtml, form2.xhtml, erreur.xhtml]. Wyświetlanie tych stron jest kontrolowane przez wartości logiczne w wierszach 27–29. Należy zauważyć, że domyślnie wyświetlana jest strona [form1.xhtml] (wiersz 27),
  • wiersze 46–47: metoda init jest wykonywana zaraz po instancjonowaniu klasy (obecność adnotacji @PostConstruct),
  • wiersze 50–51: zwracamy się do warstwy [métier] z prośbą o listę lekarzy i klientów,
  • wiersze 59–65: jeśli wszystko przebiegło pomyślnie, tworzone są słowniki lekarzy i klientów. Są one indeksowane według numerów. Następnie wyświetlana jest strona [form1.xhtml] (wiersz 27),
  • wiersz 54: w przypadku błędu tworzony jest szablon strony [erreur.xhtml]. Szablon ten zawiera listę błędów z wiersza 36,
  • wiersze 78–88: metoda [prepareVueErreur] tworzy listę błędów do wyświetlenia. Strona [index.xhtml] wyświetla następnie fragmenty [form1.xhtml] i [erreur.xhtml] (wiersz 87).

Strona [erreur.xhtml] wygląda następująco:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns: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>

Wykorzystuje ona tag <p:dataTable> (wiersze 12–28) do wyświetlenia listy błędów. Daje to stronę błędu podobną do poniższej:

Teraz zdefiniujemy poszczególne fazy cyklu życia aplikacji. Dla każdej akcji użytkownika przeanalizujemy odpowiednie widoki i procedury obsługi zdarzeń.

6.8. Wyświetlanie strony głównej

Jeśli wszystko przebiegnie pomyślnie, pierwszą wyświetlaną stroną jest [form1.xhtml]. Wygląda to następująco:

Strona [form1.xhtml] wygląda następująco:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns: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>

Pasek narzędzi zaznaczony ramką na zrzucie ekranu to komponent Primefaces o nazwie Toolbar. Jest on zdefiniowany w wierszach 8–14. Zawiera dwie grupy komponentów, z których każda jest zdefiniowana za pomocą tagu <toolbarGroup>, w wierszach 9–11 i 12–14. Jedna z grup jest wyrównana do lewej strony paska narzędzi (wiersz 9), a druga do prawej (wiersz 12).

Przyjrzyjmy się niektórym komponentom z lewej grupy:


<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>
...
  • wiersze 4–7: lista rozwijana lekarzy, do której dodano efekt (effect="fade"),
  • wiersz 6: zachowanie AJAX. Gdy nastąpi zmiana w liście rozwijanej, zostanie wykonana metoda [Form].hideAgenda (listener="#{form.hideAgenda}") zostanie wykonana, a dynamiczny obszar :formularz:treść (update=":formulaire:contenu") zostanie zaktualizowany,
  • wiersz 8: dodaje separator do paska narzędzi,
  • wiersze 10–12: pole wprowadzania daty. Wykorzystujemy tu kalendarz Primefaces. Pole wprowadzania danych jest tylko do odczytu (readOnlyInputText="true"),
  • wiersz 11: zachowanie AJAX. W przypadku zmiany daty zostanie wykonana metoda [Form].hideAgenda, a dynamiczne pole :formularz:treść zostanie zaktualizowane,
  • wiersz 14: przycisk. Kliknięcie tego przycisku powoduje wywołanie funkcji AJAX w kierunku metody [Form].getAgenda (), po czym szablon zostanie zmodyfikowany, a odpowiedź serwera zostanie wykorzystana do aktualizacji dynamicznego obszaru :formularz:treść,
  • wiersz 15: tag <tooltip> pozwala przypisać dymek pomocy do komponentu. Identyfikator tego komponentu jest określony przez atrybut „for” tagu tooltip. Tutaj (for="resa-agenda") odnosi się do przycisku z wiersza 14:

Ta strona jest oparta na następującym szablonie:


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

  public Form() {
  }
  
  // pamięć podręczna sesji
  private List<Medecin> medecins;
  private List<Client> clients;
  // szablon
  private Long idMedecin;
  private Date jour = new Date();
  
  // lista lekarzy
  public List<Medecin> getMedecins() {
    return medecins;
  }

  // lista klientów
  public List<Client> getClients() {
    return clients;
  }

  // kalendarz
  public void getAgenda() {
    ...
  }
  • Pole w wierszu 12 odczytuje i zapisuje wartość z listy w wierszu 4 strony. Przy pierwszym wyświetleniu strony ustala ona wartość wybraną w polu kombi. Przy pierwszym wyświetleniu idMedecin jest równe null, więc wybrany zostanie pierwszy lekarz,
  • metoda z wierszy 16–18 generuje elementy listy rozwijanej lekarzy (wiersz 5 strony). Każda wygenerowana opcja będzie miała jako etykietę (itemLabel) tytuł, nazwisko i imię lekarza, a jako wartość (itemValue) identyfikator lekarza,
  • pole w wierszu 13 zasilają w trybie odczytu/zapisu pole wprowadzania danych w wierszu 10 strony. Przy pierwszym wyświetleniu widoczna jest zatem bieżąca data,
  • wiersze 26–28: metoda getAgenda obsługuje kliknięcie przycisku [Agenda] w wierszu 14 strony. Jest ona niemal identyczna z wersją JSF:

  // komponent Application
  @Inject
  private Application application;
  // pamięć podręczna sesji
  private List<Medecin> medecins;
  private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
  // szablon
  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 {
      // pobieranie lekarza
      medecin = hMedecins.get(idMedecin);
      // kalendarz lekarza na dany dzień
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // wyświetlanie formularza 2
      setForms(true, true, false);
    } catch (Throwable th) {
      // widok błędów
      prepareVueErreur(th);
    }
    // na razie nie wybrano żadnego terminu
    creneauChoisi = null;
}

Nie będziemy komentować tego kodu. Zostało to już zrobione.

6.9. Wyświetl kalendarz lekarza

6.9.1. Przegląd kalendarza

Oto następujący przypadek użycia:

  • w [1] wybiera się lekarza [1] i dzień [2], a następnie za pomocą [3] wywołuje się kalendarz lekarza na wybrany dzień,
  • w [4] pojawia się on pod paskiem narzędzi.

Kod strony [form2.xhtml] jest następujący:


<?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>
    <!-- menu kontekstowe -->
    <p:contextMenu for="agenda">  
      ...
    </p:contextMenu>  
    <!-- kalendarz -->
    <p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- kolumna z harmonogramami -->
      <p:column style="width: 100px">  
        ...
      </p:column>  
      <!-- kolumna klientów -->
      <p:column style="width: 300px">  
        ...
      </p:column>  
    </p:dataTable>

    <!-- potwierdzenie usunięcia RV -->
    <p:confirmDialog id="confirmDialog" message="#{msg['form2.suppression.confirmation']}"  
                     header="#{msg['form2.suppression.message']}" severity="alert" widgetVar="confirmation">                   
      ...                
    </p:confirmDialog>  

    <!-- komunikat o błędzie -->
    <p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >  
      ...  
    </p:dialog>
    
    <!-- obsługa odpowiedzi serwera -->
    <script type="text/javascript">  
      ...
      }  
    </script> 
  </body>
</html>
  • wiersze 16–26: głównym elementem strony jest tabela <dataTable>, która wyświetla harmonogram wizyt u lekarza,
  • wiersze 12–14: do dodawania i usuwania wizyt użyjemy menu kontekstowego:
 
  • wiersze 29–32: gdy użytkownik będzie chciał usunąć wizytę, wyświetli się okno potwierdzenia:
 
  • wiersze 35–37: do zgłaszania błędów zostanie użyte okno dialogowe:
 
  • wiersze 40–43: konieczne będzie dodanie niewielkiej ilości kodu JavaScript.

6.9.2. Tabela spotkań

W tym miejscu omówimy model tabeli danych, taki jak ten przedstawiony w rozdziale 5.15, strona 327.

Przyjrzyjmy się głównemu elementowi strony, czyli tabeli wyświetlającej kalendarz:


<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- kolumna z harmonogramami -->
      <p:column style="width: 100px">  
        ...
      </p:column>  
      <!-- kolumna klientów -->
      <p:column style="width: 300px">  
        ...
      </p:column>  
    </p:dataTable>

Wygląd strony jest następujący:

Jest to tabela dwukolumnowa (wiersze 4–6 i 8–10), zasilana ze źródła [Form].getMyDataModel() (value="#{form.myDataModel}"). Jednocześnie można zaznaczyć tylko jeden wiersz (selectionMode="single"). Przy każdym wywołaniu POST odwołanie do wybranego elementu jest przypisywane do [Form].creneauChoisi (selection="#{form.creneauChoisi}").

Przypomnijmy, że metoda getAgenda zainicjowała następujące pole w szablonie:



// szablon
private AgendaMedecinJour agendaMedecinJour;

Szablon tabeli uzyskuje się poprzez wywołanie następującej metody [Form].getMyDataModel (atrybut „value” tagu <dataTable>):


  // szablon dataTable
  public MyDataModel getMyDataModel() {
    return new MyDataModel(agendaMedecinJour.getCreneauxMedecinJour());
}

Przyjrzyjmy się klasie [MyDataModel], która służy jako szablon dla tagu <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> {

  // producenci
  public MyDataModel() {
  }

  public MyDataModel(CreneauMedecinJour[] creneauxMedecinJour) {
    super(creneauxMedecinJour);
  }

  @Override
  public Object getRowKey(CreneauMedecinJour creneauMedecinJour) {
    return creneauMedecinJour.getCreneau().getId();
  }

  @Override
  public CreneauMedecinJour getRowData(String rowKey) {
    // lista przedziałów czasowych
    CreneauMedecinJour[] creneauxMedecinJour = (CreneauMedecinJour[]) getWrappedData();
    // klucz jest liczbą całkowitą typu long
    long key = Long.parseLong(rowKey);
    // wyszukiwanie wybranego przedziału czasowego
    for (CreneauMedecinJour creneauMedecinJour : creneauxMedecinJour) {
      if (creneauMedecinJour.getCreneau().getId().longValue() == key) {
        return creneauMedecinJour;
      }
    }
    // brak
    return null;
  }
}
  • wiersz 7: klasa [MyDataModel] jest wzorcem dla znacznika <p:dataTable>. Zadaniem tej klasy jest powiązanie wysyłanego elementu rowkey z elementem powiązanym z tym wierszem,
  • wiersz 7: klasa ta implementuje interfejs [SelectableDataModel] za pośrednictwem klasy [ArrayDataModel]. Oznacza to, że parametrem konstruktora jest tablica. To właśnie ta tablica zasila tag <dataTable>. W tym przypadku każdy wiersz tablicy zostanie powiązany z elementem typu [CreneauMedecinJour],
  • wiersze 13–15: konstruktor przekazuje swój parametr do klasy nadrzędnej,
  • wiersze 18–20: każdy wiersz tablicy odpowiada przedziałowi czasowemu i będzie identyfikowany przez identyfikator przedziału czasowego (wiersz 19). To właśnie ten identyfikator zostanie wysłany na serwer,
  • wiersz 23: kod, który zostanie wykonany po stronie serwera po przesłaniu identyfikatora przedziału czasowego. Celem tej metody jest zwrócenie odniesienia do obiektu [CreneauMedecinJour] powiązanego z tym identyfikatorem. Odniesienie to zostanie przypisane do wartości atrybutu „selection” tagu <dataTable>:

<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">

Pole [Form].creneauChoisi będzie zatem zawierało numer referencyjny obiektu [CreneauMedecinJour], który chcemy dodać lub usunąć.

6.9.3. Kolumna przedziałów czasowych

Kolumna przedziałów czasowych jest generowana za pomocą następującego kodu:


<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- kolumna z harmonogramami -->
      <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>  
  
      <!-- kolumna klientów -->
      <p:column style="width: 300px">  
        ...
      </p:column>  
    </p:dataTable>
  • wiersze 5–7: nagłówek kolumny,
  • wiersze 8–15: bieżący element kolumny. W wierszu 9 warto zwrócić uwagę na użycie tagu <h:outputFormat>, który pozwala formatować wyświetlane elementy. Parametr „value” określa ciąg znaków, który ma zostać wyświetlony. Notacja {i,typ,format} oznacza parametr nr i, typ tego parametru oraz jego format. W tym przypadku występują 4 parametry ponumerowane od 0 do 3, ich typem jest liczba, a będą one wyświetlane z dwoma cyframi,
  • wiersze 10–13: cztery parametry oczekiwane przez tag <h:outputFormat>.

6.9.4. Kolumna klientów

Kolumna klientów jest generowana za pomocą następującego kodu:


<!-- kalendarz -->
    <p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- kolumna z harmonogramami -->
      ...  
      <!-- kolumna klientów -->
      <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>
  • wiersze 8–10: nagłówek kolumny,
  • wiersze 11–13: bieżący element, gdy w danym przedziale czasowym zaplanowano spotkanie. W takim przypadku wyświetlane są tytuł, imię i nazwisko klienta, dla którego umówiono to spotkanie,
  • wiersze 14–16: kolejny fragment, do którego jeszcze wrócimy.

6.10. Usunięcie spotkania

Usunięcie spotkania przebiega zgodnie z następującą sekwencją:

Widok, którego dotyczy ta czynność, wygląda następująco:


<!-- menu kontekstowe -->
    <p:contextMenu for="agenda">  
...
      <p:menuitem value="#{msg['form2.supprimer']}" onclick="confirmation.show()"/>
    </p:contextMenu>  
    <!-- kalendarz -->
    <p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      ... 
    </p:dataTable>

    <!-- potwierdzenie usunięcia 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>  
  • wiersze 2–5: menu kontekstowe powiązane z tabelą danych (atrybut „for”). Zawiera ono dwie opcje [1]:
  • wiersz 4: opcja [Supprimer] powoduje wyświetlenie okna dialogowego [2] z wierszy 13–20,
  • wiersz 15: kliknięcie opcji [Oui] powoduje uruchomienie funkcji [Form.action], która usunie spotkanie. Zazwyczaj menu kontekstowe nie powinno wyświetlać opcji [Supprimer], jeśli wybrany element nie ma spotkania, ani opcji [Réserver], jeśli wybrany element ma spotkanie. Nie udało nam się sprawić, by menu kontekstowe działało w tak subtelny sposób. Udaje się to w przypadku pierwszego zaznaczonego elementu, ale potem okazuje się, że menu kontekstowe zachowuje konfigurację ustaloną dla tego pierwszego zaznaczenia. Wtedy staje się ono nieprawidłowe. Dlatego zachowaliśmy obie opcje i postanowiliśmy zapewnić użytkownikowi informację zwrotną, jeśli usunie element bez terminu,
  • wiersz 16: atrybut oncomplete, który pozwala zdefiniować kod JavaScript do wykonania po wykonaniu wywołania AJAX. Kod ten będzie wyglądał następująco:

<!-- komunikat o błędzie -->
    <p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >  
      <h:outputText value="#{form.msgErreur}" />  
    </p:dialog>

    <!-- obsługa odpowiedzi serwera -->
    <script type="text/javascript">  
      function handleRequest(xhr, status, args) {  
        // błąd?
        if(args.erreur) {  
          dlgErreur.show();  
        }  
      }  
    </script> 
  • wiersz 10: kod JavaScript sprawdza, czy słownik args posiada atrybut erreur. Jeśli tak, wyświetla okno dialogowe z wiersza 2 (atrybut widgetVar). W tym oknie wyświetlany jest szablon [Form].msgErreur.

Przyjrzyjmy się kodowi wykonywanemu w celu obsługi usuwania spotkania:


    <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>  
  • wiersz 2: zostanie wykonana metoda [Form].action,
  • wiersz 4: przed jej wykonaniem pole action otrzyma wartość „supprimer”.

Metoda [action] wygląda następująco:


// działanie na RV
  public void action() {
    // w zależności od żądanej akcji
    if (action.equals("supprimer")) {
      supprimer();
    }
    ...
  }
  
  public void supprimer() {
    // czy należy coś zrobić?
    Rv rv = creneauChoisi.getRv();
    if (rv == null) {
      signalerActionIncorrecte();
      return;
    }
    try {
      // usunięcie spotkania
      application.getMetier().supprimerRv(rv);
      // aktualizujemy kalendarz
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // wyświetlanie formularza form2
      setForms(true, true, false);
    } catch (Throwable th) {
      // widok błędów
      prepareVueErreur(th);
    }
    // wyzerowanie wybranego przedziału czasowego
    creneauChoisi = null;
}
  • wiersz 4: jeśli akcją jest „usuń”, wykonywana jest metoda [supprimer],
  • wiersz 12: pobierane jest spotkanie z wybranego przedziału czasowego. Przypomnijmy, że [creneauChoisi] zostało zainicjowane przez odwołanie do wybranego elementu [CreneauMedecinJour],
  • jeśli spotkanie to istnieje, zostaje usunięte (wiersz 19), kalendarz jest odświeżany (wiersz 21), a następnie ponownie wyświetlany (wiersz 23),
  • jeśli usunięcie nie powiodło się, wyświetlana jest strona błędów (wiersz 26),
  • jeśli wybrany element nie ma żadnego spotkania (wiersz 13), oznacza to, że użytkownik kliknął [Supprimer] na przedziale czasowym, w którym nie ma żadnego spotkania. Zgłaszany jest następujący błąd:
 

Metoda [signalerActionIncorrecte] wygląda następująco:


// zgłoszenie nieprawidłowej czynności
  private void signalerActionIncorrecte() {
    // wybrany przedział czasowy został wyzerowany
    creneauChoisi = null;
    // błąd
    msgErreur = Messages.getMessage(null, "form2.erreurAction", null).getSummary();
    RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
  }
  • wiersz 4: usuwa się zaznaczenie,
  • wiersz 6: generuje się zinternacjonalizowany komunikat o błędzie,
  • wiersz 7: dodaje się do słownika args wywołania AJAX atrybut („błąd”, true).

Wróćmy do kodu XHTML przycisku [Oui]:


<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
  • wiersz 2: po wykonaniu metody [Form].action wykonywana jest metoda JavaScript handleRequest:

    <!-- komunikat o błędzie -->
    <p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >  
      <h:outputText value="#{form.msgErreur}" />  
    </p:dialog>

    <!-- obsługa odpowiedzi serwera -->
    <script type="text/javascript">  
      function handleRequest(xhr, status, args) {  
        // błąd?
        if(args.erreur) {  
          dlgErreur.show();  
        }  
      }  
</script> 
  • wiersz 10: sprawdzane jest, czy słownik args posiada atrybut o nazwie „erreur”. Jeśli tak, wyświetlane jest okno dialogowe z wiersza 2,
  • wiersz 3: powoduje wyświetlenie komunikatu o błędzie utworzonego na podstawie szablonu.

6.11. Umówienie spotkania

Umówienie spotkania przebiega zgodnie z następującą sekwencją:

Widok związany z tą akcją jest następujący:


<!-- menu kontekstowe -->
    <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>  
    <!-- kalendarz -->
    <p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
   selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- kolumna z harmonogramami -->
      <p:column style="width: 100px">  
...
      </p:column>  
      <!-- kolumna klientów -->
      <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>
...
  • wiersze 21–31: wyświetlają następujące informacje:
  • wiersz 21: wyświetlanie ma miejsce, jeśli nie ma terminu, a dokonano wyboru i identyfikator wybranego przedziału czasowego odpowiada identyfikatorowi bieżącego elementu tabeli. Jeśli nie zastosuje się tego warunku, fragment będzie wyświetlany dla wszystkich przedziałów czasowych,
  • wiersz 22: pole wprowadzania danych będzie polem z asystą. Zakłada się tutaj, że może być wielu klientów,
  • wiersze 24–26: link [Valider],
  • wiersze 28–30: link [Annuler].

Pole wprowadzania wspomaganego jest generowane przez następujący kod:


<p:autoComplete completeMethod="#{form.completeClients}" value="#{form.identiteClient}" size="30"/>

Metoda [Form].completeClients odpowiada za generowanie propozycji dla użytkownika na podstawie znaków wpisanych w polu wprowadzania danych:

 

Propozycje mają postać [Nom prénom titre]. Kod metody [Form].completeClients jest następujący:


  // metoda autouzupełniania tekstu
  public List<String> completeClients(String query) {
    List<String> identites = new ArrayList<String>();
    // wyszukiwanie pasujących klientów
    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();
}
  • wiersz 2: query to ciąg znaków wpisany przez użytkownika,
  • wiersz 3: lista propozycji. Początkowo jest to pusta lista,
  • wiersze 5–10: tworzy się identyfikatory klientów [Nom prénom titre]. Jeśli identyfikator zaczyna się od query (wiersz 7), zostaje on uwzględniony na liście propozycji (wiersz 8).

6.12. Potwierdzenie terminu spotkania

Potwierdzenie terminu spotkania przebiega zgodnie z następującą sekwencją:

Kod linku [Valider] jest następujący:


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

Zatem to metoda [Form].action() będzie obsługiwać to zdarzenie. W międzyczasie model [Form].action otrzyma ciąg znaków „valider”. Kod wygląda następująco:


  // komponent Application
  @Inject
  private Application application;
  // pamięć podręczna sesji
...
  private Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
  // szablon
  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);
    }
  }

  // operacja na RV
  public void action() {
    // w zależności od żądanej akcji
...
    if (action.equals("valider")) {
      validerResa();
    }
}

  // potwierdzenie rezerwacji
  public void validerResa() {
    // potwierdzenie rezerwacji
    try {
      // czy klient istnieje?
      Boolean erreur = !hIdentitesClients.containsKey(identiteClient);
      if (erreur) {
        msgErreur = Messages.getMessage(null, "form2.erreurClient", new Object[]{identiteClient}).getSummary();
        RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
        return;
      }
      // dodajemy termin spotkania
      application.getMetier().ajouterRv(jour, creneauChoisi.getCreneau(), hIdentitesClients.get(identiteClient));
      // aktualizujemy kalendarz
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // wyświetla się formularz form2
      setForms(true, true, false);
    } catch (Throwable th) {
      // widok błędów
      prepareVueErreur(th);
    }
    // kasowanie wybranego terminu
    creneauChoisi = null;
    // wyzerowanie danych klienta
    identiteClient = null;
}
  • wiersze 33–35: ze względu na wartość pola action zostanie wykonana metoda [validerResa],
  • wiersz 43: najpierw sprawdzamy, czy klient istnieje. W obszarze wprowadzania wspomaganego użytkownik mógł bowiem wprowadzić dane ręcznie, nie korzystając z podanych mu propozycji. Wprowadzanie wspomagane jest powiązane z modelem [Form].identiteClient. Sprawdzamy zatem, czy ten identyfikator istnieje w słowniku identitesClients utworzonym podczas instancjonowania modelu (wiersz 20). Słownik ten przypisuje do identyfikatora klienta typu [Nom prénom titre] samego klienta (wiersz 25),
  • wiersz 44: jeśli klient nie istnieje, zwracany jest komunikat o błędzie do przeglądarki,
  • wiersz 45: wyświetlany jest zinternacjonalizowany komunikat o błędzie,
  • wiersz 46: dodaje się atrybut („błąd”, true) do słownika args wywołania funkcji AJAX. Wywołanie funkcji AJAX zostało zdefiniowane w następujący sposób:

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

W wierszu 3 powyżej widać, że link [Valider] posiada atrybut oncomplete. To właśnie ten atrybut spowoduje wyświetlenie komunikatu o błędzie zgodnie z techniką, z którą już się spotkaliśmy.

  • Wiersz 50: wysyłamy żądanie do warstwy [métier] o dodanie spotkania na wybrany dzień (jour), w wybranym przedziale czasowym (creneauChoisi.getCreneau()) oraz dla wybranego klienta (hIdentitesClients.get(identiteClient)),
  • wiersz 52: wysyłamy żądanie do warstwy [métier] o odświeżenie kalendarza lekarza. Zobaczymy dodaną wizytę oraz wszystkie zmiany, które mogli wprowadzić inni użytkownicy aplikacji,
  • wiersz 54: ponownie wyświetlany jest kalendarz [form2.xhtml],
  • wiersz 57: wyświetla się strona błędu, jeśli wystąpi błąd.

6.13. Anulowanie wizyty

Odpowiada to następującej sekwencji:

Przycisk [Annuler] na stronie [form2.xhtml] wygląda następująco:


<p:commandLink action="#{form.action()}" value="#{msg['form2.annuler']}" update=":formulaire:contenu">
            <f:setPropertyActionListener value="annuler" target="#{form.action}"/>
          </p:commandLink>

Wywoływana jest zatem metoda [Form].action:


// działanie na RV
  public void action() {
    // w zależności od wybranej czynności
...
    if (action.equals("annuler")) {
      annulerRv();
    }
  }
  
// anulowanie wizyty
  public void annulerRv() {
    // wyświetla się formularz 2
    setForms(true, true, false);
    // wyzerowanie wybranego przedziału czasowego
    creneauChoisi = null;
    // wyzerowanie danych klienta
    identiteClient = null;
  }

6.14. Nawigacja w kalendarzu

Pasek narzędzi umożliwia nawigację po kalendarzu:

Chociaż nie widać tego na powyższych zrzutach ekranu, kalendarz jest aktualizowany o terminy z wybranego nowego dnia.

Tagi trzech odpowiednich przycisków w pliku [form1.xhtml] są następujące:


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

Metody [Form].getPreviousAgenda, [Form].getNextAgenda, [Form].today są następujące:


private Date jour = new Date();

public void getPreviousAgenda() {
    // przejście do poprzedniego dnia
    Calendar cal = Calendar.getInstance();
    cal.setTime(jour);
    cal.add(Calendar.DAY_OF_YEAR, -1);
    jour = cal.getTime();
    // kalendarz
    if (form2Rendered) {
      getAgenda();
    }
  }

  public void getNextAgenda() {
    // przechodzimy do następnego dnia
    Calendar cal = Calendar.getInstance();
    cal.setTime(jour);
    cal.add(Calendar.DAY_OF_YEAR, 1);
    jour = cal.getTime();
    // kalendarz
    if (form2Rendered) {
      getAgenda();
    }
  }

  // kalendarz na dziś
  public void today() {
    jour = new Date();
    // kalendarz
    if (form2Rendered) {
      getAgenda();
    }
}
  • wiersz 1: dzień wyświetlania kalendarza,
  • wiersz 5: używany jest kalendarz,
  • wiersz 6: który jest inicjowany na bieżący dzień kalendarza,
  • wiersz 7: odejmujemy jeden dzień od kalendarza,
  • wiersz 8: i ponownie inicjujemy go, ustawiając na dzień wyświetlania kalendarza,
  • wiersz 11: ponownie wyświetla się kalendarz, jeśli jest on aktualnie wyświetlany. Użytkownik może bowiem korzystać z paska narzędzi, nawet jeśli kalendarz nie jest wyświetlany.

Pozostałe metody działają analogicznie.

6.15. Zmiana języka wyświetlania

Zmianę języka można przeprowadzić za pomocą przycisku menu na pasku narzędzi:

Tagi przycisku menu są następujące:


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

Metody wykonywane w szablonie to:


private String locale = "fr";

  public void setFrenchLocale() {
    locale = "fr";
    // odśwież stronę
    redirect();
  }

  public void setEnglishLocale() {
    locale = "en";
    // odświeżamy stronę
    redirect();
  }

  private void redirect() {
    // przekierowuje klienta do serwletu
    ExternalContext ctx = FacesContext.getCurrentInstance().getExternalContext();
    try {
      ctx.redirect(ctx.getRequestContextPath());
    } catch (IOException ex) {
      Logger.getLogger(Form.class.getName()).log(Level.SEVERE, null, ex);
    }
}

Metody w wierszach 3 i 9 ograniczają się do zainicjowania pola locale z wiersza 1, a następnie przekierowują przeglądarkę klienta na tę samą stronę. Przekierowanie to odpowiedź, w której serwer prosi przeglądarkę o załadowanie innej strony. Przeglądarka wykonuje wówczas żądanie GET do tej nowej strony.

  • wiersz 17: [ExternalContext] jest klasą JSF, która umożliwia dostęp do aktualnie uruchomionego serwletu,
  • wiersz 19: następuje przekierowanie. Parametrem metody redirect jest URL strony, do której przeglądarka klienta ma zostać przekierowana. W tym przypadku chcemy przekierować się do [/mv-rdvmedecins-pf], czyli nazwy naszej aplikacji:
  

metoda [getRequestContextPath] pozwala uzyskać tę nazwę. Nastąpi zatem załadowanie strony głównej [index.xhtml] naszej aplikacji. Strona ta jest powiązana z szablonem [Form] o zasięgu sesji. Ten model obsługuje trzy wartości logiczne, które kontrolują wygląd strony [index.xhtml]:


  private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;

Ponieważ szablon ma zasięg sesji, te trzy wartości logiczne zachowały swoje wartości. Strona [index.xhtml] pojawi się zatem w stanie, w jakim znajdowała się przed przekierowaniem. Strona ta jest formatowana za pomocą następującego szablonu faceletu [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>

Tag w wierszu 9 określa język wyświetlania strony za pomocą atrybutu „local”. Strona zostanie zatem wyświetlona w języku francuskim lub angielskim, w zależności od sytuacji. Dlaczego więc nastąpiło przekierowanie? Wróćmy do tagów opcji zmiany języka:


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

Początkowo zostały one napisane tak, aby aktualizować pole id formularza za pomocą wywołania AJAX (atrybut update w wierszach 7 i 8). Jednak podczas testów zmiana języka nie działała za każdym razem. Stąd przekierowanie, które ma rozwiązać ten problem. Być może można było również ustawić atrybut ajax='false' w tagach, aby wymusić ponowne załadowanie strony. Pozwoliłoby to uniknąć przekierowania.

6.16. Odświeżanie list

Odpowiada to następującej akcji:

 

Tag powiązany z opcją [Rafraîchir] jest następujący:


<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>  

Metoda [Form].refresh wygląda następująco:


  public void refresh() {
    // odświeżanie list
    init();
}

Metoda init jest metodą wykonywaną bezpośrednio po utworzeniu komponentu [Form]. Jej celem jest zapisanie danych z bazy danych w pamięci podręcznej modelu:


// bean aplikacji
  @Inject
  private Application application;
  // pamięć podręczna sesji
  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() {
    // lekarze i klienci są zapisywani w pamięci podręcznej
    try {
      medecins = application.getMetier().getAllMedecins();
      clients = application.getMetier().getAllClients();
    } catch (Throwable th) {
      ...
    }
    ...
    // słowniki
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
      hIdentitesClients.put(identite(c), c);
    }
  }

Metoda init tworzy listy i słowniki z wierszy 5–9. Wadą tej techniki jest to, że elementy te nie uwzględniają już zmian w bazie danych (dodanie klienta, lekarza itp.). Metoda refresh wymusza odbudowę tych list i słowników. Będzie się ją więc stosować za każdym razem, gdy nastąpi zmiana w bazie danych, na przykład dodanie nowego klienta.

6.17. Conclusion

Przypomnijmy sobie architekturę aplikacji, którą właśnie stworzyliśmy:

W dużej mierze oparliśmy się na już zbudowanej wersji JSF2:

  • warstwy [métier], [DAO], [JPA] zostały zachowane,
  • zachowano komponenty [Application] i [Form] warstwy internetowej, ale dodano do nich nowe funkcje w związku z rozbudową interfejsu użytkownika,
  • interfejs użytkownika został gruntownie zmodyfikowany. Jest on w szczególności bogatszy w funkcje i bardziej przyjazny dla użytkownika.

Przejście z komponentu JSF na Primefaces w celu stworzenia interfejsu internetowego wymaga pewnego doświadczenia, ponieważ na początku można poczuć się nieco przytłoczonym ogromną liczbą dostępnych komponentów i ostatecznie nie wiadomo, z których z nich skorzystać. Należy zatem skupić się na pożądanej ergonomii interfejsu.

6.18. Testy w Eclipse

Podobnie jak w przypadku poprzednich wersji przykładowej aplikacji, pokazujemy, jak przetestować wersję 03 za pomocą Eclipse. Najpierw importujemy do Eclipse projekty Maven z przykładu 03 [1]:

  • [mv-rdvmedecins-ejb-dao-jpa]: warstwy [DAO] i [JPA],
  • [mv-rdvmedecins-ejb-metier]: warstwa [métier],
  • [mv-rdvmedecins-pf]: warstwa [web] zaimplementowana przy użyciu JSF i Primefaces,
  • [mv-rdvmedecins-app]: projekt nadrzędny projektu korporacyjnego [mv-rdvmedecins-app-ear]. Po zaimportowaniu projektu nadrzędnego projekt podrzędny jest automatycznie importowany
  • w [2] uruchamia się projekt korporacyjny [mv-rdvmedecins-app-ear],
  • w [3] wybieramy serwer Glassfish,
  • w [4], w zakładce [Servers] aplikacja została wdrożona. Nie uruchamia się ona samodzielnie. Należy wywołać ją w przeglądarce: