Skip to content

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

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

Não alteramos nada nesta arquitetura, exceto a camada web, que será aqui implementada com a ajuda do JSF, do Primefaces e do Primefaces Mobile. O navegador de destino será o de um telemóvel.

Desenvolvemos duas aplicações com o Glassfish:

  • a aplicação 01 utilizava o JSF / EJB e tinha uma interface bastante simples,
  • a aplicação 03 utilizava o PF / EJB e apresentava uma interface rica.

Devido ao reduzido número de componentes disponíveis no PrimeFaces Mobile, vamos voltar à interface simples da aplicação 01. Além disso, as vistas terão de se adaptar ao tamanho reduzido dos ecrãs dos dispositivos móveis.

9.1. As vistas

Para dar uma ideia, apresentamos algumas vistas da aplicação executada no simulador de um iPhone 4:

  • em [1], a página inicial. Note-se que, desta vez, foi necessário indicar o nome da máquina (também é possível indicar o seu endereço IP), porque com a máquina localhost não se exibia nada,
  • em [2], a lista de médicos,
  • em [3], a data pretendida para a consulta,
  • em [4], o botão para consultar a agenda do dia,
  • em [5], a nova vista 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 para recordar o médico e o dia,
  • em [8], um horário disponível para marcar. Fazemo-lo,
  • em [9], a vista de escolha do cliente,
  • em [10], uma mensagem a recordar o médico, o dia e o intervalo horário em que a consulta se realiza,
  • em [11], a lista de clientes,
  • em [12], o botão de confirmação,
  • em [13], a validação leva-nos de volta à agenda,
  • em [14], o intervalo horário ocupado está agora reservado. Vamos agora eliminar a reserva,
  • em [15], permanecemos na mesma vista,
  • mas em [16], o compromisso foi eliminado,

É possível, na página inicial, alterar o idioma: [17], [18], [19]:

Por fim, foi prevista uma visualização de erros:

9.2. O projeto NetBeans

O projeto NetBeans é o seguinte:

  
  • [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-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 destina-se exclusivamente à implementação dos três módulos [mv-rdvmedecins-ejb-dao-jpa], [mv-rdvmedecins-ejb-metier] e [mv-rdvmedecins-pfmobile] 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>
...

  <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 do projeto empresarial,
  • linhas 14-33: as três dependências do projeto. Deve-se prestar atenção ao tipo destas (linhas 19, 25, 31).

Para executar a aplicação web, será necessário 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. Esta inclui cinco vistas: [vue1.xhtml], [vue2.xhtml], [vue3.xhtml], [vueErreurs.xhtml] e [config.xhtml],
  • em [2], os beans Java. O bean [Application] com âmbito application, o bean [Form] com âmbito session. A classe [Erreur] encapsula um erro,
  • em [3], os ficheiros de mensagens para a internacionalização,
  • em [4], as dependências. O projeto web depende do projeto EJB da camada [DAO], do projeto EJB da camada [métier] e do Primefaces Mobile para a camada [web].

9.5. 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.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-se, na linha 26, 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>
    <default-render-kit-id>PRIMEFACES_MOBILE</default-render-kit-id>
  </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>

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


# página
page.titre=Les M\u00e9decins Associ\u00e9s
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy

# vista1
vue1.header=Les M\u00e9decins Associ\u00e9s - R\u00e9servations

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

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

# formulário 3
form3.client=Client
form3.valider=Valider
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}
form3.retour=Retour

# erro
erreur.titre=Une erreur s'est produite.

# configuração
config.retour=Retour
config.titre=Configuration
config.langue=Langue
config.langue.francais=Fran\u00e7ais
config.langue.anglais=Anglais
config.valider=Valider

#exceção
exception.titre=Application indisponible. Veuillez recommencer ult\u00e9rieurement.

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


# página
page.titre=The Associated Doctors
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy

# vista1
vue1.header=The Associated Doctors - Reservations

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

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

# formulário 3
form3.client=Patient
form3.valider=Validate
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}
form3.retour=Back

# erro
erreur.titre=Some error happened

# configuração
config.retour=Back
config.titre=Configuration
config.langue=Language
config.langue.francais=French
config.langue.anglais=English
config.valider=Validate

#exceção
exception.titre=Application not available. Please try again later.

9.6. A página [index.xhtml]

O projeto continua a apresentar 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: vista1 na linha 11, vista2 na linha 19, vista3 na linha 24, vueErreurs na linha 29, config na linha 34. Em determinado momento, apenas uma destas vistas está visível. Ao iniciar a aplicação, é a vista vista1 que é apresentada. Aqui, deparámo-nos com a seguinte dificuldade: se a inicialização da aplicação decorreu sem problemas, deve ser exibida a vista [vue1.xhtml]; caso contrário, deve ser exibida a vista [vueErreurs.xhtml]. Resolvemos o problema fazendo com que o conteúdo da vista vue1 fosse gerido pelo modelo, que atribui valores às variáveis booleanas [Form].form1rendered (linha 12) e [Form].erreurInit (linha 15) para definir o conteúdo da vista1 (linha 11),

Num simulador, a vista [vue1.xhtml] tem a representação [1], a vista [vue2.xhtml] tem a representação [2], a vista [vue3.xhtml] tem a representação [3]:

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

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

9.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: será-lhe injetada uma referência à interface local da camada [métier] pelo contentor EJB do servidor de aplicações. Recordemos a arquitetura da aplicação:

A aplicação PFM e as aplicações EJB e [Metier] serão executadas na mesma JVM (Java Virtual Machine). Portanto, a camada [PFM] 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.

9.7.2. O bean [Erreur]

A classe [Erreur] é a seguinte:


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.

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 da aplicação
  @Inject
  private Application application;
  private IMetierLocal metier;
  // 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>();
  // modelo
  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");
    // inicialmente sem erros
    erreurInit = false;
    // formatação das datas
    dateFormatter = new SimpleDateFormat(Messages.getMessage(null, "format.date", null).getSummary());
    dateFormatter.setLenient(false);
    // o dia atual
    strJour = dateFormatter.format(jour);
    // recuperamos a camada de negócio
    metier = application.getMetier();
    // armazenamos os médicos e os clientes na cache
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // regista-se o erro
      erreurInit = true;
      prepareVueErreur(th);
      return;
    }
    // verificação das listas
    if (medecins.size() == 0) {
      // regista-se o erro
      erreurInit = true;
      erreurs = new ArrayList<Erreur>();
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // regista-se o erro
      erreurInit = true;
      erreurs = new ArrayList<Erreur>();
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // erro?
    if (erreurInit) {
      // A vista de erros é apresentada
      setForms(false, false, false, true);
      return;
    }

    // os dicionários
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
    // a vista 1
    setForms(true, false, false, false);
  }

  // exibição da vista
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.form3Rendered = form3Rendered;
    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(false, false, false, true);
  }

  // getters e setters
  ..
}
  • linhas 5-7: 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 12-13: o bean «form» tem uma referência ao bean «application». Esta 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 vueErreurs (linha 27),
  • linhas 43-44: o método init é executado logo após a instanciação da classe (presença da anotação @PostConstruct),
  • linhas 49-50: gerem o formato das datas. O PrimeFaces Mobile não disponibiliza um calendário. Além disso, os validadores JSF não podem ser utilizados numa página PFM. Por isso, teremos de gerir manualmente a introdução da data da agenda,
  • linha 49: define o formato das datas. Este formato é definido nos ficheiros internacionalizados:

format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
  • linha 50: indica-se que não se deve utilizar o formato «laxiste» nas datas. Se isso não for feito, uma entrada como 32/12/2011, que é incorreta, é considerada como a data válida 01/01/2012,
  • linha 54: obtém-se uma referência à camada [métier] a partir do bean [Application],
  • linhas 57-58: solicita-se à camada [métier] a lista de médicos e clientes,
  • linhas 66-91: se tudo correr bem, os dicionários de médicos e clientes são criados. São indexados pelo respetivo número. Em seguida, a vista [vue1.xhtml] será apresentada (linha 93),
  • linha 59: em caso de erro, o modelo da página [vueErreurs.xhtml] é criado. Este modelo corresponde à lista de erros da linha 35,
  • linhas 105-115: o método [prepareVueErreur] cria a lista de erros a apresentar. A página [index.xhtml] apresenta então a vista [vueErreurs.xhtml] (linha 114).

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

  <!-- Visão de erros -->
  <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 baliza <p:dataList> (linhas 21-23) para apresentar a lista de erros. O botão na linha 13 permite regressar à vista «vista1».

  • o botão [1] é gerado pela linha 13. O atributo icon define o ícone do botão. O botão remete para a 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 #{erreur.classe} da linha 22,
  • o texto [5] é gerado pelo modelo #{erreur.message} da linha 22.

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

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

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

O código da 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">

  <!-- Vista 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: gera o botão [2]. Ao clicar neste botão, é apresentada a vista de configuração (atributo href),
  • linha 19: gera o título [3],
  • linhas 21-26: geram a lista suspensa de médicos [4],
  • linhas 27-34: geram o campo de introdução da data [5]. Esta introdução é feita com uma baliza <p:inputText> sem validador. A validação da data será feita no servidor. Se a data estiver incorreta, o servidor exibirá uma mensagem de erro através das linhas 30-34,
  • linha 35: o botão que valida o formulário. Este atualiza três áreas: form1 (o formulário na vista1), vista2 e vueErreurs. Com efeito, se a data for inválida, é o form1 que deve ser atualizado. Se a data estiver correta, é a vista2 que deve ser atualizada. Por fim, se ocorrer uma exceção (por exemplo, falha na ligação à base de dados), é a vueErreurs que deve ser apresentada. Pode ser tentador substituir form1 por vue1 (atualizando toda a vista). Nesse caso, a aplicação apresenta um erro.

Esta vista está associada ao seguinte modelo:


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

  public Form() {
  }

  // Bean da aplicação
  @Inject
  private Application application;
  private IMetierLocal metier;
  // cache da sessão
  private List<Medecin> medecins;
  private List<Client> clients;
  // modelo
  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;
  
    // lista de médicos
  public List<Medecin> getMedecins() {
    return medecins;
  }

  // agenda
  public String getAgenda() {
...
    }
  }
  • o campo da linha 16 alimenta, em leitura e escrita, o valor da lista da linha 23 da página. Na exibição inicial da página, define o valor selecionado no combo,
  • o método das linhas 27-29 gera os elementos da lista suspensa dos médicos (linha 24 da vista). 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 18 alimenta, em leitura/gravação, o campo de introdução de dados da linha 29 da página,
  • linhas 32-34: o método getAgenda gere o clique no botão [Agenda] da linha 35 da página. O seu código é o seguinte:

// agenda
  public String getAgenda() {
    try {
      // verificar o dia
      jour = dateFormatter.parse(strJour);
      // sem erros
      erreurDate=false;
      msgErreurDate = "";
      // criar a agenda
      return getAgenda(jour);
    } catch (ParseException ex) {
      // mensagem de erro
      erreurDate=true;
      msgErreurDate = Messages.getMessage(null, "form1.date.invalide", null).getSummary();
      // vista 1      
      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 é parsée, de acordo com o formato de data inicializado pelo método init aquando da instanciação do modelo,
  • linha 11: se ocorrer uma exceção, é gerado um erro (linha 13), é criada uma mensagem de erro internacionalizada (linha 14), a vista vue1 é preparada (linha 16) e a vista vue1 é apresentada (linha 17),
  • linha 10: se a data estiver correta, executa-se o seguinte método:

  // agenda
  public String getAgenda(Date jour) {
    // nenhum horário selecionado por enquanto
    creneauChoisi = null;
    try {
      // recuperar o médico selecionado
      medecin = hMedecins.get(idMedecin);
      // título do formulário 2
      form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
      // a agenda do médico para um determinado dia
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // exibe-se a vista 2
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      System.out.println(th);
      // visualização dos erros
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
}

Encontramos aqui um código que já apareceu várias vezes. Linha 9, é criada uma mensagem internacionalizada para a vista vue2:


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

Note-se que incluímos o código XHTML na mensagem. Este 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">

  <!-- Vista 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]. Deve-se ter em conta o valor do atributo «escape». É isso que permite interpretar o código XHTML que colocámos em form2Titre,
  • linha 24: a exibição dos intervalos horários é feita com a ajuda de um dataList,
  • linhas 28-33: geram o título do intervalo horário [4],
  • linhas 34-37: exibem um fragmento se houver um compromisso nesse intervalo horário,
  • linha 36: apresenta a identidade do cliente que marcou o compromisso,
  • linhas 41-45: exibem o botão [Supprimer] se houver um compromisso,
  • linhas 46-50: exibem o botão [Réserver] se não houver qualquer compromisso.

Esta vista é alimentada principalmente pelo seguinte modelo:


private AgendaMedecinJour agendaMedecinJour;

que alimenta o dataList da linha 24. Este campo foi criado pelo método getAgenda, quando se passou da vista «vista1» para a vista «vista2».

9.10. Eliminação de um compromisso

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

A vista em causa 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 [Supprimer] está associado ao método [Form].action (atributo «action»),
  • linha 3: o ID do intervalo em que se encontra posicionado será enviado para o modelo [Form].idCreneauChoisi,
  • linha 2: a chamada AJAX atualizará as zonas form2 (formulário da vista 2) e a vista vueErreurs. Existem, de facto, dois casos: se tudo correr bem, a vista «vista2» será novamente apresentada; caso contrário, será apresentada a vista vueErreurs.

O método [action] é o seguinte:


  // ação em RV
  public String action() {
    // procura-se o horário disponível na agenda
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneauChoisi) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // Encontrou-se alguma?
    if (!trouvé) {
      // É estranho — voltamos a apresentar o form2
      setForms(false, true, false, false);
      return "pm:vue2";
    } else {
      creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    }
    // Encontrámos
    // de acordo com a ação pretendida
    if (creneauChoisi.getRv() == null) {
      return reserver();
    } else {
      return supprimer();
    }
  }

  // reserva
  public String reserver() {
 ...
  }

  public String supprimer() {
    try {
      // eliminação de um compromisso
      metier.supprimerRv(creneauChoisi.getRv());
      // atualiza-se a agenda
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // exibe o form2
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      // visualização de erros
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
}
  • linhas 3-12: procura-se o intervalo horário cujo ID foi recebido (linha 7),
  • se não o encontrarmos, o que é anormal, voltamos a apresentar a vista2 (linhas 16-17),
  • linha 19: se o encontrarmos, memorizamos o objeto [CreneauMedecinJour] correspondente. É este que nos dá acesso ao compromisso a eliminar,
  • linha 26: elimina-se o compromisso,
  • linhas 35-48: o método «suprimir» devolve a vista vue2 se a supressão tiver ocorrido com sucesso (linhas 42-43) ou a vista vueErreurs se tiver havido algum problema (linhas 46-47).

9.11. Marcação de compromissos

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

Passa-se da vista vue2 para a vista vue3. O código associado a esta 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 [Réserver] está associado ao método [Form].action (atributo action), ou seja, o mesmo que para o botão [Supprimer]. A chamada AJAX atualiza as vistas vue3 e vueErreurs, dependendo da existência ou não de erros durante o processamento da chamada.
  • linha 3: tal como no botão [Supprimer], o ID do intervalo horário é passado para o modelo.

O modelo que processa esta ação é o seguinte:


// ação em RV
  public String action() {
    ...
    // conforme a ação pretendida
    if (creneauChoisi.getRv() == null) {
      return reserver();
    } else {
      return supprimer();
    }
  }

  // reserva
  public String reserver() {
    try {
      // título do formulário 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();
      // é apresentado o formulário 3
      setForms(false, false, true, false);
      return "pm:vue3";
    } catch (Throwable th) {
      // visualização de erros
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
  }
  • linhas 2-10: o método action irá determinar a referência creneauChoisi do objeto [CreneauMedecinJour] que é objeto de uma reserva e, em seguida, chamar o método reserver,
  • linha 16: é criada uma mensagem internacionalizada. É a seguinte:
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,número,#00}-{6,número,#00}h:{7,número,#00}

Este será o título da vista «vue3». Tal como na vista «vue2», este título inclui o código XML. Inclui também parâmetros formatados para apresentar os horários do intervalo,

  • linhas 19-20: a vista vue3 é apresentada,
  • linhas 23-24: a vista vueErreurs é apresentada caso tenham ocorrido problemas.

9.12. Validação de um compromisso

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

Em [1], a vista vue3 e, em [2], a vista vue2 após a adição de um compromisso.

O código [vue3.xhtml] da vista3 é 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">

  <!-- Vista 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 vista [3]. De notar o valor do atributo escape, que permite a interpretação dos caracteres XHTML no título,
  • linhas 18-23: geram a lista suspensa de clientes [4],
  • linha 25: gera o botão [Valider] [5]. O método [Form].validerRv está associado a este botão:

// validação da marcação
  public String validerRv() {
    try {
      // recuperamos uma instância do intervalo de tempo selecionado
      Creneau creneau = metier.getCreneauById(idCreneauChoisi);
      // adiciona-se a Rv
      metier.ajouterRv(jour, creneau, hClients.get(idClient));
      // atualiza-se a agenda
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // exibe-se o form2
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      // visualização de erros
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
  }

Este código já foi encontrado na versão 01. Basta observar a apresentação das vistas:

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

9.13. Cancelamento de uma marcação

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. Este limita-se a apresentar a vista2:


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

9.14. Navegação no calendário

Na vista2, existem botões que permitem navegar no calendário:

Dia anterior:

Dia seguinte:

Hoje:

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

As etiquetas dos 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, [Form].today foram analisados no exemplo 03.

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

A alteração do idioma é gerida por um botão na página inicial:

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


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

Por isso, passa 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">

  <!-- Vista 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 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» da linha 23),
  • linha 28: exibe o botão [Valider]. A chamada AJAX atualiza a vista vue1 (atributo update). O método chamado é [Form].configurer:

public String configurer(){
    // após a configuração - volta a apresentar a vista 1
    redirect();
    return null;
  }
  
  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);
    }
  }

O método «configurar» (linha 1) limita-se a redirecionar o navegador do telemóvel para o URL da aplicação. Assim, será a página [index.xhtml] que 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 irá utilizar o idioma que acabou de ser alterado (atributo «locale») e irá apresentar a vista1 (linha 11).

9.16. Conclusion

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

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 [métier], [DAO] e [JPA], por sua vez, não sofreram qualquer alteração.

9.17. Testes no Eclipse

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

  • [mv-rdvmedecins-ejb-dao-jpa]: as camadas [DAO] e [JPA],
  • [mv-rdvmedecins-ejb-metier]: a camada [métier],
  • [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]. Ao importar o projeto pai, o projeto filho é automaticamente importado,
  • em [2], executa-se o projeto empresarial [mv-rdvmedecins-pfmobile-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-pfmobile/] num navegador ou num simulador de telemóvel [5]: