Tutoriel pour apprendre à développer une application web Jakarta EE avec JSF, EJB, JTA et JPA

Le but de ce tutoriel est d’apprendre à développer des applications web avec les technologies Jakarta EE (anciennement Java Enterprise Edition ou JEE) en version 8 et plus particulièrement :

  • JSF 2.3
  • EJB 3.2
  • JTA
  • JPA 2.1

Il présente une manière de faire en relation avec la bibliothèque de composants graphiques Primefaces.

Le serveur d’application certifié EE8 peut être choisi en fonction de ses affinités même si personnellement j’utilise un serveur Wildfly 17.

Notez qu’en fonction du serveur d’application que vous choisirez, l’implémentation de JPA pourra différer. Le serveur Wildfly utilise Hibernate comme implémentation de JPA.

La base de données relationnelle peut également être choisie entre plusieurs possibilités, j’utilise pour mes tests une base de données MySQL.

Pour l’EDI, je vous conseille Eclipse 2019-06 (ou supérieur) avec l’extension Jboss Tools.

Vous pourrez télécharger tous les logiciels nécessaires aux adresses suivantes ainsi que les sources de l’application :

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez Donner une note  l'article (5).

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Spécification Jakarta EE (Java EE )

Jakarta EE est une spécification qui étend Java Standard Edition (Java SE).

La première version des spécifications Java EE fut publiée en 1999.

La dernière version, Java EE8, est sortie en août 2017.

En 2018, le projet est confié par Oracle à la Fondation Eclipse, et le nom Jakarta EE est choisi par la communauté des développeurs à la place de Java EE.

La plate-forme se fonde principalement sur des composants modulaires exécutés sur un serveur d'applications dont la version open source de référence est GlassFish 5.

I-A. EJB (Enterprise Java Bean)

Enterprise Java Bean est un standard de développement de composants métiers côté serveur écrits en Java et capables d’être appelés à distance ou localement.

Initialement développé par IBM en 1997, la spécification a été adoptée par Sun Microsystems en 1999 en tant qu’EJB 1.0. 20 ans après, on est à la version 3.2 de la spécification EJB, beaucoup plus simple d’utilisation qu’à ses débuts.

Il existe (actuellement) deux types d’EJB :

  • EJB session
    EJB avec état (stateful) ou sans état (stateless), ce sont les plus utilisés pour écrire la couche métier d’une application. Ils ont la capacité d’être appelés à distance (JVM distincte) ou localement (même JVM que l’appelant), le choix se faisant par l’interface utilisée (soit avec une annotation @Local, soit @Remote).
    Une variante permet de créer un bean unique via l’annotation @Singleton, pratique pour partager des données ou faire des traitements particuliers au démarrage et à l’arrêt d’une application.
  • EJB message driven
    EJB répondant à des messages fournis par JMS (par exemple ActiveMQ, MQSeries).

I-B. JPA (Java persistence API)

Java Persistence API permet de gérer facilement la transformation du monde relationnel des bases de données et le monde Objet de Java.

Elle s’appuie sur :

  • des annotations spécifiques de paramétrage ;
  • un langage de requête (JPQL) ;
  • une implémentation de l’API (Hibernate, EclipseLink, OpenJPA…).

I-C. JTA (Java Transaction API)

Java Transaction API fournit des interfaces Java standards entre un gestionnaire de transaction et les différentes parties impliquées dans un système de transactions distribuées : le gestionnaire de ressources, le serveur d'application et les applications transactionnelles.

JTA est un protocole de commit à deux phases.

I-D. JSF (Java Server Faces)

Java Server Faces est un framework MVC2 (Modèle, Vue, Contrôleur) basé sur des composants.

Le point d’entrée d’une requête se fait par une unique servlet Faces Servlet (javax.faces.webapp.FacesServlet).

La dernière version de JSF est la version 2.3.

Depuis la version 2.0, Facelets est la technologie utilisée par défaut pour le rendu des pages en remplacement des pages JSP.

Facelets inclut un moteur de template qui simplifie la construction des pages.

Les composite components simplifient la création de composants personnels réutilisables dans plusieurs vues.

I-D-1. Le cycle JSF

Le traitement d’une requête se fait en six phases :

Image non disponible


Explications :

  1. Lorsqu’une requête arrive, on commence par restaurer le graphe des objets de la vue ou de les créer si c’est la première utilisation.
  2. Les données envoyées par un formulaire sont affectées aux composants correspondants.
    Si des convertisseurs sont nécessaires, ils sont appelés.
  3. Les valeurs sont validées par les éventuels validators.
  4. Les données sont transférées au bean sous-jacent.
  5. Les listeners de l’application sont appelés.
    Les méthodes de l’application sont exécutées.
    Le calcul de la prochaine page est effectué.
  6. La vue est construite et renvoyée au client.

I-D-2. Le cycle de vie des beans gérés (Managed beans)

Avec la version 2.3 de JSF, l’injection de dépendance est confiée à CDI (Context and Dependency Injection).

Les beans sont créés et enregistrés automatiquement dans différents contextes (scope), lesquels ont des durées de vie différentes.

Les scopes standards :

  • @RequestScoped
    Le bean est créé pour la requête en cours et sera détruit après la réponse du serveur.

  • @ViewScoped
    Le bean est créé et associé à une vue. Tant que la vue ne change pas, le bean reste accessible. Au changement de vue, le bean sera détruit.

  • @SessionScoped
    Le bean est créé et associé à la session (HttpSession) de l’utilisateur. Le bean sera accessible tant que la session de l’utilisateur sera active et il sera détruit lorsque la session sera détruite. Tous les beans au sein de la même session peuvent accéder aux autres beans de scope session.

  • @ApplicationScoped
    Le bean est créé et associé à l’application sur le serveur. Tant que l’application est active, le bean est accessible. Il sera détruit avec l’arrêt de l’application (généralement à l’arrêt du serveur). Tous les utilisateurs de l’application partagent un bean en scope application.

  • @ConversationScoped
    Le bean est créé pour une unité de travail, il sera détruit une fois l’unité stoppée.

Le choix du scope d’un bean s’avère très important, il s’agit de bien réfléchir à ce qui est lié à une requête, ce qui doit survivre à la requête et pour combien de temps.

Attention quand même à ne pas mettre trop de choses en session, la taille d’une session peut avoir des répercussions sur les performances.

I-D-3. Les convertisseurs

Les convertisseurs permettent de générer une valeur String identifiant un objet et en retour de récupérer l’objet par son identifiant.

Il existe deux manières d’écrire des convertisseurs, dans les deux cas, la classe doit implémenter l’interface javax.faces.convert.Converter.

Exemple CDI
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
@Named
@ApplicationScoped
public class TelephoneTypeConverter implements Converter<Object>
{
    @EJB
    private MonProjetFacadeLocal facade;
    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value)
    {
        if (value == null)
        {
            return null;
        }
        return facade.getTelephoneType(Integer.valueOf(value));
    }
    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value)
    {
        if (value == null)
        {
            return null;
        }
        if (value instanceof DTOTelephoneType)
        {
            return ((DTOTelephoneType)value).getUid().toString();
        }
        return value.toString();
    }
}

Dans cet exemple, nous avons accès aux objets gérés par CDI, ici la façade EJB.

Référencement dans la page
Sélectionnez
1.
<p:balise … converter="#{telephoneTypeConverter}">
Exemple JSF
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
@FacesConverter("datetimeConverter")
public class DatetimeConverter implements Converter<Object>
{
    private static final SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm");
    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value)
    {
        if (value == null)
        {
            return null;
        }
        try
        {
            return sdf.parse(value);
        }
        catch (Exception e)
        {
            return null;
        }
    }
    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value)
    {
        if (value == null)
        {
            return null;
        }
        try
        {
            return sdf.format(value);
        }
        catch (Exception e)
        {
            return null;
        }
    }
}

Dans cet exemple, nous n’avons pas accès aux objets gérés par CDI.

Référencement dans la page
Sélectionnez
1.
<p:balise … converter="datetimeConverter">

À noter qu’avec JSF 2.3, on pourrait avoir accès à l’injection en utilisant le nouvel attribut managed=true de la balise @FacesConverter.

Exemple
Sélectionnez
@FacesConverter("datetimeConverter", managed=true)

I-D-4. Les validations

Les validateurs permettent de contrôler la saisie et d’interrompre, en cas d’erreur, le cycle JSF.

Il existe deux manières de concevoir un validateur, dans les deux cas la classe doit implémenter l’interface javax.faces.validator.Validator.

Exemple CDI
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
@Named
@ApplicationScoped
public class TelephoneTypeValidator implements Validator<Object>
{
    @EJB
    private MonProjetFacadeLocal facade;
    @Override
    public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException
    {
        if (value == null)
        {
            throw new ValidatorException(new FacesMessage("Le type de numéro de téléphone est obligatoire"));
        }
        Object test = facade.getTelephoneType(((DTOTelephoneType)value).getUid());
    }
}

Dans cet exemple, nous avons accès aux objets gérés par CDI, ici la façade EJB.

Référencement dans la page
Sélectionnez
1.
2.
3.
<p:balise>
    <f:validator binding="#{telephoneTypeValidator}"/>
</p:balise>
Exemple JSF
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
@FacesValidator(value="telephoneNumeroValidator")
public class TelephoneNumeroValidator implements Validator<Object>
{
    @Override
    public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException
    {
        if (!Utils.isSet(value))
        {
            throw new ValidatorException(new FacesMessage("Le numéro de téléphone est obligatoire"));
        }
    }
}

Dans cet exemple, nous n’avons pas accès aux objets gérés par CDI.

Référencement dans la page
Sélectionnez
1.
2.
3.
4.
5.
<p:balise ... validator="telephoneNumeroValidator">
ou
<p:balise ...>
    <f:validator validatorId="telephoneTypeValidator"/>
</p:balise>

À noter qu’avec JSF 2.3, on pourrait avoir accès à l’injection en utilisant le nouvel attribut managed=true de la balise @FacesValidator.

Exemple
Sélectionnez
@FacesValidator(value="telephoneNumeroValidator", managed=true)

I-D-5. Les systèmes de navigation

Il existe différents moyens pour naviguer d’une page vers une autre avec JSF, en passant par le fichier faces-config.xml (navigation dite « explicite ») ou sans (navigation « implicite » ou par code).

I-D-5-a. Navigation explicite

Pour configurer une navigation dans le fichier faces-config.xml, on utilise les directives <navigation-rule>, <from-view-id>, <navigation-case>, <from-outcome>, etc.

Exemple
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
<navigation-rule>
    <from-view-id>index.xhtml</from-view-id>
    <navigation-case>
         <from-outcome>go</from-outcome>
         <if>#{defaultManager.ok}</if>
         <to-view-id>/WEB-INF/go.xhtml</to-view-id>
    </navigation-case>
    <navigation-case>
         <from-outcome>go</from-outcome>
         <if>#{!defaultManager.ok}</if>
         <to-view-id>/WEB-INF/nogo</to-view-id>
    </navigation-case>
</navigation-rule>

Ici, nous avons donc une règle de navigation conditionnelle à partir de la page index.xhtml.

Si le retour est « go » et que la condition defaultManager.ok == true, alors on affichera la page /WEB-INF/go.xhtml.

Si la valeur defaultManager.ok == false, alors on affichera la page /WEB-INF/nogo.xhtml.

À noter que l’extension ‘.xhtml’ n’est pas obligatoire, c’est la valeur par défaut.

Si une redirection est nécessaire ou voulue, il faut alors ajouter la directive <redirect/> après <to-view-id>.

Dans le cas d’une redirection, il est impossible d’utiliser le répertoire /WEB-INF/ de l’application, ce dernier n’étant pas visible en dehors de l’application.

I-D-5-b. Navigation implicite

On peut directement passer la référence de la page à afficher au retour d’une action ou directement comme valeur de l’attribut action dans une balise.

Exemple simple
Sélectionnez
1.
<h:commandLink action="/views/personne/recherche.xhtml" value="Action=chemin"/>
Exemple avec paramètres
Sélectionnez
1.
<h:commandLink action="/views/personne/detail?faces-redirect=true&amp;uid=1" value="Action=chemin"/>

Là aussi, l’extension .xhtml est optionnelle.

Notez l’utilisation de &amp; pour le caractère & dans l’URL, sans cela, ça ne fonctionnera pas.

Le paramètre faces-redirect=true permet de signifier qu’on veut une redirection vers la page demandée.

I-D-5-c. Navigation par code

Lorsqu’une fonction est appelée par AJAX (cas d’un listener ou actionListener), le système de navigation standard ne fonctionne plus, il est alors possible d’obtenir un objet de navigation et de lui passer des paramètres éventuels pour appeler la page voulue.

Exemple
Sélectionnez
1.
2.
3.
4.
5.
public void personneDetail()
{
    ConfigurableNavigationHandler navigationHandler = (ConfigurableNavigationHandler) getFacesContext().getApplication().getNavigationHandler();
    navigationHandler.performNavigation("/views/personne/detail.xhtml?faces-redirect=true&uid=1");
}

Là aussi, l’extension .xhtml est optionnelle.

Le paramètre faces-redirect=true permet de signifier qu’on veut une redirection vers la page demandée.

II. Internationalisation

L’internationalisation, i18n en abrégé (parce qu’il y a 18 lettres entre le i et le n) permet entre autre de modifier la langue de l’interface graphique grâce à des définitions de constantes dans des fichiers properties.

L’application embarque différentes versions d’un fichier, dans notre cas ce sera le fichier ApplicationResources.properties, avec un suffixe de langue pour les différencier.

À noter que le fichier sans suffixe sera défini comme celui par défaut si on ne dispose pas du fichier dans la langue demandée.

Quand on parle d’internationalisation, on ne parle pas que de langue mais également de régionalisation de la langue.

Pour exemple, en_GB représente l’anglais parlé en Grande-Bretagne alors que en_US représente l’anglais parlé aux États-Unis. Idem pour fr_FR et fr_CA ou fr_BE, fr_CH, etc.

Il est donc préférable de prévoir tout de suite la régionalisation, même si on se contente dans un premier temps que des langues au sens large.

Pour paramétrer les langues utilisées par l’application, on définira un bloc dans le fichier faces-config.xml dont voici un extrait :

Extrait de faces-config
Sélectionnez
<application>
        <locale-config>
            <default-locale>fr_FR</default-locale>
            <supported-locale>fr_FR</supported-locale>
            <supported-locale>en_GB</supported-locale>
        </locale-config>
        <resource-bundle>
            <base-name>ApplicationResources</base-name>
            <var>msg</var>
        </resource-bundle>
    </application>

Dans cet exemple, l’application définit deux langues, le français parlé en France et l’anglais parlé en Grande-Bretagne. La langue par défaut étant le français.

Ces paramètres s’appliquent au fichier dont le nom de base est ApplicationResources.

Lorsque l’utilisateur choisira l’anglais, l’application fera automatiquement référence au fichier ApplicationResources_en_GB.properties, si elle ne le trouve pas, elle cherchera dans ApplicationResources_fr_FR.properties, et si elle ne le trouve toujours pas, dans ApplicationsResources.properties.

Au niveau des pages de l’application, il faudra prévoir la balise <f:view locale=’’#{unManagerSession.locale}’’> pour définir qu’on utilise l’internationalisation (avec la balise fermante bien sûr). Le manager associé DOIT être placé en scope session. L’attribut « locale » représente un objet java.util.Locale.

Le plus simple est de passer par une page template, ça évitera de dupliquer ce code dans toutes les pages.

Pour changer la langue de l’interface graphique, il suffira de remplacer l’objet Locale dans ce manager par celui correspondant à la langue voulue.

III. Application type : Gestion de personnes

Dans cet exemple très simple (mais qui donne une bonne idée de ce qu’on peut faire), nous allons gérer une base de données de personnes en utilisant :

  • JSF 2.3
  • Primefaces 6.2
  • EJB 3.2
  • JPA 2.1

Le serveur d’application utilisé est un Wildfly 15 qui s’exécute sur un OpenJDK 11.

Le système de navigation est de type code hybride, le code étant défini dans la classe abstraite AbstractManager.

Toutes les pages sont définies par une énumération laquelle renvoie à un identifiant de message dans le fichier de propriétés de l’application.

Ce système permettrait de différencier les sources des pages xhtml en fonction de la langue de l’interface, même si dans cet exemple, nous n’utiliserons pas cette possibilité.

Primefaces est une bibliothèque de composants graphiques spécifiques à JSF développée par PrimeTek Informatics, open source et libre d’utilisation sous licence Apache.

Elle est largement utilisée et extrêmement puissante tout en restant simple d’utilisation.

III-A. Création et configuration des projets

Pour notre application type, nous utiliserons trois projets :

  • 1 projet EJB nommé MonProjetEJB
    Ce projet sert à définir la couche métier de l’application. C’est ce projet qui fera le lien avec la base de données en utilisant les API JPA et JTA.
    Une fois créé, faire un clic droit sur le projet → Properties
    Choisir Project Facets et cocher JPA (version 2.1) et ajuster s’il y a lieu les versions pour :

    • EJB (version 3.2) ;
    • Java (version 1.8 minimum).

      persistence.xml
      Sélectionnez
      1.
      2.
      3.
      4.
      5.
      6.
      7.
      8.
      9.
      10.
      11.
      12.
      13.
      14.
      <?xml version="1.0" encoding="UTF-8"?>
      <persistence version="2.1"
          xmlns="http://xmlns.jcp.org/xml/ns/persistence"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
      
          <persistence-unit name="MonProjetEJBPU">
      
              <jta-data-source>java:/MySQLDS</jta-data-source>
              <properties>
                  <property name="org.hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect" />
              </properties>
          </persistence-unit>
      </persistence>
      

    Cliquer sur le bouton Apply and Close.
    Le fichier persistence.xml sera créé automatiquement dans le répertoire /META-INF.
    Finalement, il contiendra ceci :

  • 1 projet Dynamic Web Module nommé MonProjetWeb
    Ce projet sert à définir l’application IHM qui sera utilisée par les différents utilisateurs.
    Une fois créé, faire un clic droit sur le projet → Properties.
    Choisir Project Facets et cocher JavaServer Faces (version 2.3) et ajuster s’il y a lieu les versions pour :

    • Dynamic Web Module (version 4.0)
    • Java (version 1.8 minimum) ;
    • Javascript (version 1.0).

      faces-config.xml
      Sélectionnez
      1.
      2.
      3.
      4.
      5.
      6.
      7.
      8.
      9.
      10.
      11.
      12.
      13.
      14.
      15.
      <?xml version="1.0" encoding="UTF-8"?>
      <faces-config
          xmlns="http://xmlns.jcp.org/xml/ns/javaee"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_3.xsd"
          version="2.3">
      
          <application>
              <resource-bundle>
                  <base-name>ApplicationResources</base-name>
                  <var>msg</var>
              </resource-bundle>
              <message-bundle>Messages</message-bundle>
          </application>
      </faces-config>
      
      web.xml
      Sélectionnez
      1.
      2.
      3.
      4.
      5.
      6.
      7.
      8.
      9.
      10.
      11.
      12.
      13.
      14.
      15.
      16.
      17.
      18.
      19.
      20.
      21.
      22.
      <?xml version="1.0" encoding="UTF-8"?>
      
      <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
        <display-name>MonProjetWeb</display-name>
        
        <context-param>
            <param-name>primefaces.THEME</param-name>
            <param-value>omega</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>*.xhtml</url-pattern>
        </servlet-mapping>
        <welcome-file-list>
            <welcome-file>index.xhtml</welcome-file>
        </welcome-file-list>
      </web-app>
      

    Cliquez sur le bouton Apply and Close.
    Le fichier faces-config.xml sera créé automatiquement dans le répertoire /WEB-INF
    et la servlet Faces Servlet sera ajoutée au fichier web.xml avec les paramètres par défaut.
    Finalement, nous aurons ces deux fichiers :

  • 1 projet EAR nommé MonProjet
    Ce projet sert à encapsuler les deux autres projets dans une archive déployable sur le serveur d’application.
    Une fois créé, faire un clic droit sur le projet → Java EE Tools → Generate Deployment Descriptor Stub.
    Le fichier application.xml sera créé dans le répertoire /META-INF.
    Finalement, le fichier contiendra ceci :

    application.xml
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    <?xml version="1.0" encoding="UTF-8"?>
    <application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/application_8.xsd" version="8">
      <display-name>MonProjet</display-name>
      <module>
        <web>
          <web-uri>MonProjetWeb.war</web-uri>
          <context-root>MonProjetWeb</context-root>
        </web>
      </module>
      <module>
        <ejb>MonProjetEJB.jar</ejb>
      </module>
    </application>
    

    Par défaut, le nom du projet web est pris pour le nom de l’application, vous pouvez modifier la ligne <context-root> pour lui donner un nom plus représentatif, ici, nous pourrions mettre ‘personne’.

    Lors de l’appel de l’application dans un navigateur, c’est ce nom qu’il faudra utiliser.

    Pour nous, ce sera :

    http://localhost:8080/MonProjetWeb/

  • Structure des projets Image non disponible

III-B. Modèle de données

Les personnes possèdent une liste de numéros de téléphone, ces numéros sont typés (mobile, fixe, bureau, etc.)

Ci-dessous le schéma de relation des tables de l’application.

Image non disponible

III-C. Scripts de création de la base de données (MySQL)

Script de création
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
create table olivier.civilite (
    uid integer AUTO_INCREMENT not null,
    abrege varchar(10) not null,
    libelle varchar(64) not null,
    
    primary key (uid)
);

insert into olivier.civilite (abrege, libelle) values ('M', 'Monsieur'), ('Mme', 'Madame'), ('Mlle', 'Mademoiselle'), ('Dr', 'Docteur'), ('Pr', 'Professeur'), ('Me', 'Maïtre'), ('Mgr', 'Monseigneur'), ('Vve', 'Veuve');

create table olivier.personne (
    uid integer AUTO_INCREMENT not null,
    uid_civilite integer not null,
    genre char(1),
    nom varchar(64) not null,
    prenom varchar(64) not null,
    adresse1 varchar(64),
    adresse2 varchar(64),
    code_postal varchar(15),
    ville varchar(64),
    maj_date timestamp not null default current_timestamp,
    
    primary key (uid),
    
    constraint check_genre check (genre in ('H', 'F')),
    
    constraint fk_personne_civilite foreign key (uid_civilite)
        references civilite (uid)
        on delete restrict
        on update restrict
);

create table olivier.telephone_type (
    uid integer AUTO_INCREMENT not null,
    code varchar(10) not null,
    libelle varchar(64),
    
    primary key (uid)
);

insert into olivier.telephone_type (code, libelle) values
('MOBILE', 'Téléphone mobile'), ('FIXE', 'Téléphone fixe'),
('BUREAU', 'Téléphone bureau');

create table olivier.personne_telephone (
    uid integer AUTO_INCREMENT not null,
    uid_personne integer not null,
    uid_telephone_type integer not null,
    numero varchar(15),
    
    primary key (uid),
    constraint fk_personne_telephone_personne foreign key (uid_personne)
        references personne (uid)
        on delete cascade
        on update restrict,
    constraint fk_personne_telephone_telephone_type foreign key (uid_telephone_type)
        references telephone_type (uid)
        on delete cascade
        on update restrict
);

III-D. Classes utilitaires et abstraites de l’application

Il est largement recommandé de factoriser tout ce qui est récurrent dans les traitements soit sous la forme d’une classe utilitaire, soit sous la forme d’une classe abstraite qui sera étendue par la classe finale.

Dans cet exemple, nous utiliserons trois classes abstraites liées aux trois types de managed bean utilisés :

  1. AbstractManager
    Classe qui centralise toutes les propriétés et fonctions utiles à un contrôleur de page en particulier l’accès au bundle de ressources de l’application, le point de retour éventuel, l’accès aux pages de l’application, le formatage des messages, l’envoi de message, etc.

    AbstractManager
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    22.
    23.
    24.
    25.
    26.
    27.
    28.
    29.
    30.
    31.
    32.
    33.
    34.
    35.
    36.
    37.
    38.
    39.
    40.
    41.
    42.
    43.
    44.
    45.
    46.
    47.
    48.
    49.
    50.
    51.
    52.
    53.
    54.
    55.
    56.
    57.
    58.
    59.
    60.
    61.
    62.
    63.
    64.
    65.
    66.
    67.
    68.
    69.
    70.
    71.
    72.
    73.
    74.
    75.
    76.
    77.
    78.
    79.
    80.
    81.
    82.
    83.
    84.
    85.
    86.
    87.
    88.
    89.
    90.
    91.
    92.
    93.
    94.
    95.
    96.
    97.
    98.
    99.
    100.
    101.
    102.
    103.
    104.
    105.
    106.
    107.
    108.
    109.
    110.
    111.
    112.
    113.
    114.
    115.
    116.
    117.
    118.
    119.
    120.
    121.
    122.
    123.
    124.
    125.
    126.
    127.
    128.
    129.
    130.
    131.
    132.
    133.
    134.
    135.
    136.
    137.
    138.
    139.
    140.
    141.
    142.
    143.
    144.
    145.
    146.
    147.
    148.
    149.
    150.
    151.
    152.
    153.
    154.
    155.
    156.
    157.
    158.
    159.
    160.
    161.
    162.
    163.
    164.
    165.
    166.
    167.
    168.
    169.
    170.
    171.
    172.
    173.
    174.
    175.
    176.
    177.
    178.
    179.
    180.
    181.
    182.
    183.
    184.
    185.
    186.
    187.
    188.
    189.
    190.
    191.
    192.
    193.
    194.
    195.
    196.
    197.
    198.
    199.
    200.
    201.
    202.
    203.
    204.
    205.
    206.
    207.
    208.
    209.
    210.
    211.
    212.
    213.
    214.
    215.
    216.
    217.
    218.
    219.
    220.
    221.
    222.
    223.
    224.
    225.
    226.
    227.
    228.
    229.
    230.
    231.
    232.
    233.
    234.
    235.
    236.
    237.
    238.
    239.
    240.
    241.
    242.
    243.
    244.
    245.
    246.
    247.
    248.
    249.
    250.
    251.
    252.
    253.
    254.
    255.
    256.
    257.
    258.
    259.
    260.
    261.
    262.
    263.
    264.
    265.
    266.
    267.
    268.
    269.
    270.
    271.
    272.
    273.
    274.
    275.
    276.
    277.
    278.
    279.
    280.
    281.
    282.
    283.
    284.
    285.
    286.
    287.
    288.
    289.
    290.
    291.
    292.
    293.
    294.
    295.
    296.
    297.
    298.
    299.
    300.
    301.
    302.
    303.
    304.
    305.
    306.
    307.
    308.
    309.
    310.
    311.
    312.
    public abstract class AbstractManager implements Serializable
    {
        public static final long serialVersionUID = 1;
        public static final String REDIRECT_PART = "faces-redirect";
        public static final String RETURN_POINT_PART = "rp";
        public static final String UID_PART = "uid";
        private String returnPoint;
        private static String contextRoot;
    
        /*
         * ----- getter/setter
         */
    
        public String getContextRoot()
        {
            if (contextRoot == null)
            {
                contextRoot = "/" + getFacesContext().getExternalContext().getContextName();
            }
    
            return contextRoot;
        }
    
        public String getReturnPoint()
        {
            return returnPoint;
        }
    
        public void setReturnPoint(String returnPoint)
        {
            this.returnPoint = returnPoint;
        }
    
        public ResourceBundle getResourceBundle()
        {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            return getFacesContext().getApplication().getResourceBundle(facesContext, "msg"); 
        }
    
        public String getResourceBundleMessage(String key)
        {
            ResourceBundle bundle = getResourceBundle();
            if (bundle.containsKey(key))
            {
                return bundle.getString(key);
            }
            return key;
        }
    
        public FacesContext getFacesContext()
        {
            return FacesContext.getCurrentInstance();
        }
    
        public Locale getLocale()
        {
            Locale locale = null;
            try
            {
                locale = getFacesContext().getViewRoot().getLocale();
            }
            catch (Exception e)
            {
                try
                {
                    locale = getFacesContext().getExternalContext().getRequestLocale();
                }
                catch (Exception e2)
                {
                    locale = new Locale("fr", "FR");
                }
            }
            return locale;
        }
    
    
        /*
         * ----- méthodes
         */
    
        /**
         * Formatage de l'URL de retour à l'appelant
         * 
         * @param redirect
         * @return
         */
        public String formatToCaller(boolean redirect)
        {
            if (!Utils.isSet(returnPoint))
            {
                return null;
            }
            return formatURL(getPageURL(ApplicationPagesNamesEnum.valueOf(returnPoint)), redirect);
        }
    
        /**
         * Formatage de l'URL avec ou sans redirection
         * 
         * @param url
         * @param redirect
         * @return
         */
        public String formatURL(String url, boolean redirect)
        {
            if (!Utils.isSet(url))
            {
                return null;
            }
            if (redirect)
            {
                return addURLParameter(new StringBuilder(url), REDIRECT_PART, true).toString();
            }
            return url;
        }
    
        /**
         * Ajout d'un paramètre à l'URL
         * 
         * @param url
         * @param attributeName
         * @param attributeValue
         * @return
         */
        public StringBuilder addURLParameter(StringBuilder url, String attributeName, Object attributeValue)
        {
            if (!Utils.isSet(url))
            {
                return null;
            }
    
            if (!Utils.isSet(attributeName))
            {
                return url;
            }
    
            if (url.indexOf("?") == -1)
            {
                url.append("?");
            }
            else
            {
                url.append("&");
            }
    
            url.append(attributeName).append("=");
    
            if (Utils.isSet(attributeValue))
            {
                url.append(attributeValue.toString());
            }
    
            return url;
        }
    
        /**
         * Formatage de l'URL standard d'appel d'un manager
         * 
         * @param url
         * @param redirect
         * @param returnPointURL
         * @param uid
         * @return
         */
        public String formatStandardCallURL(String url, boolean redirect, String returnPointURL, Integer uid)
        {
            StringBuilder sb = new StringBuilder();
            sb.append(url);
    
            if (redirect)
            {
                addURLParameter(sb, REDIRECT_PART, true);
            }
    
            if (returnPointURL != null)
            {
                addURLParameter(sb, RETURN_POINT_PART, returnPointURL);
            }
    
            if (uid != null)
            {
                addURLParameter(sb, UID_PART, uid);
            }
    
            return sb.toString();
        }
    
        /**
         * Navigation par programmation
         * 
         * @param url
         * @param redirect
         * @param returnPointURL
         * @param uid
         */
        public void navigateTo(String url, boolean redirect, String returnPointURL, Integer uid)
        {
            ConfigurableNavigationHandler navigationHandler = (ConfigurableNavigationHandler) getFacesContext()
                    .getApplication().getNavigationHandler();
            navigationHandler.performNavigation(formatStandardCallURL(url, redirect, returnPointURL, uid));
        }
    
        /**
         * Récupération de l'URL associée à l'enum
         * 
         * @param page
         * @return
         */
        public String getPageURL(ApplicationPagesNamesEnum page)
        {
            String url = getResourceBundleMessage(page.toString());
            return url;
        }
    
        /**
         * Envoi d'un message
         * 
         * @param text            : Texte ou identifiant de message dans le fichier
         *                        ApplicationResources.properties
         * @param severity        : Gravité du message
         * @param useFlashContext : Utilisation du contexte flash (dans le cadre d'une
         *                        redirection)
         */
        public void sendFacesMessage(String text, Severity severity, boolean useFlashContext)
        {
            FacesContext facesContext = getFacesContext();
    
            facesContext.getExternalContext().getFlash().setKeepMessages(useFlashContext);
    
            text = getResourceBundleMessage(text);
    
            if (severity == null)
            {
                severity = FacesMessage.SEVERITY_INFO;
            }
    
            FacesMessage facesMessage = new FacesMessage(severity, null, text);
    
            facesContext.addMessage(null, facesMessage);
        }
    
        /**
         * Envoi d'un message lié à une Exception
         * 
         * @param e               : L'Exception à traiter
         * @param useFlashContext : Utilisation du contexte flash (dans le cadre d'une
         *                        redirection)
         */
        public void sendFacesMessage(Exception e, boolean useFlashContext)
        {
            FacesContext facesContext = getFacesContext();
    
            facesContext.getExternalContext().getFlash().setKeepMessages(useFlashContext);
    
            if (e instanceof ValidationException)
            {
                for (CauseException cause : ((ValidationException) e).getCauses())
                {
                    FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, cause.toString(),
                            formatMessage(cause.getLibelle(), cause.getParameters()));
                    facesContext.addMessage(null, facesMessage);
                }
            }
            else
            {
                FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, e.toString(), e.toString());
                facesContext.addMessage(null, facesMessage);
            }
        }
    
        /**
         * Formatage d'un message avec d'éventuels paramètres
         * 
         * @param message    : Texte du message ou identifiant de message dans le
         *                   fichier ApplicationResources.properties
         * @param parameters : Tableau de paramètres ou null
         * @return Le message formaté
         */
        public String formatMessage(String message, Object[] parameters)
        {
            String formatted = message;
            int p1, p2 = 0;
            String value = "";
    
            try
            {
                if (getResourceBundle().containsKey(message))
                {
                    formatted = getResourceBundle().getString(message);
                }
    
                while ((p1 = formatted.indexOf("{")) != -1)
                {
                    p2 = formatted.indexOf("}", p1);
                    int idx = Integer.valueOf(formatted.substring(p1 + 1, p2));
                    if (idx < parameters.length)
                    {
                        value = parameters[idx].toString();
                    }
                    else
                    {
                        value = "";
                    }
                    value = getResourceBundleMessage(value);
                    formatted = formatted.substring(0, p1) + value + formatted.substring(p2 + 1);
                }
            }
            catch (Exception e)
            {
            }
            return formatted;
        }
    }
    
  2. AbstractRechercheDataModel
    Classe qui centralise toutes les fonctions utiles à la gestion d’une liste pour un composant dataTable de Primefaces.

    AbstractRechercheDataModel
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    public class AbstractRechercheDataModel<T> extends LazyDataModel<T>
    {
        public static final long serialVersionUID = 1;
        @Override
        public T getRowData(String rowKey)
        {
            for (T dto : getWrappedData())
            {
                if (dto.hashCode() == Integer.valueOf(rowKey))
                {
                    return dto;
                }
            }
            return null;
        }
        @Override
        public Object getRowKey(T object)
        {
            return object == null ? null : object.hashCode();
        }
    }
    
  3. AbstractRechercheOptions
    Classe qui centralise toutes les propriétés et fonctions utiles pour les options d’un dataTable de Primefaces.

    AbstractRechercheOptions
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    22.
    23.
    24.
    25.
    26.
    27.
    28.
    29.
    30.
    31.
    32.
    33.
    34.
    35.
    36.
    37.
    38.
    39.
    40.
    41.
    42.
    43.
    44.
    45.
    46.
    47.
    48.
    49.
    50.
    51.
    52.
    53.
    54.
    55.
    56.
    57.
    58.
    59.
    60.
    61.
    62.
    63.
    64.
    65.
    66.
    67.
    68.
    69.
    70.
    public class AbstractRechercheOptions implements Serializable
    {
        public static final long serialVersionUID = 1;
        private int first;
        private int pageSize = 20;
        private String sortField;
        private String sortOrder;
        private boolean changed;
        
        public void checkChanged(Object object1, Object object2)
        {
            if (Utils.isDifferent(object1, object2))
            {
                setChanged(true);
            }
        }
        
        public boolean isChanged()
        {
            return changed;
        }
    
        public void setChanged(boolean changed)
        {
            this.changed = changed;
        }
    
        public int getFirst()
        {
            return first;
        }
    
        public void setFirst(int first)
        {
            this.first = first;
        }
    
        public int getPageSize()
        {
            return pageSize;
        }
    
        public void setPageSize(int pageSize)
        {
            this.pageSize = pageSize;
        }
    
        public String getSortField()
        {
            return sortField;
        }
    
        public void setSortField(String sortField)
        {
            this.sortField = sortField;
        }
    
        public String getSortOrder()
        {
            return sortOrder;
        }
        public void setSortOrder(String sortOrder)
        {
            this.sortOrder = sortOrder;
        }
        public void forceLoad(AjaxBehaviorEvent event)
        {
            setChanged(true);
        }
    }
    
  4. LocaleManager
    Classe qui permet de gérer la langue de l’interface de l’application.
    Dans la méthode postConstruct, on récupère toutes les langues gérées par l’application et définies dans le fichier faces-config.xml.

    LocaleManager
    Sélectionnez
    @Named
    @SessionScoped
    public class LocaleManager implements Serializable
    {
        public static final long serialVersionUID = 1;
        private List<Locale> locales = new ArrayList<>();
        private Locale locale;
    
        @PostConstruct
        public void postConstruct()
        {
            Iterator<Locale> it = FacesContext.getCurrentInstance().getApplication().getSupportedLocales();
            while (it.hasNext())
            {
                locales.add(it.next());
            }
        }
        
        public Locale getLocale()
        {
            if (locale == null)
            {
                try
                {
                    FacesContext facesContext = FacesContext.getCurrentInstance();
                    locale = facesContext.getViewRoot().getLocale();
                    if (locale == null)
                    {
                        locale = facesContext.getExternalContext().getRequestLocale();
                    }
                }
                catch (Exception e) {}
                if (locale == null)
                {
                    locale = Locale.FRANCE;
                }
            }
            return locale;
        }
    
        public void setLocale(Locale locale)
        {
            this.locale = locale;
        }
    
        public List<Locale> getLocales()
        {
            return locales;
        }
    
        public void setLocales(List<Locale> locales)
        {
            this.locales = locales;
        }
    
        public void changeLocale(String language)
        {
            changeLocale(language, language.toUpperCase());
        }
        
        public void changeLocale(String language, String country)
        {
            Locale locale = new Locale(language, country);
            setLocale(locale);
        }
    
        public ResourceBundle getResourceBundle()
        {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            return facesContext.getApplication().getResourceBundle(facesContext, "msg"); 
        }
        public String getLocaleName(String key)
        {
            ResourceBundle bundle = getResourceBundle();
            if (bundle.containsKey(key))
            {
                return bundle.getString(key);
            }
            return key;
        }
    }
  5. Exemple d’utilisation dans le template :
Exemple de changement de Locale
Sélectionnez
<p:commandButton id="language" value="Langues"/>
<p:overlayPanel for="language" dynamic="true" style="background-color:#f0f0f0">
<ui:repeat value="#{localeManager.locales}" var="loc">
    <p:commandLink title="#{localeManager.getLocaleName(loc.language)}"
        actionListener="#{localeManager.changeLocale(loc.language, loc.country)}"
        oncomplete="window.location.replace(window.location)"
        style="text-decoration:none">
        <p:graphicImage value="/resources/images/#{loc.language}_#{loc.country}.png"/>
    </p:commandLink>
    <br/>
</ui:repeat>
</p:overlayPanel>

Pour les classes utilitaires, nous utiliserons deux classes :

  1. QueryUtils
    Fonctions liées à l’ajout conditionnel de filtre pour la requête JPQL.
  2. Utils
    Différentes fonctions transversales, ici les fonctions :
    isSet(Object)
    Permet de savoir si le paramètre est positionné.
    Pour un paramètre String, savoir s’il y a effectivement un contenu et pas une chaîne vide.
    isDifferent(Object, Object)
    Permet de déterminer si les deux paramètres sont différents.

III-E. Modèle JPA

Sans entrer dans le détail de JPA (qui nécessite un cours à lui tout seul), l’API de persistance utilise un jargon spécifique.

On parlera d’Entity pour la classe Java représentant l’enregistrement de la table de base de données (et ses liens éventuels) et d’EntityManager pour l’objet qui sert à manipuler ces entités.

L’Entity, dans sa plus simple expression, est une classe Java (pojo) avec des annotations spécifiques pour lier les propriétés de la classe aux colonnes de la table.

Exemple de la table Civilite
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
@Entity
@Table(name = "civilite", schema = "olivier")
public class Civilite implements java.io.Serializable, EntityUID
{
    private static final long serialVersionUID = 1;
    private Integer uid;
    private String abrege;
    private String libelle;

    public Civilite()
    {
        super() ;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "UID", unique = true, nullable = false)
    public Integer getUid()
    {
        return this.uid;
    }

    public void setUid(Integer uid)
    {
        this.uid = uid;
    }

    @Column(name = "ABREGE", nullable = false, length = 10)
    public String getAbrege()
    {
        return this.abrege;
    }

    public void setAbrege(String abrege)
    {
        this.abrege = abrege;
    }
    @Column(name = "LIBELLE", nullable = false, length = 64)
    public String getLibelle()
    {
        return this.libelle;
    }
    public void setLibelle(String libelle)
    {
        this.libelle = libelle;
    }
}

@Entity permet de qualifier la classe comme étant une entité JPA.

@Table permet de faire le lien avec la table de base de données.

@Column permet de qualifier la colonne cible dans la table.

@Id permet de désigner la clé primaire de l’enregistrement.

@GeneratedValue sert à préciser le type de génération automatique de la valeur.

JPA s’appuie sur un fichier de configuration nommé persistence.xml.

persistence.xml
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
    xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">

    <persistence-unit name="MonProjetEJBPU">

        <jta-data-source>java:/MySQLDS</jta-data-source>
        <properties>
            <property name="org.hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect" />
        </properties>
    </persistence-unit>
</persistence>

Ce fichier permet de paramétrer la ou les unités de persistance de l’application.

Il fait le lien avec la source de données (définie dans le serveur d’application), le type de gestionnaire de transaction (ici JTA) ainsi que des propriétés additionnelles liées à l’implémentation utilisée (ici Hibernate).

Le nom de l’unité de persistance sera dans notre cas ‘MonProjetEJBPU’, ce nom sera utilisé dans les EJB pour récupérer l’EntityManager.

Référencement
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
@Stateless
public class MonProjetFacade implements MonProjetFacadeLocal
{
    @PersistenceContext(name = "MonProjetEJBPU")
    private EntityManager entityManager;
    ...
}

Pour plus de détail, je vous invite à consulter les tutoriels suivants :

https://www.jmdoudoux.fr/java/dej/chap-jpa.htm

https://tahe.developpez.com/java/jpa/?page=page

III-F. La couche d’accès aux données (EJB métier)

Dans un projet Jakarta EE, l’accès aux données se fait par l’intermédiaire d’EJBs session (Enterprise Java Bean), stateless ou stateful.

L’EJB stateless ne conserve pas d’état entre deux appels.

Pour l’EJB stateful, l’état est conservé entre plusieurs appels.

Dans le cas d’une application web, je vous conseille d’utiliser un EJB stateless.

Un EJB peut être appelé localement ou à distance.

Dans le cas d’un appel local, le passage des paramètres se fait par référence (plus rapide). Dans le cas d’un appel distant (autre jvm), le passage des paramètres se fait par valeur (plus lent du fait de la sérialisation/désérialisation et des couches traversées).

Si la couche EJB et la couche web s’exécutent sur des serveurs dédiés, vous n’aurez d’autre choix que de passer par des appels distants.

Si, comme dans notre exemple, les deux couches sont exécutées sur le même serveur, je vous conseille l’appel local.

La mise en œuvre va dépendre de l’interface utilisée.

Pour un appel distant, on utilisera l’interface avec l’annotation @Remote, pour un appel local, on utilisera l’interface avec l’annotation @Local.

Si les méthodes sont les mêmes, qu’elles soient appelées localement ou à distance, on peut utiliser l’héritage entre les interfaces pour n’avoir qu’une liste de méthodes à gérer.

Exemple
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
@Local
public interface MonProjetFacadeLocal
{
    public QueryResult<DTOPersonne> recherchePersonnes(String prenom, String nom, Character sexe, boolean count, int first, int pageSize, String sortField, String sortOrder);
    public DTOPersonne getPersonne(Integer uid);
    public DTOPersonne saveOrUpdatePersonne(DTOPersonne dto) throws Exception;
    public void deletePersonne(Integer uid) throws Exception;
    
    public QueryResult<DTOCivilite> rechercheCivilites(String abreviation, String libelle,     boolean count, int first, int pageSize, String sortField, String sortOrder);
    public DTOCivilite getCivilite(Integer uid);
    public QueryResult<DTOTelephoneType> rechercheTelephoneTypes(boolean count, int first, int pageSize, String sortField, String sortOrder);
    public DTOTelephoneType getTelephoneType(Integer uid);
}
@Remote
public interface MonProjetFacadeRemote extends MonProjetFacadeLocal
{
}

Petite précision qui a son importance :

Une application web s’exécute dans le conteneur web, l’EJB lui s’exécute dans un conteneur d’EJB.

À chaque fois que vous passez du conteneur web au conteneur d’EJB, vous créez un contexte d’exécution dans le conteneur EJB (acquisition de l’EntityManager, donc d’une connexion, etc.), et à la sortie de l’EJB, la transaction sera validée (commit).

Ces appels ont un coût non négligeable, surtout s’ils sont faits dans une boucle ou séquentiellement pour faire des vérifications successives depuis un ManagedBean.

Il est largement recommandé d’utiliser le pattern « Façade » dans un environnement de ce type, et d’enchaîner les actions derrière la façade.

Exemple
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
@Named
public class MaClasse
{
    @EJB
    private MaFacadeLocal facade ;
    private MonBean monBean ;
...
    public boolean modifier()
    {
        if (facade.getUtilisateur().isAdmin())
        {
            facade.save(monBean) ;
            return true ;
        }
    }
    return false ;
}

Dans cet exemple, on a deux appels successifs à l’EJB ce qui créera deux initialisations du contexte EJB.

Il est préférable de faire comme ceci :

Exemple
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
@Named
public class MaClasse
{
    @EJB
    private MaFacadeLocal facade ;
    private MonBean monBean ;
...
    public boolean modifier()
    {
        return facade.modifier(monBean) ;
    }
}

où la méthode modifier(monBean) enchaînera les deux appels précédents.

III-F-1. Façade de l’application

La façade d’une application web est donc le point d’accès unique entre le conteneur web et le conteneur d’EJB. Toutes les méthodes nécessaires à la gestion des données issues de la base de données sont donc définies ici.

Elles peuvent juste faire le lien avec des méthodes d’EJB spécialisées, ici ejbPersonne, ejbTelephoneType et ejbCivilite ou être définies directement dans la façade (c’est le cas des méthodes de recherche de notre exemple d’application).

Façade
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
@Stateless
public class MonProjetFacade implements MonProjetFacadeLocal
{
    @PersistenceContext(name = "MonProjetEJBPU")
    private EntityManager entityManager;
    
    @EJB
    private PersonneFacadeLocal ejbPersonne;
    @EJB
    private TelephoneTypeFacadeLocal ejbTelephoneType;
    @EJB
    private CiviliteFacadeLocal ejbCivilite;
…
}

Les données utilisées par l’application web sont définies sous la forme de DTO (Data Transfer Object). Certains argueront que c’est une duplication des classes Entity inutiles, personnellement, je trouve que l’isolation que ça entraîne par rapport au modèle de données et les possibilités de redéfinition de certains types obsolètes issus de vieilles bases de données sont des avantages suffisants pour continuer à utiliser ce pattern. De plus, l’application peut n’utiliser qu’une partie des données du modèle, le DTO sera l’exact reflet de ce que l’application consomme. Dernier intérêt, avec le pattern DTO, fini les erreurs liées au lazy loading de sous-entités dans le conteneur de servlet.

La conversion des classes Entity vers les classes DTO (et inversement) se fait par des méthodes spécialisées, personne2DTOPersonne, dtoPersonne2Personne, telephoneType2DTOTelephoneType, dtoTelephoneType2TelephoneType, etc.

Les EJB spécialisés manipulant des Entités, la conversion se fait dans les deux sens par les méthodes de la façade.

Exemple
Sélectionnez
1.
2.
3.
4.
5.
6.
public DTOPersonne saveOrUpdatePersonne(DTOPersonne dtoPersonne) throws Exception
{
    Personne personne = dtoPersonne2Personne(dtoPersonne);
    personne = ejbPersonne.saveOrUpdate(personne);
    return personne2DTOPersonne(personne);
}

III-F-2. EJB spécialisés

Dans l’application type, la gestion des différentes entités est confiée à des EJB spécialisés qui étendront la classe abstraite AbstractFacade.

Cette dernière inclut toutes les propriétés communes à l’interface DB, avec en particulier l’objet EntityManager.

Elle définit également toutes les méthodes de gestion d’une classe Entity :

  • T get(Integer uid)
    Récupération de l’entity par son UID (clé primaire incrémentale).
  • void delete(T entity)
    Suppression d’une entité.
  • void delete(Integer uid)
    Suppression d’une entité par son UID.
  • T saveOrUpdate(T entity)
    Sauvegarde ou modification d’une entité.
  • void check(T entity, PersistenceModeEnum mode)
    Méthode standard de contrôle d’une entité en fonction des annotations des attributs.

La mise en œuvre est très simple, il suffit de créer une classe étendant AbstractFacade en précisant le type de l’entité gérée.

Exemple
Sélectionnez
1.
2.
3.
4.
5.
@Stateless
public class PersonneFacade extends AbstractFacade<Personne> implements PersonneFacadeLocal
{
    ...
}

En cas de besoin, il suffira de redéfinir la méthode pour l’adapter à la demande spécifique.

III-F-3. Requête variable d’extraction d’enregistrements

Pour exécuter des recherches en JPQL, il existe plusieurs façons de faire et d’objets dédiés à cet effet comme :

  • Query natif
    entityManager.createNativeQuery(String query)
  • Query JPQL
    entityManager.createQuery(String query)
  • Query nommé 
    entityManager.createNamedQuery(String name)
  • Criteria
    entityManager.getCriteriaBuilder()

Personnellement, dans un contexte web, j’ai une préférence pour le query JPQL, avec l’aide de la classe utilitaire QueryUtils pour simplifier la création et l’exploitation de l’objet Query.

Exemple
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
public QueryResult<DTOPersonne> recherchePersonnes(String prenom, String nom, Character genre, boolean count, int first, int pageSize, String sortField, String sortOrder)
{
    QueryResult<DTOPersonne> result = new QueryResult<>();
    StringBuilder sb = new StringBuilder();

    if (count)
    {
        StringBuilder sbCount = new StringBuilder(sb);
        sbCount.append("select count(a) from Personne a where 1=1");
        QueryUtils.addQueryParameter(sbCount, "a.genre", "genre", "and", "=", genre);
        QueryUtils.addQueryParameter(sbCount, "a.prenom", "prenom", "and",     "like", prenom);
        QueryUtils.addQueryParameter(sbCount, "a.nom", "nom", "and", "like", nom);

        Query queryCount = entityManager.createQuery(sbCount.toString());

        QueryUtils.setQueryParameter(queryCount, "genre", genre);
        QueryUtils.setQueryParameter(queryCount, "prenom", prenom);
        QueryUtils.setQueryParameter(queryCount, "nom", nom);

        result.setTotalRecords((Long) queryCount.getSingleResult());
    }
    sb.append("select a from Personne a where 1=1");

    QueryUtils.addQueryParameter(sb, "a.genre", "genre", "and", "=", genre);
    QueryUtils.addQueryParameter(sb, "a.prenom", "prenom", "and", "like", prenom);
    QueryUtils.addQueryParameter(sb, "a.nom", "nom", "and", "like", nom);

    Query query = entityManager.createQuery(sb.toString());

    QueryUtils.setQueryParameter(query, "genre", genre);
    QueryUtils.setQueryParameter(query, "prenom", prenom);
    QueryUtils.setQueryParameter(query, "nom", nom);

    if (first > 0)
    {
        query.setFirstResult(first);
    }
    if (pageSize > 0)
    {
        query.setMaxResults(pageSize);
    }
    for (Personne personne : (List<Personne>) query.getResultList())
    {
        result.getRecords().add(personne2DTOPersonne(personne));
    }
    return result;
}

Cet exemple montre la mise en œuvre des doubles requêtes nécessaires à l’objet paginator d’un dataTable de Primefaces.

On a besoin de connaître le nombre total d’enregistrements répondant aux critères de recherche (paramètre count = true) et d’extraire les n enregistrements d’un bloc à partir d’une position donnée.

L’objet QueryUtils permet de simplifier la syntaxe de création d’une chaîne de requête, si le paramètre est null ou vide, l’argument est ignoré, sinon, il est pris en compte.

Exemple
Sélectionnez
QueryUtils.addQueryParameter(sb, "a.genre", "genre", "and", "=", genre);

Si genre est non null, on ajoute à sb (le StringBuilder représentant la requête) la condition pour la propriété de l’entité ‘a.genre’ avec comme nom de paramètre ‘genre’ et comme comparateur ‘=’.

À l’arrivée, on aura donc :

Exemple
Sélectionnez
1.
2.
select a from Personne a where 1=1         // partie commune
 and a.genre = :genre

Après avoir créé l’objet Query, la méthode

Exemple
Sélectionnez
1.
QueryUtils.setQueryParameter(query, "genre", genre);

ajoutera le paramètre si celui-ci est non null et non vide.

III-F-4. Passage des erreurs de l’EJB à l’IHM

Laisser les EJB gérer les contrôles d’intégrité des données me semble être une bonne pratique, la couche métier est censée être la barrière ultime.

Ce principe a le désavantage d’envoyer des données issues de la page alors que les composants graphiques auraient été capables de faire certains contrôles basiques.

L’autre problème est que l’EJB ne peut envoyer qu’une seule Exception en cas d’erreur et elle devrait contenir les explications sur les problèmes rencontrés.

Dans notre application, la classe ValidationException est l’objet qui informera d’une erreur de validation. Cette ValidationException contient une liste de CauseException, laquelle définit de manière détaillée l’erreur rencontrée.

Exemple de contrôle
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
ValidationException ve = new ValidationException() ;

/*
 * Test de l'unicité du code
 */
if (Utils.isSet(entity.getAbrege()))
{
    StringBuilder sb = new StringBuilder();
    sb.append("select count(a) from Civilite a where upper(a.abrege) = upper(:abrege)");
    
    QueryUtils.addQueryParameter(sb, "a.uid", "uid", "and", "<>", entity.getUid());
    
    Query query = getEntityManager().createQuery(sb.toString());
    
    query.setParameter("abrege", entity.getAbrege());
    QueryUtils.setQueryParameter(query, "uid", entity.getUid());
    Number count = (Number)query.getSingleResult();
    if (count.intValue() > 0)
    {
        ve.addCauseException(new CauseException("exception.duplicate", new Object[]{"civilite.abrege", entity.getAbrege()}));
    }
}
if (ve.hasErrors())
{
    throw ve ;
}

Le fichier properties de l’application contiendra la définition du message exception.duplicate :

Exemple
Sélectionnez
exception.duplicate={0} : la valeur {1} a déjà été attribuée

qui contient deux paramètres représentant le libellé du champ et la valeur conflictuelle du code.

Du côté IHM, le managedBean devra convertir l’exception en messages compréhensibles par le composant <p :growl> de Primefaces.

Notre classe AbstractManager contient une méthode sendFacesMessage(Exception, boolean) qui se chargera de cette opération.

Exemple
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
public void sendFacesMessage(Exception e, boolean useFlashContext)
{
    FacesContext facesContext = getFacesContext();

    facesContext.getExternalContext().getFlash().setKeepMessages(useFlashContext);
    if (e instanceof ValidationException)
    {
        for (CauseException cause : ((ValidationException)e).getCauses())
        {
            FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, cause.toString(), formatMessage(cause.getLibelle(), cause.getParameters()));
            facesContext.addMessage(null, facesMessage);
        }
    }
    else
    {
        FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, e.toString(), e.toString());
        facesContext.addMessage(null, facesMessage);
    }
}

La méthode formatMessage(…) s’occupe de rechercher la définition du message et de remplacer les paramètres par leur valeur dans la liste des paramètres de l’objet CauseException.

Image non disponible

III-G. Le template de l’application

Ce template contient deux parties, l’en-tête et le corps de la page. Il centralise toutes les références des pages comme les scripts, les css, les messages, etc.

Template de l'application
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:p="http://primefaces.org/ui">
     
    <h:head>       

            <h:outputStylesheet library="css" name="omega-specific.css"/>
            <h:outputScript library="javascript" name="monProjet.js"/>
            <f:loadBundle var="msg" basename="ApplicationResources"/>

        <title>Mon projet</title> 
    </h:head>
 
    <h:body>
    
        <div id="top">
            <h:form id="formTop">
                <p:growl id="growl" keepAlive="true" showDetail="true" showSummary="false">
                    <p:autoUpdate/>
                </p:growl>
                
                <ui:insert name="top">
                        <span style="cursor:pointer" title="#{msg['navigation.accueil']}" onclick="contextRoot('#{defaultManager.contextRoot}')">
                        <h:outputText value="#{msg['application.titre']}" styleClass="applicationTitle"/>
                        </span>
                    <p:clock />
                </ui:insert>

            <p:menubar>
                <p:submenu label="#{msg['menu.personne']}">
                    <p:menuitem value="#{msg['menu.personne.rechercher']}" action="#{personneRechercheManager.display()}"/>
                </p:submenu>
                <p:submenu label="#{msg['menu.administration']}">
                    <p:menuitem value="#{msg['menu.administration.civilites']}" action="#{administrationCivilitesManager.display()}"/>
                    <p:menuitem value="#{msg['menu.administration.types.telephones']}" action="#{administrationTelephonesManager.display()}"/>
                </p:submenu>
            </p:menubar>    
            </h:form>
    </div>
        
    <div id="content">
        <div class="pageTitle"><ui:insert name="title"/></div>
      <ui:insert name="content"/>
    </div>
    </h:body>
</html>

Tout en étant extrêmement simple, voire simpliste, le template illustre la manière d’utiliser facelet.

III-H. Le module de recherche de personnes

La page de recherche permet d’appliquer des critères de recherche et affiche la liste des personnes répondant à ces critères.

Image non disponible

Nous utiliserons ici trois ManagedBean pour réaliser ce module. On pourrait le faire avec moins mais dans ma conception des choses, trois, c’est mieux.

Les beans et leur fonction :

  • PersonneRechercheManager
    C’est le contrôleur de la page, son scope peut être choisi entre @RequestScoped et éventuellement @ViewScoped.
  • PersonneRechercheDataModel
    La classe chargée de fournir les données de la liste. Son scope sera @ViewScoped, ce qui permettra de recharger la liste de manière simple lorsque nous reviendrons à la liste après avoir affiché le détail d’une personne.
  • PersonneRechercheOptions
    La classe chargée de conserver les critères de recherche. Son scope sera donc @SessionScoped, il me paraît naturel de retrouver les derniers critères utilisés lorsqu’on revient à ce module (mais ça se discute, libre à vous de faire autrement).

III-H-1. PersonneRechercheManager

Le contrôleur de page se charge d’afficher la page xhtml adéquate et d’appeler le module de gestion du détail d’une personne dans deux modes :

  • création d’une nouvelle personne ;
  • modification d’une personne.
Manager
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
@Named
@ViewScoped
public class PersonneRechercheManager extends AbstractManager
{
    public static final long serialVersionUID = 1;
    
    public String display()
    {
        return formatURL(getPageURL(ApplicationPagesNamesEnum.PERSONNE_RECHERCHE), true);
    }
    
    public void onRowClick(SelectEvent event)
    {
        DTOPersonne dtoPersonne = (DTOPersonne)event.getObject();
        if (dtoPersonne != null)
        {
            navigateTo(getPageURL(ApplicationPagesNamesEnum.PERSONNE_DETAIL), true, ApplicationPagesNamesEnum.PERSONNE_RECHERCHE.name(), dtoPersonne.getUid());
        }
    }
    public void create()
    {
        navigateTo(getPageURL(ApplicationPagesNamesEnum.PERSONNE_DETAIL), true, ApplicationPagesNamesEnum.PERSONNE_RECHERCHE.name(), null);
    }
}

III-H-2. PersonneRechercheOptions

Ce managed bean s’occupe de stocker les critères de recherche.

Lors de l’affectation d’une valeur à un critère, la méthode checkChanged, héritée de AbstractRechercheOptions, teste si la valeur a changé et monte un flag.

Options
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
@Named
@SessionScoped
public class PersonneRechercheOptions extends AbstractRechercheOptions
{
    private static final long serialVersionUID = 1;
    private Character genre;
    private String prenom;
    private String nom;
    private DTOPersonne selected;

    public PersonneRechercheOptions()
    {
        super();
    }

    public Character getGenre()
    {
        return genre;
    }

    public void setGenre(Character genre)
    {
        checkChanged(genre, this.genre);
        this.genre = genre;
    }

    public String getPrenom()
    {
        return prenom;
    }

    public void setPrenom(String prenom)
    {
        checkChanged(prenom, this.prenom);
        this.prenom = prenom;
    }

    public String getNom()
    {
        return nom;
    }

    public void setNom(String nom)
    {
        checkChanged(nom, this.nom);
        this.nom = nom;
    }
    public DTOPersonne getSelected()
    {
        return selected;
    }
    public void setSelected(DTOPersonne selected)
    {
        this.selected = selected;
    }
}

III-H-3. PersonneRechercheDataModel

Ce managed bean s’occupe de fournir les données au dataTable de la page.

Il est conçu en rapport avec le composant dataTable de Primefaces paramétré en chargement tardif, avec un paginator pour charger les enregistrements par bloc de n enregistrements.

DataModel
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
@Named
@ViewScoped
public class PersonneRechercheDataModel extends AbstractRechercheDataModel<DTOPersonne>
{
    private static final long serialVersionUID = 1;
    @Inject
    private PersonneRechercheOptions options;
    @EJB
    private MonProjetFacadeLocal facade;

    @PostConstruct
    public void postConstruct()
    {
        options.setChanged(true);
    }
    
    @Override
    public List<DTOPersonne> load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters)
    {
        boolean count = options.isChanged() ? true : false;
        options.setFirst(first);
        options.setPageSize(pageSize);
        options.setSortField(sortField);
        options.setSortOrder(sortOrder == null ? null : sortOrder.name());
        
        QueryResult<DTOPersonne> result = facade.recherchePersonnes(QueryUtils.replaceJoker(options.getPrenom()), QueryUtils.replaceJoker(options.getNom()), options.getGenre(), count, options.getFirst(), options.getPageSize(), options.getSortField(), options.getSortOrder());
        if (count)
        {
            setRowCount(result.getTotalRecords().intValue());
            options.setChanged(false);
        }
        return result.getRecords();
    }
}

La méthode load est appelée automatiquement lorsque la page est chargée.

L’objet lié au paginator a besoin de savoir combien d’enregistrements au total correspondent aux critères de recherche ce qui oblige la méthode recherchePersonne à faire deux requêtes :

  1. Compter le nombre d’enregistrements.
  2. Fournir les enregistrements pour la page en cours.

Comme ce traitement est gourmand en ressources, autant ne le faire qu’en cas de besoin, dans notre cas, lorsque les critères changent, ou la première fois.

Les critères de recherche sont stockés dans le managed bean PersonneRechercheOptions de scope session, celui-ci est injecté via l’annotation @Inject.

La façade de l’EJB de l’application est quant à elle récupérée via l’annotation @EJB.

On utilise ici l’interface locale, l’EJB s’exécutant sur la même JVM que l’application web.

III-H-4. La page xhtml associée

Comme les pages utilisent un template, il n’y a que peu de choses à définir.

La balise <ui:composition> liste toutes les bibliothèques de composants utilisées et référence notre page template.

La page définie deux parties du template :

  • le titre (<ui:define name=’’title’’>)
  • le contenu (<ui:define name=’’content’’>)
Page
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
<ui:composition  xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:p="http://primefaces.org/ui"
    template="/templates/template.xhtml">
     
     <ui:define name="title">
         <h:outputText value="#{msg['menu.personne.rechercher']}"/>
     </ui:define>
     
     <ui:define name="content">
    
    <h:form id="form1" onkeypress="return avoidEnter(event)">
        <p:accordionPanel id="filtres">
            <p:tab closable="true" title="#{msg['filtre.titre']}">
                <p:panelGrid>
                    <p:row>
                        <p:column>
                            <h:outputLabel value="#{msg['filtre.genre']}" styleClass="label"/>
                        </p:column>
                        <p:column>
                            <p:selectOneMenu id="genre" value="#{personneRechercheOptions.genre}">
                                <f:selectItem itemValue="" itemLabel=""/>
                                <f:selectItem itemValue="H" itemLabel="#{msg['genre.homme']}"/>
                                <f:selectItem itemValue="F" itemLabel="#{msg['genre.femme']}"/>
                            </p:selectOneMenu>
                        </p:column>
                        <p:column>
                            <h:outputLabel value="#{msg['personne.recherche.filtre.prenom']}" styleClass="label"/>
                        </p:column>
                        <p:column>
                            <p:inputText value="#{personneRechercheOptions.prenom}" style="width:400px"/>
                        </p:column>
                        <p:column>
                            <h:outputLabel value="#{msg['personne.recherche.filtre.nom']}" styleClass="label"/>
                        </p:column>
                        <p:column>
                            <p:inputText value="#{personneRechercheOptions.nom}" style="width:400px"/>
                        </p:column>
                    </p:row>
                    
                    <p:row>
                        <p:column colspan="4">
                            <p:commandButton id="rechercher" value="#{msg['bouton.rechercher']}" update="form1:list1"/>
                            <p:commandButton id="creer"      value="#{msg['bouton.nouveau']}" actionListener="#{personneRechercheManager.create()}"/>
                        </p:column>
                    </p:row>
                </p:panelGrid>
            </p:tab>
        </p:accordionPanel>
        <p:dataTable id="list1" value="#{personneRechercheDataModel}" var="p" lazy="true"
            paginator="true" rows="#{personneRechercheOptions.pageSize}" rowsPerPageTemplate="10,20,40,80" paginatorPosition="top"
            selectionMode="single" selection="#{personneRechercheOptions.selected}"
            emptyMessage="#{msg['personne.recherche.vide']}">
             
             <p:ajax event="rowSelect" listener="#{personneRechercheManager.onRowClick}"/>
        
            <p:column headerText="#{msg['personne.recherche.colonne.civilite']}" style="width:80px;">
                <h:outputText value="#{p.civilite.libelle}"/>
            </p:column>
        
            <p:column headerText="#{msg['personne.recherche.colonne.prenom']}">
                <h:outputText value="#{p.prenom}"/>
            </p:column>
        
            <p:column headerText="#{msg['personne.recherche.colonne.nom']}">
                <h:outputText value="#{p.nom}"/>
            </p:column>
        
            <p:column headerText="#{msg['personne.recherche.colonne.codePostal']}" style="width:80px">
                <h:outputText value="#{p.codePostal}"/>
            </p:column>
        
            <p:column headerText="#{msg['personne.recherche.colonne.ville']}">
                <h:outputText value="#{p.ville}"/>
            </p:column>
        
            <p:column headerText="uid" style="width:50px; text-align:right">
                <h:outputText value="#{p.uid}"/>
            </p:column>
            
        </p:dataTable>
    
    </h:form>
     </ui:define>
</ui:composition>

Le contenu de la page est divisé en deux parties :

  1. La saisie des critères de filtre avec les boutons d’actions.
  2. La liste de personnes correspondant aux critères.

La sélection d’une personne de la liste se fait ici sur simple clic, on aurait pu utiliser le double-clic en modifiant l’événement ajax par ‘rowDblselect’.

Toutes les constantes de la page sont définies dans un fichier de propriétés ce qui permettra, le jour venu, de très simplement ajouter d’autres langues à l’interface.

À noter que si les noms des clés de propriétés avaient été définis sans point séparateur mais avec un ‘_‘, la syntaxe aurait pu être :

Exemple
Sélectionnez
#{msg.personne_recherche_colonne_ville}

III-I. Module de gestion du détail d’une personne

Ce module permet de gérer ou de supprimer toutes les données relatives à une personne.

Le chargement se fait via les paramètres d’URL :

  • rp = return point ;
  • uid = clé primaire à charger.
Image non disponible

III-I-1. PersonneManager

Manager
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
@Named
@ViewScoped
public class PersonneManager extends AbstractManager
{
    private static final long serialVersionUID = 1;
    private DTOPersonne personne;
    private Integer uid;

    @EJB
    private MonProjetFacadeLocal facade;

    public Integer getUid()
    {
        return uid;
    }

    public void setUid(Integer uid)
    {
        this.uid = uid;
        if (personne == null || !uid.equals(personne.getUid()))
        {
            personne = facade.getPersonne(uid);
        }
    }

    public DTOPersonne getPersonne()
    {
        if (personne == null)
        {
            personne = new DTOPersonne();
        }
        return personne;
    }

    public void setPersonne(DTOPersonne personne)
    {
        this.personne = personne;
    }

    public String display()
    {
        return formatURL(getPageURL(ApplicationPagesNamesEnum.PERSONNE_DETAIL), true);
    }
    
    public String saveOrUpdate()
    {
        try
        {
            facade.saveOrUpdatePersonne(personne);
            sendFacesMessage("Personne enregistrée", FacesMessage.SEVERITY_INFO, true);
            return formatToCaller(true);
        }
        catch (Exception e)
        {
            sendFacesMessage(e, false);            
        }
        return null;
    }
    
    public String delete()
    {
        try
        {
            facade.deletePersonne(personne.getUid());
            sendFacesMessage("Personne supprimée", FacesMessage.SEVERITY_INFO, true);
            return formatToCaller(true);
        }
        catch (Exception e)
        {
            sendFacesMessage("Erreur lors de la suppression de la personne", FacesMessage.SEVERITY_ERROR, false);            
        }
        return null;
    }
    public void addTelephone()
    {
        personne.getTelephones().add(new DTOPersonneTelephone());
    }
    public boolean autorisation(String action)
    {
        if (personne == null)
        {
            return false;
        }
        if ("delete".equals(action) && personne.getUid() != null)
        {
            return true;
        }
        if ("save".equals(action))
        {
            return true;
        }
        return false;
    }
}

III-I-2. La page xhtml associée

La balise <f:metadata> permet de lier le paramètre d’URL ‘rp’ à la propriété ‘returnPoint’ du bean personneManager ainsi que le paramètre d’URL ‘uid’ à la propriété ‘uid’ du bean.

Le setter setUid(Integer uid) se chargera, si besoin, de charger le DTOPersonne.

Page
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
<ui:composition  xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:p="http://primefaces.org/ui"
    template="/templates/template.xhtml">
     
     <ui:define name="title">
         <h:outputText value="#{msg['personne.detail.titre']}"/>
     </ui:define>
     
     <ui:define name="content">
     
     <f:metadata>
        <f:viewParam name="rp" value="#{personneManager.returnPoint}"/>     
        <f:viewParam name="uid" value="#{personneManager.uid}"/>
     </f:metadata>
    
    <h:form id="form1">
        <p:panel style="width:600px;">
            <f:facet name="footer">
                <span style="float:left">
                    <h:outputText value="#{msg['derniere.mise.a.jour']}" styleClass="label"/> 
                    <h:outputText value="#{personneManager.personne.majDate}" converter="datetimeConverter" style="margin-left:5px"/>
                </span>
                
                <p:commandButton value="#{msg['bouton.annuler']}" immediate="true" action="#{personneManager.formatToCaller(true)}"/>
                <p:commandButton value="#{msg['bouton.enregistrer']}" action="#{personneManager.saveOrUpdate()}"/>
                <p:commandButton value="#{msg['bouton.supprimer']}" action="#{personneManager.delete()}" rendered="#{personneManager.autorisation('delete')}"/>
            </f:facet>
            
            <p:panelGrid>
                <p:row>
                    <p:column>
                        <p:outputLabel for="civilite" value="#{msg['personne.civilite']}" styleClass="label required"/>
                    </p:column>
                    <p:column>
                        <p:selectOneMenu id="civilite" value="#{personneManager.personne.civilite}" required="false" converter="#{civiliteConverter}">
                            <f:selectItems value="#{civiliteRechercheDataModel.civilites}"  var="c" itemValue="#{c}" itemLabel="#{c.libelle}"/>
                        </p:selectOneMenu>
                    </p:column>
                    <p:column>
                        <p:outputLabel for="genre" value="#{msg['personne.genre']}" styleClass="label"/>
                    </p:column>
                    <p:column>
                        <p:selectOneMenu id="genre" value="#{personneManager.personne.genre}">
                            <f:selectItem itemValue="H" itemLabel="#{msg['personne.genre.homme']}"/>
                            <f:selectItem itemValue="F" itemLabel="#{msg['personne.genre.femme']}"/>
                        </p:selectOneMenu>
                    </p:column>
                </p:row>
                <p:row>
                    <p:column>
                        <p:outputLabel for="prenom" value="#{msg['personne.prenom']}" styleClass="label required"/>
                    </p:column>
                    <p:column colspan="3">
                        <p:inputText id="prenom" value="#{personneManager.personne.prenom}" required="false" style="width:400px"/>
                    </p:column>
                </p:row>
                <p:row>
                    <p:column>
                        <p:outputLabel for="nom" value="#{msg['personne.nom']}" styleClass="label required"/>
                    </p:column>
                    <p:column colspan="3">
                        <p:inputText id="nom" value="#{personneManager.personne.nom}" required="false" style="width:400px" 
                            onkeyup="this.value = value.toUpperCase()"/>
                    </p:column>
                </p:row>
                <p:row>
                    <p:column style="height:10px"></p:column>
                </p:row>
                <p:row>
                    <p:column>
                        <p:outputLabel for="adresse" value="#{msg['personne.adresse']}" styleClass="label"/>
                    </p:column>
                    <p:column colspan="3">
                        <p:inputText id="adresse" value="#{personneManager.personne.adresse1}" style="width:400px"/>
                    </p:column>
                </p:row>
                <p:row>
                    <p:column>
                    </p:column>
                    <p:column colspan="3">
                        <p:inputText value="#{personneManager.personne.adresse2}" style="width:400px"/>
                    </p:column>
                </p:row>
                <p:row>
                    <p:column>
                        <p:outputLabel for="codePostal" value="#{msg['personne.codePostal']}" styleClass="label"/>
                    </p:column>
                    <p:column colspan="3">
                        <p:inputText id="codePostal" value="#{personneManager.personne.codePostal}"/>
                    </p:column>
                </p:row>
                <p:row>
                    <p:column>
                        <p:outputLabel for="ville" value="#{msg['personne.ville']}" styleClass="label"/>
                    </p:column>
                    <p:column colspan="3">
                        <p:inputText id="ville" value="#{personneManager.personne.ville}" style="width:400px" 
                            onkeyup="this.value = value.toUpperCase()"/>
                    </p:column>
                </p:row>
            </p:panelGrid>
    
        
            <p:dataTable id="list1" value="#{personneManager.personne.telephones}" var="t" style="margin-top:20px">
                    
                <f:facet name="header">
                    #{msg['personne.telephones']}
                    <p:commandButton value="#{msg['bouton.ajouter']}" actionListener="#{personneManager.addTelephone()}" 
                        update="list1" style="margin-left:60px"/>
                </f:facet>
                <p:column headerText="#{msg['personne.telephone.type']}">
                    <p:selectOneMenu value="#{t.telephoneType}" converter="#{telephoneTypeConverter}" style="width:90%">
                        <f:selectItems value="#{telephoneTypeRechercheDataModel.types}" var="tt" itemValue="#{tt}" itemLabel="#{tt.libelle}"/>
                    </p:selectOneMenu>
                </p:column>
                <p:column headerText="#{msg['personne.telephone.numero']}">
                    <p:inputText value="#{t.numero}" style="width:90%"/>
                </p:column>
            </p:dataTable>
        </p:panel>
    </h:form>
     </ui:define>
</ui:composition>

Lorsqu’on utilise une liste d’objets comme source de données d’un composant comme dans cet exemple avec <p:selectOneMenu>, n’oubliez pas qu’en HTML, seules les chaînes de caractères existent.

Il convient donc de passer par un converter pour transformer l’identifiant de la ligne sélectionnée en objet.

III-J. Module de gestion des civilités

Ce module permet de gérer les civilités de l’application.

Image non disponible

Contrairement aux exemples précédents et au vu de la simplicité des données, la gestion se fait sur une seule page regroupant la liste de sélection et la boite de dialogue permettant d’ajouter ou de modifier un enregistrement.

III-J-1. CiviliteRechercheManager

Comme la liste des civilités est placée dans un scope Application (voir plus bas CiviliteRechercheDataModel), tous les utilisateurs partagent la même liste.

Il convient donc, lors d’une modification, de copier l’élément de la liste dans un objet tampon pour éviter de propager des données non validées.

Cette opération est effectuée par le listener updateRow(SelectEvent) lié à l’événement de sélection d’une ligne du dataTable.

Manager
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
@Named
@ViewScoped
public class CiviliteRechercheManager extends AbstractManager
{
    public static final long serialVersionUID = 1;
    private DTOCivilite civilite;
    @EJB
    private MonProjetFacadeLocal facade;
    @Inject
    private CiviliteRechercheDataModel dataModel;


    public DTOCivilite getCivilite()
    {
        return civilite;
    }

    public void setCivilite(DTOCivilite civilite)
    {
        this.civilite = civilite;
    }

    public String getTitre()
    {
        if (civilite == null)
        {
            return null;
        }
        if (civilite.getUid() == null)
        {
            return formatMessage("civilite.dialogue.titre.create", null);
        }
        return formatMessage("civilite.dialogue.titre.update", null);
    }

    public String display()
    {
        return formatURL(getPageURL(ApplicationPagesNamesEnum.CIVILITE_RECHERCHE), true);
    }

    public void create()
    {
        civilite = new DTOCivilite();
    }
    
    
    
    public void updateRow(SelectEvent event)
    {
        DTOCivilite dto = (DTOCivilite)event.getObject();
        civilite = new DTOCivilite(dto.getUid(), dto.getAbrege(), dto.getLibelle());
    }
    public void delete(Integer uid)
    {
        try
        {
            facade.deleteCivilite(uid);
            dataModel.reset();
            PrimeFaces.current().ajax().update("form1:list1");
        }
        catch (Exception e)
        {
            sendFacesMessage(e, false);
        }
    }
    public String saveOrUpdate()
    {
        try
        {
            civilite = facade.saveOrUpdateCivilite(civilite);
            dataModel.reset();
            PrimeFaces.current().executeScript("PF('createWidget').hide()");
            PrimeFaces.current().ajax().update("form1:list1");
        }
        catch (Exception e)
        {
            sendFacesMessage(e, false);
        }
        return null;
    }
}

III-J-2. CiviliteRechercheDataModel

DataModel
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
@Named
@ApplicationScoped
public class CiviliteRechercheDataModel implements Serializable
{
    public static final long serialVersionUID = 1;
    private List<DTOCivilite> civilites;
    @EJB
    private MonProjetFacadeLocal facade;
    public List<DTOCivilite> getCivilites()
    {
        if (civilites == null)
        {
            civilites = facade.rechercheCivilites(null, null, false, 0, 0, null,null).getRecords();
        }
        return civilites;
    }
    public void reset()
    {
        civilites = null;
    }
}

III-J-3. La page xhtml associée

Page
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
<ui:composition  xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:p="http://primefaces.org/ui"
    template="/templates/template.xhtml">
     
     <ui:define name="title">
         <h:outputText value="#{msg['menu.administration.civilites']}"/>
     </ui:define>
     
     <ui:define name="content">
    
    <h:form id="form1" onkeypress="return avoidEnter(event)">
    
        <p:commandButton id="creer" value="#{msg['bouton.nouveau']}" actionListener="#{civiliteRechercheManager.create()}" 
            update="createDialog" 
            oncomplete="PF('createWidget').show()"/>
        
        <div style="width:600px">
            <p:dataTable id="list1" value="#{civiliteRechercheDataModel.civilites}" var="c"
                paginator="true" rows="20" paginatorPosition="top"
                rowKey="#{c.uid}"
                selectionMode="single">
                
                <p:ajax event="rowSelect" listener="#{civiliteRechercheManager.updateRow}" update="createDialog" 
                    oncomplete="PF('createWidget').show()"/>
                 
                <p:column headerText="#{msg['civilite.recherche.abrege']}" style="width:100px">
                    <h:outputText value="#{c.abrege}"/>
                </p:column>
            
                <p:column headerText="#{msg['civilite.recherche.libelle']}" style="width:300px">
                    <h:outputText value="#{c.libelle}" />
                </p:column>
            
                <p:column headerText="uid" style="width:50px; text-align:right">
                    <h:outputText value="#{c.uid}"/>
                </p:column>
                
                <p:column style="width:30px; text-align:center">
                    <p:commandButton icon="ui-icon-trash" actionListener="#{civiliteRechercheManager.delete(c.uid)}"/>
                </p:column>
                
            </p:dataTable>
        </div>
            
    </h:form>
    
    
    <!-- Dialogue de création / modification d'un type de numéro de téléphone -->
    
    <p:dialog id="createDialog" widgetVar="createWidget"
        header="#{civiliteRechercheManager.getTitre()}" dynamic="true"
        closable="true" closeOnEscape="true" 
        appendTo="@(body)" modal="false">

        <h:form id="formDialog">        
        <p:panel styleClass="no-border">
            
            <p:panelGrid columns="2">
            
                <h:outputText value="#{msg['civilite.abrege']}" styleClass="label"/>
                <p:inputText value="#{civiliteRechercheManager.civilite.abrege}"/>
                
                <h:outputText value="#{msg['civilite.libelle']}" styleClass="label"/>
                <p:inputText value="#{civiliteRechercheManager.civilite.libelle}" style="width:300px"/>
            
            </p:panelGrid>
            
            <f:facet name="footer">
                <div style="text-align:right">
                    <p:commandButton id="save" value="#{msg['bouton.sauvegarder']}" 
                        actionListener="#{civiliteRechercheManager.saveOrUpdate()}" />
                </div>
            </f:facet>
            
        </p:panel>
        </h:form>
    </p:dialog>
     </ui:define>
</ui:composition>

III-K. Module de gestion des types de téléphones

Ce module permet de gérer les types de téléphones d’une personne.

Le principe est exactement le même que pour la gestion des civilités, la liste est en scope Application, partagée par tous les utilisateurs de l’application.

Image non disponible

Pour le détail des objets et de la page xhtml liée, je vous renvoie aux codes sources de l’application en pièce jointe.

IV. Conclusion et remerciements

Nous avons vu à quel point il était simple de réaliser une application web avec les standards EE8 et la bibliothèque de composants Primefaces.

Certaines problématiques n’ont pas été abordées ici, en particulier l’authentification JAAS et la propagation du Prinicipal à la couche EJB, le test des rôles autorisés par annotations, etc.

Le but étant de montrer quelque chose de suffisamment abouti tout en restant simple, ces aspects feront l’objet d’autres publications.

Je tiens à remercier f-leb pour sa relecture avisée et Mickael Baron pour son suivi dans la mise au point de ce document.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2019 Olivier BUTTERLIN. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.