Skip to content

10. Wersja 5 – Aplikacja internetowa PAM / JSF

10.1. Architektura aplikacji

Architektura aplikacji internetowej PAM będzie wyglądała następująco:

W tej wersji serwer Glassfish będzie obsługiwał wszystkie warstwy aplikacji:

  • warstwa [web] jest obsługiwana przez kontener serwletów serwera (1 poniżej)
  • pozostałe warstwy [metier, DAO, jpa] są hostowane przez kontener EJB3 serwera (2 poniżej)

Elementy [metier, DAO] aplikacji działającej w kontenerze EJB3 zostały już opisane w aplikacji klient-serwer omówionej w punkcie 7.1, której architektura wyglądała następująco:

Warstwy [metier, DAO] działały w kontenerze EJB3 serwera Glassfish, a warstwa [ui] w aplikacji konsolowej lub Swing na innym komputerze:

W architekturze nowej aplikacji:

należy zapisać jedynie warstwę [web / jsf]. Pozostałe warstwy [metier, DAO, jpa] są już uwzględnione.

W dokumencie [ref3] pokazano, że aplikacja internetowa, w której warstwa internetowa jest zaimplementowana przy użyciu Java Server Faces, ma architekturę podobną do poniższej:

Architektura ta realizuje wzorzec projektowy MVC (Model, Widok, Kontroler). Przetwarzanie żądania od klienta przebiega w następujący sposób:

Jeśli żądanie jest wysyłane za pomocą GET, wykonywane są dwa następujące etapy:

  1. żądanie – przeglądarka klienta wysyła żądanie do kontrolera [Faces Servlet]. Kontroler ten obsługuje wszystkie żądania klientów. Stanowi on punkt wejścia do aplikacji. Jest to element „C” w skrócie MVC.
  2. odpowiedź – kontroler C zleca wyświetlenie wybranej strony JSF. Jest to widok, czyli „V” w nazwie MVC. Strona JSF wykorzystuje szablon M do zainicjowania dynamicznych części odpowiedzi, którą ma wysłać do klienta. Szablon ten jest klasą Java, która może odwoływać się do warstwy [métier] [4a] w celu dostarczenia widokowi V potrzebnych danych.

Jeśli żądanie jest wysyłane za pomocą POST, między żądaniem a odpowiedzią pojawiają się dwa dodatkowe etapy:

  1. żądanie – przeglądarka klienta wysyła żądanie do kontrolera [Faces Servlet].
  2. przetwarzanie – kontroler C przetwarza to żądanie. Żądanie POST zawiera bowiem dane, które należy przetworzyć. W tym celu kontroler korzysta z pomocy menedżerów zdarzeń specyficznych dla aplikacji [2a]. Menedżery te mogą potrzebować warstwy biznesowej [2b]. Menedżer zdarzenia może być zmuszony do aktualizacji niektórych modeli M [2c]. Po przetworzeniu żądania klienta może ono wywołać różne odpowiedzi. Klasycznym przykładem jest:
    • strona z komunikatami o błędach, jeśli żądania nie udało się poprawnie przetworzyć
    • w przeciwnym razie strona potwierdzenia

Obsługa zdarzenia zwraca do kontrolera [Faces Servlet] wynik w postaci ciągu znaków zwanego kluczem nawigacyjnym.

  1. nawigacja – kontroler wybiera stronę JSF (= widok), która ma zostać wysłana do klienta. Wybór ten opiera się na kluczu nawigacyjnym zwróconym przez menedżera zdarzeń.
  2. odpowiedź – wybrana strona JSF wyśle odpowiedź do klienta. Wykorzystuje ona swój szablon M do zainicjowania swoich części dynamicznych. Szablon ten może również odwoływać się do warstwy [métier] [4a] w celu dostarczenia stronie JSF potrzebnych jej danych.

W projekcie JSF:

  • kontrolerem C jest serwlet [javax.faces.webapp.FacesServlet]. Znajduje się on w bibliotece [jsf-api.jar].
  • Widoki V są zaimplementowane przez strony JSF.
  • modele M i menedżery zdarzeń są zaimplementowane przez klasy Java, często nazywane „backing beans”.
  • W wersjach JSF i 1.x definicje beanów oraz reguły nawigacji między stronami są określone w pliku [faces-config.xml]. Znajduje się tam lista widoków oraz reguły przejścia między nimi. Począwszy od wersji JSF 2, definicje beanów można tworzyć za pomocą adnotacji, a przejścia między stronami można realizować „na stałe” w kodzie beanów.

10.2. Działanie aplikacji

Po pierwszym wywołaniu aplikacji wyświetla się następująca strona:

Następnie wypełnia się formularz, a potem wyświetla się wysokość wynagrodzenia:

Otrzymujemy następujący wynik:

Ta wersja oblicza fikcyjne wynagrodzenie. Nie należy zwracać uwagi na treść strony, ale na jej formatowanie. Po kliknięciu przycisku [Raz] następuje powrót do strony [A].

Błędne wpisy są sygnalizowane, jak pokazuje poniższy przykład:

10.3. Projekt NetBeans

Stworzymy pierwszą wersję aplikacji, w której warstwa [métier] będzie symulowana. Będziemy mieli następującą architekturę:

Gdy menedżery zdarzeń lub modele będą żądać danych od warstwy [métier] [2b, 4a], ta dostarczy im dane fikcyjne. Celem jest uzyskanie warstwy internetowej prawidłowo reagującej na żądania użytkownika. Gdy to osiągniemy, pozostanie nam jedynie zainstalować warstwę serwerową opracowaną w punkcie 7.1:

Będzie to wersja 2 internetowej wersji naszej aplikacji PAM.

Projekt NetBeans dla wersji 1 to następujący projekt Maven:

  • w [1], pliki konfiguracyjne
  • w [2], strony w XHTML oraz arkusz stylów
  • w [3], klasy warstwy w [web]
  • w pliku [4], obiekty wymieniane między warstwą [web] a warstwą [métier] oraz samą warstwą [métier]
  • w [5], plik komunikatów dotyczących internacjonalizacji aplikacji
  • w [6] – zależności aplikacji

Przyjrzyjmy się niektórym z tych elementów.

10.3.1. Pliki konfiguracyjne

Plik [web.xml] jest generowany domyślnie przez NetBeans i zawiera dodatkowo konfigurację strony wyjątków:

<?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.FACELETS_SKIP_COMMENTS</param-name>
    <param-value>true</param-value>
  </context-param> 
  <context-param>
    <param-name>javax.faces.PROJECT_STAGE</param-name>
    <param-value>Development</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>java.lang.Exception</exception-type>
    <location>/faces/exception.xhtml</location>
  </error-page>
</web-app>
  • wiersz 30: [index.html] to strona główna aplikacji
  • wiersze 32–39: konfiguracja strony wyjątku

Strona [exception.html] pochodzi ze strony [ref3]. Jej kod wygląda następująco:

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

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
  <f:view locale="#{changeLocale.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">
        <h3><h:outputText value="#{msg['exception.header']}"/></h3>
        <h:panelGrid columnClasses="col1,col2" columns="2" border="1">
          <h:outputText value="#{msg['exception.httpCode']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.status_code']}"/>
          <h:outputText value="#{msg['exception.message']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.exception']}"/>
          <h:outputText value="#{msg['exception.requestUri']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.request_uri']}"/>
          <h:outputText value="#{msg['exception.servletName']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.servlet_name']}"/>
        </h:panelGrid>
      </h:form>
    </h:body>
  </f:view>
</html>

Każdy wyjątek, który nie jest wyraźnie obsługiwany przez kod aplikacji internetowej, spowoduje wyświetlenie strony podobnej do poniższej:

Plik [faces-config.xml] będzie wyglądał następująco:

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

Należy zwrócić uwagę na następujące kwestie:

  • wiersze 9–14: plik [messages.properties] zostanie wykorzystany do internacjonalizacji stron. Będzie on dostępny na stronach XHTML za pośrednictwem klucza msg.
  • wiersz 15: określa plik [messages.properties] jako plik, który ma być przeszukiwany w pierwszej kolejności w przypadku komunikatów o błędach wyświetlanych przez tagi <h:messages> i <h:message>. Pozwala to na zmianę niektórych domyślnych komunikatów o błędach z pliku JSF. Możliwość ta nie jest tutaj wykorzystywana.

10.3.2. Arkusz stylów

Plik [styles.css] ma następującą treść:


.libelle{
   background-color: #ccffff;
   font-family: 'Times New Roman',Times,serif;
   font-size: 14px;
   font-weight: bold
}
body{
   background-color: #ffccff
}

.error{
   color: #ff3333
}

.info{
   background-color: #99cc00
}

.titreInfos{
   background-color: #ffcc00
}

Oto przykłady kodu JSF wykorzystujące te style:


<h:outputText value="#{msg['form.infos.employé']}"
 styleClass="titreInfos"/>

<h:panelGrid columns="3" 
rowClasses="libelle,info">

<h:message for="heuresTravaillées"
 styleClass="error"/>

10.3.3. Plik komunikatów

Plik komunikatów [messages_fr.properties] ma następującą treść:


form.titre=Feuille de salaire
form.comboEmployes.libell\u00e9=Employ\u00e9
form.heuresTravaill\u00e9es.libell\u00e9=Heures travaill\u00e9es
form.joursTravaill\u00e9s.libell\u00e9=Jours travaill\u00e9s
form.heuresTravaill\u00e9es.required=Indiquez le nombre d'heures travaill\u00e9es
form.heuresTravaill\u00e9es.validation=Donn\u00e9e incorrecte
form.joursTravaill\u00e9s.required=Indiquez le nombre de jours travaill\u00e9s
form.joursTravaill\u00e9s.validation=Donn\u00e9e incorrecte
form.btnSalaire.libell\u00e9=Salaire
form.btnRaz.libell\u00e9=Raz
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
form.infos.employ\u00e9=Informations Employ\u00e9
form.employe.nom=Nom
form.employe.pr\u00e9nom=Pr\u00e9nom
form.employe.adresse=Adresse
form.employe.ville=Ville
form.employe.codePostal=Code postal
form.employe.indice=Indice
form.infos.cotisations=Informations Cotisations sociales
form.cotisations.csgrds=CSGRDS
form.cotisations.csgd=CSGD
form.cotisations.retraite=Retraite
form.cotisations.secu=S\u00e9curit\u00e9 sociale
form.infos.indemnites=Informations Indemnit\u00e9s
form.indemnites.salaireHoraire=Salaire horaire
form.indemnites.entretienJour=Entretien / Jour
form.indemnites.repasJour=Repas / Jour
form.indemnites.cong\u00e9sPay\u00e9s=Cong\u00e9s pay\u00e9s
form.infos.salaire=Informations Salaire
form.salaire.base=Salaire de base
form.salaire.cotisationsSociales=Cotisations sociales
form.salaire.entretien=Indemnit\u00e9s d'entretien
form.salaire.repas=Indemnit\u00e9s de repas
form.salaire.net=Salaire net

Wszystkie te komunikaty są wykorzystywane na stronie [index.xhtml], z wyjątkiem tych z wierszy 11–15, które są wykorzystywane na stronie [exception.xhtml].

10.3.4. Zakres działania beanów

Bean [web.forms.Form] będzie miał zakres request:


import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;

@ManagedBean
@RequestScoped
public class Form implements Serializable {

Bean [web.utils.ChangeLocale] będzie miał zakres application:


package web.utils;

import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;

@ManagedBean
@SessionScoped
public class ChangeLocale implements Serializable{
  // ustawienia regionalne stron
  private String locale="fr";
  
  public ChangeLocale() {
  }
  
  public String setFrenchLocale(){
    locale="fr";
    return null;
  }
  
  public String setEnglishLocale(){
    locale="en";
    return null;
  }

  public String getLocale() {
    return locale;
  }

  public void setLocale(String locale) {
    this.locale = locale;
  }
  
  
}

10.3.5. Warstwa [métier]

Warstwa [métier] implementuje następujący interfejs IMetierLocal:


package metier;

import java.util.List;
import javax.ejb.Local;
import jpa.Employe;

@Local
public interface IMetierLocal {
  // pobierz listę płac
  FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
  // lista pracowników
  List<Employe> findAllEmployes();
}

Interfejs ten jest wykorzystywany w części serwerowej aplikacji klient-serwer opisanej w paragrafie 7.1.

Klasa Metier, której użyjemy do przetestowania warstwy [web], implementuje ten interfejs w następujący sposób:


package metier;

...
public class Metier implements IMetierLocal {
  
  // słownik pracowników indeksowany według numeru SS
  private Map<String,Employe> hashEmployes=new HashMap<String,Employe>();
  // lista pracowników 
  private List<Employe> listEmployes;
  
  // pobierz listę płac
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
    double nbHeuresTravaillées, int nbJoursTravaillés) {
    // pobieranie pracownika o numerze SS
    Employe e=hashEmployes.get(SS);
    // zwracamy fikcyjny arkusz płacowy
    return new FeuilleSalaire(e,new Cotisation(3.49,6.15,9.39,7.88),new ElementsSalaire(100,100,100,100,100));
  }
  
  // lista pracowników
  public List<Employe> findAllEmployes() {
    if(listEmployes==null){
      // tworzy się listę dwóch pracowników
      listEmployes=new ArrayList<Employe>();
      listEmployes.add(new Employe("254104940426058","Jouveinal","Marie","5 rue des oiseaux","St Corentin","49203",new Indemnite(2,2.1,2.1,3.1,15)));
      listEmployes.add(new Employe("260124402111742","Laverti","Justine","La brûlerie","St Marcel","49014",new Indemnite(1,1.93,2,3,12)));
      // słownik pracowników indeksowany według numeru SS
      for(Employe e:listEmployes){
        hashEmployes.put(e.getSS(),e);
      }
    }
    // zwracamy listę pracowników
    return listEmployes;
  }
}

Rozszyfrowanie tego kodu pozostawiamy czytelnikowi. Należy zwrócić uwagę na zastosowaną metodę: aby uniknąć konieczności wdrażania części aplikacji o nazwie EJB, symulujemy warstwę [métier]. Gdy warstwa [web] zostanie uznana za poprawną, będziemy mogli zastąpić ją prawdziwą warstwą [métier].

10.4. Formularz [index.xhtml] i jego szablon [Form.java]

Teraz tworzymy stronę XHTML formularza oraz jej szablon.

Zalecane materiały do przeczytania w [ref3]:

  • przykład nr 3 (mv-jsf2-03) dotyczący listy tagów, których można używać w formularzu
  • przykład nr 4 (mv-jsf2-04) dotyczący list rozwijanych wypełnianych przez szablon
  • przykład nr 6 (mv-jsf2-06) dotyczący walidacji danych wprowadzonych przez użytkownika
  • przykład nr 7 (mv-jsf2-07) dotyczący obsługi przycisku [Raz]

10.4.1. krok 1


Zadanie: Utwórz formularz [index.xhtml] oraz jego model [Form.java], niezbędne do wygenerowania następującej strony:


Elementy do wprowadzania danych są następujące:

id
typ JSF
szablon
rola
1
comboEmployes
<h:selectOneMenu>
Ciąg znaków comboEmployesValue
List<Pracownik> getEmployes()
zawiera listę pracowników w postaci
„imię nazwisko”.
2
heuresTravaillees
<h:inputText>
Ciąg znaków heuresTravaillées
liczba przepracowanych godzin – liczba rzeczywista
3
joursTravailles
<h:inputText>
Ciąg znaków joursTravaillés
liczba przepracowanych dni – liczba całkowita
4
btnSalaire
<h:commandButton>
 
uruchamia obliczanie wynagrodzenia
5
btnRaz
<h:commandButton>
 
przywraca formularz do stanu początkowego
  • metoda getEmployes zwróci listę pracowników, którą uzyska z warstwy [métier]. Obiekty wyświetlane przez listę rozwijaną będą miały jako atrybut itemValue numer pracownika SS, a jako atrybut itemLabel ciąg znaków składający się z imienia i nazwiska pracownika.
  • Przyciski [Salaire] i [Raz] nie będą na razie powiązane z procedurami obsługi zdarzeń.
  • Sprawdzana będzie poprawność wprowadzonych danych.

Image

Przetestuj tę wersję. Sprawdź w szczególności, czy błędy wprowadzania danych są prawidłowo sygnalizowane.

Uwaga: ważne jest, aby atrybuty id elementów strony nie zawierały znaków akcentowanych. W przypadku Glassfish 3.1.2 powoduje to awarię aplikacji.

10.4.2. Krok 2


Zadanie: wypełnij formularz [index.xhtml] i jego szablon [Form.java], aby po kliknięciu przycisku [Salaire] wyświetliła się następująca strona:


Przycisk [Salaire] zostanie powiązany z menedżerem zdarzeń calculerSalaire w szablonie. Metoda ta będzie korzystać z metody calculerFeuilleSalaire z warstwy [métier]. Ten arkusz płacowy zostanie sporządzony dla pracownika wybranego w [1].

W szablonie list płac będzie reprezentowany przez następujące pole prywatne:


  private FeuilleSalaire feuilleSalaire;

posiadające metody get i set.

Aby uzyskać informacje zawarte w tym obiekcie, na stronie JSF można wpisać wyrażenia takie jak poniższe:


<h:outputText value="#{form.feuilleSalaire.employe.nom}"/>

Wyrażenie atrybutu „value” zostanie obliczone w następujący sposób:

[form].getFeuilleSalaire().getEmploye().getNom(), gdzie [form] reprezentuje instancję klasy [Form.java]. Czytelnik może sprawdzić, czy metody get użyte w tym miejscu rzeczywiście istnieją odpowiednio w klasach [Form], [FeuilleSalaire] i [Employe]. Gdyby tak nie było, podczas oceny wyrażenia zostałby zgłoszony wyjątek.

Przetestuj tę nową wersję.

10.4.3. Krok 3


Zadanie: uzupełnij formularz [index.xhtml] oraz jego szablon [Form.java], aby uzyskać następujące dodatkowe informacje:


Postępujemy tak samo jak poprzednio. Pojawia się jednak problem z symbolem waluty euro, który występuje na przykład w [1]. W przypadku aplikacji międzynarodowej lepiej byłoby mieć format wyświetlania i znak waluty z używanego pliku locale (en, de, fr, ...). Można to uzyskać w następujący sposób:


          <h:outputFormat value="{0,number,currency}">
            <f:param value="#{form.feuilleSalaire.employe.indemnite.entretienJour}"/>
</h:outputFormat>

Można by zapisać:


          <h:outputText value="#{form.feuilleSalaire.employe.indemnite.entretienJour} є">

ale przy ustawieniu regionalnym en_GB (angielski GB) nadal wyświetlałaby się kwota w euro, podczas gdy należałoby użyć funta brytyjskiego (£). Tag <h:outputFormat> pozwala wyświetlać informacje w zależności od wartości locale na wyświetlanej stronie JSF:

  • wiersz 1: wyświetla parametr {0}, który jest liczbą (number) reprezentującą kwotę pieniężną (currency)
  • wiersz 2: tag <f:param> nadaje wartość parametrowi {0}. Drugi tag <f:param> nadałby wartość parametrowi oznaczonemu jako {1} i tak dalej.

10.4.4. krok 4

Materiały do przeczytania: przykład nr 7 (mv-jsf2-07) w [ref3].


Zadanie: uzupełnij formularz [index.xhtml] oraz jego szablon [Form.java], aby obsłużyć przycisk [Raz].


Przycisk [Raz] przywraca formularz do stanu, w jakim znajdował się w momencie pierwszego wywołania za pomocą GET. W tym przypadku pojawia się kilka trudności. Niektóre z nich zostały wyjaśnione w [ref3].

Formularz wygenerowany przez przycisk [Raz] nie obejmuje całego formularza, a jedynie jego część saisie:

Image

Wynik ten można uzyskać za pomocą tagu <f:subview> użytego w następujący sposób:


      <f:subview id="viewInfos" rendered="#{form.viewInfosIsRendered}">
... la partie du formulaire qu'on veut pouvoir ne pas afficher
</f:subview>

Tag <f:subview> otacza całą część formularza, która może być wyświetlana lub ukrywana. Każdy komponent można wyświetlić lub ukryć za pomocą atrybutu rendered. Jeśli rendered="true", komponent jest wyświetlany, a jeśli rendered="false", nie jest wyświetlany. Jeśli atrybut rendered przyjmuje określoną wartość w szablonie, wyświetlanie komponentu można kontrolować programowo.

W powyższym przykładzie wyświetlanie widoku viewInfos będzie kontrolowane za pomocą następującego pola:


  private boolean viewInfosIsRendered;

wraz z metodami get i set. Metody obsługujące kliknięcia przycisków [Salaire] i [Raz] zaktualizują tę wartość logiczną w zależności od tego, czy widok viewInfos ma zostać wyświetlony, czy nie.