Skip to content

6. Aplicação de exemplo-03: rdvmedecins-pf-ejb

Recorde-se a estrutura da aplicação de exemplo desenvolvida para o servidor Glassfish:

Não alteramos nada nesta arquitetura, exceto a camada web, que será aqui implementada com a ajuda de JSF e Primefaces.

6.1. O projeto NetBeans

Acima, as camadas [métier] e [DAO] são as do exemplo 01 JSF / EJB / Glassfish. Vamos reutilizá-las.

  
  • [mv-rdvmedecins-ejb-dao-jpa]: projeto EJB das camadas [DAO] e [JPA] do exemplo 01,
  • [mv-rdvmedecins-ejb-metier]: projeto EJB da camada [métier] do exemplo 01,
  • [mv-rdvmedecins-pf]: projeto da camada [web] / Primefaces – novo,
  • [mv-rdvmedecins-app-ear]: projeto empresarial para implementar a aplicação no servidor Glassfish – novo.

6.2. O projeto empresarial

O projeto empresarial destina-se exclusivamente à implementação dos três módulos [mv-rdvmedecins-ejb-dao-jpa], [mv-rdvmedecins-ejb-metier] e [mv-rdvmedecins-pf] no servidor Glassfish. O projeto NetBeans é o seguinte:

O projeto existe apenas para estas três dependências [1], definidas no ficheiro [pom.xml] da seguinte forma:


<?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>
  • linhas 10-13: o artefacto Maven do projeto empresarial,
  • linhas 18-37: as três dependências do projeto. Deve-se prestar atenção ao tipo destas (linhas 23, 29, 35).

Para executar a aplicação web, será necessário executar este projeto empresarial.

6.3. O projeto web Primefaces

O projeto web Primefaces é o seguinte:

  • em [1], as páginas do projeto. A página [index.xhtml] é a única página do projeto. Inclui três fragmentos: [form1.xhtml], [form2.xhtml] e [erreur.xhtml]. As outras páginas servem apenas para fins de formatação.
  • Em [2], os beans Java. O bean [Application] com âmbito application, o bean [Form] com âmbito session. A classe [Erreur] encapsula um erro. A classe [MyDataModel] serve de modelo para uma tag <dataTable> do PrimeFaces,
  • em [3], os ficheiros de mensagens para a internacionalização,
  • em [4], as dependências. O projeto web tem dependências do projeto EJB da camada [DAO], o projeto EJB da camada [métier] e do Primefaces para a camada [web].

6.4. A configuração do projeto

A configuração do projeto é idêntica à dos projetos Primefaces ou JSF que analisámos. Enumeramos os ficheiros de configuração sem os voltar a explicar.

 

[web.xml]: configura a aplicação 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>

Note-se, na linha 30, que a página [index.xhtml] é a página inicial da aplicação.

[faces-config.xml]: configura a aplicação 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]: está vazio, mas é necessário para a anotação @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]: a folha de estilo da aplicação


.col1{
   background-color: #ccccff
}

.col2{
   background-color: #ffcccc
}

A biblioteca Primefaces inclui as suas próprias folhas de estilo. A folha de estilo acima é utilizada apenas para a página a apresentar em caso de exceção, uma página não gerida pela aplicação. É então apresentada a página [exception.xhtml].

[messages_fr.properties]: o ficheiro de mensagens em francês


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

# exceção
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

# formulário 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

# formulário 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

# erro
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]: o ficheiro de mensagens em inglês


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

# exceção
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

# formulário 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

# formulário 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

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

6.5. O modelo das páginas [layout.xhtml]

O modelo [layout.xhtml] é o seguinte:


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

A única parte variável deste modelo é a área das linhas 28-30. Esta área encontra-se na zona com o ID :formulário:conteúdo (linha 27). É importante ter isto em conta. As chamadas AJAX que atualizam esta área terão o atributo update=":formulário:conteúdo". Além disso, o formulário começa na linha 15. Assim, o fragmento inserido nas linhas 28-30 é inserido neste formulário.

O aspeto resultante deste modelo é o seguinte:

A parte dinâmica da página será inserida na área emoldurada acima.

6.6. A página [index.xhtml]

O projeto apresenta sempre a mesma página, a página [index.xhtml] seguinte:


<?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>
  • linhas 8-9: este fragmento XHTML será inserido na zona dinâmica do modelo [layout.xhtml],
  • A página inclui três subfragmentos:
  • [form1.xhtml], linhas 10-12;
  • [form2.xhtml], linhas 13-15;
  • [erreur.xhtml], linhas 16-18.

A presença destes fragmentos em [index.xhtml] é controlada por valores booleanos do modelo [Form.java] associado à página. Assim, ao alterar esses valores, a página apresentada fica diferente.

O fragmento [form1.xhtml] apresenta a seguinte renderização:

O fragmento [form2.xhtml] apresenta o seguinte resultado:

O fragmento [erreur.xhtml] apresenta o seguinte resultado:

6.7. Os beans do projeto

A classe do pacote [utils] já foi apresentada: a classe [Messages] é uma classe que facilita a internacionalização das mensagens de uma aplicação. Foi analisada no parágrafo 2.8.5.7.

6.7.1. O bean Application

O bean [Application.java] é um bean de âmbito application. Recorde-se que este tipo de bean serve para armazenar dados de leitura única, disponíveis para todos os utilizadores da aplicação. Este bean é o seguinte:


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 {

  // camada de negócio
  @EJB
  private IMetierLocal metier;

  public Application() {
  }
  
  // getters

  public IMetierLocal getMetier() {
    return metier;
  }
  
}
  • linha 8: atribui-se ao bean o nome «application»,
  • linha 9: tem âmbito «application»,
  • linhas 13-14: uma referência à interface local da camada [métier] será injetada nele pelo contentor EJB do servidor de aplicações. Recordemos a arquitetura da aplicação:

As aplicações JSF, EJB e [Metier] serão executadas na mesma JVM (Java Virtual Machine). Portanto, a camada [JSF] irá utilizar a interface local da EJB. É tudo. O bean [Application] não contém mais nada. Para aceder à camada [métier], os outros beans irão procurá-la neste bean.

6.7.2. O bean [Erreur]

A classe [Erreur] é a seguinte:

  1. package beans;
    
    
    
        public class Erreur {
        
    
    
    
          public Erreur() {
        
      }
    
    
    
    
          // campo
        
    
          private String classe;
        
    
          private String message;
        
    
    
    
          // construtor
        
    
          public Erreur(String classe, String message){
        
    
            this.setClasse(classe);
        
    
            this.message=message;
        
      }
    
    
    
    
          // getters e setters
        
    ...  
    
    }
    
  • na linha 9, o nome de uma classe de exceção, caso tenha sido lançada uma exceção,
  • linha 10: uma mensagem de erro.

6.7.3. O bean [Form]

O seu código é o seguinte:


package beans;

import java.io.IOException;
...

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

  public Form() {
  }
  
// bean da aplicação
  @Inject
  private Application application;

  // cache da sessão
  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>();

  // modelo
  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() {
    // armazenamos os médicos e os clientes em cache
    try {
      medecins = application.getMetier().getAllMedecins();
      clients = application.getMetier().getAllClients();
    } catch (Throwable th) {
      // regista-se o erro
      prepareVueErreur(th);
      return;
    }

    // os dicionários
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
      hIdentitesClients.put(identite(c), c);
    }
  }

  ...

  // visualização da vista
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.erreurRendered = erreurRendered;
  }

  // preparação vueErreur
  private void prepareVueErreur(Throwable th) {
    // é criada a lista de erros
    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()));
    }
// a vista de erros é apresentada
    setForms(true, false, true);
  }

  // getters e setters
  ...
}
  • linhas 6-8: a classe [Form] é um bean com o nome «form» e com âmbito de sessão. Recorde-se que, nesse caso, a classe deve ser serializável,
  • linhas 14-15: o bean «form» tem uma referência ao bean «application». Esta referência será injetada pelo contentor de servlets no qual a aplicação é executada (presença da anotação @Inject).
  • linhas 17-44: o modelo das páginas [form1.xhtml, form2.xhtml, erreur.xhtml]. A exibição destas páginas é controlada pelos valores booleanos das linhas 27-29. Note-se que, por predefinição, é a página [form1.xhtml] que é apresentada (linha 27),
  • linhas 46-47: o método init é executado logo após a instanciação da classe (presença da anotação @PostConstruct),
  • linhas 50-51: solicita-se à camada [métier] a lista de médicos e clientes,
  • linhas 59-65: se tudo correr bem, os dicionários dos médicos e dos clientes são criados. São indexados pelo seu número. Em seguida, a página [form1.xhtml] será apresentada (linha 27),
  • linha 54: em caso de erro, é criado o modelo da página [erreur.xhtml]. Este modelo corresponde à lista de erros da linha 36,
  • linhas 78-88: o método [prepareVueErreur] constrói a lista de erros a apresentar. A página [index.xhtml] apresenta então os fragmentos [form1.xhtml] e [erreur.xhtml] (linha 87).

A página [erreur.xhtml] é a seguinte:


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

Esta página utiliza uma baliza <p:dataTable> (linhas 12-28) para apresentar a lista de erros. O resultado é uma página de erro semelhante à seguinte:

Vamos agora definir as diferentes fases do ciclo de vida da aplicação. Para cada ação do utilizador, analisaremos as vistas envolvidas e os gestores de eventos.

6.8. A exibição da página inicial

Se tudo correr bem, a primeira página apresentada é a [form1.xhtml]. Isto resulta na seguinte vista:

A página [form1.xhtml] é a seguinte:


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

A barra de ferramentas destacada na captura de ecrã é o componente Primefaces Toolbar. Este está definido nas linhas 8 a 14. Contém dois grupos de componentes, cada um deles definido por uma baliza <toolbarGroup>, nas linhas 9 a 11 e 12 a 14. Um dos grupos está alinhado à esquerda da barra de ferramentas (linha 9) e o outro à direita (linha 12).

Vamos analisar alguns componentes do grupo da esquerda:


<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>
...
  • linhas 4-7: o menu suspenso dos médicos ao qual foi adicionado um efeito (effect="fade"),
  • linha 6: um comportamento AJAX. Quando houver uma alteração na lista suspensa, o método [Form].hideAgenda (listener="#{form.hideAgenda}") será executado e a área dinâmica :formulário:conteúdo (update=":formulaire:contenu") será atualizada,
  • linha 8: inclui um separador na barra de ferramentas,
  • linhas 10-12: o campo de introdução da data. Aqui utiliza-se o calendário do Primefaces. O campo de introdução é de leitura única (readOnlyInputText="true"),
  • linha 11: um comportamento AJAX. Quando houver uma alteração na data, o método [Form].hideAgenda será executado e a área dinâmica :formulário:conteúdo será atualizada,
  • linha 14: um botão. Ao clicar nele, é executada uma chamada AJAX para o método [Form].getAgenda (), o modelo será então alterado e a resposta do servidor será utilizada para atualizar a zona dinâmica :formulário:conteúdo,
  • linha 15: a baliza <tooltip> permite associar uma bolha de ajuda a um componente. O ID deste último é indicado pelo atributo «for» da baliza <tooltip>. Aqui, «for="resa-agenda"» refere-se ao botão da linha 14:

Esta página é alimentada pelo seguinte modelo:


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

  public Form() {
  }
  
  // cache da sessão
  private List<Medecin> medecins;
  private List<Client> clients;
  // modelo
  private Long idMedecin;
  private Date jour = new Date();
  
  // lista de médicos
  public List<Medecin> getMedecins() {
    return medecins;
  }

  // lista de clientes
  public List<Client> getClients() {
    return clients;
  }

  // agenda
  public void getAgenda() {
    ...
  }
  • o campo da linha 12 fornece, em leitura e escrita, o valor da lista da linha 4 da página. Na exibição inicial da página, este campo define o valor selecionado na lista suspensa. Na exibição inicial, idMedecin é igual a null, pelo que será selecionado o primeiro médico,
  • o método das linhas 16-18 gera os elementos da lista suspensa de médicos (linha 5 da página). Cada opção gerada terá como rótulo (itemLabel) o título, apelido e nome próprio do médico e, como valor (itemValue), o ID do médico,
  • o campo da linha 13 alimenta, em leitura/gravação, o campo de introdução de dados da linha 10 da página. Na exibição inicial, é, portanto, a data de hoje que é apresentada,
  • linhas 26-28: o método getAgenda gere o clique no botão [Agenda] da linha 14 da página. É praticamente idêntico ao que era na versão JSF:

  // bean da aplicação
  @Inject
  private Application application;
  // cache da sessão
  private List<Medecin> medecins;
  private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
  // modelo
  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 {
      // recuperar o médico
      medecin = hMedecins.get(idMedecin);
      // a agenda do médico para um determinado dia
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // exibição do formulário 2
      setForms(true, true, false);
    } catch (Throwable th) {
      // visualização dos erros
      prepareVueErreur(th);
    }
    // nenhum horário selecionado por enquanto
    creneauChoisi = null;
}

Não iremos comentar este código. Isso já foi feito.

6.9. Mostrar a agenda de um médico

6.9.1. Visão geral da agenda

Trata-se do seguinte caso de utilização:

  • em [1], seleciona-se um médico [1] e um dia [2] e, em seguida, solicita-se [3] a agenda do médico para o dia escolhido,
  • em [4], esta aparece abaixo da barra de ferramentas.

O código da página [form2.xhtml] é o seguinte:


<?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 contextual -->
    <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']}">
      <!-- coluna dos horários -->
      <p:column style="width: 100px">  
        ...
      </p:column>  
      <!-- coluna dos clientes -->
      <p:column style="width: 300px">  
        ...
      </p:column>  
    </p:dataTable>

    <!-- confirmação de eliminação RV -->
    <p:confirmDialog id="confirmDialog" message="#{msg['form2.suppression.confirmation']}"  
                     header="#{msg['form2.suppression.message']}" severity="alert" widgetVar="confirmation">                   
      ...                
    </p:confirmDialog>  

    <!-- mensagem de erro -->
    <p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >  
      ...  
    </p:dialog>
    
    <!-- gestão da resposta do servidor -->
    <script type="text/javascript">  
      ...
      }  
    </script> 
  </body>
</html>
  • linhas 16-26: o elemento principal da página é a tabela <dataTable>, que apresenta a agenda do médico,
  • linhas 12-14: vamos utilizar um menu contextual para adicionar/eliminar uma consulta:
 
  • linhas 29-32: será exibida uma caixa de confirmação quando o utilizador pretender eliminar uma consulta:
 
  • linhas 35-37: será utilizada uma caixa de diálogo para sinalizar um erro:
 
  • linhas 40-43: teremos de introduzir um pouco de JavaScript.

6.9.2. A tabela de compromissos

Abordamos aqui o modelo de uma tabela de dados, tal como estudado no parágrafo 5.15, página 327.

Vamos analisar o elemento principal da página, a tabela que apresenta a agenda:


<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- coluna dos horários -->
      <p:column style="width: 100px">  
        ...
      </p:column>  
      <!-- coluna dos clientes -->
      <p:column style="width: 300px">  
        ...
      </p:column>  
    </p:dataTable>

O resultado é o seguinte:

Trata-se de uma tabela com duas colunas (linhas 4-6 e 8-10) alimentada pela fonte [Form].getMyDataModel() (value="#{form.myDataModel}"). Só é possível selecionar uma linha de cada vez (selectionMode="single"). A cada POST, é atribuída uma referência do elemento selecionado a [Form].creneauChoisi (selection="#{form.creneauChoisi}").

Recorde-se que o método getAgenda inicializou o seguinte campo no modelo:



// modelo
private AgendaMedecinJour agendaMedecinJour;

O modelo da tabela é obtido através da chamada do método [Form].getMyDataModel (atributo «value» da baliza <dataTable>) seguinte:


  // o modelo do dataTable
  public MyDataModel getMyDataModel() {
    return new MyDataModel(agendaMedecinJour.getCreneauxMedecinJour());
}

Analisemos a classe [MyDataModel], que serve de modelo para a baliza <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> {

  // fabricantes
  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 de intervalos
    CreneauMedecinJour[] creneauxMedecinJour = (CreneauMedecinJour[]) getWrappedData();
    // a chave é um inteiro longo
    long key = Long.parseLong(rowKey);
    // procura-se o intervalo selecionado
    for (CreneauMedecinJour creneauMedecinJour : creneauxMedecinJour) {
      if (creneauMedecinJour.getCreneau().getId().longValue() == key) {
        return creneauMedecinJour;
      }
    }
    // nada
    return null;
  }
}
  • linha 7: a classe [MyDataModel] é o modelo da baliza <p:dataTable>. Esta classe tem como objetivo estabelecer a ligação entre o elemento rowkey que é enviado e o elemento associado a essa linha,
  • linha 7: a classe implementa a interface [SelectableDataModel] através da classe [ArrayDataModel]. Isto significa que o parâmetro do construtor é um array. É este array que alimenta a baliza <dataTable>. Aqui, cada linha do array será associada a um elemento do tipo [CreneauMedecinJour],
  • linhas 13-15: o construtor passa o seu parâmetro à sua classe pai,
  • linhas 18-20: cada linha da matriz corresponde a um intervalo horário e será identificada pelo ID do intervalo horário (linha 19). É este ID que será enviado para o servidor,
  • linha 23: o código que será executado no servidor quando o ID de um intervalo horário for enviado. O objetivo deste método é obter a referência do objeto [CreneauMedecinJour] associado a este ID. Esta referência será atribuída ao valor do atributo «selection» da baliza <dataTable>:

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

O campo [Form].creneauChoisi conterá, portanto, a referência do objeto [CreneauMedecinJour] que se pretende adicionar ou eliminar.

6.9.3. A coluna dos intervalos horários

A coluna dos intervalos horários é obtida com o seguinte código:


<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- coluna dos horários -->
      <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>  
  
      <!-- coluna dos clientes -->
      <p:column style="width: 300px">  
        ...
      </p:column>  
    </p:dataTable>
  • linhas 5-7: o cabeçalho da coluna,
  • linhas 8-15: o elemento atual da coluna. De notar, na linha 9, a utilização da baliza <h:outputFormat>, que permite formatar os elementos a apresentar. O parâmetro «value» indica a cadeia de caracteres a apresentar. A notação {i,type,format} designa o parâmetro n.º i, o tipo desse parâmetro e o seu formato. Existem aqui 4 parâmetros numerados de 0 a 3; o tipo destes é numérico e serão apresentados com dois algarismos,
  • linhas 10-13: os quatro parâmetros esperados pela baliza <h:outputFormat>.

6.9.4. A coluna dos clientes

A coluna dos clientes é obtida com o seguinte código:


<!-- agenda -->
    <p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
                 selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
      <!-- coluna dos horários -->
      ...  
      <!-- coluna dos clientes -->
      <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>
  • linhas 8-10: o cabeçalho da coluna,
  • linhas 11-13: o elemento atual quando existe um compromisso para o intervalo horário. Neste caso, são apresentados o título, o nome próprio e o apelido do cliente para quem foi marcado esse compromisso,
  • linhas 14-16: outro fragmento ao qual voltaremos mais tarde.

6.10. Eliminação de um compromisso

A eliminação de um compromisso segue a seguinte sequência:

A vista afetada por esta ação é a seguinte:


<!-- menu contextual -->
    <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>

    <!-- confirmação de eliminação 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>  
  • linhas 2-5: um menu contextual associado à tabela de dados (atributo «for»). Tem duas opções [1]:
  • linha 4: a opção [Supprimer] aciona a exibição da caixa de diálogo [2] das linhas 13-20,
  • linha 15: ao clicar em [Oui], é executado o [Form.action], que irá eliminar o compromisso. Normalmente, o menu contextual não deveria apresentar a opção [Supprimer] se o elemento selecionado não tiver um compromisso, nem a opção [Réserver] se o elemento selecionado tiver um compromisso. Não conseguimos que o menu contextual fosse assim tão subtil. Conseguimos fazê-lo para o primeiro elemento selecionado, mas depois verificamos que o menu contextual mantém a configuração adquirida para essa primeira seleção. Torna-se, então, incorreto. Por isso, mantivemos as duas opções e decidimos fornecer um aviso ao utilizador caso este eliminasse um elemento sem compromisso,
  • linha 16: o atributo oncomplete, que permite definir código JavaScript a ser executado após a execução da chamada AJAX. Este código será o seguinte:

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

    <!-- gestão da resposta do servidor -->
    <script type="text/javascript">  
      function handleRequest(xhr, status, args) {  
        // erro?
        if(args.erreur) {  
          dlgErreur.show();  
        }  
      }  
    </script> 
  • linha 10: o código JavaScript verifica se o dicionário args possui o atributo erreur. Se sim, exibe a caixa de diálogo da linha 2 (atributo widgetVar). Esta caixa de diálogo apresenta o modelo [Form].msgErreur.

Vejamos o código executado para gerir a eliminação de um compromisso:


    <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>  
  • linha 2: o método [Form].action será executado,
  • linha 4: antes da sua execução, o campo action terá recebido o valor «supprimer».

O método [action] é o seguinte:


// ação em RV
  public void action() {
    // dependendo da ação pretendida
    if (action.equals("supprimer")) {
      supprimer();
    }
    ...
  }
  
  public void supprimer() {
    // é preciso fazer alguma coisa?
    Rv rv = creneauChoisi.getRv();
    if (rv == null) {
      signalerActionIncorrecte();
      return;
    }
    try {
      // eliminação de um compromisso
      application.getMetier().supprimerRv(rv);
      // atualiza-se a agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // exibir o formulário 2
      setForms(true, true, false);
    } catch (Throwable th) {
      // visualização de erros
      prepareVueErreur(th);
    }
    // limpar o intervalo selecionado
    creneauChoisi = null;
}
  • linha 4: se a ação for «eliminar», executa-se o método [supprimer],
  • linha 12: recupera-se o compromisso do intervalo selecionado. Recorde-se que o [creneauChoisi] foi inicializado pela referência do elemento [CreneauMedecinJour] selecionado,
  • se esse compromisso existir, é eliminado (linha 19), a agenda é regenerada (linha 21) e, em seguida, voltada a ser apresentada (linha 23),
  • se a eliminação falhar, é apresentada a página de erros (linha 26),
  • se o elemento selecionado não tiver qualquer compromisso (linha 13), então estamos na situação em que o utilizador clicou em [Supprimer] num intervalo de tempo que não tem qualquer compromisso. Este erro é assinalado:
 

O método [signalerActionIncorrecte] é o seguinte:


// notificar uma ação incorreta
  private void signalerActionIncorrecte() {
    // anular o intervalo selecionado
    creneauChoisi = null;
    // erro
    msgErreur = Messages.getMessage(null, "form2.erreurAction", null).getSummary();
    RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
  }
  • linha 4: remove-se a seleção,
  • linha 6: gera-se uma mensagem de erro internacionalizada,
  • linha 7: adiciona-se ao dicionário args da chamada AJAX o atributo («erro», true).

Voltemos ao código XHTML do botão [Oui]:


<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
  • linha 2: após a execução do método [Form].action, é executado o método JavaScript handleRequest:

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

    <!-- gestão da resposta do servidor -->
    <script type="text/javascript">  
      function handleRequest(xhr, status, args) {  
        // erro?
        if(args.erreur) {  
          dlgErreur.show();  
        }  
      }  
</script> 
  • linha 10: verifica-se se o dicionário args possui o atributo denominado «erreur». Se sim, é apresentada a caixa de diálogo da linha 2,
  • linha 3: esta faz com que seja exibida a mensagem de erro criada pelo modelo.

6.11. Marcação de consultas

A marcação de uma consulta corresponde à seguinte sequência:

A vista envolvida nesta ação é a seguinte:


<!-- menu contextual -->
    <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']}">
      <!-- coluna dos horários -->
      <p:column style="width: 100px">  
...
      </p:column>  
      <!-- coluna dos clientes -->
      <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>
...
  • linhas 21-31: apresentam o seguinte:
  • linha 21: a exibição ocorre se não houver qualquer compromisso, se tiver havido uma seleção e se o ID do intervalo escolhido corresponder ao do elemento atual da tabela. Se esta condição não for definida, o fragmento é exibido para todos os intervalos,
  • linha 22: o campo de introdução de dados será um campo de introdução assistida. Partimos do princípio de que pode haver muitos clientes,
  • linhas 24-26: o link [Valider],
  • linhas 28-30: o link [Annuler].

O campo de introdução assistida é gerado pelo seguinte código:


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

O método [Form].completeClients é responsável por apresentar sugestões ao utilizador com base nos caracteres digitados no campo de entrada:

 

As propostas têm o formato [Nom prénom titre]. O código do método [Form].completeClients é o seguinte:


  // o método de preenchimento automático de texto
  public List<String> completeClients(String query) {
    List<String> identites = new ArrayList<String>();
    // procura-se os clientes que correspondem
    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();
}
  • linha 2: query é a cadeia de caracteres introduzida pelo utilizador,
  • linha 3: a lista de sugestões. Inicialmente, uma lista vazia,
  • linhas 5-10: constroem-se as identidades [Nom prénom titre] dos clientes. Se uma identidade começar por query (linha 7), é incluída na lista de sugestões (linha 8).

6.12. Validação de um compromisso

A validação de um compromisso corresponde à seguinte sequência:

O código do link [Valider] é o seguinte:


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

É, portanto, o método [Form].action() que irá gerir este evento. Entretanto, o modelo [Form].action terá recebido a cadeia «validar». O código é o seguinte:


  // bean Application
  @Inject
  private Application application;
  // cache da sessão
...
  private Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
  // modelo
  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);
    }
  }

  // ação sobre RV
  public void action() {
    // conforme a ação pretendida
...
    if (action.equals("valider")) {
      validerResa();
    }
}

  // validação da reserva
  public void validerResa() {
    // validação da reserva
    try {
      // o cliente existe?
      Boolean erreur = !hIdentitesClients.containsKey(identiteClient);
      if (erreur) {
        msgErreur = Messages.getMessage(null, "form2.erreurClient", new Object[]{identiteClient}).getSummary();
        RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
        return;
      }
      // adiciona-se a marcação
      application.getMetier().ajouterRv(jour, creneauChoisi.getCreneau(), hIdentitesClients.get(identiteClient));
      // atualiza-se a agenda
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // exibe-se o form2
      setForms(true, true, false);
    } catch (Throwable th) {
      // visualização de erros
      prepareVueErreur(th);
    }
    // anular o intervalo selecionado
    creneauChoisi = null;
    // limpar dados do cliente
    identiteClient = null;
}
  • linhas 33-35: devido ao valor do campo action, o método [validerResa] será executado,
  • linha 43: verifica-se primeiro se o cliente existe. Com efeito, na zona de introdução assistida, o utilizador pode ter introduzido um valor manualmente sem recorrer às sugestões que lhe foram apresentadas. A introdução assistida está associada ao modelo [Form].identiteClient. Verifica-se, portanto, se essa identidade existe no dicionário identitesClients, criado aquando da instanciação do modelo (linha 20). Este associa a uma identidade de cliente do tipo [Nom prénom titre] o próprio cliente (linha 25),
  • linha 44: se o cliente não existir, é devolvida uma mensagem de erro ao navegador,
  • linha 45: uma mensagem de erro internacionalizada,
  • linha 46: adiciona-se o atributo («erro», true) ao dicionário args da chamada AJAX. A chamada AJAX foi definida da seguinte forma:

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

Na linha 3 acima, verifica-se que o link [Valider] possui um atributo oncomplete. É este atributo que fará com que a mensagem de erro seja exibida, de acordo com uma técnica já abordada.

  • linha 50: solicita-se à camada [métier] que adicione um compromisso para o dia selecionado (jour), no intervalo horário selecionado (creneauChoisi.getCreneau()) e para o cliente selecionado (hIdentitesClients.get(identiteClient)),
  • linha 52: solicita-se à camada [métier] que atualize a agenda do médico. Ver-se-á a consulta adicionada, bem como todas as alterações que outros utilizadores da aplicação possam ter feito,
  • linha 54: volta a ser apresentada a agenda [form2.xhtml],
  • linha 57: exibe-se a página de erro caso ocorra algum erro.

6.13. Cancelamento de uma marcação

Isto corresponde à seguinte sequência:

O botão [Annuler] na página [form2.xhtml] é o seguinte:


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

O método [Form].action é, portanto, chamado:


// ação em RV
  public void action() {
    // conforme a ação pretendida
...
    if (action.equals("annuler")) {
      annulerRv();
    }
  }
  
// cancelamento da marcação de consulta
  public void annulerRv() {
    // é apresentado o formulário 2
    setForms(true, true, false);
    // anulação do horário selecionado
    creneauChoisi = null;
    // limpar dados do cliente
    identiteClient = null;
  }

6.14. Navegação no calendário

A barra de ferramentas permite navegar no calendário:

Embora não apareça nas capturas de ecrã acima, a agenda é atualizada com os compromissos do novo dia selecionado.

As etiquetas dos três botões em questão são as seguintes no [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>

Os métodos [Form].getPreviousAgenda, [Form].getNextAgenda, [Form].today são os seguintes:


private Date jour = new Date();

public void getPreviousAgenda() {
    // passar para o dia anterior
    Calendar cal = Calendar.getInstance();
    cal.setTime(jour);
    cal.add(Calendar.DAY_OF_YEAR, -1);
    jour = cal.getTime();
    // agenda
    if (form2Rendered) {
      getAgenda();
    }
  }

  public void getNextAgenda() {
    // passa-se para o dia seguinte
    Calendar cal = Calendar.getInstance();
    cal.setTime(jour);
    cal.add(Calendar.DAY_OF_YEAR, 1);
    jour = cal.getTime();
    // agenda
    if (form2Rendered) {
      getAgenda();
    }
  }

  // agenda de hoje
  public void today() {
    jour = new Date();
    // agenda
    if (form2Rendered) {
      getAgenda();
    }
}
  • linha 1: o dia de visualização da agenda,
  • linha 5: utiliza-se um calendário,
  • linha 6: que se inicializa com o dia atual da agenda,
  • linha 7: subtraímos um dia do calendário,
  • linha 8: e reinicializa-se com o dia de visualização da agenda,
  • linha 11: volta a exibir a agenda, caso esta esteja a ser exibida. Com efeito, o utilizador pode utilizar a barra de ferramentas sem que a agenda esteja a ser exibida.

Os outros métodos são semelhantes.

6.15. Alteração do idioma de visualização

A mudança de idioma é gerida através do botão «Menu» da barra de ferramentas:

As etiquetas do botão do menu são as seguintes:


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

Os métodos executados no modelo são os seguintes:


private String locale = "fr";

  public void setFrenchLocale() {
    locale = "fr";
    // atualizar a página
    redirect();
  }

  public void setEnglishLocale() {
    locale = "en";
    // recarregar a página
    redirect();
  }

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

Os métodos das linhas 3 e 9 limitam-se a inicializar o campo locale da linha 1 e, em seguida, redirecionam o navegador do cliente para a mesma página. Um redirecionamento é uma resposta em que o servidor solicita ao navegador que carregue outra página. O navegador efetua então um GET para essa nova página.

  • linha 17: [ExternalContext] é uma classe JSF que permite aceder ao servlet em execução,
  • linha 19: efetua-se o redirecionamento. O parâmetro do método redirect é o URL da página para a qual o navegador do cliente deve ser redirecionado. Aqui, queremos redirecionar para [/mv-rdvmedecins-pf], que é o nome da nossa aplicação:
  

O método [getRequestContextPath] permite obter esse nome. Assim, será carregada a página inicial [index.xhtml] da nossa aplicação. Esta página está associada ao modelo [Form] com âmbito de sessão. Este modelo gere três valores booleanos que controlam a aparência da página [index.xhtml]:


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

Como o modelo tem âmbito de sessão, estas três variáveis booleanas mantiveram os seus valores. A página [index.xhtml] irá, portanto, aparecer no estado em que se encontrava antes do redirecionamento. Esta página é formatada com o modelo facelet [layout.xhtml] seguinte:


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

A baliza da linha 9 define o idioma de exibição da página através do seu atributo «local». A página será, assim, apresentada em francês ou em inglês, consoante o caso. Mas por que razão ocorre um redirecionamento? Voltemos às balizas das opções de mudança de idioma:


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

Estas tinham sido inicialmente escritas para atualizar, através de uma chamada AJAX, o campo «id» do formulário (atributo «update» das linhas 7 e 8). No entanto, durante os testes, a mudança de idioma nem sempre funcionava. Daí a redireção para resolver este problema. Talvez também fosse possível definir o atributo ajax='false' nas tags para provocar uma atualização da página. Isso teria evitado o redirecionamento.

6.16. Atualização das listas

Isto corresponde à seguinte ação:

 

A baliza associada à opção [Rafraîchir] é a seguinte:


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

O método [Form].refresh é o seguinte:


  public void refresh() {
    // atualização das listas
    init();
}

O método init é o método executado imediatamente após a criação do bean [Form]. Tem como objetivo armazenar em cache os dados da base de dados no modelo:


// bean Application
  @Inject
  private Application application;
  // cache da sessão
  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() {
    // os médicos e os clientes são armazenados em cache
    try {
      medecins = application.getMetier().getAllMedecins();
      clients = application.getMetier().getAllClients();
    } catch (Throwable th) {
      ...
    }
    ...
    // os dicionários
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
      hIdentitesClients.put(identite(c), c);
    }
  }

O método init cria as listas e os dicionários das linhas 5 a 9. A desvantagem desta técnica é que estes elementos deixam de ter em conta as alterações na base de dados (adição de um cliente, de um médico, etc.). O método refresh força a reconstrução dessas listas e dicionários. Por isso, será utilizado sempre que for feita uma alteração na base de dados, como, por exemplo, a adição de um novo cliente.

6.17. Conclusion

Recorde-se a arquitetura da aplicação que acabámos de construir:

Baseámo-nos em grande parte na versão JSF2 já construída:

  • as camadas [métier], [DAO] e [JPA] foram mantidas,
  • os beans [Application] e [Form] da camada web foram mantidos, mas foram-lhes adicionadas novas funcionalidades devido ao enriquecimento da interface do utilizador,
  • a interface do utilizador foi profundamente alterada. Em particular, é agora mais rica em funcionalidades e mais intuitiva.

A transição do JSF para o Primefaces na construção da interface web requer alguma experiência, pois, no início, ficamos um pouco perdidos perante o grande número de componentes disponíveis e, no final, não sabemos bem quais utilizar. É, portanto, necessário analisar a ergonomia pretendida para a interface.

6.18. Testes no Eclipse

Tal como fizemos nas versões anteriores da aplicação de exemplo, mostramos como testar esta versão 03 com o Eclipse. Em primeiro lugar, importamos para o Eclipse os projetos Maven do exemplo 03 [1]:

  • [mv-rdvmedecins-ejb-dao-jpa]: as camadas [DAO] e [JPA],
  • [mv-rdvmedecins-ejb-metier]: a camada [métier],
  • [mv-rdvmedecins-pf]: a camada [web] implementada com JSF e Primefaces,
  • [mv-rdvmedecins-app]: o projeto pai do projeto empresarial [mv-rdvmedecins-app-ear]. Ao importar o projeto pai, o projeto filho é automaticamente importado,
  • no [2], executa-se o projeto empresarial [mv-rdvmedecins-app-ear],
  • em [3], seleciona-se o servidor Glassfish,
  • no [4], no separador [Servers], a aplicação foi implementada. Não é executada automaticamente. É necessário aceder ao URL [http://localhost:8080/mv-rdvmedecins-pf/] num navegador [5]: