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 :
Explications :
- 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.
- Les données envoyées par un formulaire sont affectées aux composants correspondants.
Si des convertisseurs sont nécessaires, ils sont appelés. - Les valeurs sont validées par les éventuels validators.
- Les données sont transférées au bean sous-jacent.
- 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é. - 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.
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.
<p
:
balise …
converter
=
"
#{telephoneTypeConverter}
"
>
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.
<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.
@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.
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.
2.
3.
<p
:
balise>
<f
:
validator
binding
=
"
#{telephoneTypeValidator}
"
/>
</p
:
balise>
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.
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.
@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.
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.
<h
:
commandLink
action
=
"/views/personne/recherche.xhtml"
value
=
"Action=chemin"
/>
<h
:
commandLink
action
=
"/views/personne/detail?faces-redirect=true&uid=1"
value
=
"Action=chemin"
/>
Là aussi, l’extension .xhtml est optionnelle.
Notez l’utilisation de & 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.
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 :
<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.xmlSélectionnez1.
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.xmlSélectionnez1.
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.xmlSélectionnez1.
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.xmlSélectionnez1.
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
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.
III-C. Scripts de création de la base de données (MySQL)▲
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 :
-
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.AbstractManagerSélectionnez1.
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
AbstractManagerimplements
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
StringgetContextRoot
(
){
if
(
contextRoot==
null
){
contextRoot=
"/"
+
getFacesContext
(
).getExternalContext
(
).getContextName
(
);}
return
contextRoot;}
public
StringgetReturnPoint
(
){
return
returnPoint;}
public
void
setReturnPoint
(
String returnPoint){
this
.returnPoint=
returnPoint;}
public
ResourceBundlegetResourceBundle
(
){
FacesContext facesContext=
FacesContext.getCurrentInstance
(
);return
getFacesContext
(
).getApplication
(
).getResourceBundle
(
facesContext,"msg"
);}
public
StringgetResourceBundleMessage
(
String key){
ResourceBundle bundle=
getResourceBundle
(
);if
(
bundle.containsKey
(
key)){
return
bundle.getString
(
key);}
return
key;}
public
FacesContextgetFacesContext
(
){
return
FacesContext.getCurrentInstance
(
);}
public
LocalegetLocale
(
){
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
StringformatToCaller
(
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
StringformatURL
(
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
StringBuilderaddURLParameter
(
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
StringformatStandardCallURL
(
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
StringgetPageURL
(
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
(
einstanceof
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
StringformatMessage
(
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;}
}
-
AbstractRechercheDataModel
Classe qui centralise toutes les fonctions utiles à la gestion d’une liste pour un composant dataTable de Primefaces.AbstractRechercheDataModelSélectionnez1.
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
TgetRowData
(
String rowKey){
for
(
T dto :getWrappedData
(
)){
if
(
dto.hashCode
(
)==
Integer.valueOf
(
rowKey)){
return
dto;}
}
return
null
;}
@Override
public
ObjectgetRowKey
(
T object){
return
object==
null
?null
: object.hashCode
(
);}
}
-
AbstractRechercheOptions
Classe qui centralise toutes les propriétés et fonctions utiles pour les options d’un dataTable de Primefaces.AbstractRechercheOptionsSélectionnez1.
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
AbstractRechercheOptionsimplements
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
StringgetSortField
(
){
return
sortField;}
public
void
setSortField
(
String sortField){
this
.sortField=
sortField;}
public
StringgetSortOrder
(
){
return
sortOrder;}
public
void
setSortOrder
(
String sortOrder){
this
.sortOrder=
sortOrder;}
public
void
forceLoad
(
AjaxBehaviorEvent event){
setChanged
(
true
);}
}
-
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.LocaleManagerSélectionnez@Named
@SessionScoped
public
class
LocaleManagerimplements
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
LocalegetLocale
(
){
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
ResourceBundlegetResourceBundle
(
){
FacesContext facesContext=
FacesContext.getCurrentInstance
(
);return
facesContext.getApplication
(
).getResourceBundle
(
facesContext,"msg"
);}
public
StringgetLocaleName
(
String key){
ResourceBundle bundle=
getResourceBundle
(
);if
(
bundle.containsKey
(
key)){
return
bundle.getString
(
key);}
return
key;}
}
- Exemple d’utilisation dans le template :
<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 :
- QueryUtils
Fonctions liées à l’ajout conditionnel de filtre pour la requête JPQL. - 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.
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.
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.
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 :
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.
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.
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 :
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).
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.
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.
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.
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.
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 :
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
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.
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 :
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.
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.
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.
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.
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.
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.
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.
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 :
- Compter le nombre d’enregistrements.
- 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"
>
)
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 :
- La saisie des critères de filtre avec les boutons d’actions.
- 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 :
#{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.
III-I-1. PersonneManager▲
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.
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.
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.
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▲
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▲
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.
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.