Skip to content

6. Applicazione di esempio-03: rdvmedecins-pf-ejb

Ricordiamo la struttura dell’applicazione di esempio sviluppata per il server Glassfish:

Non modifichiamo nulla di questa architettura, se non il livello web che in questo caso sarà realizzato utilizzando JSF e Primefaces.

6.1. Il progetto NetBeans

Nell’immagine sopra, i livelli [métier] e [DAO] corrispondono a quelli dell’esempio 01 JSF / EJB / Glassfish. Li riutilizziamo.

  
  • [mv-rdvmedecins-ejb-dao-jpa]: progetto EJB dei livelli [DAO] e [JPA] dell'esempio 01,
  • [mv-rdvmedecins-ejb-metier]: progetto EJB del livello [métier] dell'esempio 01,
  • [mv-rdvmedecins-pf]: progetto del livello [web] / Primefaces – nuovo,
  • [mv-rdvmedecins-app-ear]: progetto aziendale per l'implementazione dell'applicazione sul server Glassfish – nuovo.

6.2. Il progetto aziendale

Il progetto aziendale serve esclusivamente per la distribuzione dei tre moduli [mv-rdvmedecins-ejb-dao-jpa], [mv-rdvmedecins-ejb-metier], [mv-rdvmedecins-pf] sul server Glassfish. Il progetto NetBeans è il seguente:

Il progetto esiste esclusivamente per queste tre dipendenze [1] definite nel file [pom.xml] come segue:


<?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>
  • righe 10-13: l'artefatto Maven del progetto aziendale,
  • righe 18-37: le tre dipendenze del progetto. Si noti bene il tipo di queste ultime (righe 23, 29, 35).

Per eseguire l’applicazione web, sarà necessario eseguire questo progetto aziendale.

6.3. Il progetto web Primefaces

Il progetto web Primefaces è il seguente:

  • in [1], le pagine del progetto. La pagina [index.xhtml] è l'unica pagina del progetto. Comprende tre frammenti: [form1.xhtml], [form2.xhtml] e [erreur.xhtml]. Le altre pagine servono solo per la formattazione.
  • In [2], i bean Java. Il bean [Application] con ambito application, il bean [Form] con ambito session. La classe [Erreur] incapsula un errore. La classe [MyDataModel] funge da modello per un tag <dataTable> di Primefaces,
  • in [3], i file dei messaggi per l'internazionalizzazione,
  • in [4], le dipendenze. Il progetto web dipende dal progetto EJB del livello [DAO], il progetto EJB del livello [métier] e Primefaces per il livello [web].

6.4. La configurazione del progetto

La configurazione del progetto è quella dei progetti Primefaces o JSF che abbiamo esaminato. Elenchiamo i file di configurazione senza ripeterne la spiegazione.

 

[web.xml]: configura l’applicazione web.


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

Si noti, alla riga 30, che la pagina [index.xhtml] è la pagina iniziale dell'applicazione.

[faces-config.xml]: configura l'applicazione 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]: vuota ma necessaria per l'annotazione @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]: il foglio di stile dell'applicazione


.col1{
   background-color: #ccccff
}

.col2{
   background-color: #ffcccc
}

La libreria Primefaces include i propri fogli di stile. Il foglio di stile sopra indicato viene utilizzato solo per la pagina da visualizzare in caso di eccezione, una pagina non gestita dall'applicazione. In tal caso viene visualizzata la pagina [exception.xhtml].

[messages_fr.properties]: il file dei messaggi in francese


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

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

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

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

# errore
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]: il file dei messaggi in inglese


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

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

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

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

# errore
erreur.titre=The following exceptions occurred
erreur.exceptions=Exceptions' chain
erreur.type=Exception type
erreur.message=Associated Message
erreur.accueil=Welcome

6.5. Il modello delle pagine [layout.xhtml]

Il modello [layout.xhtml] è il seguente:


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

L'unica parte variabile di questo modello è l'area delle righe 28-30. Quest'area si trova nell'area con id:form:contenuto (riga 27). È bene tenerlo presente. Le chiamate AJAX che aggiornano quest'area avranno l'attributo update=":form:contenuto". Inoltre, il modulo inizia alla riga 15. Pertanto, il frammento inserito nelle righe 28-30 viene inserito in questo modulo.

L’aspetto risultante da questo modello è il seguente:

La parte dinamica della pagina verrà inserita nell’area incorniciata sopra.

6.6. La pagina [index.xhtml]

Il progetto visualizza sempre la stessa pagina, ovvero la pagina [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>
  • righe 8-9: questo frammento XHTML verrà inserito nell’area dinamica del modello [layout.xhtml],
  • la pagina comprende tre sottoframmenti:
  • [form1.xhtml], righe 10-12;
  • [form2.xhtml], righe 13-15;
  • [erreur.xhtml], righe 16-18.

La presenza di questi frammenti in [index.xhtml] è controllata da valori booleani del modello [Form.java] associato alla pagina. Pertanto, modificando tali valori, la pagina visualizzata cambia.

Il frammento [form1.xhtml] viene visualizzato come segue:

Il frammento [form2.xhtml] viene visualizzato come segue:

Il frammento [erreur.xhtml] viene visualizzato come segue:

6.7. I bean del progetto

La classe del pacchetto [utils] è già stata presentata: la classe [Messages] è una classe che facilita l'internazionalizzazione dei messaggi di un'applicazione. È stata analizzata nel paragrafo 2.8.5.7.

6.7.1. Il bean Application

Il bean [Application.java] è un bean con ambito application. Ricordiamo che questo tipo di bean serve a memorizzare dati in sola lettura e disponibili per tutti gli utenti dell’applicazione. Questo bean è il seguente:


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 {

  // livello business
  @EJB
  private IMetierLocal metier;

  public Application() {
  }
  
  // getters

  public IMetierLocal getMetier() {
    return metier;
  }
  
}
  • riga 8: si assegna al bean il nome "application",
  • riga 9: ha ambito "application",
  • righe 13-14: un riferimento all'interfaccia locale del livello [métier] gli verrà iniettato dal contenitore EJB del server dell'applicazione. Ricordiamo l'architettura dell'applicazione:

L'applicazione JSF e le applicazioni EJB e [Metier] verranno eseguite nella stessa JVM (Java Virtual Machine). Pertanto, il livello [JSF] utilizzerà l'interfaccia locale di EJB. Tutto qui. Il bean [Application] non contiene nient’altro. Per accedere al livello [métier], gli altri bean lo richiederanno a questo bean.

6.7.2. Il bean [Erreur]

La classe [Erreur] è la seguente:

  1. package beans;
    
    
    
        public class Erreur {
        
    
    
    
          public Erreur() {
        
      }
    
    
    
    
          // campo
        
    
          private String classe;
        
    
          private String message;
        
    
    
    
          // costruttore
        
    
          public Erreur(String classe, String message){
        
    
            this.setClasse(classe);
        
    
            this.message=message;
        
      }
    
    
    
    
          // getter e setter
        
    ...  
    
    }
    
  • riga 9, il nome di una classe di eccezione se è stata generata un'eccezione,
  • riga 10: un messaggio di errore.

6.7.3. Il bean [Form]

Il suo codice è il seguente:


package beans;

import java.io.IOException;
...

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

  public Form() {
  }
  
// bean dell'applicazione
  @Inject
  private Application application;

  // cache della sessione
  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>();

  // modello
  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() {
    // si memorizzano nella cache medici e clienti
    try {
      medecins = application.getMetier().getAllMedecins();
      clients = application.getMetier().getAllClients();
    } catch (Throwable th) {
      // si registra l'errore
      prepareVueErreur(th);
      return;
    }

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

  ...

  // visualizzazione della vista
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.erreurRendered = erreurRendered;
  }

  // preparazione vueErreur
  private void prepareVueErreur(Throwable th) {
    // si crea l'elenco degli errori
    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()));
    }
// viene visualizzata la vista degli errori
    setForms(true, false, true);
  }

  // getter e setter
  ...
}
  • righe 6-8: la classe [Form] è un bean denominato «form» e con ambito di sessione. Si ricorda che in questo caso la classe deve essere serializzabile,
  • righe 14-15: il bean form ha un riferimento al bean application. Questo verrà iniettato dal contenitore di servlet in cui viene eseguita l’applicazione (presenza dell’annotazione @Inject).
  • righe 17-44: il modello delle pagine [form1.xhtml, form2.xhtml, erreur.xhtml]. La visualizzazione di queste pagine è controllata dai valori booleani delle righe 27-29. Si noti che, per impostazione predefinita, viene visualizzata la pagina [form1.xhtml] (riga 27),
  • righe 46-47: il metodo init viene eseguito subito dopo l'istanziazione della classe (presenza dell'annotazione @PostConstruct),
  • righe 50-51: si richiede al livello [métier] l’elenco dei medici e dei clienti,
  • righe 59-65: se tutto è andato a buon fine, vengono creati i dizionari dei medici e dei clienti. Sono indicizzati in base al loro numero. Successivamente, verrà visualizzata la pagina [form1.xhtml] (riga 27),
  • riga 54: in caso di errore, viene generato il modello della pagina [erreur.xhtml]. Questo modello corrisponde all’elenco degli errori della riga 36,
  • righe 78-88: il metodo [prepareVueErreur] genera l'elenco degli errori da visualizzare. La pagina [index.xhtml] visualizza quindi i frammenti [form1.xhtml] e [erreur.xhtml] (riga 87).

La pagina [erreur.xhtml] è la seguente:


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

Utilizza un tag <p:dataTable> (righe 12-28) per visualizzare l'elenco degli errori. Il risultato è una pagina di errore simile alla seguente:

Definiremo ora le diverse fasi del ciclo di vita dell'applicazione. Per ogni azione dell'utente, esamineremo le viste interessate e i gestori degli eventi.

6.8. Visualizzazione della pagina iniziale

Se tutto va bene, la prima pagina visualizzata è [form1.xhtml]. Il risultato è la seguente vista:

La pagina [form1.xhtml] è la seguente:


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

La barra degli strumenti evidenziata nella schermata è il componente Primefaces Toolbar. Questo è definito alle righe 8-14. Contiene due gruppi di componenti, ciascuno definito da un tag <toolbarGroup>, alle righe 9-11 e 12-14. Uno dei gruppi è allineato a sinistra della barra degli strumenti (riga 9), l’altro a destra (riga 12).

Esaminiamo alcuni componenti del gruppo di sinistra:


<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>
...
  • righe 4-7: il menu a tendina dei medici a cui è stato aggiunto un effetto (effect="fade"),
  • riga 6: un comportamento AJAX. Quando si verificherà una modifica nel menu a discesa, verrà eseguito il metodo [Form].hideAgenda (listener="#{form.hideAgenda}") verrà eseguito e l'area dinamica :form:contenuto (update=":formulaire:contenu") verrà aggiornata,
  • riga 8: include un separatore nella barra degli strumenti,
  • righe 10-12: il campo di immissione della data. Qui si utilizza il calendario di Primefaces. Il campo di immissione è di sola lettura (readOnlyInputText="true"),
  • riga 11: un comportamento AJAX. Quando ci sarà una modifica della data, verrà eseguito il metodo [Form].hideAgenda e l’area dinamica :form:contenuto verrà aggiornata,
  • riga 14: un pulsante. Cliccandoci sopra si esegue una chiamata AJAX al metodo [Form].getAgenda (), il modello verrà quindi modificato e la risposta del server verrà utilizzata per aggiornare l'area dinamica :form:contenuto,
  • riga 15: il tag <tooltip> consente di associare un tooltip a un componente. L'ID di quest'ultimo è indicato dall'attributo "for" del tooltip. In questo caso (for="resa-agenda") indica il pulsante della riga 14:

Questa pagina è alimentata dal seguente modello:


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

  public Form() {
  }
  
  // cache della sessione
  private List<Medecin> medecins;
  private List<Client> clients;
  // modello
  private Long idMedecin;
  private Date jour = new Date();
  
  // elenco dei medici
  public List<Medecin> getMedecins() {
    return medecins;
  }

  // elenco dei clienti
  public List<Client> getClients() {
    return clients;
  }

  // agenda
  public void getAgenda() {
    ...
  }
  • il campo della riga 12 fornisce in lettura e scrittura il valore dell’elenco della riga 4 della pagina. Alla visualizzazione iniziale della pagina, imposta il valore selezionato nel menu a discesa. Alla visualizzazione iniziale, idMedecin è uguale a null, quindi sarà selezionato il primo medico,
  • il metodo delle righe 16-18 genera gli elementi del menu a tendina dei medici (riga 5 della pagina). Ogni opzione generata avrà come etichetta (itemLabel) il titolo, il cognome e il nome del medico e come valore (itemValue) l’ID del medico,
  • il campo della riga 13 alimenta in lettura/scrittura il campo di immissione della riga 10 della pagina. Alla visualizzazione iniziale, viene quindi visualizzata la data odierna,
  • righe 26-28: il metodo getAgenda gestisce il clic sul pulsante [Agenda] della riga 14 della pagina. È quasi identico a com’era nella versione JSF:

  // bean dell'applicazione
  @Inject
  private Application application;
  // cache della sessione
  private List<Medecin> medecins;
  private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
  // modello
  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 {
      // si recupera il medico
      medecin = hMedecins.get(idMedecin);
      // l'agenda del medico per un determinato giorno
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // visualizzazione del modulo 2
      setForms(true, true, false);
    } catch (Throwable th) {
      // visualizzazione degli errori
      prepareVueErreur(th);
    }
    // nessuna fascia oraria selezionata al momento
    creneauChoisi = null;
}

Non commenteremo questo codice. È già stato fatto.

6.9. Visualizza l'agenda di un medico

6.9.1. Panoramica dell’agenda

Si tratta del seguente caso d’uso:

  • in [1], si seleziona un medico [1] e un giorno [2], quindi si richiede [3] l'agenda del medico per il giorno scelto,
  • in [4], questa appare sotto la barra degli strumenti.

Il codice della pagina [form2.xhtml] è il seguente:


<?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 contestuale -->
    <p:contextMenu for="agenda">  
      ...
    </p:contextMenu>  
    <!-- agenda -->
    <p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- colonna degli orari -->
      <p:column style="width: 100px">  
        ...
      </p:column>  
      <!-- colonna dei clienti -->
      <p:column style="width: 300px">  
        ...
      </p:column>  
    </p:dataTable>

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

    <!-- messaggio di errore -->
    <p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >  
      ...  
    </p:dialog>
    
    <!-- gestione della risposta del server -->
    <script type="text/javascript">  
      ...
      }  
    </script> 
  </body>
</html>
  • righe 16-26: l'elemento principale della pagina è la tabella <dataTable> che mostra l'agenda del medico,
  • righe 12-14: useremo un menu contestuale per aggiungere/eliminare un appuntamento:
 
  • righe 29-32: verrà visualizzata una finestra di conferma quando l'utente vorrà eliminare un appuntamento:
 
  • righe 35-37: verrà utilizzata una finestra di dialogo per segnalare un errore:
 
  • righe 40-43: dovremo inserire un po' di JavaScript.

6.9.2. La tabella degli appuntamenti

Qui affrontiamo il modello di una tabella di dati come studiato nel paragrafo 5.15, a pagina 327.

Esaminiamo l’elemento principale della pagina, la tabella che visualizza l’agenda:


<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- colonna degli orari -->
      <p:column style="width: 100px">  
        ...
      </p:column>  
      <!-- colonna dei clienti -->
      <p:column style="width: 300px">  
        ...
      </p:column>  
    </p:dataTable>

Il risultato è il seguente:

Si tratta di una tabella a due colonne (righe 4-6 e 8-10) alimentata dalla sorgente [Form].getMyDataModel() (value="#{form.myDataModel}"). È possibile selezionare una sola riga alla volta (selectionMode="single"). Ad ogni POST, un riferimento all'elemento selezionato viene assegnato a [Form].creneauChoisi (selection="#{form.creneauChoisi}").

Ricordiamo che il metodo getAgenda ha inizializzato il seguente campo nel modello:



// modello
private AgendaMedecinJour agendaMedecinJour;

Il modello della tabella si ottiene richiamando il metodo [Form].getMyDataModel (attributo value del tag <dataTable>) seguente:


  // il modello di dataTable
  public MyDataModel getMyDataModel() {
    return new MyDataModel(agendaMedecinJour.getCreneauxMedecinJour());
}

Esaminiamo la classe [MyDataModel] che funge da modello per il tag <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> {

  // costruttori
  public MyDataModel() {
  }

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

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

  @Override
  public CreneauMedecinJour getRowData(String rowKey) {
    // elenco delle fasce orarie
    CreneauMedecinJour[] creneauxMedecinJour = (CreneauMedecinJour[]) getWrappedData();
    // la chiave è un numero intero lungo
    long key = Long.parseLong(rowKey);
    // si cerca la fascia oraria selezionata
    for (CreneauMedecinJour creneauMedecinJour : creneauxMedecinJour) {
      if (creneauMedecinJour.getCreneau().getId().longValue() == key) {
        return creneauMedecinJour;
      }
    }
    // nulla
    return null;
  }
}
  • riga 7: la classe [MyDataModel] è il modello del tag <p:dataTable>. Lo scopo di questa classe è quello di collegare l'elemento rowkey che viene inviato con l'elemento associato a quella riga,
  • riga 7: la classe implementa l’interfaccia [SelectableDataModel] tramite la classe [ArrayDataModel]. Ciò significa che il parametro del costruttore è un array. È proprio questo array che alimenta il tag <dataTable>. In questo caso, ogni riga dell’array sarà associata a un elemento di tipo [CreneauMedecinJour],
  • righe 13-15: il costruttore passa il proprio parametro alla classe padre,
  • righe 18-20: ogni riga dell’array corrisponde a una fascia oraria e sarà identificata dall’ID della fascia oraria (riga 19). È questo ID che verrà inviato al server,
  • riga 23: il codice che verrà eseguito sul lato server quando verrà inviato l’ID di una fascia oraria. Lo scopo di questo metodo è ottenere il riferimento all’oggetto [CreneauMedecinJour] associato a tale ID. Tale riferimento verrà assegnato al valore dell’attributo «selection» del tag <dataTable>:

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

Il campo [Form].creneauChoisi conterrà quindi il riferimento all'oggetto [CreneauMedecinJour] che si desidera aggiungere o eliminare.

6.9.3. La colonna delle fasce orarie

La colonna delle fasce orarie si ottiene con il seguente codice:


<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- colonna degli orari -->
      <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>  
  
      <!-- colonna dei clienti -->
      <p:column style="width: 300px">  
        ...
      </p:column>  
    </p:dataTable>
  • righe 5-7: l'intestazione della colonna,
  • righe 8-15: l'elemento corrente della colonna. Si noti, alla riga 9, l'uso del tag <h:outputFormat>, che consente di formattare gli elementi da visualizzare. Il parametro value indica la stringa di caratteri da visualizzare. La notazione {i,type,format} indica il parametro n. i, il tipo di tale parametro e il suo formato. In questo caso ci sono 4 parametri numerati da 0 a 3; il loro tipo è numerico e verranno visualizzati con due cifre,
  • righe 10-13: i quattro parametri previsti dal tag <h:outputFormat>.

6.9.4. La colonna dei clienti

La colonna dei clienti si ottiene con il seguente codice:


<!-- agenda -->
    <p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- colonna degli orari -->
      ...  
      <!-- colonna dei clienti -->
      <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>
  • righe 8-10: l'intestazione della colonna,
  • righe 11-13: l'elemento corrente quando è presente un appuntamento per quella fascia oraria. In questo caso, vengono visualizzati il titolo, il nome e il cognome del cliente per il quale è stato fissato l'appuntamento,
  • righe 14-16: un altro frammento su cui torneremo.

6.10. Cancellazione di un appuntamento

L’eliminazione di un appuntamento segue la sequenza seguente:

La vista interessata da questa azione è la seguente:


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

    <!-- conferma eliminazione 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>  
  • righe 2-5: un menu contestuale associato alla tabella dei dati (attributo for). Presenta due opzioni [1]:
  • riga 4: l’opzione [Supprimer] attiva la visualizzazione della finestra di dialogo [2] delle righe 13-20,
  • riga 15: cliccando su [Oui] si avvia l’esecuzione di [Form.action], che eliminerà l’appuntamento. Normalmente, il menu contestuale non dovrebbe offrire l’opzione [Supprimer] se l’elemento selezionato non ha un appuntamento, né l’opzione [Réserver] se l’elemento selezionato ha un appuntamento. Non siamo riusciti a rendere il menu contestuale così «sottile». Ci riusciamo per il primo elemento selezionato, ma poi ci accorgiamo che il menu contestuale mantiene la configurazione acquisita per quella prima selezione. Diventa quindi errato. Abbiamo quindi mantenuto entrambe le opzioni e abbiamo deciso di fornire un feedback all’utente nel caso in cui eliminasse un elemento senza appuntamento,
  • riga 16: l’attributo oncomplete che consente di definire il codice JavaScript da eseguire dopo l’esecuzione della chiamata AJAX. Il codice sarà il seguente:

<!-- messaggio di errore -->
    <p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >  
      <h:outputText value="#{form.msgErreur}" />  
    </p:dialog>

    <!-- gestione del ritorno dal server -->
    <script type="text/javascript">  
      function handleRequest(xhr, status, args) {  
        // errore?
        if(args.erreur) {  
          dlgErreur.show();  
        }  
      }  
    </script> 
  • riga 10: il codice JavaScript verifica se il dizionario args possiede l’attributo erreur. In caso affermativo, fa apparire la finestra di dialogo della riga 2 (attributo widgetVar). Questa finestra di dialogo visualizza il modello [Form].msgErreur.

Diamo un'occhiata al codice eseguito per gestire l'eliminazione di un appuntamento:


    <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>  
  • riga 2: verrà eseguito il metodo [Form].action,
  • riga 4: prima della sua esecuzione, il campo action avrà ricevuto il valore 'supprimer'.

Il metodo [action] è il seguente:


// azione su RV
  public void action() {
    // a seconda dell'azione desiderata
    if (action.equals("supprimer")) {
      supprimer();
    }
    ...
  }
  
  public void supprimer() {
    // è necessario fare qualcosa?
    Rv rv = creneauChoisi.getRv();
    if (rv == null) {
      signalerActionIncorrecte();
      return;
    }
    try {
      // cancellazione di un appuntamento
      application.getMetier().supprimerRv(rv);
      // si aggiorna l'agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // visualizza form2
      setForms(true, true, false);
    } catch (Throwable th) {
      // visualizzazione errori
      prepareVueErreur(th);
    }
    // azzeramento della fascia oraria selezionata
    creneauChoisi = null;
}
  • riga 4: se l'azione è 'elimina', si esegue il metodo [supprimer],
  • riga 12: si recupera l'appuntamento della fascia oraria selezionata. Si ricorda che [creneauChoisi] è stato inizializzato dal riferimento dell'elemento [CreneauMedecinJour] selezionato;
  • se l'appuntamento esiste, viene eliminato (riga 19), l'agenda viene rigenerata (riga 21) e quindi visualizzata nuovamente (riga 23),
  • se l’eliminazione non è andata a buon fine, viene visualizzata la pagina degli errori (riga 26),
  • se l’elemento selezionato non contiene alcun appuntamento (riga 13), significa che l’utente ha cliccato su [Supprimer] in una fascia oraria priva di appuntamenti. Viene segnalato il seguente errore:
 

Il metodo [signalerActionIncorrecte] è il seguente:


// segnalazione di un'azione errata
  private void signalerActionIncorrecte() {
    // azzeramento della fascia oraria selezionata
    creneauChoisi = null;
    // errore
    msgErreur = Messages.getMessage(null, "form2.erreurAction", null).getSummary();
    RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
  }
  • riga 4: si rimuove la selezione,
  • riga 6: si genera un messaggio di errore internazionalizzato,
  • riga 7: si aggiunge nel dizionario args della chiamata AJAX l'attributo ('errore', true).

Torniamo al codice XHTML del pulsante [Oui]:


<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
  • riga 2: dopo l'esecuzione del metodo [Form].action, viene eseguito il metodo JavaScript handleRequest:

    <!-- messaggio di errore -->
    <p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >  
      <h:outputText value="#{form.msgErreur}" />  
    </p:dialog>

    <!-- gestione della risposta del server -->
    <script type="text/javascript">  
      function handleRequest(xhr, status, args) {  
        // errore?
        if(args.erreur) {  
          dlgErreur.show();  
        }  
      }  
</script> 
  • riga 10: si verifica se il dizionario args possiede l'attributo denominato 'erreur'. In caso affermativo, viene visualizzata la finestra di dialogo della riga 2,
  • riga 3: viene visualizzato il messaggio di errore generato dal modello.

6.11. Fissazione di un appuntamento

La prenotazione di un appuntamento corrisponde alla seguente sequenza:

La vista coinvolta in questa azione è la seguente:


<!-- menu contestuale -->
    <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>  
    <!-- agenda -->
    <p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
   selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- colonna degli orari -->
      <p:column style="width: 100px">  
...
      </p:column>  
      <!-- colonna dei clienti -->
      <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>
...
  • righe 21-31: visualizzano quanto segue:
  • riga 21: la visualizzazione avviene se non ci sono appuntamenti, se è stata effettuata una selezione e se l’ID della fascia oraria scelta corrisponde a quello dell’elemento corrente della tabella. Se non si imposta questa condizione, il frammento viene visualizzato per tutte le fasce orarie,
  • riga 22: il campo di immissione sarà un campo di immissione assistita. Si presume qui che possano esserci molti clienti,
  • righe 24-26: il collegamento [Valider],
  • righe 28-30: il collegamento [Annuler].

Il campo di immissione assistita viene generato dal seguente codice:


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

Il metodo [Form].completeClients ha il compito di fornire suggerimenti all’utente in base ai caratteri digitati nell’area di immissione:

 

Le proposte hanno il formato [Nom prénom titre]. Il codice del metodo [Form].completeClients è il seguente:


  // metodo di completamento automatico del testo
  public List<String> completeClients(String query) {
    List<String> identites = new ArrayList<String>();
    // si cercano i clienti corrispondenti
    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();
}
  • riga 2: query è la stringa digitata dall'utente,
  • riga 3: l’elenco delle proposte. Inizialmente è un elenco vuoto,
  • righe 5-10: si costruiscono le identità [Nom prénom titre] dei clienti. Se un'identità inizia con query (riga 7), viene inserita nell'elenco dei suggerimenti (riga 8).

6.12. Conferma di un appuntamento

La convalida di un appuntamento corrisponde alla seguente sequenza:

Il codice del link [Valider] è il seguente:


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

È quindi il metodo [Form].action() che gestirà questo evento. Nel frattempo, il modello [Form].action avrà ricevuto la stringa 'conferma'. Il codice è il seguente:


  // Bean dell'applicazione
  @Inject
  private Application application;
  // cache della sessione
...
  private Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
  // modello
  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);
    }
  }

  // azione su RV
  public void action() {
    // a seconda dell'azione desiderata
...
    if (action.equals("valider")) {
      validerResa();
    }
}

  // conferma appuntamento
  public void validerResa() {
    // conferma della prenotazione
    try {
      // il cliente esiste?
      Boolean erreur = !hIdentitesClients.containsKey(identiteClient);
      if (erreur) {
        msgErreur = Messages.getMessage(null, "form2.erreurClient", new Object[]{identiteClient}).getSummary();
        RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
        return;
      }
      // si aggiunge l'appuntamento
      application.getMetier().ajouterRv(jour, creneauChoisi.getCreneau(), hIdentitesClients.get(identiteClient));
      // si aggiorna l'agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // visualizzazione di form2
      setForms(true, true, false);
    } catch (Throwable th) {
      // visualizzazione errori
      prepareVueErreur(th);
    }
    // azzeramento della fascia oraria selezionata
    creneauChoisi = null;
    // azzeramento cliente
    identiteClient = null;
}
  • righe 33-35: a causa del valore del campo action, verrà eseguito il metodo [validerResa],
  • riga 43: si verifica innanzitutto che il cliente esista. Infatti, nell’area di immissione assistita, l’utente potrebbe aver effettuato un’immissione manuale senza avvalersi dei suggerimenti proposti. L’immissione assistita è associata al modello [Form].identiteClient. Si verifica quindi se tale identità esiste nel dizionario identitesClients creato al momento dell’istanziazione del modello (riga 20). Quest’ultimo associa a un’identità cliente di tipo [Nom prénom titre] il cliente stesso (riga 25),
  • riga 44: se il cliente non esiste, viene restituito un errore al browser,
  • riga 45: un messaggio di errore internazionalizzato,
  • riga 46: si aggiunge l'attributo ('errore', true) al dizionario args della chiamata a AJAX. La chiamata a AJAX è stata definita come segue:

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

Alla riga 3 sopra riportata, si nota che il collegamento [Valider] presenta un attributo oncomplete. È proprio questo attributo che farà visualizzare il messaggio di errore secondo una tecnica già vista in precedenza.

  • Riga 50: si richiede al livello [métier] di aggiungere un appuntamento per il giorno selezionato (jour), nella fascia oraria selezionata (creneauChoisi.getCreneau()) e per il cliente selezionato (hIdentitesClients.get(identiteClient)),
  • riga 52: si richiede al livello [métier] di aggiornare l'agenda del medico. Verrà visualizzato l'appuntamento aggiunto insieme a tutte le modifiche che altri utenti dell'applicazione potrebbero aver apportato,
  • riga 54: si visualizza nuovamente l'agenda [form2.xhtml],
  • riga 57: se si verifica un errore, viene visualizzata la pagina di errore.

6.13. Annullamento di un appuntamento

Ciò corrisponde alla seguente sequenza:

Il pulsante [Annuler] nella pagina [form2.xhtml] è il seguente:


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

Viene quindi richiamato il metodo [Form].action:


// azione su RV
  public void action() {
    // a seconda dell'azione desiderata
...
    if (action.equals("annuler")) {
      annulerRv();
    }
  }
  
// annullamento dell'appuntamento
  public void annulerRv() {
    // viene visualizzato il modulo 2
    setForms(true, true, false);
    // azzeramento della fascia oraria selezionata
    creneauChoisi = null;
    // azzeramento cliente
    identiteClient = null;
  }

6.14. Navigazione nel calendario

La barra degli strumenti consente di navigare nel calendario:

Sebbene non sia visibile nelle schermate sopra riportate, l'agenda viene aggiornata con gli appuntamenti del nuovo giorno selezionato.

I tag dei tre pulsanti in questione sono i seguenti in [form1.xhtml]:


  <p:toolbar>
    <p:toolbarGroup align="left">  
...
      <h:outputText value="#{msg['form1.jour']}"/>
      <p:calendar id="calendrier" value="#{form.jour}" readOnlyInputText="true">
        <p:ajax event="dateSelect" listener="#{form.hideAgenda}" update=":formulaire:contenu"/>  
      </p:calendar>
      <p:separator/>
      <p:commandButton id="resa-agenda" icon="ui-icon-check" actionListener="#{form.getAgenda}" update=":formulaire:contenu"/>  
      <p:tooltip for="resa-agenda" value="#{msg['form1.agenda']}"/>  
      <p:commandButton id="resa-precedent" icon="ui-icon-seek-prev" actionListener="#{form.getPreviousAgenda}" update=":formulaire:contenu"/>  
      <p:tooltip for="resa-precedent" value="#{msg['form1.precedent']}"/>  
      <p:commandButton id="resa-suivant" icon="ui-icon-seek-next" actionListener="#{form.getNextAgenda}" update=":formulaire:contenu"/>          
      <p:tooltip for="resa-suivant" value="#{msg['form1.suivant']}"/>  
      <p:commandButton id="resa-today" icon="ui-icon-home" actionListener="#{form.today}" update=":formulaire:contenu"/>          
      <p:tooltip for="resa-today" value="#{msg['form1.today']}"/>  
    </p:toolbarGroup>
    <p:toolbarGroup align="right">  
      ...  
    </p:toolbarGroup>  
</p:toolbar>

I metodi [Form].getPreviousAgenda, [Form].getNextAgenda, [Form].today sono i seguenti:


private Date jour = new Date();

public void getPreviousAgenda() {
    // si passa al giorno precedente
    Calendar cal = Calendar.getInstance();
    cal.setTime(jour);
    cal.add(Calendar.DAY_OF_YEAR, -1);
    jour = cal.getTime();
    // agenda
    if (form2Rendered) {
      getAgenda();
    }
  }

  public void getNextAgenda() {
    // si passa al giorno successivo
    Calendar cal = Calendar.getInstance();
    cal.setTime(jour);
    cal.add(Calendar.DAY_OF_YEAR, 1);
    jour = cal.getTime();
    // agenda
    if (form2Rendered) {
      getAgenda();
    }
  }

  // agenda di oggi
  public void today() {
    jour = new Date();
    // agenda
    if (form2Rendered) {
      getAgenda();
    }
}
  • riga 1: il giorno di visualizzazione dell'agenda,
  • riga 5: si utilizza un calendario,
  • riga 6: che viene inizializzato al giorno corrente dell'agenda,
  • riga 7: si sottrae un giorno dal calendario,
  • riga 8: e lo si reinizializza con il giorno di visualizzazione dell'agenda,
  • riga 11: si visualizza nuovamente l'agenda se questa è già visualizzata. Infatti, l'utente può utilizzare la barra degli strumenti anche se l'agenda non è visualizzata.

Gli altri metodi sono analoghi.

6.15. Cambio della lingua di visualizzazione

Il cambio di lingua viene gestito tramite il pulsante "Menu" della barra degli strumenti:

I tag del pulsante del menu sono i seguenti:


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

I metodi eseguiti nel modello sono i seguenti:


private String locale = "fr";

  public void setFrenchLocale() {
    locale = "fr";
    // aggiorna la pagina
    redirect();
  }

  public void setEnglishLocale() {
    locale = "en";
    // si ricarica la pagina
    redirect();
  }

  private void redirect() {
    // reindirizzamento del client al servlet
    ExternalContext ctx = FacesContext.getCurrentInstance().getExternalContext();
    try {
      ctx.redirect(ctx.getRequestContextPath());
    } catch (IOException ex) {
      Logger.getLogger(Form.class.getName()).log(Level.SEVERE, null, ex);
    }
}

I metodi delle righe 3 e 9 si limitano a inizializzare il campo locale della riga 1, quindi reindirizzano il browser del cliente alla stessa pagina. Un reindirizzamento è una risposta in cui il server chiede al browser di caricare un'altra pagina. Il browser effettua quindi un GET verso questa nuova pagina.

  • riga 17: [ExternalContext] è una classe JSF che consente di accedere al servlet attualmente in esecuzione,
  • riga 19: si esegue il reindirizzamento. Il parametro del metodo redirect è l’URL della pagina verso la quale il browser client deve essere reindirizzato. In questo caso vogliamo reindirizzarci verso [/mv-rdvmedecins-pf], che è il nome della nostra applicazione:
  

il metodo [getRequestContextPath] consente di ottenere questo nome. Verrà quindi caricata la pagina iniziale [index.xhtml] della nostra applicazione. Questa pagina è associata al modello [Form] con ambito di sessione. Questo modello gestisce tre valori booleani che controllano l’aspetto della pagina [index.xhtml]:


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

Poiché il modello ha ambito di sessione, questi tre valori booleani hanno mantenuto i propri valori. La pagina [index.xhtml] apparirà quindi nello stato in cui si trovava prima del reindirizzamento. Questa pagina è formattata con il template facelet [layout.xhtml] seguente:


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

Il tag alla riga 9 imposta la lingua di visualizzazione della pagina tramite il suo attributo «local». La pagina verrà quindi visualizzata in francese o in inglese a seconda dei casi. Ma perché un reindirizzamento? Torniamo ai tag delle opzioni di cambio lingua:


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

Inizialmente erano state scritte per aggiornare, tramite una chiamata AJAX, il campo ID del modulo (attributo update delle righe 7 e 8). Tuttavia, durante i test, il cambio di lingua non funzionava sempre. Da qui il reindirizzamento per risolvere il problema. Si sarebbe forse potuto anche impostare l’attributo ajax='false' nei tag per provocare un ricaricamento della pagina. Ciò avrebbe quindi evitato il reindirizzamento.

6.16. Aggiornamento degli elenchi

Ciò corrisponde alla seguente azione:

 

Il tag associato all’opzione [Rafraîchir] è il seguente:


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

Il metodo [Form].refresh è il seguente:


  public void refresh() {
    // si aggiornano gli elenchi
    init();
}

Il metodo init è il metodo eseguito subito dopo la creazione del bean [Form]. Ha lo scopo di memorizzare nella cache i dati del database nel modello:


// bean Application
  @Inject
  private Application application;
  // cache della sessione
  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() {
    // si memorizzano nella cache medici e clienti
    try {
      medecins = application.getMetier().getAllMedecins();
      clients = application.getMetier().getAllClients();
    } catch (Throwable th) {
      ...
    }
    ...
    // i dizionari
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
      hIdentitesClients.put(identite(c), c);
    }
  }

Il metodo init crea gli elenchi e i dizionari delle righe 5-9. Lo svantaggio di questa tecnica è che tali elementi non tengono più conto delle modifiche apportate al database (aggiunta di un cliente, di un medico, ecc.). Il metodo refresh forza la ricostruzione di questi elenchi e dizionari. Pertanto, lo si utilizzerà ogni volta che viene apportata una modifica al database, ad esempio l’aggiunta di un nuovo cliente.

6.17. Conclusion

Ricordiamo l’architettura dell’applicazione che abbiamo appena realizzato:

Ci siamo basati in gran parte sulla versione JSF2 già realizzata:

  • gli strati [métier], [DAO], [JPA] sono stati mantenuti,
  • i bean [Application] e [Form] del livello web sono stati mantenuti, ma sono state aggiunte nuove funzionalità a causa dell’arricchimento dell’interfaccia utente,
  • l’interfaccia utente è stata profondamente modificata. In particolare, è ora più ricca di funzionalità e più intuitiva.

Il passaggio da JSF a Primefaces per la realizzazione dell’interfaccia web richiede una certa esperienza, poiché all’inizio ci si sente un po’ sopraffatti dal gran numero di componenti disponibili e alla fine non si sa bene quali utilizzare. È quindi necessario concentrarsi sull’ergonomia desiderata per l’interfaccia.

6.18. Test con Eclipse

Come abbiamo fatto per le versioni precedenti dell’applicazione di esempio, mostriamo come testare questa versione 03 con Eclipse. Innanzitutto, importiamo in Eclipse i progetti Maven dell’esempio 03 [1]:

  • [mv-rdvmedecins-ejb-dao-jpa]: i livelli [DAO] e [JPA],
  • [mv-rdvmedecins-ejb-metier]: il livello [métier],
  • [mv-rdvmedecins-pf]: il livello [web] implementato con JSF e Primefaces,
  • [mv-rdvmedecins-app]: il progetto padre del progetto aziendale [mv-rdvmedecins-app-ear]. Quando si importa il progetto padre, il progetto figlio viene importato automaticamente,
  • in [2], si esegue il progetto aziendale [mv-rdvmedecins-app-ear],
  • in [3], si seleziona il server Glassfish,
  • in [4], nella scheda [Servers], l'applicazione è stata distribuita. Non si avvia automaticamente. È necessario richiederla tramite un browser: