Skip to content

9. Aplicação de exemplo 05: rdvmedecins-pfm-ejb

Vamos rever a estrutura da aplicação de exemplo JSF/EJB 01 desenvolvida para o servidor GlassFish:

Não vamos alterar nada nesta arquitetura, exceto a camada web, que será implementada aqui utilizando JSF, PrimeFaces e PrimeFaces Mobile. O navegador de destino será um navegador móvel.

Desenvolvemos duas aplicações com o GlassFish:

  • A Aplicação 01 utilizava JSF/EJB e tinha uma interface bastante básica,
  • A Aplicação 03 utilizava PF/EJB e tinha uma interface rica.

Devido ao número limitado de componentes disponíveis no PrimeFaces Mobile, vamos voltar à interface básica da Aplicação 01. Além disso, as visualizações devem ser capazes de se adaptar ao tamanho reduzido do ecrã dos dispositivos móveis.

9.1. As Visualizações

Para se ter uma ideia, eis algumas capturas de ecrã da aplicação em execução num simulador do iPhone 4:

  • em [1], a página inicial. Note que, desta vez, tivemos de especificar o nome da máquina (também é possível introduzir o seu endereço IP), porque a utilização da máquina localhost resultou na não exibição de nada,
  • em [2], o menu suspenso dos médicos,
  • em [3], a data desejada para a consulta,
  • em [4], o botão para solicitar a agenda do dia,
  • em [5], a nova visualização apresenta os horários disponíveis do médico,
  • em [6], uma série de botões para navegar no calendário,
  • em [7], uma mensagem a lembrar-lhe do médico e do dia,
  • em [8], um horário para marcar. Vamos lá,
  • em [9], a vista de seleção do cliente,
  • em [10], uma mensagem a lembrar o utilizador do médico, do dia e do horário da consulta,
  • em [11], o menu suspenso do cliente,
  • em [12], o botão de confirmação,
  • em [13], clicar no botão leva-nos de volta ao calendário,
  • em [14], o horário ocupado está agora reservado. Vamos agora eliminar a reserva,
  • em [15], permanecemos na mesma vista,
  • mas em [16], o compromisso foi eliminado,

Na página inicial, pode alterar o idioma [17], [18], [19]:

Por fim, incluímos uma página de erro:

9.2. O Projeto NetBeans

O projeto NetBeans é o seguinte:

  
  • [mv-rdvmedecins-ejb-dao-jpa]: Projeto EJB para as camadas [DAO] e [JPA] do Exemplo 01,
  • [mv-rdvmedecins-ejb-metier]: Projeto EJB para a camada [de negócios] do Exemplo 01,
  • [mv-rdvmedecins-pfmobile]: projeto da camada [web] / PrimeFaces Mobile – novo,
  • [mv-rdvmedecins-pfmobile-app-ear]: projeto empresarial para implementar a aplicação no servidor GlassFish – novo.

9.3. O projeto empresarial

O projeto empresarial é utilizado exclusivamente para implementar os três módulos [mv-rdvmedecins-ejb-dao-jpa], [mv-rdvmedecins-ejb-metier], [mv-rdvmedecins-pfmobile] no servidor GlassFish. O projeto NetBeans é o seguinte:

O projeto existe exclusivamente 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>
...
 
  <groupId>istia.st</groupId>
  <artifactId>mv-rdvmedecins-pfmobile-app-ear</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>ear</packaging>
 
  <name>mv-rdvmedecins-pfmobile-app-ear</name>
 
...
    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
            <version>${project.version}</version>
            <type>ejb</type>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>mv-rdvmedecins-ejb-metier</artifactId>
            <version>${project.version}</version>
            <type>ejb</type>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>mv-rdvmedecins-pfmobile</artifactId>
            <version>${project.version}</version>
            <type>war</type>
        </dependency>
    </dependencies>
</project>
  • linhas 6–9: o artefacto Maven para o projeto empresarial,
  • linhas 14–33: as três dependências do projeto. Repare nos seus tipos (linhas 19, 25, 31).

Para executar a aplicação web, deve executar este projeto empresarial.

9.4. O projeto web PrimeFaces Mobile

O projeto web PrimeFaces Mobile é o seguinte:

  • em [1], as páginas do projeto. A página [index.xhtml] é a única página do projeto. Contém cinco vistas: [vue1.xhtml], [vue2.xhtml], [vue3.xhtml], [vueErreurs.xhtml] e [config.xhtml],
  • em [2], os Java beans. O bean [Application] tem âmbito de aplicação e o bean [Form] tem âmbito de sessão. A classe [Error] encapsula um erro,
  • em [3], os ficheiros de mensagens para internacionalização,
  • em [4], as dependências. O projeto web depende do projeto EJB para a camada [DAO], do projeto EJB para a camada [business] e do PrimeFaces Mobile para a camada [web].

9.5. Configuração do projeto

A configuração do projeto é idêntica à dos projetos PrimeFaces ou JSF que já estudámos. Apresentamos os ficheiros de configuração sem voltar a explicá-los.

 

[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.PROJECT_STAGE</param-name>
    <param-value>Development</param-value>
  </context-param>
  <context-param>
    <param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
    <param-value>true</param-value>
  </context-param> 
  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/faces/*</url-pattern>
  </servlet-mapping>
  <session-config>
    <session-timeout>
      30
    </session-timeout>
  </session-config>
  <welcome-file-list>
    <welcome-file>faces/index.xhtml</welcome-file>
  </welcome-file-list>
  <error-page>
    <error-code>500</error-code>
    <location>/faces/exception.xhtml</location>
  </error-page>
  <error-page>
    <exception-type>Exception</exception-type>
    <location>/faces/exception.xhtml</location>
  </error-page>
 
</web-app>

Note que, na linha 26, 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>
    <default-render-kit-id>PRIMEFACES_MOBILE</default-render-kit-id>
  </application>
</faces-config>

[beans.xml]: 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>

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


# page
page.titre=Les M\u00e9decins Associ\u00e9s
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
 
# vue1
vue1.header=Les M\u00e9decins Associ\u00e9s - R\u00e9servations
 
# formulaire 1
form1.titre=R\u00e9servations
form1.medecin=M\u00e9decin
form1.jour=Jour (jj/mm/aaaa)
form1.date.requise=La date est n\u00e9cessaire
form1.date.invalide=La date est invalide
form1.date.invalide_detail=La date est invalide
form1.agenda=Agenda
form1.options=Options
 
# formulaire 2
form2.titre={0} {1} {2}<br/>{3}
form2.titre_detail={0} {1} {2}<br/>{3}
form2.retour=Retour
form2.supprimer=Supprimer
form2.reserver=R\u00e9server
form2.precedent=Jour pr\u00e9c\u00e9dent
form2.suivant=Jour suivant
form2.today=Aujourd'today
 
# formulaire 3
form3.client=Client
form3.valider=Valider
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.retour=Retour
 
# erreur
erreur.titre=Une erreur s'is produced.
 
# config
config.retour=Retour
config.titre=Configuration
config.langue=Langue
config.langue.francais=Fran\u00e7ais
config.langue.anglais=Anglais
config.valider=Valider
 
#exception
exception.titre=Application indisponible. Veuillez recommencer ult\u00e9rieurement.

[messages_en.properties]: o ficheiro de mensagens em inglês


# page
page.titre=The Associated Doctors
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
 
# vue1
vue1.header=The Associated Doctors - Reservations
 
# formulaire 1
form1.titre=Reservations
form1.medecin=Doctor
form1.jour=day (dd/mm/yyyy)
form1.date.requise=The date is necessary
form1.date.invalide=Invalid date
form1.date.invalide_detail=invalid date
form1.agenda=Diary
form1.options=Options

# formulaire 2
form2.titre={0} {1} {2}<br/>{3}
form2.titre_detail={0} {1} {2}<br/>{3}
form2.retour=Back
form2.supprimer=Delete
form2.reserver=Reserve
form2.precedent=Previous Day
form2.suivant=Next Day
form2.today=Today
 
# formulaire 3
form3.client=Patient
form3.valider=Validate
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.retour=Back
 
# erreur
erreur.titre=Some error happened
 
# config
config.retour=Back
config.titre=Configuration
config.langue=Language
config.langue.francais=French
config.langue.anglais=English
config.valider=Validate
 
#exception
exception.titre=Application not available. Please try again later.

9.6. A página [index.xhtml]

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


<f:view xmlns="http://www.w3.org/1999/xhtml"
        xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html"
        xmlns:ui="http://java.sun.com/jsf/facelets"
        xmlns:p="http://primefaces.org/ui"
        xmlns:pm="http://primefaces.org/mobile"
        contentType="text/html"
        locale="#{form.locale}">
 
  <pm:page title="#{msg['page.titre']}">
    <pm:view id="vue1">
      <ui:fragment rendered="#{form.form1Rendered}">
        <ui:include src="vue1.xhtml"/>
      </ui:fragment>
      <ui:fragment rendered="#{form.erreurInit}">
        <ui:include src="vueErreurs.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="vue2">
      <ui:fragment rendered="#{form.form2Rendered}">
        <ui:include src="vue2.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="vue3">
      <ui:fragment rendered="#{form.form3Rendered}">
        <ui:include src="vue3.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="vueErreurs">
      <ui:fragment rendered="#{form.erreurRendered}">
        <ui:include src="vueErreurs.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="config">
      <ui:include src="config.xhtml"/>
    </pm:view>
  </pm:page>    
</f:view>
  • linha 8: a página está internacionalizada (atributo locale),
  • linha 10: a página contém cinco vistas: view1 na linha 11, view2 na linha 19, view3 na linha 24, viewErrors na linha 29 e config na linha 34. Em qualquer momento, apenas uma destas vistas está visível. Quando a aplicação é iniciada, a view1 é apresentada. Aqui, deparámo-nos com o seguinte problema: se a aplicação for inicializada com sucesso, devemos apresentar [view1.xhtml]; caso contrário, deve ser apresentado [errorView.xhtml]. Resolvemos o problema garantindo que o conteúdo da vista vue1 é gerido pelo modelo, que define os valores dos booleanos [Form].form1rendered (linha 12) e [Form].erreurInit (linha 15) para determinar o conteúdo de vue1 (linha 11),

Num simulador, a vista [vue1.xhtml] é renderizada como [1], a vista [vue2.xhtml] é renderizada como [2] e a vista [vue3.xhtml] é renderizada como [3]:

a vista [vueErreurs.xhtml] tem a renderização [4], a vista [config.xhtml] tem a renderização [5]:

9.7. Os beans do projeto

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

9.7.1. O bean Application

O bean [Application.java] é um bean com âmbito de aplicação. Recorde-se que este tipo de bean é utilizado para armazenar dados de leitura apenas, 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 {
 
  // business layer
  @EJB
  private IMetierLocal metier;
 
  public Application() {
  }
  
  // getters
 
  public IMetierLocal getMetier() {
    return metier;
  }
  
}
  • linha 8: atribuímos ao bean o nome "application",
  • linha 9: ele tem âmbito de aplicação,
  • linhas 13–14: uma referência à interface local da camada [de negócios] será injetada nele pelo contentor EJB do servidor de aplicações. Vamos rever a arquitetura da aplicação:

A aplicação PFM e o EJB [Business] serão executados na mesma JVM (Java Virtual Machine). Portanto, a camada [PFM] utilizará a interface local do EJB. É tudo. O bean [Application] não contém mais nada. Para aceder à camada [Business], os outros beans irão obtê-la a partir deste bean.

9.7.2. O bean [Error]

A classe [Error] é a seguinte:


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

9.7.3. O bean [Form]

O seu código é o seguinte:


package beans;
 
...
 
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
 
  public Form() {
  }
  // bean Application
  @Inject
  private Application application;
  private IMetierLocal metier;
  // session cache
  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>();
  // model
  private Long idMedecin;
  private Date jour = new Date();
  private String strJour;
  private Boolean form1Rendered;
  private Boolean form2Rendered;
  private Boolean form3Rendered;
  private Boolean erreurRendered;
  private String form2Titre;
  private String form3Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Long idCreneauChoisi;
  private Medecin medecin;
  private Long idClient;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;
  private Boolean erreurInit = false;
  private String action;
  private String locale = "fr";
  private String msgErreurDate = "";
  private SimpleDateFormat dateFormatter;
  private Boolean erreurDate;
  
  @PostConstruct
  private void init() {
    System.out.println("init");
    // initially no error
    erreurInit = false;
    // date formatting
    dateFormatter = new SimpleDateFormat(Messages.getMessage(null, "format.date", null).getSummary());
    dateFormatter.setLenient(false);
    // the current day
    strJour = dateFormatter.format(jour);
    // recover the business layer
    metier = application.getMetier();
    // caching doctors and customers
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // we note the error
      erreurInit = true;
      prepareVueErreur(th);
      return;
    }
    // list checking
    if (medecins.size() == 0) {
      // we note the error
      erreurInit = true;
      erreurs = new ArrayList<Erreur>();
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // we note the error
      erreurInit = true;
      erreurs = new ArrayList<Erreur>();
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // mistake?
    if (erreurInit) {
      // the error view is displayed
      setForms(false, false, false, true);
      return;
    }
 
    // dictionaries
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
    // view 1
    setForms(true, false, false, false);
  }
 
  // view display
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.form3Rendered = form3Rendered;
    this.erreurRendered = erreurRendered;
  }
 
  // preparation vueErreur
  private void prepareVueErreur(Throwable th) {
    // create an error list
    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()));
    }
// the error view is displayed
    setForms(false, false, false, true);
  }
 
  // getters and setters
  ..
}
  • linhas 5-7: a classe [Form] é um bean chamado «form» com âmbito de sessão. Note-se que a classe deve, portanto, ser serializável,
  • linhas 12–13: 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 24–27: controlam a exibição das vistas vue1 (linha 24), vue2 (linha 25), vue3 (linha 26) e vueErrors (linha 27),
  • linhas 43–44: O método init é executado imediatamente após a instância da classe (presença da anotação @PostConstruct),
  • linhas 49-50: tratam do formato da data. O PrimeFaces Mobile não fornece um calendário. Além disso, os validadores JSF não podem ser utilizados numa página PFM. Por conseguinte, teremos de tratar manualmente a introdução da data do calendário,
  • linha 49: define o formato da data. Este formato é retirado dos ficheiros de internacionalização:

format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
  • Linha 50: Especificamos que não devemos ser «lenientes» em relação às datas. Se não o fizermos, uma entrada como 32/12/2011 — que é uma entrada incorreta — é considerada a data válida 01/01/2012,
  • linha 54: recuperamos uma referência à camada [business] a partir do bean [Application],
  • linhas 57-58: solicitamos a lista de médicos e clientes da camada [business],
  • linhas 66–91: se tudo correu bem, os dicionários de médicos e clientes são construídos. São indexados pelo seu número. Em seguida, a vista [vue1.xhtml] será exibida (linha 93),
  • linha 59: em caso de erro, o modelo de página [errorView.xhtml] é construído. Este modelo é a lista de erros da linha 35,
  • linhas 105–115: O método [prepareVueErreur] constrói a lista de erros a ser exibida. A página [index.xhtml] exibe então a vista [vueErreurs.xhtml] (linha 114).

A página [error.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">
 
  <!-- Errors view -->
  <pm:header title="#{msg['page.titre']}" swatch="b">
    <f:facet name="left">
      <p:button icon="home" value=" " href="#vue1?reverse=true" />
    </f:facet>
  </pm:header>
  <pm:content>
    <div align="center">
      <h1><h:outputText value="#{msg['erreur.titre']}" style="color: blue"/></h1>
    </div>
 
    <p:dataList value="#{form.erreurs}" var="erreur">
      <b>#{erreur.classe}</b> : <i>#{erreur.message}</i>
    </p:dataList>
  </pm:content>
</html>

Utiliza uma tag <p:dataList> (linhas 21–23) para apresentar a lista de erros. O botão na linha 13 permite-lhe regressar à vista vue1.

  • O botão [1] é gerado pela linha 13. O atributo icon define o ícone do botão. O botão volta à vista vue1 (atributo href),
  • o cabeçalho [2] é gerado pelas linhas 11–15,
  • o título [3] é gerado pela linha 18,
  • o texto [4] é gerado pelo modelo #{error.class} na linha 22,
  • o texto [5] é gerado pelo modelo #{erreur.message} na linha 22.

Vamos agora definir as diferentes fases do ciclo de vida da aplicação. Para cada ação do utilizador, iremos examinar as vistas relevantes e os manipuladores de eventos que ocorrem dentro delas.

9.8. Exibição da página inicial

Se tudo correr bem, a primeira vista apresentada é [vue1.xhtml]. Isto resulta na seguinte vista:

O código para a vista [vue1.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">
 
  <!-- View 1 -->
  <pm:header title="#{msg['page.titre']}" swatch="b">
    <f:facet name="left">
      <p:button icon="gear" value=" "  href="#config" />
    </f:facet>
  </pm:header>
  <pm:content>
    <h:form id="form1">
      <div align="center">
        <h1><h:outputText value="#{msg['form1.titre']}" style="color: blue"/></h1>
      </div>
      <pm:field>
        <h:outputLabel value="#{msg['form1.medecin']}" for="choixMedecin"/>
        <h:selectOneMenu id="choixMedecin" value="#{form.idMedecin}">  
          <f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>  
        </h:selectOneMenu>              
      </pm:field>
      <pm:field>
        <h:outputLabel value="#{msg['form1.jour']}" for="jour"/>
        <p:inputText id="jour" value="#{form.strJour}"/>
        <ui:fragment rendered="#{form.erreurDate}">
          <p:spacer width="50px"/>
          <h:outputText id="msgErreurDate" value="#{form.msgErreurDate}" style="color: red"/>
        </ui:fragment>
      </pm:field>
      <p:commandButton value="#{msg['form1.agenda']}" update=":form1, :vue2, :vueErreurs" action="#{form.getAgenda}" />
    </h:form>
  </pm:content>
</html>
  • linhas 11–15: geram o cabeçalho [1],
  • Linha 13: cria o botão [2]. Ao clicar nele, é apresentada a vista de configuração (atributo href),
  • linha 19: exibe o título [3],
  • Linhas 21–26: geram o menu suspenso do médico [4],
  • linhas 27–34: exibem o campo de entrada de data [5]. Este campo de entrada utiliza uma tag <p:inputText> sem validador. A validação da data será realizada no lado do servidor. Se a data estiver incorreta, o servidor definirá uma mensagem de erro exibida pelas linhas 30–34,
  • linha 35: o botão que envia o formulário. Ele atualiza três áreas: form1 (o formulário em vue1), vue2 e vueErrors. De facto, se a data for inválida, form1 deve ser atualizado. Se a data estiver correta, vue2 deve ser atualizado. Por fim, se ocorrer uma exceção (como uma ligação à base de dados interrompida), vueErrors deve ser exibido. Poderá sentir-se tentado a usar vue1 em vez de form1 (atualizando toda a vista). Nesse caso, a aplicação irá falhar.

Esta vista é suportada pelo seguinte modelo:


@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
 
  public Form() {
  }
 
  // bean Application
  @Inject
  private Application application;
  private IMetierLocal metier;
  // session cache
  private List<Medecin> medecins;
  private List<Client> clients;
  // model
  private Long idMedecin;
  private Date jour = new Date();
  private String strJour;
  private Boolean form1Rendered;
  private Boolean form2Rendered;
  private Boolean form3Rendered;
  private Boolean erreurRendered;
  private String msgErreurDate = "";
  private Boolean erreurDate;
  
    // list of doctors
  public List<Medecin> getMedecins() {
    return medecins;
  }
 
  // agenda
  public String getAgenda() {
...
    }
  }
  • O campo na linha 16 lê e grava o valor da lista na linha 23 da página. Quando a página é exibida pela primeira vez, define o valor selecionado na caixa combinada,
  • O método nas linhas 27–29 gera os itens para a lista suspensa de médicos (linha 24 da vista). Cada opção gerada terá como rótulo (itemLabel) o título, o apelido e o nome do médico e, como valor (itemValue), o ID do médico.
  • O campo na linha 18 fornece acesso de leitura/gravação ao campo de entrada na linha 29 da página,
  • Linhas 32–34: O método getAgenda trata do clique no botão [Agenda] na linha 35 da página. O seu código é o seguinte:

// agenda
  public String getAgenda() {
    try {
      // on vérifie le jour
      jour = dateFormatter.parse(strJour);
      // pas d'erreur
      erreurDate=false;
      msgErreurDate = "";
      // on crée l'agenda
      return getAgenda(jour);
    } catch (ParseException ex) {
      // msg d'erreur
      erreurDate=true;
      msgErreurDate = Messages.getMessage(null, "form1.date.invalide", null).getSummary();
      // vue1      
      setForms(true, false, false, false);
      return "pm:vue1";
    }
  }
  • O método começa por verificar a validade da data introduzida pelo utilizador,
  • linha 5: a data é analisada de acordo com o formato de data inicializado pelo método init quando o modelo é instanciado,
  • linha 11: se ocorrer uma exceção, é definido um erro (linha 13), é construída uma mensagem de erro internacionalizada (linha 14), a vista vue1 é preparada (linha 16) e a vista vue1 é exibida (linha 17),
  • linha 10: se a data for válida, o método seguinte é executado:

  // agenda
  public String getAgenda(Date jour) {
    // no slots selected yet
    creneauChoisi = null;
    try {
      // we pick up the chosen doctor
      medecin = hMedecins.get(idMedecin);
      // title form 2
      form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
      // the doctor's diary for a given day
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // view 2 is displayed
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      System.out.println(th);
      // error view
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
}

Aqui vemos código que já apareceu várias vezes anteriormente. Na linha 9, é construída uma mensagem internacionalizada para a vista vue2:


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

Note que incluímos XHTML na mensagem. Será apresentado da seguinte forma:

9.9. Exibir a agenda de um médico

A agenda do médico é apresentada pela vista [vue2.xhtml]:

O código da vista [vue2.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">
 
  <!-- View 2 -->
  <pm:header title="#{msg['page.titre']}" swatch="b"/>
  <pm:content>
    <h:form id="form2">
      <div align="center">
        <pm:buttonGroup orientation="horizontal">
          <p:commandButton inline="true" icon="back" value=" " action="#{form.showVue1}" update=":vue1"/>
          <p:commandButton inline="true" icon="minus" value=" " action="#{form.getPreviousAgenda}" update=":form2"/>
          <p:commandButton inline="true" icon="home" value=" " action="#{form.getTodayAgenda}" update=":form2"/>
          <p:commandButton inline="true" icon="plus" value=" " action="#{form.getNextAgenda}" update=":form2"/>
        </pm:buttonGroup>
        <h3><h:outputText value="#{form.form2Titre}" style="color: blue" escape="false"/></h3>
      </div>
 
      <p:dataList id="creneaux" type="inset" value="#{form.agendaMedecinJour.creneauxMedecinJour}" var="creneauMedecinJour">
        <p:column>
          <div align="center">
            <h2>
              <h:outputFormat value="{0,number,#00}h:{1,number,#00} - {2,number,#00}h:{3,number,#00}">
                <f:param value="#{creneauMedecinJour.creneau.hdebut}" />
                <f:param value="#{creneauMedecinJour.creneau.mdebut}" />
                <f:param value="#{creneauMedecinJour.creneau.hfin}" />
                <f:param value="#{creneauMedecinJour.creneau.mfin}" />
              </h:outputFormat>
              <ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
                <br/>
                <h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}" style="color: blue"/>
              </ui:fragment>
            </h2>
          </div>
          <div align="center">
            <ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.supprimer']}" icon="minus" update=":form2, :vue3, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
            <ui:fragment rendered="#{creneauMedecinJour.rv==null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.reserver']}" icon="plus" update=":form2, :vue3, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
          </div>
        </p:column>
      </p:dataList>
    </h:form>
  </pm:content>
</html>
  • linha 11: gera o cabeçalho [1],
  • linhas 15–20: gera o grupo de botões [2],
  • linha 21: gera o título [3]. Repare no valor do atributo escape. É isto que permite que o código XHTML que colocámos em form2Titre seja interpretado,
  • linha 24: os intervalos de tempo são apresentados utilizando um dataList,
  • linhas 28–33: geram o rótulo do intervalo de tempo [4],
  • linhas 34–37: exibem um trecho se houver um compromisso no intervalo de tempo,
  • linha 36: exibe o nome do cliente que marcou o compromisso,
  • linhas 41–45: exibem o botão [Apagar] se houver um compromisso,
  • Linhas 46–50: exibem o botão [Marcar] se não houver compromissos.

Esta vista é preenchida principalmente pelo seguinte modelo:


private AgendaMedecinJour agendaMedecinJour;

que preenche a dataList na linha 24. Este campo foi criado pelo método getAgenda ao mudar da vista1 para a vista2.

9.10. Eliminar um compromisso

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

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


<ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.supprimer']}" icon="minus" update=":form2, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
  • Linha 2: O botão [Delete] está associado ao método [Form].action (atributo action),
  • linha 3: o ID do intervalo atualmente selecionado será enviado para o modelo [Form].idCreneauChoisi,
  • linha 2: a chamada AJAX atualizará os campos do form2 (formulário view2) e a vista vueErreurs. Existem dois cenários possíveis: se tudo correr bem, a vista vue2 será exibida novamente; caso contrário, a vista vueErreurs será exibida.

O método [action] é o seguinte:


  // action on RV
  public String action() {
    // search for the time slot in the calendar
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneauChoisi) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // have we found?
    if (!trouvé) {
      // it's weird - form2 is redisplayed
      setForms(false, true, false, false);
      return "pm:vue2";
    } else {
      creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    }
    // we found
    // according to desired action
    if (creneauChoisi.getRv() == null) {
      return reserver();
    } else {
      return supprimer();
    }
  }
 
  // reservation
  public String reserver() {
 ...
  }
 
  public String supprimer() {
    try {
      // deleting an appointment
      metier.supprimerRv(creneauChoisi.getRv());
      // updating the agenda
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // form2 is displayed
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
}
  • linhas 3-12: procuramos o intervalo de tempo cujo ID recebemos (linha 7);
  • se não for encontrado, o que é anormal, voltamos a apresentar a view2 (linhas 16-17),
  • linha 19: se for encontrado, o objeto [CreneauMedecinJour] correspondente é armazenado. Este objeto dá-nos acesso à consulta a ser eliminada,
  • linha 26: eliminamo-lo,
  • linhas 35–48: o método delete devolve a vista vue2 se a eliminação tiver sido bem-sucedida (linhas 42–43) ou a vista vueErrors se tiver havido um problema (linhas 46–47).

9.11. Marcar uma consulta

A marcação de uma consulta segue esta sequência:

Navegamos da vista vue2 para a vista vue3. O código envolvido nesta ação é o seguinte:


<ui:fragment rendered="#{creneauMedecinJour.rv==null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.reserver']}" icon="plus" update=":vue3, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
  • Linha 2: O botão [Book] está associado ao método [Form].action (atributo action), pelo que é idêntico ao botão [Delete]. A chamada AJAX atualiza as vistas vue3 e vueErrors, dependendo da existência ou não de erros durante o processamento da chamada.
  • linha 3: Tal como no botão [Delete], o ID do intervalo de tempo é passado para o modelo.

O modelo que lida com esta ação é o seguinte:


// action on RV
  public String action() {
    ...
    // according to desired action
    if (creneauChoisi.getRv() == null) {
      return reserver();
    } else {
      return supprimer();
    }
  }
 
  // reservation
  public String reserver() {
    try {
      // title form 3
      form3Titre = Messages.getMessage(null, "form3.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour),
                creneauChoisi.getCreneau().getHdebut(), creneauChoisi.getCreneau().getMdebut(), creneauChoisi.getCreneau().getHfin(), creneauChoisi.getCreneau().getMfin()}).getSummary();
      // form 3 is displayed
      setForms(false, false, true, false);
      return "pm:vue3";
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
  }
  • linhas 2–10: O método de ação recupera a referência creneauChoisi do objeto [CreneauMedecinJour] que está a ser reservado e, em seguida, chama o método reserver,
  • linha 16: É construída uma mensagem internacionalizada. É a seguinte:

form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}

Este será o título da vista vue3. Tal como na vista vue2, este título contém XML. Também inclui parâmetros formatados para apresentar os intervalos de tempo,

  • linhas 19-20: a vista vue3 é exibida,
  • linhas 23-24: a vista vueErrors é exibida caso tenham sido encontrados problemas.

9.12. Confirmação de uma marcação

A validação de um compromisso segue esta sequência:

Em [1], a vista do vue3, e em [2], a vista do vue2 após adicionar um compromisso.

O código [vue3.xhtml] para o vue3 é 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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">
 
  <!-- View 3 -->
  <pm:header title="#{msg['page.titre']}" swatch="b"/>
  <pm:content>
    <h:form id="form3">
      <p:commandButton inline="true" value=" " icon="back" action="#{form.showVue2}" update=":vue2"/>
      <div align="center">
        <h3><h:outputText value="#{form.form3Titre}" style="color: blue" escape="false"/></h3>
      </div>
      <pm:field>
        <h:outputLabel value="#{msg['form3.client']}" for="choixClient"/>
        <h:selectOneMenu id="choixClient" value="#{form.idClient}">
          <f:selectItems value="#{form.clients}" var="client" itemLabel="#{client.titre} #{client.prenom} #{client.nom}" itemValue="#{client.id}"/>
        </h:selectOneMenu>
      </pm:field>
      <div align="center">
        <p:commandButton inline="true" value="#{msg['form3.valider']}" action="#{form.validerRv}" update=":vue2, :vueErreurs" icon="check"/>
      </div>
    </h:form>
  </pm:content>
</html>
  • linha 16: gera o título da visualização [3]. Observe o valor do atributo escape, que permite que os caracteres XHTML sejam interpretados no título,
  • linhas 18–23: geram a lista suspensa do cliente [4],
  • linha 25: gera o botão [Validate] [5]. O método [Form].validateRv está associado a este botão:

// rv validation
  public String validerRv() {
    try {
      // retrieve an instance of the chosen slot
      Creneau creneau = metier.getCreneauById(idCreneauChoisi);
      // we add the Rv
      metier.ajouterRv(jour, creneau, hClients.get(idClient));
      // updating the agenda
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // form2 is displayed
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
  }

Este código já foi encontrado na versão 01. Observe simplesmente a exibição das vistas:

  • a vista vue2 (linhas 11–12) se tudo correu bem,
  • a vista vueErreurs (linhas 15–16) caso contrário.

9.13. Cancelar um compromisso

Isto corresponde à seguinte sequência:

O botão [1] na vista [vue3.xhtml] é o seguinte:


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

O método [Form].showVue2 é, portanto, chamado. Ele simplesmente exibe a vue2:


  public String showVue2() {
    // vue2
    setForms(false, true, false, false);
    return "pm:vue2?reverse=true";
}

9.14. Navegar no calendário

No view2, os botões permitem-lhe navegar no calendário:

Dia anterior:

Dia seguinte:

Hoje:

Embora não apareça nas imagens acima, o calendário é atualizado e apresenta os compromissos do dia recém-selecionado.

As tags para os três botões em questão são as seguintes em [vue2.xhtml]:


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

Os métodos [Form].getPreviousAgenda, [Form].getNextAgenda e [Form].today foram abordados no Exemplo 03.

9.15. Alterar o idioma de exibição

O idioma é alterado através de um botão na página inicial:

O código do botão é o seguinte:


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

Por conseguinte, navega para a vista de configuração [2]. A vista [config.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">
 
  <!-- View 1 -->
  <pm:header title="#{msg['page.titre']}" swatch="b">
    <f:facet name="left">
      <p:button icon="back" value=" " href="#vue1?reverse=true" />
    </f:facet>
  </pm:header>
  <pm:content>
    <h:form id="frmConfig">
      <div align="center">
        <h3><h:outputText value="#{msg['config.titre']}" style="color: blue"/></h3>
      </div>
      <pm:field>
        <h:outputLabel value="#{msg['config.langue']}" for="langue"/>
        <h:selectOneRadio id="langue" value="#{form.locale}">
          <f:selectItem itemLabel="#{msg['config.langue.francais']}" itemValue="fr"/>
          <f:selectItem itemLabel="#{msg['config.langue.anglais']}" itemValue="en" />
        </h:selectOneRadio>
      </pm:field>
      <p:commandButton value="#{msg['config.valider']}" action="#{form.configurer}" update=":vue1"/>
    </h:form>
  </pm:content>
</html>
  • linha 11: exibe [3],
  • linha 13: exibe o botão [4]. Este botão permite-lhe regressar à vista vue1,
  • linha 17: o formulário da vista,
  • linha 19: exibe o título da vista [5],
  • linhas 21–27: exibem os botões de opção. O valor (itemValue) do botão de opção selecionado será enviado para o modelo [Form].locale (atributo value na linha 23),
  • linha 28: apresenta o botão [Submit]. A chamada AJAX atualiza a vista vue1 (atributo update). O método chamado é [Form].configure:

public String configurer(){
    // after configuration - redisplay view1
    redirect();
    return null;
  }
  
  private void redirect() {
    // redirect the client to the servlet
    ExternalContext ctx = FacesContext.getCurrentInstance().getExternalContext();
    try {
      ctx.redirect(ctx.getRequestContextPath());
    } catch (IOException ex) {
      Logger.getLogger(Form.class.getName()).log(Level.SEVERE, null, ex);
    }
  }

O método configure (linha 1) simplesmente redireciona o navegador móvel para o URL da aplicação. Portanto, a página [index.xhtml] será carregada:


<f:view xmlns="http://www.w3.org/1999/xhtml"
        xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html"
        xmlns:ui="http://java.sun.com/jsf/facelets"
        xmlns:p="http://primefaces.org/ui"
        xmlns:pm="http://primefaces.org/mobile"
        contentType="text/html"
        locale="#{form.locale}">
 
  <pm:page title="#{msg['page.titre']}">
    <pm:view id="vue1">
      ...
    </pm:view>
    ...
  </pm:page>    
</f:view>
  • Linha 8: A vista utilizará o idioma que acabou de ser alterado (atributo locale) e exibirá a vista1 (linha 11).

9.16. Conclusão

Vamos rever a arquitetura da aplicação que acabámos de criar:

A transição para uma interface móvel exigiu a reescrita das páginas XHTML. O modelo, por outro lado, sofreu poucas alterações. As camadas inferiores [business], [DAO] e [JPA] não sofreram qualquer alteração.

9.17. Testes no Eclipse

Tal como fizemos nas versões anteriores da aplicação de exemplo, vamos mostrar como testar esta versão 05 com o Eclipse. Primeiro, importamos os projetos Maven do Exemplo 05 [1] para o Eclipse:

  • [mv-rdvmedecins-ejb-dao-jpa]: as camadas [DAO] e [JPA],
  • [mv-rdvmedecins-ejb-metier]: a camada [business],
  • [mv-rdvmedecins-pfmobile]: a camada [web] implementada pelo PrimeFaces Mobile,
  • [mv-rdvmedecins-pfmobile-app]: o projeto pai do projeto empresarial [mv-rdvmedecins-pfmobile-app-ear]. Quando o projeto pai é importado, o projeto filho é importado automaticamente,
  • em [2], execute o projeto empresarial [mv-rdvmedecins-pfmobile-app-ear],
  • em [3], selecione o servidor Glassfish,
  • em [4], no separador [Servers], a aplicação foi implementada. Não é executada automaticamente. Deve introduzir o seu URL [http://localhost:8080/mv-rdvmedecins-pfmobile/] num navegador ou simulador móvel [5]: