AngularJS et Spring 4 : Guide Pratique
AngularJS et Spring 4 : Guide Pratique
AngularJS / Spring 4
[Link]
1/315
Table des matières
1 INTRODUCTION......................................................................................................................................................................6
1.1 L'ARCHITECTURE DE L'APPLICATION.........................................................................................................................................6
1.2 LES OUTILS UTILISÉS.................................................................................................................................................................7
1.3 LES FONCTIONNALITÉS DE L'APPLICATION................................................................................................................................7
1.3.1 CRÉATION DE LA BASE DE DONNÉES..........................................................................................................................................8
1.3.2 MISE EN OEUVRE DU SERVEUR WEB / JSON..............................................................................................................................9
1.3.3 MISE EN OEUVRE DU CLIENT ANGULAR...................................................................................................................................11
2 LE SERVEUR SPRING 4.......................................................................................................................................................17
2.1 LA BASE DE DONNÉES...............................................................................................................................................................17
2.1.1 LA TABLE [MEDECINS].......................................................................................................................................................18
2.1.2 LA TABLE [CLIENTS]...........................................................................................................................................................19
2.1.3 LA TABLE [CRENEAUX]......................................................................................................................................................19
2.1.4 LA TABLE [RV]......................................................................................................................................................................20
2.2 INTRODUCTION À SPRING DATA...............................................................................................................................................20
2.2.1 LA CONFIGURATION MAVEN DU PROJET...................................................................................................................................21
2.2.2 LA COUCHE [JPA]..................................................................................................................................................................23
2.2.3 LA COUCHE [DAO]................................................................................................................................................................24
2.2.4 LA COUCHE [CONSOLE]...........................................................................................................................................................26
2.2.5 CONFIGURATION MANUELLE DU PROJET SPRING DATA..............................................................................................................29
2.2.6 CRÉATION D'UNE ARCHIVE EXÉCUTABLE..................................................................................................................................33
2.2.7 CRÉER UN NOUVEAU PROJET SPRING DATA.............................................................................................................................35
2.3 LE PROJET ECLIPSE DU SERVEUR............................................................................................................................................38
2.4 LA CONFIGURATION MAVEN....................................................................................................................................................38
2.5 LES ENTITÉS JPA....................................................................................................................................................................41
2.6 LA COUCHE [DAO].................................................................................................................................................................47
2.7 LA COUCHE [MÉTIER]..............................................................................................................................................................49
2.7.1 LES ENTITÉS...........................................................................................................................................................................49
2.7.2 LE SERVICE............................................................................................................................................................................50
2.8 LA CONFIGURATION DU PROJET...............................................................................................................................................53
2.9 LES TESTS DE LA COUCHE [MÉTIER]........................................................................................................................................54
2.10 LE PROGRAMME CONSOLE.....................................................................................................................................................57
2.11 INTRODUCTION À SPRING MVC............................................................................................................................................59
2.11.1 LE PROJET DE DÉMONSTRATION.............................................................................................................................................59
2.11.2 CONFIGURATION MAVEN.......................................................................................................................................................60
2.11.3 L'ARCHITECTURE D'UN SERVICE SPRING REST......................................................................................................................61
2.11.4 LE CONTRÔLEUR C...............................................................................................................................................................62
2.11.5 LE MODÈLE M.....................................................................................................................................................................63
2.11.6 CONFIGURATION DU PROJET..................................................................................................................................................63
2.11.7 EXÉCUTION DU PROJET.........................................................................................................................................................64
2.11.8 CRÉATION D'UNE ARCHIVE EXÉCUTABLE................................................................................................................................66
2.11.9 DÉPLOYER L'APPLICATION SUR UN SERVEUR TOMCAT.............................................................................................................68
2.11.10 CRÉER UN NOUVEAU PROJET WEB.......................................................................................................................................70
2.12 LA COUCHE [WEB].................................................................................................................................................................71
2.12.1 CONFIGURATION MAVEN.......................................................................................................................................................71
2.12.2 L'INTERFACE DU SERVICE WEB...............................................................................................................................................72
2.12.3 LE SQUELETTE DU CONTRÔLEUR [RDVMEDECINSCONTROLLER].............................................................................................78
2.12.4 LES MODÈLES DU SERVICE WEB.............................................................................................................................................80
2.12.5 LA CLASSE STATIC................................................................................................................................................................82
2.12.6 LA MÉTHODE [INIT] DU CONTRÔLEUR....................................................................................................................................83
2.12.7 L'URL [/GETALLMEDECINS]................................................................................................................................................83
2.12.8 L'URL [/GETALLCLIENTS]...................................................................................................................................................84
2.12.9 L'URL [/GETALLCRENEAUX/{IDMEDECIN}].........................................................................................................................85
2.12.10 L'URL [/GETRVMEDECINJOUR/{IDMEDECIN}/{JOUR}].......................................................................................................88
2.12.11 L'URL [/GETAGENDAMEDECINJOUR/{IDMEDECIN}/{JOUR}]..............................................................................................90
2.12.12 L'URL [/GETMEDECINBYID/{ID}].....................................................................................................................................92
2.12.13 L'URL [/GETCLIENTBYID/{ID}].........................................................................................................................................93
2.12.14 L'URL [/GETCRENEAUBYID/{ID}].....................................................................................................................................94
2.12.15 L'URL [/GETRVBYID/{ID}]...............................................................................................................................................95
2.12.16 L'URL [/AJOUTERRV]........................................................................................................................................................96
2.12.17 L'URL [/SUPPRIMERRV].....................................................................................................................................................99
2.12.18 CONFIGURATION DU SERVICE WEB.....................................................................................................................................101
[Link]
2/315
2.12.19 LA CLASSE EXÉCUTABLE DU SERVICE WEB.........................................................................................................................102
2.13 INTRODUCTION À SPRING SECURITY...................................................................................................................................104
2.13.1 CONFIGURATION MAVEN.....................................................................................................................................................105
2.13.2 LES VUES THYMELEAF.......................................................................................................................................................106
2.13.3 CONFIGURATION SPRING MVC...........................................................................................................................................109
2.13.4 CONFIGURATION SPRING SECURITY.....................................................................................................................................110
2.13.5 CLASSE EXÉCUTABLE..........................................................................................................................................................111
2.13.6 TESTS DE L'APPLICATION.....................................................................................................................................................111
2.13.7 CONCLUSION......................................................................................................................................................................113
2.14 MISE EN PLACE DE LA SÉCURITÉ SUR LE SERVICE WEB DES RENDEZ-VOUS..........................................................................114
2.14.1 LA BASE DE DONNÉES.........................................................................................................................................................114
2.14.2 LE NOUVEAU PROJET ECLIPSE DU [MÉTIER, DAO, JPA]......................................................................................................115
2.14.3 LES NOUVELLES ENTITÉS [JPA]..........................................................................................................................................116
2.14.4 MODIFICATIONS DE LA COUCHE [DAO]...............................................................................................................................117
2.14.5 LES CLASSES DE GESTION DES UTILISATEURS ET DES RÔLES..................................................................................................119
2.14.6 TESTS DE LA COUCHE [DAO].............................................................................................................................................121
2.14.7 CONCLUSION INTERMÉDIAIRE..............................................................................................................................................125
2.14.8 LE PROJET ECLIPSE DE LA COUCHE [WEB]...........................................................................................................................125
2.14.9 TESTS DU SERVICE WEB......................................................................................................................................................128
2.15 CONCLUSION........................................................................................................................................................................131
3 LE CLIENT ANGULAR JS..................................................................................................................................................133
3.1 RÉFÉRENCES DU FRAMEWORK ANGULAR JS.........................................................................................................................133
3.2 ARCHITECTURE DU CLIENT ANGULAR....................................................................................................................................133
3.3 LES VUES DU CLIENT ANGULAR.............................................................................................................................................136
3.4 CONFIGURATION DU PROJET ANGULAR.................................................................................................................................140
3.5 LA PAGE INITIALE DU CLIENT ANGULAR................................................................................................................................144
3.6 DÉCOUVERTE DE BOOTSTRAP................................................................................................................................................147
3.6.1 EXEMPLE 1..........................................................................................................................................................................147
3.6.2 EXEMPLE 2..........................................................................................................................................................................147
3.6.3 EXEMPLE 3..........................................................................................................................................................................148
3.6.4 EXEMPLE 4..........................................................................................................................................................................149
3.6.5 EXEMPLE 5..........................................................................................................................................................................150
3.6.6 EXEMPLE 6..........................................................................................................................................................................151
3.6.7 EXEMPLE 7..........................................................................................................................................................................152
3.6.8 EXEMPLE 8..........................................................................................................................................................................154
3.6.9 EXEMPLE 9..........................................................................................................................................................................155
3.6.10 CONCLUSION......................................................................................................................................................................157
3.7 DÉCOUVERTE D'ANGULAR JS................................................................................................................................................157
3.7.1 EXEMPLE 1 : LE MODÈLE MVC D'ANGULAR.........................................................................................................................158
3.7.2 EXEMPLE 2 : LOCALISATION DES DATES.................................................................................................................................161
3.7.3 EXEMPLE 3 : INTERNATIONALISATION DES TEXTES..................................................................................................................167
3.7.4 EXEMPLE 4 : UN SERVICE DE CONFIGURATION........................................................................................................................173
3.7.5 EXEMPLE 5 : PROGRAMMATION ASYNCHRONE........................................................................................................................177
3.7.6 EXEMPLE 6 : LES SERVICES HTTP........................................................................................................................................184
[Link] La vue V..........................................................................................................................................................................184
[Link] Le contrôleur C et le modèle M......................................................................................................................................186
[Link] Le contrôleur C...............................................................................................................................................................187
[Link] Le service [dao]..............................................................................................................................................................191
[Link] Tests de l'application - 1..................................................................................................................................................195
[Link] Modification du serveur web / JSON..............................................................................................................................198
[Link] Tests de l'application - 2..................................................................................................................................................200
3.7.7 EXEMPLE 7 : LISTE DES CLIENTS............................................................................................................................................203
[Link] La vue V..........................................................................................................................................................................203
[Link] Le contrôleur C et le modèle M......................................................................................................................................204
[Link] Modification du service web - 1.....................................................................................................................................206
[Link] Tests de l'application – 1.................................................................................................................................................207
[Link] Modification du service web – 2.....................................................................................................................................209
[Link] Tests de l'application – 2.................................................................................................................................................210
[Link] Utilisation d'une directive...............................................................................................................................................213
[Link] Tests de l'application – 3.................................................................................................................................................215
3.7.8 EXEMPLE 8 : L'AGENDA D'UN MÉDECIN..................................................................................................................................216
[Link] La vue V de l'application................................................................................................................................................216
[Link] Le formulaire...................................................................................................................................................................217
[Link]
3/315
[Link] Le contrôleur C...............................................................................................................................................................218
[Link] Affichage de l'agenda......................................................................................................................................................222
[Link] Modification du serveur web..........................................................................................................................................223
[Link] Utilisation de directives..................................................................................................................................................224
3.7.9 EXEMPLE 9 : CRÉER ET ANNULER DES RÉSERVATIONS.............................................................................................................225
[Link] La vue V de l'application................................................................................................................................................225
[Link] Le contrôleur C...............................................................................................................................................................227
[Link] Initialisation du contrôleur C..........................................................................................................................................227
[Link] Obtention de l'agenda......................................................................................................................................................229
[Link] Réservation d'un créneau horaire....................................................................................................................................229
[Link] Modification serveur.......................................................................................................................................................231
[Link] Tests................................................................................................................................................................................232
[Link] Suppression d'un rendez-vous.........................................................................................................................................234
[Link] Modification serveur.......................................................................................................................................................235
3.7.10 EXEMPLE 10 : CRÉER ET ANNULER DES RÉSERVATIONS - 2....................................................................................................236
[Link] La vue V de l'application..............................................................................................................................................236
[Link] Le contrôleur C.............................................................................................................................................................237
3.7.11 EXEMPLE 11 : UNE DIRECTIVE [SELECTENABLE2]................................................................................................................239
[Link] La vue V........................................................................................................................................................................239
[Link] Le code HTML de la vue..............................................................................................................................................239
[Link] La directive [selectEnable2]..........................................................................................................................................240
[Link] Le contrôleur C.............................................................................................................................................................240
[Link] Les tests.........................................................................................................................................................................241
3.7.12 EXEMPLE 12 : UNE DIRECTIVE [LIST]...................................................................................................................................241
[Link] La directive [list]...........................................................................................................................................................242
[Link] Le code HTML.............................................................................................................................................................243
[Link] Le contrôleur C.............................................................................................................................................................244
[Link] Les tests.........................................................................................................................................................................244
3.7.13 EXEMPLE 13 : MISE À JOUR DU MODÈLE D'UNE DIRECTIVE...................................................................................................244
[Link] Les vues V.....................................................................................................................................................................244
[Link] La page HTML.............................................................................................................................................................245
[Link] La directive [list2].........................................................................................................................................................246
[Link] Le contrôleur C.............................................................................................................................................................246
3.7.14 EXEMPLE 14 : LES DIRECTIVES [WAITING] ET [ERRORS]........................................................................................................250
[Link] Le nouveau code HTML...............................................................................................................................................250
[Link] La directive [waiting]....................................................................................................................................................251
[Link] La directive [errors]......................................................................................................................................................251
3.7.15 EXEMPLE 15 : NAVIGATION.................................................................................................................................................252
[Link] Les vues V de l'application...........................................................................................................................................252
[Link] Organisation du code....................................................................................................................................................253
[Link] Le conteneur des vues...................................................................................................................................................253
[Link] Le module de l'application............................................................................................................................................254
[Link] Le contrôleur du conteneur de vues..............................................................................................................................254
[Link] La barre de navigation..................................................................................................................................................255
[Link] La vue [/page1] et son contrôleur.................................................................................................................................257
[Link] Contrôle de la navigation..............................................................................................................................................257
3.7.16 CONCLUSION......................................................................................................................................................................258
3.8 LE CLIENT FINAL ANGULAR...................................................................................................................................................259
3.8.1 STRUCTURE DU PROJET.........................................................................................................................................................259
3.8.2 LES DÉPENDANCES DU PROJET..............................................................................................................................................259
3.8.3 LA PAGE MAÎTRE [[Link]]................................................................................................................................................260
3.8.4 LES VUES DE L'APPLICATION..................................................................................................................................................262
3.8.5 FONCTIONNALITÉS DE L'APPLICATION.....................................................................................................................................264
3.8.6 LE MODULE [[Link]]..........................................................................................................................................................269
3.8.7 LE CONTRÔLEUR DE LA PAGE MAÎTRE....................................................................................................................................270
3.8.8 GESTION DE LA TÂCHE ASYNCHRONE.....................................................................................................................................272
3.8.9 CONTRÔLE DE LA NAVIGATION...............................................................................................................................................273
3.8.10 LES SERVICES.....................................................................................................................................................................276
3.8.11 LES DIRECTIVES..................................................................................................................................................................278
3.8.12 LE CONTRÔLEUR [LOGINCTRL]............................................................................................................................................281
3.8.13 LE CONTRÔLEUR [HOMECTRL]............................................................................................................................................284
3.8.14 LE CONTRÔLEUR [AGENDACTRL]........................................................................................................................................286
3.8.15 LE CONTRÔLEUR [RESACTRL].............................................................................................................................................288
[Link]
4/315
3.8.16 LA GESTION DES LANGUES..................................................................................................................................................290
4 EXPLOITATION DE L'APPLICATION............................................................................................................................291
4.1 DÉPLOIEMENT DU SERVICE WEB SUR UN SERVEUR TOMCAT ..................................................................................................291
4.2 DÉPLOIEMENT DU CLIENT ANGULAR SUR LE SERVEUR TOMCAT...........................................................................................296
4.3 LES ENTÊTES CORS..............................................................................................................................................................299
4.4 DÉPLOIEMENT DU CLIENT ANGULAR SUR UNE TABLETTE ANDROID.......................................................................................300
4.5 DÉPLOIEMENT DU CLIENT ANGULAR SUR L'ÉMULATEUR D'UN SMARTPHONE ANDROID........................................................304
5 CONCLUSION...................................................................................................................................................................... 306
6 ANNEXES...............................................................................................................................................................................308
6.1 INSTALLATION DE STS (SPRING TOOL SUITE).......................................................................................................................308
6.2 INSTALLATION DE [WAMPSERVER]........................................................................................................................................309
6.3 INSTALLATION DE [WEBSTORM]............................................................................................................................................310
6.3.1 INSTALLATION DE [[Link]]..................................................................................................................................................311
6.3.2 INSTALLATION DE L'OUTIL [BOWER].......................................................................................................................................311
6.3.3 INSTALLATION DE [GIT]........................................................................................................................................................311
6.3.4 CONFIGURATION DE [WEBSTORM].........................................................................................................................................312
6.4 INSTALLATION D'UN ÉMULATEUR POUR ANDROID..................................................................................................................313
6.5 INSTALLATION DU PLUGIN CHROME [ADVANCED REST CLIENT]...........................................................................................314
[Link]
5/315
1 Introduction
Nous nous proposons ici d'introduire deux frameworks à l'aide d'un exemple de client / serveur :
• AngularJS utilisé pour le client. Pour simplifier, il sera noté Angular par la suite ;
• Spring 4 utilisé pour le serveur. Pour simplifier, il sera noté Spring par la suite ;
Les autres connaissances nécessaires seront introduites et expliquées au fur et à mesure de l'étude de cas.
Ce document n'est pas un cours et il est incomplet à bien des égards. Pour approfondir les deux frameworks, on pourra
utiliser les références suivantes :
• [ref1] : le livre " Pro AngularJS " écrit par Adam Freeman aux éditions Apress. C'est un excellent livre. Les codes
source des exemples de ce livre sont disponibles gratuitement à l'URL
[[Link] ;
• [ref2] : la documentation officielle d'Angular JS [[Link]
• [ref3] : le livre " Spring Data " chez o'Reilly [[Link] qui présente
l'utilisation du framework [Spring Data] pour l'accès aux données que ce soit des bases de données relationnelles ou pas
(NoSQL) ;
• [ref4] : le livre " Pro Spring3 " aux éditions Apress. C'est la version précédente de Spring 4 mais les principaux concepts
sont déjà là ;
• [ref5] : la documentation de référence de Spring 4 [[Link]
reference/pdf/[Link]].
Les sources qui ont nourri ce document sont celles citées ci-dessus plus l'indispensable [ [Link] pour les très
nombreuses séances de débogage.
[Link]
6/315
• en [1], un serveur web délivre des pages statiques à un navigateur. Ces pages contiennent une application AngularJS
construite sur le modèle MVC (Modèle – Vue – Contrôleur). Le modèle ici est à la fois celui des vues et celui du domaine
représenté ici par la couche [Services] ;
• l'utilisateur va interagir avec les vues qui lui sont présentées dans le navigateur. Ses actions vont parfois nécessiter
l'interrogation du serveur Spring 4 [2]. Celui-ci traitera la demande et rendra une réponse JSON (JavaScript Object
Notation) [3]. Celle-ci sera utilisée pour mettre à jour la vue présentée à l'utilisateur.
[Link]
7/315
• le serveur est contenu dans les dossiers [rdvmedecins-metier-dao-v2] et [rdvmedecins-webapi-v3] ;
• le client est contenu dans le dossier [rdvmedecins-angular-v2] ;
• le script SQL de génération de la base de données MySQL5 est dans le dossier [database] ;
3
5
[Link]
8/315
1.3.2 Mise en oeuvre du serveur web / JSON
Avec Spring Tool Suite (STS), on importe les deux projets Maven du serveur Spring 4 :
3a
3b
4
6
• en [3b], les projets importés. Il se peut que les projets présentent des erreurs. Il faut que chacun d'eux utilise un
compilateur >=1.7 :
[Link]
9/315
Lorsqu'il n'y a plus d'erreurs de JVM, on peut exécuter le projet [rdvmedecins-webapi-v3] :
• en [4], [5] et [6] nous exécutons le projet [rdvmedecins-webapi-v3] comme une application Spring Boot (il faut que le
SGBD MySQL soit lancé) ;
1. . ____ _ __ _ _
2. /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
3. ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
4. \\/ ___)| |_)| | | | | || (_| | ) ) ) )
5. ' |____| .__|_| |_|_| |_\__, | / / / /
6. =========|_|==============|___/=/_/_/_/
7. :: Spring Boot :: ([Link])
8.
9. 2014-06-05 [Link].049 INFO 9296 --- [ main] [Link] :
Starting Boot on Gportpers3 with PID 9296 (D:\data\istia-1314\polys\istia\angularjs-spring4\rdvmedecins-
webapi\target\classes started by ST)
10. 2014-06-05 [Link].122 INFO 9296 --- [ main] ationConfigEmbeddedWebApplicationContext :
Refreshing
[Link]@4b4bee22:
startup date [Thu Jun 05 [Link] CEST 2014]; root of context hierarchy
11. 2014-06-05 [Link].083 INFO 9296 --- [ main] [Link] :
Overriding bean definition for bean '[Link]':
replacing [Generic bean: class
[[Link]$BasePackages]; scope=; abstract=false;
lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false;
factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null] with [Generic
bean: class [[Link]$BasePackages]; scope=;
abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true;
primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null;
destroyMethodName=null]
12. ...
13. [Link] : Tomcat started on port(s): 8080/http
14. 2014-06-05 [Link].630 INFO 9296 --- [ main] [Link] : Started Boot in 8.0
seconds (JVM running for 8.944)
[Link]
10/315
• lignes 13-14 : l'application a démarré sur un serveur Tomcat.
[Link]
11/315
7 9
10 11 12 13 14
• en [6], la page d'entrée de l'application. Il s'agit d'une application de prise de rendez-vous pour des médecins. Cette
application a déjà été traitée dans le document Introduction aux frameworks JSF2, Primefaces et Primefaces mobile ;
• en [7], une case à cocher qui permet d'être ou non en mode [debug]. Ce dernier se caractérise par la présence du cadre [8]
qui affiche le modèle de la vue courante ;
• en [9], une durée d'attente artificielle en millisecondes. Elle vaut 0 par défaut (pas d'attente). Si N est la valeur de ce temps
d'attente, toute action de l'utilisateur sera exécutée après un temps d'attente de N millisecondes. Cela permet de voir la
gestion de l'attente mise en place par l'application ;
• en [10], l'URL du serveur Spring 4. Si on suit ce qui a précédé, c'est [[Link]
• en [11] et [12], l'identifiant et le mot de passe de celui qui veut utiliser l'application. Il y a deux utilisateurs : admin/admin
(login/password) avec un rôle (ADMIN) et user/user avec un rôle (USER). Seul le rôle ADMIN a le droit d'utiliser
l'application. Le rôle USER n'est là que pour montrer ce que répond le serveur dans ce cas d'utilisation ;
• en [13], le bouton qui permet de se connecter au serveur ;
• en [14], la langue de l'application. Il y en a deux : le français par défaut et l'anglais.
• en [1], on se connecte ;
[Link]
12/315
4
2 3
• une fois connecté, on peut choisir le médecin avec lequel on veut un rendez-vous [2] et le jour de celui-ci [3] ;
• on demande en [4] à voir l'agenda du médecin choisi pour le jour choisi ;
[Link]
13/315
5
[Link]
14/315
7
Une fois le rendez-vous validé, on est ramené automatiquement à l'agenda où le nouveau rendez-vous est désormais inscrit. Ce
rendez-vous pourra être ultérieurement supprimé [7].
Les principales fonctionnalités ont été décrites. Elles sont simples. Celles qui n'ont pas été décrites sont des fonctions de navigation
pour revenir à une vue précédente. Terminons par la gestion de la langue :
[Link]
15/315
• en [1], on passe du français à l'anglais ;
[Link]
16/315
2 Le serveur Spring 4
Dans l'architecture ci-dessus, nous abordons maintenant la construction du service web / JSON construit avec le framework Spring
4. Nous allons l'écrire en plusieurs étapes :
• d'abord les couches [métier] et [DAO] (Data Access Object). Nous utiliserons ici Spring Data ;
• puis le service web JSON sans authentification. Nous utiliserons ici Spring MVC ;
• puis on ajoutera la partie authentification avec Spring Security.
Spring 4 7
La base de données appelée par la suite [dbrdvmedecins] est une base de données MySQL5 avec les tables suivantes :
[Link]
17/315
Les rendez-vous sont gérés par les tables suivantes :
• [medecins] : contient la liste des médecins du cabinet ;
• [clients] : contient la liste des patienst du cabinet ;
• [creneaux] : contient les créneaux horaires de chacun des médecins ;
• [rv] : contient la liste des rendez-vous des médecins.
Les tables [roles], [users] et [users_roles] sont des tables liées à l'authentification. Dans un premier temps, nous n'allons pas nous en
occuper.
Le relations entre les tables gérant les rendez-vous sont les suivantes :
[Link]
18/315
• ID : n° identifiant le médecin - clé primaire de la table
• VERSION : n° identifiant la version de la ligne dans la table. Ce nombre est incrémenté de 1 à chaque fois qu'une
modification est apportée à la ligne.
• NOM : le nom du médecin
• PRENOM : son prénom
• TITRE : son titre (Melle, Mme, Mr)
Les clients des différents médecins sont enregistrés dans la table [CLIENTS] :
[Link]
19/315
• HFIN : heure fin créneau
• MFIN : minutes fin créneau
La seconde ligne de la table [CRENEAUX] (cf [1] ci-dessus) indique, par exemple, que le créneau n° 2 commence à 8 h 20 et se
termine à 8 h 40 et appartient au médecin n° 1 (Mme Marie PELISSIER).
Cette table a une contrainte d'unicité sur les valeurs des colonnes jointes (JOUR, ID_CRENEAU) :
Si une ligne de la table[RV] a la valeur (JOUR1, ID_CRENEAU1) pour les colonnes (JOUR, ID_CRENEAU), cette valeur ne peut
se retrouver nulle part ailleurs. Sinon, cela signifierait que deux RV ont été pris au même moment pour le même médecin. D'un
point de vue programmation Java, le pilote JDBC de la base lance une SQLException lorsque ce cas se produit.
La ligne d'id égal à 3 (cf [1] ci-dessus) signifie qu'un RV a été pris pour le créneau n° 20 et le client n° 4 le 23/08/2006. La table
[CRENEAUX] nous apprend que le créneau n° 20 correspond au créneau horaire 16 h 20 - 16 h 40 et appartient au médecin n° 1
(Mme Marie PELISSIER). La table [CLIENTS] nous apprend que le client n° 4 est Melle Brigitte BISTROU.
Spring
7 4
Sur le site de Spring existent de nombreux tutoriels pour démarrer avec Spring [ [Link] Nous allons utiliser l'un
d'eux pour introduire Spring Data. Nous utilisons pour cela Spring Tool Suite (STS).
[Link]
20/315
1
• en [2], on choisit le tutoriel [Accessing Data Jpa] qui montre comment accéder à une base de données avec Spring Data ;
• en [3], on choisit un projet configuré par Maven ;
• en [4], le tutoriel peut être délivré sous deux formes : [initial] qui est une version vide qu'on remplit en suivant le tutoriel
ou [complete] qui est la version finale du tutoriel. Nous choisissons cette dernière ;
• en [5], on peut choisir de visualiser le tutoriel dans un navigateur ;
• en [6], le projet final.
1. <groupId>[Link]</groupId>
2. <artifactId>gs-accessing-data-jpa</artifactId>
3. <version>0.1.0</version>
4.
5. <parent>
6. <groupId>[Link]</groupId>
7. <artifactId>spring-boot-starter-parent</artifactId>
8. <version>[Link]</version>
9. </parent>
10.
11. <dependencies>
[Link]
21/315
12. <dependency>
13. <groupId>[Link]</groupId>
14. <artifactId>spring-boot-starter-data-jpa</artifactId>
15. </dependency>
16. <dependency>
17. <groupId>com.h2database</groupId>
18. <artifactId>h2</artifactId>
19. </dependency>
20. </dependencies>
21.
22. <properties>
23. <!-- use UTF-8 for everything -->
24. <[Link]>UTF-8</[Link]>
25. <[Link]>UTF-8</[Link]>
26. <start-class>[Link]</start-class>
27. </properties>
• lignes 5-9 : définissent un projet Maven parent. C'est lui qui définit l'essentiel des dépendances du projet. Elles peuvent
être suffisantes, auquel cas on n'en rajoute pas, ou pas, auquel cas on rajoute les dépendances manquantes ;
• lignes 12-15 : définissent une dépendance sur [spring-boot-starter-data-jpa]. Cet artifact contient les classes de Spring
Data ;
• lignes 16-19 : définissent une dépendance sur le SGBD H2 qui permet de créer et gérer des bases de données en mémoire.
Nous allons les garder toutes. Pour une application en production, il faudrait ne garder que celles qui sont nécessaires.
<start-class>[Link]</start-class>
1. <build>
2. <plugins>
3. <plugin>
4. <artifactId>maven-compiler-plugin</artifactId>
5. </plugin>
6. <plugin>
7. <groupId>[Link]</groupId>
8. <artifactId>spring-boot-maven-plugin</artifactId>
9. </plugin>
[Link]
22/315
10. </plugins>
11. </build>
Lignes 6-9, le plugin [spring-boot-maven-plugin] permet de générer le jar exécutable de l'application. La ligne 26 du fichier
[[Link]] désigne alors la classe exécutable de ce jar.
Spring
7 4
L'application est basique et gère des clients [Customer]. La classe [Customer] fait partie de la couche [JPA] et est la suivante :
1. package hello;
2.
3. import [Link];
4. import [Link];
5. import [Link];
6. import [Link];
7.
8. @Entity
9. public class Customer {
10.
11. @Id
12. @GeneratedValue(strategy = [Link])
13. private long id;
14. private String firstName;
15. private String lastName;
16.
17. protected Customer() {
18. }
19.
20. public Customer(String firstName, String lastName) {
21. [Link] = firstName;
22. [Link] = lastName;
23. }
24.
25. @Override
26. public String toString() {
27. return [Link]("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
28. }
29.
30. }
Un client a un identifiant [id], un prénom [firstName] et un nom [lastName]. Chaque instance [Customer] représente une ligne
d'une table de la base de données.
• ligne 8 : annotation JPA qui fait que la persistance des instances [Customer] (Create, Read, Update, Delete) va être gérée
par une implémentation JPA. D'après les dépendances Maven, on voit que c'est l'implémentation JPA / Hibernate qui est
utilisée ;
[Link]
23/315
• lignes 11-12 : annotations JPA qui associent le champ [id] à la clé primaire de la table des [Customer]. La ligne 12, indique
que l'implémentation JPA utilisera la méthode de génération de clé primaire propre au SGBD utilisé, ici H2 ;
Il n'y a pas d'autres annotations JPA. Des valeurs par défaut seront alors utilisées :
• la table des [Customer] portera le nom de la classe, ç-à-d [Customer] ;
• les colonnes de cette table porteront le nom des champs de la classe : [id, firstName, lastName] sachant que la casse n'est
pas prise en compte dans le nom d'une colonne de table ;
Spring
7 4
1. package hello;
2.
3. import [Link];
4.
5. import [Link];
6.
7. public interface CustomerRepository extends CrudRepository<Customer, Long> {
8.
9. List<Customer> findByLastName(String lastName);
10. }
C'est donc une interface et non une classe (ligne 7). Elle étend l'interface [CrudRepository], une interface de Spring Data (ligne 5).
Cette interface est paramétrée par deux types : le premier est le type des éléments gérés, ici le type [Customer], le second le type de
la clé primaire des éléments gérés, ici un type [Long]. L'interface [CrudRepository] est la suivante :
1. package [Link];
2.
3. import [Link];
4.
5. @NoRepositoryBean
6. public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
7.
8. <S extends T> S save(S entity);
9.
10. <S extends T> Iterable<S> save(Iterable<S> entities);
11.
12. T findOne(ID id);
13.
14. boolean exists(ID id);
15.
16. Iterable<T> findAll();
17.
18. Iterable<T> findAll(Iterable<ID> ids);
19.
20. long count();
[Link]
24/315
21.
22. void delete(ID id);
23.
24. void delete(T entity);
25.
26. void delete(Iterable<? extends T> entities);
27.
28. void deleteAll();
29. }
Cette interface définit les opération CRUD (Create – Read – Update – Delete) qu'on peut faire sur un type JPA T :
• ligne 8 : la méthode save permet de persister une entité T en base. Elle rend l'entité persistée avec la clé primaire que lui a
donnée le SGBD. Elle permet également de mettre à jour une entité T identifiée par sa clé primaire id. Le choix de l'une
ou l'autre action se fait selon la valeur de la clé primaire id : si celle-ci vaut null c'est l'opération de persistance qui a lieu,
sinon c'est l'opération de mise à jour ;
• ligne 10 : idem mais pour une liste d'entités ;
• ligne 12 : la méthode findOne permet de retrouver une entité T identifiée par sa clé primaire id ;
• ligne 22 : la méthode delete permet de supprimer une entité T identifiée par sa clé primaire id ;
• lignes 24-28 : des variantes de la méthode [delete] ;
• ligne 16 : la méthode [findAll] permet de retrouver toutes les entités persistées T ;
• ligne 18 : idem mais limitée aux entités dont on a passé la liste des identifiants ;
1. package hello;
2.
3. import [Link];
4.
5. import [Link];
6.
7. public interface CustomerRepository extends CrudRepository<Customer, Long> {
8.
9. List<Customer> findByLastName(String lastName);
10. }
Et c'est tout pour la couche [DAO]. Il n'y a pas de classe d'implémentation de l'interface précédente. Celle-ci est générée à
l'exécution par [Spring Data]. Les méthodes de l'interface [CrudRepository] sont automatiquement implémentées. Pour les
méthodes rajoutées dans l'interface [CustomerRepository], ça dépend. Revenons à la définition de [Customer] :
La méthode de la ligne 9 est implémentée automatiquement par [Spring Data] parce qu'elle référence le champ [lastName] (ligne 3)
de [Customer]. Lorsqu'il rencontre une méthode [findBySomething] dans l'interface à implémenter, Spring Data l'implémente par la
requête JPQL (Java Persistence Query Language) suivante :
Il faut donc que le type T ait un champ nommé [something]. Ainsi la méthode
où [em] désigne le contexte de persistance JPA. Cela n'est possible que si la classe [Customer] a un champ nommé [lastName], ce
qui est le cas.
En conclusion, dans les cas simples, Spring Data nous permet d'implémenter la couche [DAO] avec une simple interface.
[Link]
25/315
2.2.4 La couche [console]
Spring
7 4
1. package hello;
2.
3. import [Link];
4.
5. import [Link];
6. import [Link];
7. import [Link];
8. import [Link];
9.
10. @Configuration
11. @EnableAutoConfiguration
12. public class Application {
13.
14. public static void main(String[] args) {
15.
16. ConfigurableApplicationContext context = [Link]([Link]);
17. CustomerRepository repository = [Link]([Link]);
18.
19. // save a couple of customers
20. [Link](new Customer("Jack", "Bauer"));
21. [Link](new Customer("Chloe", "O'Brian"));
22. [Link](new Customer("Kim", "Bauer"));
23. [Link](new Customer("David", "Palmer"));
24. [Link](new Customer("Michelle", "Dessler"));
25.
26. // fetch all customers
27. Iterable<Customer> customers = [Link]();
28. [Link]("Customers found with findAll():");
29. [Link]("-------------------------------");
30. for (Customer customer : customers) {
31. [Link](customer);
32. }
33. [Link]();
34.
35. // fetch an individual customer by ID
36. Customer customer = [Link](1L);
37. [Link]("Customer found with findOne(1L):");
38. [Link]("--------------------------------");
39. [Link](customer);
40. [Link]();
41.
42. // fetch customers by last name
43. List<Customer> bauers = [Link]("Bauer");
44. [Link]("Customer found with findByLastName('Bauer'):");
45. [Link]("--------------------------------------------");
46. for (Customer bauer : bauers) {
[Link]
26/315
47. [Link](bauer);
48. }
49.
50. [Link]();
51. }
52.
53. }
• la ligne 10 : indique que la classe sert à configurer Spring. Les versions récentes de Spring peuvent en effet être configurées
en Java plutôt qu'en XML. Les deux méthodes peuvent être utilisées simultanément. Dans le code d'une classe ayant
l'annotation [Configuration] on trouve normalement des beans Spring, ç-à-d des définitions de classe à instancier.
Lorsqu'on travaille avec un SGBD, divers beans Spring doivent être définis :
◦ un [EntityManagerFactory] qui définit l'implémentation JPA à utiliser ;
◦ un [DataSource] qui définit la source de données à utiliser ;
◦ un [TransactionManager] qui définit le gestionnaire de transactions à utiliser ;
• la ligne 11 : l'annotation [EnableAutoConfiguration] est une annotation provenant du projet [Spring Boot] (lignes 5-6).
Cette annotation demande à Spring Boot via la classe [SpringApplication] (ligne 16) de configurer l'application en fonction
des bibliothèques trouvées dans son Classpath. Parce que les bibliothèques Hibernate sont dans le Classpath, le bean
[entityManagerFactory] sera implémenté avec Hibernate. Parce que la bibliothèque du SGBD H2 est dans le Classpath, le
bean [dataSource] sera implémenté avec H2. Dans le bean [dataSource], on doit définir également l'utilisateur et son mot
de passe. Ici Spring Boot utilisera l'administrateur par défaut de H2, sa sans mot de passe. Parce que la bibliothèque
[spring-tx] est dans le Classpath, c'est le gestionnaire de transactions de Spring qui sera utilisé.
Par ailleurs, le dossier dans lequel se trouve la classe [Application] va être scanné à la recherche de beans implicitement
reconnus par Spring ou définis explicitement par des annotations Spring. Ainsi les classes [Customer] et
[CustomerRepository] vont-elles être inspectées. Parce que la première a l'annotation [@Entity] elle sera cataloguée
comme entité à gérer par Hibernate. Parce que la seconde étend l'interface [CrudRepository] elle sera enregistrée comme
bean Spring.
• ligne 1 : la méthode statique [run] de la classe [SpringApplication] du projet Spring Boot est exécutée. Son paramètre est la
classe qui a une annotation [Configuration] ou [EnableAutoConfiguration]. Tout ce qui a été expliqué précédemment va
alors se dérouler. Le résultat est un contexte d'application Spring, ç-à-d un ensemble de beans gérés par Spring ;
• ligne 17 : on demande à ce contexte Spring, un bean implémentant l'interface [CustomerRepository]. Nous récupérons ici,
la classe générée par Spring Data pour implémenter cette interface.
Les opérations qui suivent ne font qu'utiliser les méthodes du bean implémentant l'interface [CustomerRepository]. On notera ligne
50, que le contexte est fermé. Les résultats console sont les suivants :
1. . ____ _ __ _ _
2. /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
3. ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
4. \\/ ___)| |_)| | | | | || (_| | ) ) ) )
5. ' |____| .__|_| |_|_| |_\__, | / / / /
6. =========|_|==============|___/=/_/_/_/
7. :: Spring Boot :: ([Link])
8.
9. 2014-06-05 [Link].877 INFO 11664 --- [ main] [Link] :
Starting Application on Gportpers3 with PID 11664 (D:\Temp\wksSTS\gs-accessing-data-jpa-
complete\target\classes started by ST in D:\Temp\wksSTS\gs-accessing-data-jpa-complete)
10. 2014-06-05 [Link].936 INFO 11664 --- [ main] [Link] :
Refreshing [Link]@331a8fa0: startup
date [Thu Jun 05 [Link] CEST 2014]; root of context hierarchy
11. 2014-06-05 [Link].424 INFO 11664 --- [ main] [Link] :
Building JPA container EntityManagerFactory for persistence unit 'default'
12. 2014-06-05 [Link].518 INFO 11664 --- [ main] [Link] :
HHH000204: Processing PersistenceUnitInfo [
13. name: default
14. ...]
15. 2014-06-05 [Link].690 INFO 11664 --- [ main] [Link] :
HHH000412: Hibernate Core {[Link]}
[Link]
27/315
16. 2014-06-05 [Link].692 INFO 11664 --- [ main] [Link] :
HHH000206: [Link] not found
17. 2014-06-05 [Link].694 INFO 11664 --- [ main] [Link] :
HHH000021: Bytecode provider name : javassist
18. 2014-06-05 [Link].988 INFO 11664 --- [ main] [Link] :
HCANN000001: Hibernate Commons Annotations {[Link]}
19. 2014-06-05 [Link].078 INFO 11664 --- [ main] [Link] :
HHH000400: Using dialect: [Link].H2Dialect
20. 2014-06-05 [Link].300 INFO 11664 --- [ main] [Link] :
HHH000397: Using ASTQueryTranslatorFactory
21. 2014-06-05 [Link].613 INFO 11664 --- [ main] [Link] :
HHH000227: Running hbm2ddl schema export
22. Hibernate: drop table customer if exists
23. Hibernate: create table customer (id bigint generated by default as identity, first_name varchar(255),
last_name varchar(255), primary key (id))
24. 2014-06-05 [Link].619 INFO 11664 --- [ main] [Link] :
HHH000230: Schema export complete
25. 2014-06-05 [Link].074 INFO 11664 --- [ main] [Link] :
Registering beans for JMX exposure on startup
26. 2014-06-05 [Link].094 INFO 11664 --- [ main] [Link] :
Started Application in 3.906 seconds (JVM running for 5.013)
27. Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
28. Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
29. Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
30. Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
31. Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
32. Hibernate: select customer0_.id as id1_0_, customer0_.first_name as first_na2_0_, customer0_.last_name as
last_nam3_0_ from customer customer0_
33. Customers found with findAll():
34. -------------------------------
35. Customer[id=1, firstName='Jack', lastName='Bauer']
36. Customer[id=2, firstName='Chloe', lastName='O'Brian']
37. Customer[id=3, firstName='Kim', lastName='Bauer']
38. Customer[id=4, firstName='David', lastName='Palmer']
39. Customer[id=5, firstName='Michelle', lastName='Dessler']
40.
41. Hibernate: select customer0_.id as id1_0_0_, customer0_.first_name as first_na2_0_0_, customer0_.last_name
as last_nam3_0_0_ from customer customer0_ where customer0_.id=?
42. Customer found with findOne(1L):
43. --------------------------------
44. Customer[id=1, firstName='Jack', lastName='Bauer']
45.
46. Hibernate: select customer0_.id as id1_0_, customer0_.first_name as first_na2_0_, customer0_.last_name as
last_nam3_0_ from customer customer0_ where customer0_.last_name=?
47. Customer found with findByLastName('Bauer'):
48. --------------------------------------------
49. Customer[id=1, firstName='Jack', lastName='Bauer']
50. Customer[id=3, firstName='Kim', lastName='Bauer']
51. 2014-06-05 [Link].330 INFO 11664 --- [ main] [Link] :
Closing [Link]@331a8fa0: startup date
[Thu Jun 05 [Link] CEST 2014]; root of context hierarchy
52. 2014-06-05 [Link].332 INFO 11664 --- [ main] [Link] :
Unregistering JMX-exposed beans on shutdown
53. 2014-06-05 [Link].333 INFO 11664 --- [ main] [Link] :
Closing JPA EntityManagerFactory for persistence unit 'default'
54. 2014-06-05 [Link].334 INFO 11664 --- [ main] [Link] :
HHH000227: Running hbm2ddl schema export
55. Hibernate: drop table customer if exists
56. 2014-06-05 [Link].336 INFO 11664 --- [ main] [Link] :
HHH000230: Schema export complete
[Link]
28/315
• lignes 27-32 : des logs d'Hibernate montrant les insertions de lignes dans la table [CUSTOMER]. Cela signifie
qu'Hibernate a été configuré pour générer des logs ;
• lignes 35-39 : les cinq clients insérés ;
• lignes 42-44 : résultat de la méthode [findOne] de l'interface ;
• lignes 47-50 : résultats de la méthode [findByLastName] ;
• lignes 51 et suivantes : logs de la fermeture du contexte Spring.
Dans ce nouveau projet, nous n'allons pas nous reposer sur la configuration automatique faite par Spring Boot. Nous allons la faire
manuellement. Cela peut être utile si les configurations par défaut ne nous conviennent pas.
Tout d'abord, nous allons expliciter les dépendances nécessaires dans le fichier [[Link]] :
1. <dependencies>
2. <!-- Spring Core -->
3. <dependency>
4. <groupId>[Link]</groupId>
5. <artifactId>spring-core</artifactId>
6. <version>[Link]</version>
7. </dependency>
8. <dependency>
9. <groupId>[Link]</groupId>
10. <artifactId>spring-context</artifactId>
11. <version>[Link]</version>
12. </dependency>
13. <dependency>
14. <groupId>[Link]</groupId>
15. <artifactId>spring-beans</artifactId>
16. <version>[Link]</version>
17. </dependency>
18. <!-- Spring transactions -->
19. <dependency>
20. <groupId>[Link]</groupId>
21. <artifactId>spring-aop</artifactId>
22. <version>[Link]</version>
23. </dependency>
24. <dependency>
25. <groupId>[Link]</groupId>
26. <artifactId>spring-tx</artifactId>
27. <version>[Link]</version>
28. </dependency>
29. <!-- Spring Data -->
30. <dependency>
31. <groupId>[Link]</groupId>
32. <artifactId>spring-data-jpa</artifactId>
33. <version>[Link]</version>
[Link]
29/315
34. </dependency>
35. <!-- Spring Boot -->
36. <dependency>
37. <groupId>[Link]</groupId>
38. <artifactId>spring-boot</artifactId>
39. <version>[Link]</version>
40. </dependency>
41. <!-- Hibernate -->
42. <dependency>
43. <groupId>[Link]</groupId>
44. <artifactId>hibernate-entitymanager</artifactId>
45. <version>[Link]</version>
46. </dependency>
47. <!-- H2 Database -->
48. <dependency>
49. <groupId>com.h2database</groupId>
50. <artifactId>h2</artifactId>
51. <version>1.4.178</version>
52. </dependency>
53. <!-- Commons DBCP -->
54. <dependency>
55. <groupId>commons-dbcp</groupId>
56. <artifactId>commons-dbcp</artifactId>
57. <version>1.4</version>
58. </dependency>
59. <dependency>
60. <groupId>commons-pool</groupId>
61. <artifactId>commons-pool</artifactId>
62. <version>1.6</version>
63. </dependency>
64. </dependencies>
1. <properties>
2. ...
3. <start-class>[Link]</start-class>
4. </properties>
Dans le nouveau projet, l'entité [Customer] et l'interface [CustomerRepository] ne changent pas. On va changer la classe
[Application] qui va être scindée en deux classes :
• [Config] qui sera la classe de configuration :
• [Main] qui sera la classe exécutable ;
La classe exécutable [Main] est la même que précédemment sans les annotations de configuration :
1. package [Link];
2.
3. import [Link];
4.
[Link]
30/315
5. import [Link];
6. import [Link];
7.
8. import [Link];
9. import [Link];
10. import [Link];
11.
12. public class Main {
13.
14. public static void main(String[] args) {
15.
16. ConfigurableApplicationContext context = [Link]([Link]);
17. CustomerRepository repository = [Link]([Link]);
18. ...
19.
20. [Link]();
21. }
22.
23. }
1. package [Link];
2.
3. import [Link];
4. import [Link];
5.
6. import [Link];
7. import [Link];
8. import [Link];
9. import [Link];
10. import [Link];
11. import [Link];
12. import [Link];
13. import [Link];
14. import [Link];
15. import [Link];
16. import [Link];
17.
18. //@ComponentScan(basePackages = { "demo" })
19. //@EntityScan(basePackages = { "[Link]" })
20. @EnableTransactionManagement
21. @EnableJpaRepositories(basePackages = { "[Link]" })
22. @Configuration
23. public class Config {
24. // la source de données H2
25. @Bean
26. public DataSource dataSource() {
27. BasicDataSource dataSource = new BasicDataSource();
28. [Link]("[Link]");
29. [Link]("jdbc:h2:./demo");
30. [Link]("sa");
31. [Link]("");
32. return dataSource;
33. }
34.
35. // le provider JPA
36. @Bean
37. public JpaVendorAdapter jpaVendorAdapter() {
38. HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
39. [Link](false);
40. [Link](true);
41. [Link](Database.H2);
42. return hibernateJpaVendorAdapter;
43. }
44.
45. // EntityManagerFactory
46. @Bean
[Link]
31/315
47. public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource
dataSource) {
48. LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
49. [Link](jpaVendorAdapter);
50. [Link]("[Link]");
51. [Link](dataSource);
52. [Link]();
53. return [Link]();
54. }
55.
56. // Transaction manager
57. @Bean
58. public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
59. JpaTransactionManager txManager = new JpaTransactionManager();
60. [Link](entityManagerFactory);
61. return txManager;
62. }
63.
64. }
• ligne 22 : l'annotation [@Configuration] fait de la classe [Config] une classe de configuration Spring ;
• ligne 21 : l'annotation [@EnableJpaRepositories] permet de désigner les dossiers où se trouvent les interfaces Spring Data
[CrudRepository]. Ces interfaces vont devenir des composants Spring et être disponibles dans son contexte ;
• ligne 20 : l'annotation [@EnableTransactionManagement] indique que les méthodes des interfaces [CrudRepository]
doivent se dérouler à l'intérieur d'une transaction ;
• ligne 19 : l'annotation [@EntityScan] permet de nommer les dossiers où doivent être cherchées les entités JPA. Ici elle a
été mise en commentaires, parce que cette information a été donnée explicitement ligne 50. Cette annotation devrait être
présente si on utilise le mode [@EnableAutoConfiguration] et que les entités JPA ne sont pas dans le même dossier que la
classe de configuration ;
• ligne 18 : l'annotation [@ComponentScan] permet de lister les dossiers où les composants Spring doivent être recherchés.
Les composants Spring sont des classes taguées avec des annotations Spring telles que @Service, @Component,
@Controller, ... Ici il n'y en a pas d'autres que ceux qui sont définis au sein de la classe [Config], aussi l'annotation a-t-elle
été mise en commentaires ;
• lignes 25-33 : définissent la source de données, la base de données H2. C'est l'annotation @Bean de la ligne 25 qui fait de
l'objet créé par cette méthode un composant géré par Spring. Le nom de la méthode peut être ici quelconque. Cependant
elle doit être appelée [dataSource] si l'EntityManagerFactory de la ligne 47 est absent et défini par autoconfiguration ;
• ligne 29 : la base de données s'appellera [demo] et sera générée dans le dossier du projet ;
• lignes 36-43 : définissent l'implémentation JPA utilisée, ici une implémentation Hibernate. Le nom de la méthode peut être
ici quelconque ;
• ligne 39 : pas de logs SQL ;
• ligne 30 : la base de données sera créée si elle n'existe pas ;
• lignes 46-54 : définissent l'EntityManagerFactory qui va gérer la persistance JPA. La méthode doit s'appeler
obligatoirement [entityManagerFactory] ;
• ligne 47 : la méthode reçoit deux paramètres ayant le type des deux beans définis précédemment. Ceux-ci seront alors
construits puis injectés par Spring comme paramètres de la méthode ;
• ligne 49 : fixe l'implémentation JPA utilisée ;
• ligne 50 : fixent les dossiers où trouver les entités JPA ;
• ligne 51 : fixe la source de donnée à gérer ;
• lignes 57-62 : le gestionnaire de transactions. La méthode doit s'appeler obligatoirement [transactionManager]. Elle
reçoit pour paramètre le bean des lignes 46-54 ;
• ligne 60 : le gestionnaire de transactions est associé à l'EntityManagerFactory ;
L'exécution du projet donne les mêmes résultats. Un nouveau fichier apparaît dans le dossier du projet, celui de la base de données
H2 :
[Link]
32/315
Enfin, on peut se passer de Spring Boot. On crée une seconde classe exécutable [Main2] :
1. package [Link];
2.
3. import [Link];
4.
5. import [Link];
6.
7. import [Link];
8. import [Link];
9. import [Link];
10.
11. public class Main2 {
12.
13. public static void main(String[] args) {
14.
15. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext([Link]);
16. CustomerRepository repository = [Link]([Link]);
17. ....
18.
19. [Link]();
20. }
21.
22. }
• ligne 15 : la classe de configuration [Config] est désormais exploitée par la classe Spring
[AnnotationConfigApplicationContext]. On peut voir ligne 5 qu'il n'y a maintenant plus de dépendance vis à vis de Spring
Boot.
[Link]
33/315
5
2
4
1
Ceci fait, on ouvre une console dans le dossier contenant l'archive exécutable :
.....\dist>dir
12/06/2014 09:11 15 104 869 [Link]
[Link]
34/315
5. juin 12, 2014 [Link] AM [Link] logDeprecation
6. WARN: HHH015016: Encountered a deprecated [Link]
[[Link]]; use [[Link]] instead.
7. juin 12, 2014 [Link] AM [Link] logPersistenceUnitInformation
8. INFO: HHH000204: Processing PersistenceUnitInfo [
9. name: default
10. ...]
11. juin 12, 2014 [Link] AM [Link] logVersion
12. INFO: HHH000412: Hibernate Core {[Link]}
13. juin 12, 2014 [Link] AM [Link] <clinit>
14. INFO: HHH000206: [Link] not found
15. juin 12, 2014 [Link] AM [Link] buildBytecodeProvider
16. INFO: HHH000021: Bytecode provider name : javassist
17. juin 12, 2014 [Link] AM [Link] <clinit>
18. INFO: HCANN000001: Hibernate Commons Annotations {[Link]}
19. juin 12, 2014 [Link] AM [Link] <init>
20. INFO: HHH000400: Using dialect: [Link].H2Dialect
21. juin 12, 2014 [Link] AM [Link] <init>
22. INFO: HHH000397: Using ASTQueryTranslatorFactory
23. juin 12, 2014 [Link] AM [Link] execute
24. INFO: HHH000228: Running hbm2ddl schema update
25. juin 12, 2014 [Link] AM [Link] execute
26. INFO: HHH000102: Fetching database metadata
27. juin 12, 2014 [Link] AM [Link] execute
28. INFO: HHH000396: Updating schema
29. juin 12, 2014 [Link] AM [Link] getTableMetadata
30. INFO: HHH000262: Table not found: Customer
31. juin 12, 2014 [Link] AM [Link] getTableMetadata
32. INFO: HHH000262: Table not found: Customer
33. juin 12, 2014 [Link] AM [Link] getTableMetadata
34. INFO: HHH000262: Table not found: Customer
35. juin 12, 2014 [Link] AM [Link] execute
36. INFO: HHH000232: Schema update complete
37. Customers found with findAll():
38. -------------------------------
39. Customer[id=1, firstName='Jack', lastName='Bauer']
40. Customer[id=2, firstName='Chloe', lastName='O'Brian']
41. Customer[id=3, firstName='Kim', lastName='Bauer']
42. Customer[id=4, firstName='David', lastName='Palmer']
43. Customer[id=5, firstName='Michelle', lastName='Dessler']
44.
45. Customer found with findOne(1L):
46. --------------------------------
47. Customer[id=1, firstName='Jack', lastName='Bauer']
48.
49. Customer found with findByLastName('Bauer'):
50. --------------------------------------------
51. Customer[id=1, firstName='Jack', lastName='Bauer']
52. Customer[id=3, firstName='Kim', lastName='Bauer']
[Link]
35/315
6
2
3
4
1
5
1. <parent>
2. <groupId>[Link]</groupId>
3. <artifactId>spring-boot-starter-parent</artifactId>
4. <version>[Link]</version>
5. <relativePath/> <!-- lookup parent from repository -->
6. </parent>
7.
8. <dependencies>
9. <dependency>
10. <groupId>[Link]</groupId>
[Link]
36/315
11. <artifactId>spring-boot-starter-data-jpa</artifactId>
12. </dependency>
13. <dependency>
14. <groupId>[Link]</groupId>
15. <artifactId>spring-boot-starter-test</artifactId>
16. <scope>test</scope>
17. </dependency>
18. </dependencies>
• lignes 9-12 : les dépendances nécessaires à JPA – vont inclure [Spring Data] ;
• lignes 13-17 : les dépendances nécessaires aux test JUnit intégrés avec Spring ;
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6. import [Link];
7.
8. @Configuration
9. @ComponentScan
10. @EnableAutoConfiguration
11. public class Application {
12.
13. public static void main(String[] args) {
14. [Link]([Link], args);
15. }
16. }
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6. import [Link].junit4.SpringJUnit4ClassRunner;
7.
8. @RunWith([Link])
9. @SpringApplicationConfiguration(classes = [Link])
10. public class ApplicationTests {
11.
12. @Test
13. public void contextLoads() {
14. }
15.
16. }
Maintenant que nous avons un squelette d'application JPA, nous pouvons le compléter pour écrire le projet de la couche de
persistance du serveur de notre application de gestion de rendez-vous.
[Link]
37/315
2.3 Le projet Eclipse du serveur
Spring
7 4
[Link]
38/315
Le fichier [[Link]] du projet est le suivant :
[Link]
39/315
48. <[Link]>UTF-8</[Link]>
49. <start-class>[Link]</start-class>
50. </properties>
51. <build>
52. <plugins>
53. <plugin>
54. <artifactId>maven-compiler-plugin</artifactId>
55. </plugin>
56. <plugin>
57. <groupId>[Link]</groupId>
58. <artifactId>spring-boot-maven-plugin</artifactId>
59. </plugin>
60. </plugins>
61. </build>
62. <repositories>
63. <repository>
64. <id>spring-milestones</id>
65. <name>Spring Milestones</name>
66. <url>[Link]
67. <snapshots>
68. <enabled>false</enabled>
69. </snapshots>
70. </repository>
71. <repository>
72. <id>[Link]</id>
73. <name>JBoss Maven Release Repository</name>
74. <url>[Link]
75. <snapshots>
76. <enabled>false</enabled>
77. </snapshots>
78. </repository>
79. </repositories>
80. <pluginRepositories>
81. <pluginRepository>
82. <id>spring-milestones</id>
83. <name>Spring Milestones</name>
84. <url>[Link]
85. <snapshots>
86. <enabled>false</enabled>
87. </snapshots>
88. </pluginRepository>
89. </pluginRepositories>
90. </project>
• lignes 8-12 : le projet s'appuie sur le projet parent [spring-boot-starter-parent]. Pour les dépendances déjà présentes dans le
projet parent, on ne précise pas de version. C'est la version définie dans le parent qui sera utilisée. Pour les autres
dépendances, on les déclare normalement ;
• lignes 14-17 : pour Spring Data ;
• lignes 18-22 : pour les tests JUnit ;
• lignes 23-26 : pilote JDBC du SGBD MySQL5 ;
• lignes 27-34 : pool de connexions Commons DBCP ;
• lignes 35-38 : bibliothèque Jackson de gestion du JSON ;
• lignes 39-43 : bibliothèque Google de gestion des collections ;
1. <[Link]>5.9.1</[Link]>
2. <[Link]>1.8.0</[Link]>
3. <[Link]>3.0.2</[Link]>
4. <[Link]>1.9.1</[Link]>
5. <[Link]>3.2.1</[Link]>
6. <[Link]>1.4</[Link]>
7. <[Link]>2.1</[Link]>
8. <[Link]>1.6</[Link]>
9. <[Link]>2.2</[Link]>
10. <[Link]>1.3.0-beta20</[Link]>
11. <[Link]>3.0</[Link]>
12. <[Link]>2.3.20</[Link]>
13. <[Link]>7.0.2</[Link]>
14. <[Link]>1.6</[Link]>
15. <[Link]>2.3.2</[Link]>
16. <[Link]>1.3.175</[Link]>
[Link]
40/315
17. <[Link]>1.3</[Link]>
18. <[Link]>[Link]</[Link]>
19. <[Link]>[Link]</[Link]>
20. <[Link]>[Link]</[Link]>
21. <[Link]>[Link]</[Link]>
22. <[Link]>1.3.8</[Link]>
23. <[Link]>[Link]</[Link]>
24. <[Link]>2.3.2</[Link]>
25. <[Link]>4.0.1</[Link]>
26. <[Link]>4.3.3</[Link]>
27. <[Link]>2.3.3</[Link]>
28. <[Link]>1.6</[Link]>
29. <[Link]>3.18.1-GA</[Link]>
30. <[Link]>2.4.1</[Link]>
31. <[Link]>2.2.0.v201112011158</[Link]>
32. <[Link]>8.1.14.v20131031</[Link]>
33. <[Link]>2.3</[Link]>
34. <[Link]>1.2.0</[Link]>
35. <[Link]>1.2</[Link]>
36. <[Link]>4.11</[Link]>
37. <[Link]>3.0.8</[Link]>
38. <[Link]>1.2.17</[Link]>
39. <[Link]>1.1.2</[Link]>
40. <[Link]>1.9.5</[Link]>
41. <[Link]>2.12.1</[Link]>
42. <[Link]>5.1.30</[Link]>
43. <[Link]>UTF-8</[Link]>
44. <[Link]>UTF-8</[Link]>
45. <[Link]>[Link]</[Link]>
46. <[Link]>3.0.1</[Link]>
47. <[Link]>1.7.7</[Link]>
48. <[Link]>1.13</[Link]>
49. <[Link]>4.7.2</[Link]>
50. <[Link]>0.7-groovy-2.0</[Link]>
51. <[Link]>[Link]</[Link]>
52. <[Link]>[Link]</[Link]>
53. <[Link]>1.1.0.RC1</[Link]>
54. <[Link]>Dijkstra-RELEASE</[Link]>
55. <[Link]>[Link]</[Link]>
56. <[Link]>[Link]</[Link]>
57. <[Link]>[Link]</[Link]>
58. <[Link]>[Link]</[Link]>
59. <[Link]>[Link]</[Link]>
60. <[Link]>[Link]</[Link]>
61. <[Link]>[Link]</[Link]>
62. <[Link]>[Link]</[Link]>
63. <[Link]>[Link]</[Link]>
64. <[Link]>[Link]</[Link]>
65. <[Link]>[Link]</[Link]>
66. <[Link]>[Link]</[Link]>
67. <[Link]>1.2.4</[Link]>
68. <[Link]>[Link]</[Link]>
69. <[Link]>7.0.54</[Link]>
70. <[Link]>2.0</[Link]>
71. <[Link]>1.7</[Link]>
Spring
7 4
Les entités JPA sont les objets qui vont encapsuler les lignes des tables de la base de données.
[Link]
41/315
La classe [AbstractEntity] est la classe parent des entités [Personne, Creneau, Rv]. Sa définition est la suivante :
1. package [Link];
2.
3. import [Link];
4.
5. import [Link];
6. import [Link];
7. import [Link];
8. import [Link];
9. import [Link];
10.
11. @MappedSuperclass
12. public class AbstractEntity implements Serializable {
13.
14. private static final long serialVersionUID = 1L;
15. @Id
16. @GeneratedValue(strategy = [Link])
17. protected Long id;
18. @Version
19. protected Long version;
20.
21. @Override
22. public int hashCode() {
23. int hash = 0;
24. hash += (id != null ? [Link]() : 0);
25. return hash;
26. }
27.
28. // initialisation
29. public AbstractEntity build(Long id, Long version) {
30. [Link] = id;
31. [Link] = version;
32. return this;
33. }
34.
35. @Override
36. public boolean equals(Object entity) {
37. String class1 = [Link]().getName();
38. String class2 = [Link]().getName();
39. if () {
40. return false;
41. }
42. AbstractEntity other = (AbstractEntity) entity;
43. return [Link] == [Link];
44. }
45.
46. // getters et setters
47. ..
48. }
• ligne 11 : l'annotation [@MappedSuperclass] indique que la classe annotée est parente d'entités JPA [@Entity] ;
• lignes 15-17 : définissent la clé primaire [id] de chaque entité. C'est l'annotation [@Id] qui fait du champ [id] une clé
primaire. L'annotation [@GeneratedValue(strategy = [Link])] indique que la valeur de cette clé primaire
est générée par le SGBD et qu'aucun mode de génération n'est imposé ;
[Link]
42/315
• lignes 18-19 : définissent la version de chaque entité. L'implémentation JPA va incrémenter ce n° de version à chaque fois
que l'entité sera modifiée. Ce n° sert à empêcher la mise à jour simultanée de l'entité par deux utilisateur différents : deux
utilisateurs U1 et U2 lisent l'entité E avec un n° de version égal à V1. U1 modifie E et persiste cette modification en base :
le n° de version passe alors à V1+1. U2 modifie E à son tour et persiste cette modification en base : il recevra une
exception car il possède une version (V1) différente de celle en base (V1+1) ;
• lignes 29-33 : la méthode [build] permet d'initialiser les deux champs de [AbstractEntity]. Cette méthode rend la référence
de l'instance [AbstractEntity] ainsi initialisée ;
• lignes 36-44 : la méthode [equals] de la classe est redéfinie : deux entités seront dites égales si elles ont le même nom de
classe et le même identifiant id ;
1. package [Link];
2.
3. import [Link];
4. import [Link];
5.
6. @MappedSuperclass
7. public class Personne extends AbstractEntity {
8. private static final long serialVersionUID = 1L;
9. // attributs d'une personne
10. @Column(length = 5)
11. private String titre;
12. @Column(length = 20)
13. private String nom;
14. @Column(length = 20)
15. private String prenom;
16.
17. // constructeur par défaut
18. public Personne() {
19. }
20.
21. // constructeur avec paramètres
22. public Personne(String titre, String nom, String prenom) {
23. [Link] = titre;
24. [Link] = nom;
25. [Link] = prenom;
26. }
27.
28. // toString
29. public String toString() {
30. return [Link]("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
31. }
32.
33. // getters et setters
34. ...
35. }
• ligne 6 : l'annotation [@MappedSuperclass] indique que la classe annotée est parente d'entités JPA [@Entity] ;
• lignes 10-15 : une personne a un titre (Melle), un prénom (Jacqueline), un nom (Tatou). Aucune information n'est donnée
sur les colonnes de la table. Elles porteront donc par défaut les mêmes noms que les champs ;
1. package [Link];
2.
3. import [Link];
4. import [Link];
5.
6. @Entity
7. @Table(name = "medecins")
8. public class Medecin extends Personne {
9.
10. private static final long serialVersionUID = 1L;
11.
12. // constructeur par défaut
13. public Medecin() {
14. }
15.
16. // constructeur avec paramètres
17. public Medecin(String titre, String nom, String prenom) {
[Link]
43/315
18. super(titre, nom, prenom);
19. }
20.
21. public String toString() {
22. return [Link]("Medecin[%s]", [Link]());
23. }
24.
25. }
1. package [Link];
2.
3. import [Link];
4. import [Link];
5.
6. @Entity
7. @Table(name = "clients")
8. public class Client extends Personne {
9.
10. private static final long serialVersionUID = 1L;
11.
12. // constructeur par défaut
13. public Client() {
14. }
15.
16. // constructeur avec paramètres
17. public Client(String titre, String nom, String prenom) {
18. super(titre, nom, prenom);
19. }
20.
21. // identité
22. public String toString() {
23. return [Link]("Client[%s]", [Link]());
24. }
25.
26. }
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6. import [Link];
7. import [Link];
8. import [Link];
9.
10. @Entity
11. @Table(name = "creneaux")
12. public class Creneau extends AbstractEntity {
[Link]
44/315
13.
14. private static final long serialVersionUID = 1L;
15. // caractéristiques d'un créneau de RV
16. private int hdebut;
17. private int mdebut;
18. private int hfin;
19. private int mfin;
20.
21. // un créneau est lié à un médecin
22. @ManyToOne(fetch = [Link])
23. @JoinColumn(name = "id_medecin")
24. private Medecin medecin;
25.
26. // clé étrangère
27. @Column(name = "id_medecin", insertable = false, updatable = false)
28. private long idMedecin;
29.
30. // constructeur par défaut
31. public Creneau() {
32. }
33.
34. // constructeur avec paramètres
35. public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
36. [Link] = medecin;
37. [Link] = hdebut;
38. [Link] = mdebut;
39. [Link] = hfin;
40. [Link] = mfin;
41. }
42.
43. // toString
44. public String toString() {
45. return [Link]("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut,
hfin, mfin);
46. }
47.
48. // clé étrangère
49. public long getIdMedecin() {
50. return idMedecin;
51. }
52.
53. // setters - getters
54. ...
55. }
1. package [Link];
2.
3. import [Link];
[Link]
45/315
4.
5. import [Link];
6. import [Link];
7. import [Link];
8. import [Link];
9. import [Link];
10. import [Link];
11. import [Link];
12. import [Link];
13.
14. @Entity
15. @Table(name = "rv")
16. public class Rv extends AbstractEntity {
17. private static final long serialVersionUID = 1L;
18.
19. // caractéristiques d'un Rv
20. @Temporal([Link])
21. private Date jour;
22.
23. // un rv est lié à un client
24. @ManyToOne(fetch = [Link])
25. @JoinColumn(name = "id_client")
26. private Client client;
27.
28. // un rv est lié à un créneau
29. @ManyToOne(fetch = [Link])
30. @JoinColumn(name = "id_creneau")
31. private Creneau creneau;
32.
33. // clés étrangères
34. @Column(name = "id_client", insertable = false, updatable = false)
35. private long idClient;
36. @Column(name = "id_creneau", insertable = false, updatable = false)
37. private long idCreneau;
38.
39. // constructeur par défaut
40. public Rv() {
41. }
42.
43. // avec paramètres
44. public Rv(Date jour, Client client, Creneau creneau) {
45. [Link] = jour;
46. [Link] = client;
47. [Link] = creneau;
48. }
49.
50. // toString
51. public String toString() {
52. return [Link]("Rv[%d, %s, %d, %d]", id, jour, [Link], [Link]);
53. }
54.
55. // clés étrangères
56. public long getIdCreneau() {
57. return idCreneau;
58. }
59.
60. public long getIdClient() {
61. return idClient;
62. }
63.
64. // getters et setters
65. ...
66. }
[Link]
46/315
• lignes 34-35 : la clé étrangère [idClient] ;
• lignes 36-37 : la clé étrangère [idCreneau] ;
Spring
7 4
1. package [Link];
2.
3. import [Link];
4.
5. import [Link];
6.
7. public interface MedecinRepository extends CrudRepository<Medecin, Long> {
8. }
• ligne 7 : l'interface [MedecinRepository] se contente d'hériter des méthodes de l'interface [CrudRepository] sans en ajouter
d'autres ;
1. package [Link];
2.
3. import [Link];
4.
5. import [Link];
6.
7. public interface ClientRepository extends CrudRepository<Client, Long> {
8. }
[Link]
47/315
• ligne 7 : l'interface [ClientRepository] se contente d'hériter des méthodes de l'interface [CrudRepository] sans en ajouter
d'autres ;
1. package [Link];
2.
3. import [Link];
4. import [Link];
5.
6. import [Link];
7.
8. public interface CreneauRepository extends CrudRepository<Creneau, Long> {
9. // liste des créneaux horaires d'un médecin
10. @Query("select c from Creneau c where [Link]=?1")
11. Iterable<Creneau> getAllCreneaux(long idMedecin);
12. }
1. package [Link];
2.
3. import [Link];
4.
5. import [Link];
6. import [Link];
7.
8. import [Link];
9.
10. public interface RvRepository extends CrudRepository<Rv, Long> {
11.
12. @Query("select rv from Rv rv left join fetch [Link] c left join fetch [Link] cr where
[Link]=?1 and [Link]=?2")
13. Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
14. }
car les champs de la classe Rv, de types [Client] et [Creneau] sont obtenus en mode [[Link]], ce qui signifie
qu'ils doivent être demandés explicitement pour être obtenus. Ceci est fait dans la requête JPQL avec la syntaxe [left join
fetch entité] qui demande qu'une jointure soit faite avec la table sur laquelle pointe la clé étrangère afin de récupérer
l'entité pointée ;
[Link]
48/315
2.7 La couche [métier]
Spring
7 4
1. package [Link];
2.
3. import [Link];
4.
5. import [Link];
6. import [Link];
7.
8. public class CreneauMedecinJour implements Serializable {
9.
10. private static final long serialVersionUID = 1L;
11. // champs
12. private Creneau creneau;
13. private Rv rv;
14.
15. // constructeurs
16. public CreneauMedecinJour() {
17.
18. }
19.
20. public CreneauMedecinJour(Creneau creneau, Rv rv) {
21. [Link]=creneau;
22. [Link]=rv;
23. }
24.
25. // toString
26. @Override
27. public String toString() {
28. return [Link]("[%s %s]", creneau, rv);
29. }
30.
31. // getters et setters
32. ...
33. }
L'entité [AgendaMedecinJour] est l'agenda d'un médecin pour un jour donné, ç-à-d la liste de ses rendez-vous :
[Link]
49/315
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6.
7. import [Link];
8.
9. public class AgendaMedecinJour implements Serializable {
10.
11. private static final long serialVersionUID = 1L;
12. // champs
13. private Medecin medecin;
14. private Date jour;
15. private CreneauMedecinJour[] creneauxMedecinJour;
16.
17. // constructeurs
18. public AgendaMedecinJour() {
19.
20. }
21.
22. public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
23. [Link] = medecin;
24. [Link] = jour;
25. [Link] = creneauxMedecinJour;
26. }
27.
28. public String toString() {
29. StringBuffer str = new StringBuffer("");
30. for (CreneauMedecinJour cr : creneauxMedecinJour) {
31. [Link](" ");
32. [Link]([Link]());
33. }
34. return [Link]("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour),
[Link]());
35. }
36.
37. // getters et setters
38. ...
39. }
• ligne 13 : le médecin ;
• ligne 14 : le jour dans l'agenda ;
• ligne 15 : ses créneaux horaires avec ou sans rendez-vous ;
2.7.2 Le service
L'interface de la couche [métier] est la suivante :
1. package [Link];
2.
3. import [Link];
4. import [Link];
5.
6. import [Link];
7. import [Link];
8. import [Link];
9. import [Link];
10. import [Link];
11.
12. public interface IMetier {
13.
14. // liste des clients
15. public List<Client> getAllClients();
16.
17. // liste des Médecins
18. public List<Medecin> getAllMedecins();
19.
20. // liste des créneaux horaires d'un médecin
21. public List<Creneau> getAllCreneaux(long idMedecin);
22.
23. // liste des Rv d'un médecin, un jour donné
[Link]
50/315
24. public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
25.
26. // trouver un client identifié par son id
27. public Client getClientById(long id);
28.
29. // trouver un client identifié par son id
30. public Medecin getMedecinById(long id);
31.
32. // trouver un Rv identifié par son id
33. public Rv getRvById(long id);
34.
35. // trouver un créneau horaire identifié par son id
36. public Creneau getCreneauById(long id);
37.
38. // ajouter un RV
39. public Rv ajouterRv(Date jour, Creneau créneau, Client client);
40.
41. // supprimer un RV
42. public void supprimerRv(Rv rv);
43.
44. // metier
45. public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
46.
47. }
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6. import [Link];
7.
8. import [Link];
9. import [Link];
10.
11. import [Link];
12. import [Link];
13. import [Link];
14. import [Link];
15. import [Link];
16. import [Link];
17. import [Link];
18. import [Link];
19. import [Link];
20. import [Link];
21.
22. import [Link];
23.
24. @Service("métier")
25. public class Metier implements IMetier {
26.
27. // répositories
28. @Autowired
29. private MedecinRepository medecinRepository;
30. @Autowired
31. private ClientRepository clientRepository;
32. @Autowired
33. private CreneauRepository creneauRepository;
34. @Autowired
35. private RvRepository rvRepository;
36.
37. // implémentation interface
38. @Override
39. public List<Client> getAllClients() {
40. return [Link]([Link]());
41. }
42.
43. @Override
44. public List<Medecin> getAllMedecins() {
45. return [Link]([Link]());
[Link]
51/315
46. }
47.
48. @Override
49. public List<Creneau> getAllCreneaux(long idMedecin) {
50. return [Link]([Link](idMedecin));
51. }
52.
53. @Override
54. public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
55. return [Link]([Link](idMedecin, jour));
56. }
57.
58. @Override
59. public Client getClientById(long id) {
60. return [Link](id);
61. }
62.
63. @Override
64. public Medecin getMedecinById(long id) {
65. return [Link](id);
66. }
67.
68. @Override
69. public Rv getRvById(long id) {
70. return [Link](id);
71. }
72.
73. @Override
74. public Creneau getCreneauById(long id) {
75. return [Link](id);
76. }
77.
78. @Override
79. public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
80. return [Link](new Rv(jour, client, créneau));
81. }
82.
83. @Override
84. public void supprimerRv(Rv rv) {
85. [Link]([Link]());
86. }
87.
88. public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
89. ...
90. }
91.
92. }
• ligne 24 : l'annotation [@Service] est une annotation Spring qui fait de la classe annotée un composant géré par Spring. On
peut ou non donner un nom à un composant. Celui-ci est nommé [métier] ;
• ligne 25 : la classe [Metier] implémente l'interface [IMetier] ;
• ligne 28 : l'annotation [@Autowired] est une annotation Spring. La valeur du champ ainsi annoté sera initialisée (injectée)
par Spring avec la référence d'un composant Spring du type ou du nom précisés. Ici l'annotation [@Autowired] ne précise
pas de nom. Ce sera donc une injection par type qui sera faite ;
• ligne 29 : le champ [medecinRepository] sera initialisé avec la référence d'un composant Spring de type
[MedecinRepository]. Ce sera la référence de la classe générée par Spring Data pour implémenter l'interface
[MedecinRepository] que nous avons déjà présentée ;
• lignes 30-35 : ce processus est répété pour les trois autres interfaces étudiées ;
• lignes 39-41 : implémentation de la méthode [getAllClients] ;
• ligne 40 : nous utilisons la méthode [findAll] de l'interface [ClientRepository]. Cette méthode rend un type
[Iterable<Client>] que nous transformons en [List<Client>] avec la méthode statique [[Link]]. La classe
[Lists] est définie dans la bibliothèque Google Guava. Dans [[Link]] cette dépendance a été importée :
<dependency>
<groupId>[Link]</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
• lignes 38-86 : les méthodes de l'interface [IMetier] sont implémentées avec l'aide des classes de la couche [DAO] ;
[Link]
52/315
Seule la méthode de la ligne 88 est spécifique à la couche [métier]. Elle a été placée ici parce qu'elle fait un traitement métier qui
n'est pas qu'un simple accès aux données. Sans cette méthode, il n'y avait pas de raison de créer une couche [métier]. La méthode
[getAgendaMedecinJour] est la suivante :
1. package [Link];
2.
3. import [Link];
4.
5. import [Link];
6. import [Link];
[Link]
53/315
7. import [Link];
8. import [Link];
9. import [Link];
10. import [Link];
11. import [Link];
12. import [Link];
13. import [Link];
14. import [Link];
15.
16. @EnableJpaRepositories(basePackages = { "[Link]" })
17. @EnableAutoConfiguration
18. @ComponentScan(basePackages = { "rdvmedecins" })
19. @EntityScan(basePackages = { "[Link]" })
20. @EnableTransactionManagement
21. public class DomainAndPersistenceConfig {
22.
23. // la source de données MySQL
24. @Bean
25. public DataSource dataSource() {
26. BasicDataSource dataSource = new BasicDataSource();
27. [Link]("[Link]");
28. [Link]("jdbc:mysql://localhost:3306/dbrdvmedecins");
29. [Link]("root");
30. [Link]("");
31. return dataSource;
32. }
33.
34. // le provider JPA - n'est pas nécessaire si on est satisfait des valeurs par défaut utilisées par
Spring boot
35. // ici on le définit pour activer / désactiver les logs SQL
36. @Bean
37. public JpaVendorAdapter jpaVendorAdapter() {
38. HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
39. [Link](false);
40. [Link](false);
41. [Link]([Link]);
42. return hibernateJpaVendorAdapter;
43. }
44.
45. // l'EntityManagerFactory et le TransactionManager sont définis avec des valeurs par défaut par Spring
boot
46.
47. }
• lignes 45 : nous n'allons pas définir les beans [EntityManagerFactory] et [TransactionManager]. Nous allons pour cela nous
appuyer sur l'annotation [@EnableAutoConfiguration] de Spring Boot (ligne 17) ;
• lignes 24-32 : définissent la source de données MySQL5. C'est un bean qui en général ne peut être deviné par Spring
Boot ;
• lignes 36-43 : nous configurons également l'implémentation JPA pour mettre l'attribut [showSql] d'Hibernate à faux (ligne
39). Par défaut, il est à vrai ;
• pour l'instant, les seuls composants gérés par Spring sont les beans des lignes 25 et 37 plus les beans
[EntityManagerFactory] et [TransactionManager] par autoconfiguration. Il nous faut ajouter les beans des couches [métier]
et [DAO] ;
• la ligne 16 ajoute au contexte de Spring les interfaces du package [[Link]] qui héritent de l'interface
[CrudRepository] ;
• la ligne 18 ajoute au contexte de Spring toutes les classes du package [rdvmedecins] et ses descendants ayant une
annotation Spring. Dans le package [[Link]], la classe [Metier] avec son annotation [@Service] va être trouvée
et ajoutée au contexte Spring ;
• ligne 45 : un bean [entityManagerFactory] va être défini par défaut par Spring Boot. On doit indiquer à ce bean où se
trouvent les entités JPA qu'il doit gérer. C'est la ligne 19 qui fait cela ;
• ligne 20 : indique que les méthodes des interfaces héritant de l'interface [CrudRepository] doivent être exécutées au sein
d'une transaction ;
[Link]
54/315
La classe [[Link]] est une classe de test Spring / JUnit 4 :
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6.
7. import [Link];
8. import [Link];
9. import [Link];
10. import [Link];
11. import [Link];
12. import [Link].junit4.SpringJUnit4ClassRunner;
13.
14. import [Link];
15. import [Link];
16. import [Link];
17. import [Link];
18. import [Link];
19. import [Link];
20. import [Link];
21.
22. @SpringApplicationConfiguration(classes = [Link])
23. @RunWith([Link])
24. public class Metier {
25.
26. @Autowired
27. private IMetier métier;
28.
29. @Test
30. public void test1(){
31. // affichage clients
32. List<Client> clients = mé[Link]();
33. display("Liste des clients :", clients);
34. // affichage médecins
35. List<Medecin> medecins = mé[Link]();
36. display("Liste des médecins :", medecins);
37. // affichage créneaux d'un médecin
38. Medecin médecin = [Link](0);
39. List<Creneau> creneaux = mé[Link](mé[Link]());
40. display([Link]("Liste des créneaux du médecin %s", médecin), creneaux);
41. // liste des Rv d'un médecin, un jour donné
42. Date jour = new Date();
43. display([Link]("Liste des rv du médecin %s, le [%s]", médecin, jour),
mé[Link](mé[Link](), jour));
44. // ajouter un RV
45. Rv rv = null;
46. Creneau créneau = [Link](2);
47. Client client = [Link](0);
48. [Link]([Link]("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour,
créneau,
49. client));
50. rv = mé[Link](jour, créneau, client);
51. // vérification
52. Rv rv2 = mé[Link]([Link]());
53. [Link](rv, rv2);
54. display([Link]("Liste des Rv du médecin %s, le [%s]", médecin, jour),
mé[Link](mé[Link](), jour));
55. // ajouter un RV dans le même créneau du même jour
56. // doit provoquer une exception
57. [Link]([Link]("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour,
créneau,
58. client));
59. Boolean erreur = false;
60. try {
61. rv = mé[Link](jour, créneau, client);
62. [Link]("Rv ajouté");
[Link]
55/315
63. } catch (Exception ex) {
64. Throwable th = ex;
65. while (th != null) {
66. [Link]([Link]());
67. th = [Link]();
68. }
69. // on note l'erreur
70. erreur = true;
71. }
72. // on vérifie qu'il y a eu une erreur
73. [Link](erreur);
74. // liste des RV
75. display([Link]("Liste des Rv du médecin %s, le [%s]", médecin, jour),
mé[Link](mé[Link](), jour));
76. // affichage agenda
77. AgendaMedecinJour agenda = mé[Link](mé[Link](), jour);
78. [Link](agenda);
79. [Link](rv, [Link]()[2].getRv());
80. // supprimer un RV
81. [Link]("Suppression du Rv ajouté");
82. mé[Link](rv);
83. // vérification
84. rv2 = mé[Link]([Link]());
85. [Link](rv2);
86. display([Link]("Liste des Rv du médecin %s, le [%s]", médecin, jour),
mé[Link](mé[Link](), jour));
87. }
88.
89. // méthode utilitaire - affiche les éléments d'une collection
90. private void display(String message, Iterable<?> elements) {
91. [Link](message);
92. for (Object element : elements) {
93. [Link](element);
94. }
95. }
96.
97. }
[Link]
56/315
La ligne 8 ci-dessus indique que la combinaison [JOUR, ID_CRENEAU] doit être unique, ce qui empêche de mettre deux
rendez-vous le même jour dans le même créneau horaire.
• ligne 73 : on vérifie qu'une exception s'est bien produite ;
• ligne 77 : on demande l'agenda du médecin pour lequel on vient d'ajouter un rendez-vous ;
• ligne 79 : on vérifie que le rendez-vous ajouté est bien présent dans son agenda ;
• ligne 82 : on supprime le rendez-vous ajouté ;
• ligne 84 : on va chercher en base le rendez-vous supprimé ;
• ligne 85 : on vérifie qu'on a récupéré un pointeur null, montrant par là que le rendez-vous cherché n'existe pas ;
Spring
7 4
Le programme console est basique. Il illustre comment récupérer une clé étrangère :
1. package [Link];
2.
3. import [Link];
4. import [Link];
5.
6. import [Link];
7. import [Link];
8.
9. import [Link];
10. import [Link];
11. import [Link];
12. import [Link];
13. import [Link];
14.
15. public class Boot {
16. // le boot
17. public static void main(String[] args) {
18. // on prépare la configuration
19. SpringApplication app = new SpringApplication([Link]);
[Link]
57/315
20. [Link](false);
21. // on la lance
22. ConfigurableApplicationContext context = [Link](args);
23. // métier
24. IMetier métier = [Link]([Link]);
25. try {
26. // ajouter un RV
27. Date jour = new Date();
28. [Link]([Link]("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new
SimpleDateFormat("dd/MM/yyyy").format(jour)));
29. Client client = (Client) new Client().build(1L, 1L);
30. Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
31. Rv rv = mé[Link](jour, créneau, client);
32. [Link]([Link]("Rv ajouté = %s", rv));
33. // vérification
34. créneau = mé[Link](1L);
35. long idMedecin = cré[Link]();
36. display("Liste des rendez-vous", mé[Link](idMedecin, jour));
37. } catch (Exception ex) {
38. [Link]("Exception : " + [Link]());
39. }
40. // fermeture du contexte Spring
41. [Link]();
42. }
43.
44. // méthode utilitaire - affiche les éléments d'une collection
45. private static <T> void display(String message, Iterable<T> elements) {
46. [Link](message);
47. for (T element : elements) {
48. [Link](element);
49. }
50. }
51.
52. }
[Link]
58/315
2.11 Introduction à Spring MVC
Spring
7 4
Nous abordons maintenant la construction de la couche web. Celle-ci est principalement constituée de méthodes qui traitent des
URL précises et répondent avec une ligne de texte au format JSON (Javascript Object Notation). Cette couche web est une
interface web qu'on appelle parfois une API web. Nous allons implémenter cette interface avec Spring MVC, une autre branche de
l'écosystème Spring. Nous commençons par étudier l'un des guides disponibles sur [[Link]
2
6
[Link]
59/315
• en [2], nous choisissons l'exemple [Rest Service] ;
• en [3], on choisit le projet Maven ;
• en [4], on prend la version finale du guide ;
• en [5], on valide ;
• en [6], le projet importé ;
Les services web accessibles via des URL standard et qui délivrent du texte JSON sont souvent appelés des services REST
(REpresentational State Transfer). Dans ce document, je me contenterai d'appeler le service que nous allons construire, un service
web / JSON. Un service est dit Restful s'il respecte certaines règles. Je n'ai pas cherché à respecter celles-ci.
[Link]
60/315
• lignes 10-14 : comme dans le projet [Spring Data], on trouve le projet parent [Spring Boot] ;
• lignes 17-20 : l'artifact [spring-boot-starter-web] amène avec lui les bibliothèques nécessaires à un projet spring MVC. Il
amène en particulier avec lui un serveur Tomcat embarqué. C'est sur ce serveur que l'application sera exécutée ;
• lignes 21-24 : la bibliothèque Jackson gère le JSON : transformation d'un objet Java en chaîne JSON et inversement ;
Application web
couche [web]
2a 2b
1 Dispatcher
Servlet Contrôleurs/
3 Actions couches Données
Navigateur Vue1
[métier, DAO, JPA]
4b Vue2 2c
Modèles
Vuen
[Link]
61/315
3. réponse - la vue V choisie utilise le modèle M construit par l'action pour initialiser les parties dynamiques de la réponse HTML
qu'elle doit envoyer au client puis envoie cette réponse.
Application web
couche [web]
2a 2b
1 Dispatcher
Servlet Contrôleurs/ couches
3 Actions Données
Navigateur [métier, DAO,
4b ORM]
JSON 2c
Modèles
4a
• en [4a], le modèle qui est une classe Java est transformé en chaîne JSON par une bibliothèque JSON ;
• en [4b], cette chaîne JSON est envoyée au navigateur ;
2.11.4 Le contrôleur C
1. package hello;
2.
3. import [Link];
4. import [Link];
5. import [Link];
6. import [Link];
7. import [Link];
8.
9. @Controller
10. public class GreetingController {
11.
12. private static final String template = "Hello, %s!";
13. private final AtomicLong counter = new AtomicLong();
14.
15. @RequestMapping("/greeting")
16. public @ResponseBody
17. Greeting greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name)
{
18. return new Greeting([Link](), [Link](template, name));
19. }
20. }
• ligne 9 : l'annotation [@Controller] fait de la classe [GreetingController] un contrôleur Spring, ç-à-d que ses méthodes
sont enregistrées pour traiter des URL ;
• ligne 15 : l'annotation [@RequestMapping] indique l'URL que traite la méthode, ici l'URL [/greeting]. Nous verrons
ultérieurement que cette URL peut être paramétrée et qu'il est possible de récupérer ces paramètres ;
• ligne 16 : l'annotation [@ResponseBody] indique que la méthode ne produit pas un modèle pour une vue (JSP, JSF,
Thymeleaf, ...) qui sera envoyée ensuite au navigateur client mais produit elle-même la réponse faite au navigateur. Ici, elle
produit un objet de type [Greeting] (ligne 18). De façon non apparente ici, cet objet va d'abord être transformé en JSON
avant d'être envoyé au navigateur. C'est la présence d'une bibliothèque JSON dans les dépendances du projet qui fait que
Spring Boot va, par autoconfiguration, configurer le projet de cette façon ;
[Link]
62/315
• ligne 17 : la méthode [greeting] a un paramètre [String name]. L'annotation [@RequestParam(value = "name", required =
false, defaultValue = "World"] indique que ce paramètre doit être initialisé avec un paramètre nommé [name]
(@RequestParam(value = "name"). Celui-ci peut être le paramètre d'un GET ou d'un POST. Ce paramètre n'est pas
obligatoire (required = false). Dans ce dernier cas, le paramètre [name] de la méthode sera initialisé avec la valeur [World]
(defaultValue = "World").
2.11.5 Le modèle M
Le modèle M produit par la méthode précédente est l'objet [Greeting] suivant :
1. package hello;
2.
3. public class Greeting {
4.
5. private final long id;
6. private final String content;
7.
8. public Greeting(long id, String content) {
9. [Link] = id;
10. [Link] = content;
11. }
12.
13. public long getId() {
14. return id;
15. }
16.
17. public String getContent() {
18. return content;
19. }
20. }
La transformation JSON de cet objet créera la chaîne de caractères {"id":n,"content":"texte"}. Au final, la chaîne JSON
produite par la méthode du contrôleur sera de la forme :
{"id":2,"content":"Hello, World!"}
ou
{"id":2,"content":"Hello, John!"}
1. package hello;
2.
3. import [Link];
4. import [Link];
[Link]
63/315
5. import [Link];
6.
7. @ComponentScan
8. @EnableAutoConfiguration
9. public class Application {
10.
11. public static void main(String[] args) {
12. [Link]([Link], args);
13. }
14. }
• ligne 11 : curieusement cette classe est exécutable avec une méthode [main] propre aux applications console. C'est bien le
cas. La classe [SpringApplication] de la ligne 12 va lancer le serveur Tomcat présent dans les dépendances et déployer le
service REST dessus ;
• ligne 4 : on voit que la classe [SpringApplication] appartient au projet [Spring Boot] ;
• ligne 12 : le premier paramètre est la classe qui configure le projet, le second d'éventuels paramètres ;
• ligne 8 : l'annotation [@EnableAutoConfiguration] demande à Spring Boot de faire la configuration du projet ;
• ligne 7 : l'annotation [@ComponentScan] fait que le dossier qui contient la classe [Application] va être exploré pour
rechercher les composants Spring. Un sera trouvé, la classe [GreetingController] qui a l'annotation [@Controller] qui en
fait un composant Spring ;
[Link]
64/315
dependencyCheck=0; autowireCandidate=true; primary=false;
factoryBeanName=[Link]$WebMvcAutoConfiguration
Adapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred);
defined in class path resource
[org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$[Link]]]
11. 2014-06-11 [Link].760 INFO 11744 --- [ main] .[Link] :
Server initialized with port: 8080
12. 2014-06-11 [Link].955 INFO 11744 --- [ main] [Link] :
Starting service Tomcat
13. 2014-06-11 [Link].956 INFO 11744 --- [ main] [Link] :
Starting Servlet Engine: Apache Tomcat/7.0.54
14. 2014-06-11 [Link].053 INFO 11744 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] :
Initializing Spring embedded WebApplicationContext
15. 2014-06-11 [Link].054 INFO 11744 --- [ost-startStop-1] [Link] : Root
WebApplicationContext: initialization completed in 1584 ms
16. 2014-06-11 [Link].596 INFO 11744 --- [ost-startStop-1] [Link] :
Mapping servlet: 'dispatcherServlet' to [/]
17. 2014-06-11 [Link].598 INFO 11744 --- [ost-startStop-1] [Link] :
Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
18. 2014-06-11 [Link].919 INFO 11744 --- [ main] [Link] :
Mapped URL path [/**/[Link]] onto handler of type [class
[Link]]
19. 2014-06-11 [Link].125 INFO 11744 --- [ main] [Link] :
Mapped "{[/greeting],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public
[Link] [Link]([Link])
20. 2014-06-11 [Link].129 INFO 11744 --- [ main] [Link] :
Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public
[Link]<[Link]<[Link], [Link]>>
[Link]([Link]
t)
21. 2014-06-11 [Link].130 INFO 11744 --- [ main] [Link] :
Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public
[Link]
[Link]([Link]
quest)
22. 2014-06-11 [Link].160 INFO 11744 --- [ main] [Link] :
Mapped URL path [/**] onto handler of type [class
[Link]]
23. 2014-06-11 [Link].160 INFO 11744 --- [ main] [Link] :
Mapped URL path [/webjars/**] onto handler of type [class
[Link]]
24. 2014-06-11 [Link].448 INFO 11744 --- [ main] [Link] :
Registering beans for JMX exposure on startup
25. 2014-06-11 [Link].490 INFO 11744 --- [ main] [Link] :
Tomcat started on port(s): 8080/http
26. 2014-06-11 [Link].492 INFO 11744 --- [ main] [Link] :
Started Application in 3.45 seconds (JVM running for 3.93)
On reçoit bien la chaîne JSON attendue. Il peut être intéressant de voir les entêtes HTTP envoyés par le serveur. Pour cela, on va
utiliser le plugin de Chrome appelé [Advanced Rest Client] (cf Annexes) :
[Link]
65/315
1
2 5
8
3
1. <properties>
2. <[Link]>UTF-8</[Link]>
3. <start-class>[Link]</start-class>
4. <[Link]>1.7</[Link]>
5. </properties>
6.
7. <build>
8. <plugins>
[Link]
66/315
9. <plugin>
10. <groupId>[Link]</groupId>
11. <artifactId>spring-boot-maven-plugin</artifactId>
12. </plugin>
13. </plugins>
14. </build>
On procède ainsi :
1
2
Dans les logs qui apparaissent dans la console, il est important de voir apparaître le plugin [spring-boot-maven-plugin]. C'est
lui qui génère l'archive exécutable.
1. D:\Temp\wksSTS\gs-rest-service-complete\target>dir
2. ...
3. 11/06/2014 15:30 <DIR> classes
4. 11/06/2014 15:30 <DIR> generated-sources
5. 11/06/2014 15:30 11 073 572 [Link]
6. 11/06/2014 15:30 3 690 [Link]
7. 11/06/2014 15:30 <DIR> maven-archiver
8. 11/06/2014 15:30 <DIR> maven-status
9. ...
[Link]
67/315
1. D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar [Link]
2.
3. . ____ _ __ _ _
4. /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
5. ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
6. \\/ ___)| |_)| | | | | || (_| | ) ) ) )
7. ' |____| .__|_| |_|_| |_\__, | / / / /
8. =========|_|==============|___/=/_/_/_/
9. :: Spring Boot :: ([Link])
10.
11. 2014-06-11 [Link].088 INFO 4972 --- [ main] [Link]
12. : Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
13. sSTS\gs-rest-service-complete\target\[Link] started by ST in
14. D:\Temp\wksSTS\gs-rest-service-complete\target)
15. ...
Maintenant que l'application web est lancée, on peut l'interroger avec un navigateur :
[Link]
68/315
37. </project>
• ligne 9 : il faut indiquer qu'on va générer une archive war (Web ARchive) ;
• lignes 26-30 : il faut ajouter une dépendance sur l'artifact [spring-boot-starter-tomcat]. Cet artifact amène toutes les classes
de Tomcat dans les dépendances du projet ;
• ligne 29 : cet artifact est [provided], ç-à-d que les archives correspondantes ne seront pas placées dans le war généré. En
effet, ces archives seront trouvées sur le serveur Tomcat sur lequel s'exécutera l'application ;
Il faut par ailleurs configurer l'application web. En l'absence de fichier [[Link]], cela se fait avec une classe héritant de
[SpringBootServletInitializer] :
1. package hello;
2.
3. import [Link];
4. import [Link];
5.
6. public class ApplicationInitializer extends SpringBootServletInitializer {
7.
8. @Override
9. protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
10. return [Link]([Link]);
11. }
12.
13. }
• en [1], on exécute le projet sur l'un des serveurs enregistrés dans l'IDE Eclipse ;
• en [2], on choisit [tc Server Developer] qui est présent par défaut. C'est une variante de Tomcat ;
[Link]
69/315
Nous savons désormais générer une archive war. Par la suite, nous continuerons à travailler avec Spring Boot et son archive jar
exécutable.
1
3
[Link]
70/315
2.12 La couche [web]
Application web
couche [web]
2a 2b
1 Dispatcher
Servlet Contrôleurs/
3 Actions couches Données
Navigateur [métier, DAO, JPA]
4b
JSON 2c
Modèles
4a
1. <modelVersion>4.0.0</modelVersion>
2. <groupId>[Link]</groupId>
3. <artifactId>rdvmedecins-webapi-v1</artifactId>
4. <version>0.0.1-SNAPSHOT</version>
5. <name>rdvmedecins-webapi-v1</name>
6. <description>Gestion de RV Médecins</description>
7. <parent>
8. <groupId>[Link]</groupId>
9. <artifactId>spring-boot-starter-parent</artifactId>
10. <version>[Link]</version>
11. </parent>
12. <dependencies>
13. <dependency>
14. <groupId>[Link]</groupId>
15. <artifactId>spring-boot-starter-web</artifactId>
16. </dependency>
17. <dependency>
18. <groupId>[Link]</groupId>
19. <artifactId>rdvmedecins-metier-dao</artifactId>
20. <version>0.0.1-SNAPSHOT</version>
21. </dependency>
22. </dependencies>
[Link]
71/315
2.12.2 L'interface du service web
Application web
couche [web]
2a 2b
1 Dispatcher
Servlet Contrôleurs/
3 Actions couches Données
Navigateur [métier, DAO, JPA]
4b
JSON 2c
Modèles
4a
• en [1], ci-dessus, le navigateur ne peut demander qu'un nombre restreint d'URL avec une syntaxe précise ;
• en [4], il reçoit une réponse JSON ;
Les réponses de notre service web auront toutes la même forme correspondant à la transformation JSON d'un objet de type
[Reponse] suivant :
1. package [Link];
2.
3. public class Reponse {
4.
5. // ----------------- propriétés
6. // statut de l'opération
7. private int status;
8. // la réponse JSON
9. private Object data;
10.
11. // ---------------constructeurs
12. public Reponse() {
13. }
14.
15. public Reponse(int status, Object data) {
16. [Link] = status;
17. [Link] = data;
18. }
19.
20. // méthodes
21. public void incrStatusBy(int increment) {
22. status += increment;
23. }
24.
25. // ----------------------getters et setters
26. ...
27. }
Nous présentons maintenant les copies d'écran qui illustrent l'interface du service web / JSON :
[Link]
72/315
Liste de tous les médecins du cabinet médical [/getAllMedecins]
[Link]
73/315
Pour ajouter / supprimer un rendez-vous nous utilisons le complément Chrome [Advanced Rest Client] car ces opérations se font
avec un POST.
[Link]
74/315
La réponse est alors la suivante :
• en [4] : le client envoie l'entête signifiant que les données qu'il envoie sont au format JSON ;
• en [5] : le service web répond qu'il envoie lui aussi du JSON ;
• en [6] : la réponse JSON du service web. Le champ [data] contient la forme JSON du rendez-vous ajouté ;
[Link]
75/315
1
[Link]
76/315
Ci-dessus, le rendez-vous du patient [Mme GERMAN] n'est plus présent.
Le service web permet également de récupérer des entités via leur identifiant :
Toutes ces URL sont traitées par le contrôleur [RdvMedecinsController] que nous présentons maintenant.
[Link]
77/315
2.12.3 Le squelette du contrôleur [RdvMedecinsController]
1. package [Link];
2.
3. import [Link];
4. ...
5.
6. @RestController
7. public class RdvMedecinsController {
8.
9. @Autowired
10. private ApplicationModel application;
11. private List<String> messages;
12.
13. @PostConstruct
14. public void init() {
15. // messages d'erreur de l'application
16. messages = [Link]();
17. }
18.
19. // liste des médecins
20. @RequestMapping(value = "/getAllMedecins", method = [Link])
21. public Reponse getAllMedecins() {
22. ...
23. }
24.
25. // liste des clients
26. @RequestMapping(value = "/getAllClients", method = [Link])
27. public Reponse getAllClients() {
28. ...
29. }
30.
31. // liste des créneaux d'un médecin
32. @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = [Link])
33. public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
34. ...
35. }
36.
37. // liste des rendez-vous d'un médecin
38. @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = [Link])
39. public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin,
40. @PathVariable("jour") String jour) {
41. ...
42. }
43.
44. @RequestMapping(value = "/getClientById/{id}", method = [Link])
45. public Reponse getClientById(@PathVariable("id") long id) {
46. ...
47. }
48.
49. @RequestMapping(value = "/getMedecinById/{id}", method = [Link])
50. public Reponse getMedecinById(@PathVariable("id") long id) {
51. ...
52. }
53.
54. @RequestMapping(value = "/getRvById/{id}", method = [Link])
55. public Reponse getRvById(@PathVariable("id") long id) {
56. ...
57. }
[Link]
78/315
58.
59. @RequestMapping(value = "/getCreneauById/{id}", method = [Link])
60. public Reponse getCreneauById(@PathVariable("id") long id) {
61. ...
62. }
63.
64. @RequestMapping(value = "/ajouterRv", method = [Link], consumes = "application/json;
charset=UTF-8")
65. public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
66. ...
67. }
68.
69. @RequestMapping(value = "/supprimerRv", method = [Link], consumes = "application/json;
charset=UTF-8")
70. public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
71. ...
72. }
73.
74. @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = [Link])
75. public Reponse getAgendaMedecinJour(
76. @PathVariable("idMedecin") long idMedecin,
77. @PathVariable("jour") String jour) {
78. ...
79. }
80. }
• ligne 6 : l'annotation [@RestController] fait de la classe [RdvMedecinsController] un contrôleur Spring. Par ailleurs, elle
entraîne également que les méthodes traitant les URL vont générer une réponse qui sera automatiquement transformée en
JSON ;
• lignes 9-10 : un objet de type [ApplicationModel] sera injecté ici par Spring ;
• ligne 13 : l'annotation [@PostConstruct] tague une méthode à exécuter juste après l'instanciation de la classe. Lorsqu'elle
celle-ci s'exécute, les objets injectés par Spring sont disponibles ;
• toutes les méthodes rendent un objet de type [Reponse] suivant :
1. package [Link];
2.
3. public class Reponse {
4.
5. // ----------------- propriétés
6. // statut de l'opération
7. private int status;
8. // la réponse
9. private Object data;
10. ...
11. }
Cet objet est sérialisé en JSON avant d'être envoyé au navigateur client ;
• ligne 20 : l'annotation [@RequestMapping] fixe les conditions d'appel de la méthode. Ici la méthode traite une demande
GET de l'URL [/getAllMedecins]. Si cette URL était demandée par un POST, elle serait refusée et Spring MVC enverrait
un code HTTP d'erreur au client web ;
• ligne 32 : l'URL est paramétrée par {idMedecin}. Ce paramètre est récupéré avec l'annotation [@PathVariable] ligne 33 ;
• ligne 33 : l'unique paramètre [long idMedecin] reçoit sa valeur du paramètre {idMedecin} de l'URL
[@PathVariable("idMedecin")]. Le paramètre dans l'URL et celui de la méthode peuvent porter des noms différents. Il faut
noter ici que [@PathVariable("idMedecin")] est de type String (toute l'URL est un String) alors que le paramètre [long
idMedecin] est de type [long]. Le changement de type est fait automatiquement. Un code d'erreur HTTP est renvoyé si ce
changement de type échoue ;
• ligne 65 : l'annotation [@RequestBody] désigne le corps de la requête. Dans une requête GET, il n'y a quasiment jamais
de corps (mais il est possible d'en mettre un). Dans une requête POST, il y en a le plus souvent (mais il est possible de ne
pas en mettre). Pour l'URL [ajouterRv], le client web envoie dans son POST la chaîne JSON suivante :
La syntaxe [@RequestBody PostAjouterRv post] (ligne 65) ajoutée au fait que la méthode attend du JSON [consumes =
"application/json; charset=UTF-8"] ligne 64 va faire que la chaîne JSON envoyée par le client web va être désérialisée en
un objet de type [PostAjouter]. Celui-ci est le suivant :
1. package [Link];
2.
[Link]
79/315
3. public class PostAjouterRv {
4.
5. // données du post
6. private String jour;
7. private long idClient;
8. private long idCreneau;
9.
10. // getters et setters
11. ...
12. }
{"idRv":116}
1. package [Link];
2.
3. public class PostSupprimerRv {
4.
5. // données du post
6. private long idRv;
7.
8. // getters et setters
9. ...
10. }
Nous avons déjà présenté les modèles [Reponse, PostAjouterRv, PostSupprimerRv]. Le modèle [ApplicationModel] est le suivant :
1. package [Link];
2.
3. import [Link];
4. ...
5.
6. @Component
7. public class ApplicationModel implements IMetier {
8.
9. // la couche [métier]
10. @Autowired
11. private IMetier métier;
12.
13. // données provenant de la couche [métier]
14. private List<Medecin> médecins;
15. private List<Client> clients;
16. // messages d'erreur
17. private List<String> messages;
18.
19. @PostConstruct
20. public void init() {
21. // on récupère les médecins et les clients
22. try {
23. médecins = mé[Link]();
24. clients = mé[Link]();
[Link]
80/315
25. } catch (Exception ex) {
26. messages = [Link](ex);
27. }
28. }
29.
30. // getter
31. public List<String> getMessages() {
32. return messages;
33. }
34.
35. // ------------------------- interface couche [métier]
36. @Override
37. public List<Client> getAllClients() {
38. return clients;
39. }
40.
41. @Override
42. public List<Medecin> getAllMedecins() {
43. return médecins;
44. }
45.
46. @Override
47. public List<Creneau> getAllCreneaux(long idMedecin) {
48. return mé[Link](idMedecin);
49. }
50.
51. @Override
52. public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
53. return mé[Link](idMedecin, jour);
54. }
55.
56. @Override
57. public Client getClientById(long id) {
58. return mé[Link](id);
59. }
60.
61. @Override
62. public Medecin getMedecinById(long id) {
63. return mé[Link](id);
64. }
65.
66. @Override
67. public Rv getRvById(long id) {
68. return mé[Link](id);
69. }
70.
71. @Override
72. public Creneau getCreneauById(long id) {
73. return mé[Link](id);
74. }
75.
76. @Override
77. public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
78. return mé[Link](jour, creneau, client);
79. }
80.
81. @Override
82. public void supprimerRv(Rv rv) {
83. mé[Link](rv);
84. }
85.
86. @Override
87. public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
88. return mé[Link](idMedecin, jour);
89. }
90.
91. }
• ligne 6 : l'annotation [@Component] fait de la classe [ApplicationModel] un composant Spring. Comme tous les
composants Spring vus jusqu'ici (à l'exception de @Controller), un seul objet de ce type sera instancié (singleton) ;
• ligne 7 : la classe [ApplicationModel] implémente l'interface [IMetier] ;
• lignes 10-11 : une référence sur la couche [métier] est injectée par Spring ;
[Link]
81/315
• ligne 19 : l'annotation [@PostConstruct] fait que la méthode [init] va être exécutée juste après l'instanciation de la classe
[ApplicationModel] ;
• lignes 23-24 : on récupère les listes de médecins et de clients auprès de la couche [métier] ;
• ligne 26 : si une exception se produit, on stocke les messages de la pile d'exceptions dans le champ de la ligne 17 ;
Application web
couche [web]
1 Dispatcher 2a
Servlet Contrôleurs/ 2b couches
3 Actions Données
Navigateur [Application [métier,
4b
JSON 2c
Model] DAO,
Modèles JPA]
4a
Cette stratégie amène de la souplesse quant à la gestion du cache. Actuellement les créneaux horaires des médecins ne sont pas mis
en cache. Pour les y mettre, il suffit de modifier la classe [ApplicationModel]. Cela n'a aucun impact sur le contrôleur qui continuera
à utiliser la méthode [List<Creneau> getAllCreneaux(long idMedecin)] comme il le faisait auparavant. C'est l'implémentation de
cette méthode dans [ApplicationModel]qui sera changée.
1. package [Link];
2.
3. import [Link];
4. ...
5.
6. public class Static {
7.
8. public Static() {
9. }
10.
11. // liste des messages d'erreur d'une exception
12. public static List<String> getErreursForException(Exception exception) {
13. // on récupère la liste des messages d'erreur de l'exception
14. Throwable cause = exception;
15. List<String> erreurs = new ArrayList<String>();
16. while (cause != null) {
17. [Link]([Link]());
18. cause = [Link]();
19. }
[Link]
82/315
20. return erreurs;
21. }
22.
23. // mappers Object --> Map
24. // --------------------------------------------------------
25. ....
26. }
• ligne 12 : la méthode [[Link]] qui a été utilisée (ligne 8 ci-dessous) dans la méthode [init] de la
classe [ApplicationModel] :
1. @PostConstruct
2. public void init() {
3. // on récupère les médecins et les clients
4. try {
5. médecins = mé[Link]();
6. clients = mé[Link]();
7. } catch (Exception ex) {
8. messages = [Link](ex);
9. }
10. }
La méthode construit un objet [List<String>] avec les messages d'erreur [[Link]()] d'une exception
[exception] et de celles qu'elle contient [[Link]()].
La classe [Static] contient d'autres méthodes utilitaires sur lesquelles nous reviendrons lorsqe nous les rencontrerons.
Nous allons maintenant détailler le traitement des URL du service web. Trois classes principales sont en jeu dans ce traitement :
• le contrôleur [RdvMedecinsController] ;
• la classe de méthodes utilitaires [Static] ;
• la classe de cache [ApplicationModel] ;
1. @Autowired
2. private ApplicationModel application;
3. private List<String> messages;
4.
5. @PostConstruct
6. public void init() {
7. // messages d'erreur de l'application
8. messages = [Link]();
9. }
• ligne 8 : les messages d'erreur stockés dans l'application cache [ApplicationModel] sont mémorisés en local dans le champ
de la ligne 3. Cela va permettre aux méthodes de savoir si l'application s'est initialisée correctement.
[Link]
83/315
1. // liste des médecins
2. @RequestMapping(value = "/getAllMedecins", method = [Link])
3. public Reponse getAllMedecins() {
4. // état de l'application
5. if (messages != null) {
6. return new Reponse(-1, messages);
7. }
8. // liste des médecins
9. try {
10. return new Reponse(0, [Link]());
11. } catch (Exception e) {
12. return new Reponse(1, [Link](e));
13. }
14. }
• ligne 5 : on regarde si l'application s'est correctement initialisée (messages==null). Si ce n'est pas le cas, on renvoie une
réponse avec status=-1 et data=messages ;
• ligne 10 : sinon on renvoie la liste des médecins avec un status égal à 0. La méthode [[Link]()] ne lance
pas d'exception car elle se contente de rendre une liste qui est en cache. Néanmoins on gardera cette gestion d'exception
pour le cas où les médecins ne seraient plus mis en cache ;
Nous n'avons pas encore illustré le cas où l'application s'est mal initialisée. Arrêtons le SGBD MySQL5, lançons le service web puis
demandons l'URL [/getAllMedecins] :
On obtient bien une erreur. Dans un contexte normal, on obtient la vue suivante :
[Link]
84/315
8. // liste des clients
9. try {
10. return new Reponse(0, [Link]());
11. } catch (Exception e) {
12. return new Reponse(1, [Link](e));
13. }
14. }
Elle est analogue à la méthode [getAllMedecins] déjà étudiée. Les résultats obtenus sont les suivants :
• ligne 9 : le médecin identifié par le paramètre [id] est demandé à une méthode locale :
On revient de cette méthode avec un status dans [0,1,2]. Revenons au code de la méthode [getAllCreneaux] :
[Link]
85/315
• lignes 10-12 : si status!=0, on rend immédiatement la réponse ;
• ligne 13 : on récupère le médecin ;
• ligne 17 : on récupère les créneaux de ce médecin ;
• ligne 22 : on envoie comme réponse un objet [[Link](créneaux)] ;
1. @Entity
2. @Table(name = "creneaux")
3. public class Creneau extends AbstractEntity {
4.
5. private static final long serialVersionUID = 1L;
6. // caractéristiques d'un créneau de RV
7. private int hdebut;
8. private int mdebut;
9. private int hfin;
10. private int mfin;
11.
12. // un créneau est lié à un médecin
13. @ManyToOne(fetch = [Link])
14. @JoinColumn(name = "id_medecin")
15. private Medecin medecin;
16.
17. // clé étrangère
18. @Column(name = "id_medecin", insertable = false, updatable = false)
19. private long idMedecin;
20. ...
21. }
Rappelons la requête JPQL qui implémente la méthode [getAllCreneaux] dans la couche [DAO] :
La notation [[Link]] force la jointure entre les tables [CRENEAUX] et [MEDECINS]. Aussi la requête ramène-t-elle tous les
créneaux du médecin avec dans chacun d'eux le médecin. Lorsqu'on sérialize en JSON ces créneaux, on voit apparaître la chaîne
JSON du médecin dans chacun d'eux. C'est inutile. Aussi plutôt que de sérialiser un objet [Creneau], on va sérialiser un objet [Map]
dans lequel on ne mettra que les champs désirés.
1. // on rend la réponse
2. return new Reponse(0, [Link](créneaux));
[Link]
86/315
9. [Link]("id", cré[Link]());
10. [Link]("hDebut", cré[Link]());
11. [Link]("mDebut", cré[Link]());
12. [Link]("hFin", cré[Link]());
13. [Link]("mFin", cré[Link]());
14. // on rend le dictionnaire
15. return hash;
16. }
[Link]
87/315
2.12.10 L'URL [/getRvMedecinJour/{idMedecin}/{jour}]
L'URL [/getRvMedecinJour/{idMedecin}/{jour}] est traitée par la méthode suivante du contrôleur [RdvMedecinsController] :
• ligne 31 : on rend un objet List<Map<String,Object>> au lieu d'un objet List<Rv>. Rappelons la définition de la
classe [Rv] :
1. @Entity
2. @Table(name = "rv")
3. public class Rv extends AbstractEntity {
4. private static final long serialVersionUID = 1L;
5.
6. // caractéristiques d'un Rv
7. @Temporal([Link])
8. private Date jour;
9.
[Link]
88/315
10. // un rv est lié à un client
11. @ManyToOne(fetch = [Link])
12. @JoinColumn(name = "id_client")
13. private Client client;
14.
15. // un rv est lié à un créneau
16. @ManyToOne(fetch = [Link])
17. @JoinColumn(name = "id_creneau")
18. private Creneau creneau;
19.
20. // clés étrangères
21. @Column(name = "id_client", insertable = false, updatable = false)
22. private long idClient;
23. @Column(name = "id_creneau", insertable = false, updatable = false)
24. private long idCreneau;
25.
26. ...
27.
28. }
@Query("select rv from Rv rv left join fetch [Link] c left join fetch [Link] cr where [Link]=?1 and
[Link]=?2")
De jointures sont faites explicitement pour ramener les champs [client] et [creneau]. Par ailleurs à cause de la jointure
[[Link]=?1], nous aurons également le médecin. Le médecin va donc apparaître dans la chaîne JSON de chaque rendez-vous.
Or cette information dupliquée est en outre inutile. Revenons au code de la méthode :
1. // Rv --> Map
2. public static Map<String, Object> getMapForRv(Rv rv) {
3. // qq chose à faire ?
4. if (rv == null) {
5. return null;
6. }
7. // dictionnaire <String,Object>
8. Map<String, Object> hash = new HashMap<String, Object>();
9. [Link]("id", [Link]());
10. [Link]("client", [Link]());
11. [Link]("creneau", getMapForCreneau([Link]()));
12. // on rend le dictionnaire
13. return hash;
14. }
• ligne 11 : nous reprenons le dictionnaire de l'objet [Creneau] que nous avons présenté précédemment ;
[Link]
89/315
ou encore ceux-ci avec un médecin incorrect :
[Link]
90/315
4. if (agenda == null) {
5. return null;
6. }
7. // dictionnaire <String,Object>
8. Map<String, Object> hash = new HashMap<String, Object>();
9. [Link]("medecin", [Link]());
10. [Link]("jour", new SimpleDateFormat("yyyy-MM-dd").format([Link]()));
11. List<Map<String, Object>> créneaux = new ArrayList<Map<String, Object>>();
12. for (CreneauMedecinJour créneau : [Link]()) {
13. cré[Link](getMapForCreneauMedecinJour(créneau));
14. }
15. [Link]("creneauxMedecin", créneaux);
16. // on rend le dictionnaire
17. return hash;
18. }
• lignes 9-10 : on utilise les dictionnaires déjà étudiés pour les types [Creneau] et [Rv] qui n'embarquent donc pas d'objet
[Medecin] ;
[Link]
91/315
ou bien ceux-ci si le jour est erroné :
[Link]
92/315
9. }
[Link]
93/315
8. }
9. // client existant ?
10. if (client == null) {
11. return new Reponse(2, null);
12. }
13. // ok
14. return new Reponse(0, client);
15. }
[Link]
94/315
15. }
1. // Rv --> Map
2. public static Map<String, Object> getMapForRv2(Rv rv) {
3. // qq chose à faire ?
[Link]
95/315
4. if (rv == null) {
5. return null;
6. }
7. // dictionnaire <String,Object>
8. Map<String, Object> hash = new HashMap<String, Object>();
9. [Link]("id", [Link]());
10. [Link]("idClient", [Link]());
11. [Link]("idCreneau", [Link]());
12. // on rend le dictionnaire
13. return hash;
14. }
[Link]
96/315
30. return réponse;
31. }
32. Client client = (Client) ré[Link]();
33. // on ajoute le Rv
34. Rv rv = null;
35. try {
36. rv = [Link](jourAgenda, créneau, client);
37. } catch (Exception e1) {
38. return new Reponse(5, [Link](e1));
39. }
40. // on rend la réponse
41. return new Reponse(0, [Link](rv));
42. }
Il n'y a là rien qui n'ait été déjà vu. Ligne 41, on rend le rendez-vous qui a été ajouté ligne 36.
Les résultats obtenus ressemblent à ceci avec le client [Advanced Rest Client] :
[Link]
97/315
4
[Link]
98/315
2.12.17 L'URL [/supprimerRv]
L'URL [/supprimerRv] est traitée par la méthode suivante du contrôleur [RdvMedecinsController] :
[Link]
99/315
1
[Link]
100/315
5
Nous en avons terminé avec le contrôleur. Nous voyons maintenant comment configurer le projet.
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6.
7. import [Link];
8.
9. @EnableAutoConfiguration
10. @ComponentScan(basePackages = { "[Link]" })
11. @Import({ [Link] })
12. public class AppConfig {
13.
14. }
• ligne 9 : on se met en mode [AutoConfiguration] afin que Spring Boot puisse configurer le projet en fonction des archives
qu'il trouvera dans le Classpath du projet ;
• ligne 10 : on demande à ce que les composants Spring soient cherchés dans le package [[Link]] et ses
descendants. C'est ainsi que seront découverts les composants :
◦ [@RestController RdvMedecinsController] dans le package [[Link]] ;
◦ [@Component ApplicationModel] dans le package [[Link]] ;
• ligne 11 : on importe la classe [DomainAndPersistenceConfig] qui configure le projet [rdvmedecins-metier-dao] afin
d'avoir accès aux beans de ce projet ;
[Link]
101/315
2.12.19 La classe exécutable du service web
1. package [Link];
2.
3. import [Link];
4.
5. import [Link];
6.
7. public class Boot {
8.
9. public static void main(String[] args) {
10. [Link]([Link], args);
11. }
12. }
Ligne 10, la méthode statique [[Link]] est exécutée avec comme premier paramètre, la classe [AppConfig] de
configuration du projet. Cette méthode va procéder à l'auto-configuration du projet, lancer le serveur Tomcat embarqué dans les
dépendances et y déployer le contrôleur [RdvMedecinsController].
1. . ____ _ __ _ _
2. /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
3. ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
4. \\/ ___)| |_)| | | | | || (_| | ) ) ) )
5. ' |____| .__|_| |_|_| |_\__, | / / / /
6. =========|_|==============|___/=/_/_/_/
7. :: Spring Boot :: ([Link])
8.
9. 2014-06-12 [Link].261 INFO 9388 --- [ main] [Link] :
Starting Boot on Gportpers3 with PID 9388 (D:\data\istia-1314\polys\istia\angularjs-
spring4\dvp\rdvmedecins-webapi\target\classes started by ST)
10. 2014-06-12 [Link].306 INFO 9388 --- [ main] ationConfigEmbeddedWebApplicationContext :
Refreshing
[Link]@a1e932e: startup
date [Thu Jun 12 [Link] CEST 2014]; root of context hierarchy
11. 2014-06-12 [Link].058 INFO 9388 --- [ main] [Link] :
Overriding bean definition for bean '[Link]':
replacing [Generic bean: class
[[Link]$BasePackages]; scope=; abstract=false;
lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false;
factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null] with [Generic
bean: class [[Link]$BasePackages]; scope=;
abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true;
primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null;
destroyMethodName=null]
12. 2014-06-12 [Link].866 INFO 9388 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean
'[Link]' of type [class
[Link]$
$EnhancerBySpringCGLIB$$fd7a7b18] is not eligible for getting processed by all BeanPostProcessors (for
example: not eligible for auto-proxying)
13. 2014-06-12 [Link].900 INFO 9388 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean
'transactionAttributeSource' of type [class
[Link]] is not eligible for
getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
14. 2014-06-12 [Link].915 INFO 9388 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean
'transactionInterceptor' of type [class
[Link]
102/315
[Link]] is not eligible for getting
processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
15. 2014-06-12 [Link].920 INFO 9388 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean
'[Link]' of type [class
[Link]] is not
eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
16. 2014-06-12 [Link].164 INFO 9388 --- [ main] .[Link] :
Server initialized with port: 8080
17. 2014-06-12 [Link].403 INFO 9388 --- [ main] [Link] :
Starting service Tomcat
18. 2014-06-12 [Link].403 INFO 9388 --- [ main] [Link] :
Starting Servlet Engine: Apache Tomcat/7.0.52
19. 2014-06-12 [Link].582 INFO 9388 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] :
Initializing Spring embedded WebApplicationContext
20. 2014-06-12 [Link].582 INFO 9388 --- [ost-startStop-1] [Link] : Root
WebApplicationContext: initialization completed in 2279 ms
21. 2014-06-12 [Link].117 INFO 9388 --- [ost-startStop-1] [Link] :
Mapping servlet: 'dispatcherServlet' to [/]
22. 2014-06-12 [Link].119 INFO 9388 --- [ost-startStop-1] [Link] :
Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
23. 2014-06-12 [Link].662 INFO 9388 --- [ main] [Link] :
Building JPA container EntityManagerFactory for persistence unit 'default'
24. 2014-06-12 [Link].707 INFO 9388 --- [ main] [Link] :
HHH000204: Processing PersistenceUnitInfo [
25. name: default
26. ...]
27. 2014-06-12 [Link].839 INFO 9388 --- [ main] [Link] :
HHH000412: Hibernate Core {[Link]}
28. 2014-06-12 [Link].842 INFO 9388 --- [ main] [Link] :
HHH000206: [Link] not found
29. 2014-06-12 [Link].844 INFO 9388 --- [ main] [Link] :
HHH000021: Bytecode provider name : javassist
30. 2014-06-12 [Link].189 INFO 9388 --- [ main] [Link] :
HCANN000001: Hibernate Commons Annotations {[Link]}
31. 2014-06-12 [Link].616 INFO 9388 --- [ main] [Link] :
HHH000400: Using dialect: [Link]
32. 2014-06-12 [Link].783 INFO 9388 --- [ main] [Link] :
HHH000397: Using ASTQueryTranslatorFactory
33. 2014-06-12 [Link].729 INFO 9388 --- [ main] [Link] :
Mapped URL path [/**/[Link]] onto handler of type [class
[Link]]
34. 2014-06-12 [Link].825 INFO 9388 --- [ main] [Link] :
Mapped "{[/getRvMedecinJour/{idMedecin}/
{jour}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public
[Link]
[Link](long,[Link])
35. 2014-06-12 [Link].826 INFO 9388 --- [ main] [Link] :
Mapped "{[/getAllCreneaux/
{idMedecin}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public
[Link] [Link](long)
36. 2014-06-12 [Link].826 INFO 9388 --- [ main] [Link] :
Mapped "{[/getAgendaMedecinJour/{idMedecin}/
{jour}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public
[Link]
[Link](long,[Link])
37. 2014-06-12 [Link].826 INFO 9388 --- [ main] [Link] :
Mapped "{[/getAllMedecins],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto
public [Link] [Link]()
38. 2014-06-12 [Link].826 INFO 9388 --- [ main] [Link] :
Mapped "{[/getMedecinById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}"
onto public [Link]
[Link](long)
39. 2014-06-12 [Link].827 INFO 9388 --- [ main] [Link] :
Mapped "{[/getCreneauById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}"
onto public [Link]
[Link](long)
40. 2014-06-12 [Link].827 INFO 9388 --- [ main] [Link] :
Mapped "{[/getClientById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}"
onto public [Link]
[Link](long)
41. 2014-06-12 [Link].827 INFO 9388 --- [ main] [Link] :
Mapped "{[/getRvById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto
public [Link] [Link](long)
[Link]
103/315
42. 2014-06-12 [Link].827 INFO 9388 --- [ main] [Link] :
Mapped "{[/getAllClients],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto
public [Link] [Link]()
43. 2014-06-12 [Link].827 INFO 9388 --- [ main] [Link] :
Mapped "{[/ajouterRv],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-
8],produces=[],custom=[]}" onto public [Link]
[Link]([Link])
44. 2014-06-12 [Link].828 INFO 9388 --- [ main] [Link] :
Mapped "{[/supprimerRv],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-
8],produces=[],custom=[]}" onto public [Link]
[Link]([Link])
45. 2014-06-12 [Link].851 INFO 9388 --- [ main] [Link] :
Mapped URL path [/**] onto handler of type [class
[Link]]
46. 2014-06-12 [Link].851 INFO 9388 --- [ main] [Link] :
Mapped URL path [/webjars/**] onto handler of type [class
[Link]]
47. 2014-06-12 [Link].131 INFO 9388 --- [ main] [Link] :
Registering beans for JMX exposure on startup
48. 2014-06-12 [Link].169 INFO 9388 --- [ main] [Link] :
Tomcat started on port(s): 8080/http
49. 2014-06-12 [Link].170 INFO 9388 --- [ main] [Link] :
Started Boot in 6.302 seconds (JVM running for 6.906)
50. 2014-06-12 [Link].520 INFO 9388 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] :
Initializing Spring FrameworkServlet 'dispatcherServlet'
51. 2014-06-12 [Link].520 INFO 9388 --- [nio-8080-exec-1] [Link] :
FrameworkServlet 'dispatcherServlet': initialization started
52. 2014-06-12 [Link].538 INFO 9388 --- [nio-8080-exec-1] [Link] :
FrameworkServlet 'dispatcherServlet': initialization completed in 18 ms
Nous avons désormais un service web opérationnel interrogeable avec un client web. Nous abordons maintenant la sécurisation de
ce service : nous voulons que seules certaines personnes puissent gérer les rendez-vous des médecins. Nous allons utiliser pour cela
le framework Spring Security, une branche de l'écosystème Spring.
[Link]
104/315
1 2
1. <parent>
2. <groupId>[Link]</groupId>
3. <artifactId>spring-boot-starter-parent</artifactId>
4. <version>[Link]</version>
5. </parent>
6.
7. <dependencies>
8. <dependency>
9. <groupId>[Link]</groupId>
10. <artifactId>spring-boot-starter-thymeleaf</artifactId>
11. </dependency>
[Link]
105/315
12. <dependency>
13. <groupId>[Link]</groupId>
14. <artifactId>spring-boot-starter-security</artifactId>
15. </dependency>
16. </dependencies>
1. <!DOCTYPE html>
2. <html xmlns="[Link]
3. xmlns:th="[Link]
4. xmlns:sec="[Link]
5. <head>
6. <title>Spring Security Example</title>
7. </head>
8. <body>
9. <h1>Welcome!</h1>
10.
11. <p>
12. Click <a th:href="@{/hello}">here</a> to see a greeting.
13. </p>
14. </body>
15. </html>
• les attributs [th:xx] sont des attributs Thymeleaf. Ils sont interpétés par Thymeleaf avant que la page HTML ne soit
envoyée au client. Celui ne les voit pas ;
• ligne 12 : l'attribut [th:href="@{/hello}"] va générer l'attribut [href] de la balise <a>. La valeur [@{/hello}] va générer le
chemin [<context>/hello] où [context] est le contexte de l'application web ;
1. <!DOCTYPE html>
2.
3. <html xmlns="[Link] xmlns:sec="[Link]
springsecurity3">
4. <head>
5. <title>Spring Security Example</title>
[Link]
106/315
6. </head>
7. <body>
8. <h1>Welcome!</h1>
9. <p>
10. Click <a href="/hello">here</a> to see a greeting.
11. </p>
12. </body>
13. </html>
1. <!DOCTYPE html>
2. <html xmlns="[Link]
3. xmlns:th="[Link]
4. xmlns:sec="[Link]
5. <head>
6. <title>Hello World!</title>
7. </head>
8. <body>
9. <h1 th:inline="text">Hello [[${#[Link]}]]!</h1>
10. <form th:action="@{/logout}" method="post">
11. <input type="submit" value="Sign Out" />
12. </form>
13. </body>
14. </html>
• ligne 9 : L'attribut [th:inline="text"] va générer le texte de la balise <h1>. Ce texte contient une expression $ qui doit être
évaluée. L'élément [[${#[Link]}]] est la valeur de l'attribut [RemoteUser] de la requête HTTP
courante. C'est le nom de l'utilisateur connecté ;
• ligne 10 : un formulaire HTML. L'attribut [th:action="@{/logout}"] va générer l'attribut [action] de la balise [form]. La
valeur [@{/logout}] va générer le chemin [<context>/logout] où [context] est le contexte de l'application web ;
1. <!DOCTYPE html>
2.
3. <html xmlns="[Link] xmlns:sec="[Link]
springsecurity3">
4. <head>
5. <title>Hello World!</title>
6. </head>
7. <body>
8. <h1>Hello user!</h1>
9. <form method="post" action="/logout">
10. <input type="submit" value="Sign Out" />
11. <input type="hidden" name="_csrf" value="c60cf557-1f3b-415f-a628-39380de7b69a" /></form>
12. </body>
13. </html>
[Link]
107/315
La dernière vue [[Link]] est la suivante :
1. <!DOCTYPE html>
2. <html xmlns="[Link]
3. xmlns:th="[Link]
4. xmlns:sec="[Link]
5. <head>
6. <title>Spring Security Example</title>
7. </head>
8. <body>
9. <div th:if="${[Link]}">Invalid username and password.</div>
10. <div th:if="${[Link]}">You have been logged out.</div>
11. <form th:action="@{/login}" method="post">
12. <div>
13. <label> User Name : <input type="text" name="username" />
14. </label>
15. </div>
16. <div>
17. <label> Password: <input type="password" name="password" />
18. </label>
19. </div>
20. <div>
21. <input type="submit" value="Sign In" />
22. </div>
23. </form>
24. </body>
25. </html>
• ligne 9 : l'attribut [th:if="${[Link]}"] fait que la balise <div> ne sera générée que si l'URL qui affiche la page de login
contient le paramètre [error] ([Link]
• ligne 10 : l'attribut [th:if="${[Link]}"] fait que la balise <div> ne sera générée que si l'URL qui affiche la page de
login contient le paramètre [logout] ([Link]
• lignes 11-23 : un formulare HTML ;
• ligne 11 : le formulaire sera posté à l'URL [<context>/login] où <context> est le contexte de l'application web ;
• ligne 13 : un champ de saisie nommé [username] ;
• ligne 17 : un champ de saisie nommé [password] ;
1. <!DOCTYPE html>
2.
3. <html xmlns="[Link] xmlns:sec="[Link]
springsecurity3">
4. <head>
5. <title>Spring Security Example</title>
6. </head>
7. <body>
8.
9. <form method="post" action="/login">
10. <div>
11. <label> User Name : <input type="text" name="username" />
12. </label>
13. </div>
14. <div>
15. <label> Password: <input type="password" name="password" />
16. </label>
[Link]
108/315
17. </div>
18. <div>
19. <input type="submit" value="Sign In" />
20. </div>
21. <input type="hidden" name="_csrf" value="c60cf557-1f3b-415f-a628-39380de7b69a" /></form>
22. </body>
23. </html>
1. package hello;
2.
3. import [Link];
4. import [Link];
5. import [Link];
6.
7. @Configuration
8. public class MvcConfig extends WebMvcConfigurerAdapter {
9.
10. @Override
11. public void addViewControllers(ViewControllerRegistry registry) {
12. [Link]("/home").setViewName("home");
13. [Link]("/").setViewName("home");
14. [Link]("/hello").setViewName("hello");
15. [Link]("/login").setViewName("login");
16. }
17.
18. }
URL vue
/, /home /templates/[Link]
/hello /templates/[Link]
/login /templates/[Link]
Le suffixe [html] et le dossier [templates] sont les valeurs par défaut utilisées par Thymeleaf. Elles peuvent être changées par
configuration. Le dossier [templates] doit être à la racine du Classpath du projet :
[Link]
109/315
1
Ci-dessus [1], les dossiers [main] et [resources] sont tous les deux des dossier source (source folders). Cela implique que leur
contenu sera à la racine du Classpath du projet. Donc en [2], les dossiers [hello] et [templates] seront à la racine du Classpath.
1. package hello;
2.
3. import [Link];
4. import
[Link];
5. import [Link];
6. import [Link];
7. import [Link];
8.
9. @Configuration
10. @EnableWebMvcSecurity
11. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
12. @Override
13. protected void configure(HttpSecurity http) throws Exception {
14. [Link]().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
15. [Link]().loginPage("/login").permitAll().and().logout().permitAll();
16. }
17.
18. @Override
19. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
20. [Link]().withUser("user").password("password").roles("USER");
21. }
22. }
[Link]
110/315
/, /home accès sans être authentifié [Link]().antMatchers("/", "/home").permitAll()
• ligne 15 : définit la méthode d'authentification. L'authentification se fait via un formulaire d'URL [/login] accessible à tous
[[Link]().loginPage("/login").permitAll()]. La déconnexion (logout) est également accessible à tous.
• lignes 19-21 : redéfinissent la méthode [configure(AuthenticationManagerBuilder auth)] qui gère les utilisateurs ;
• ligne 20 : l'autentification se fait avec des utilisateurs définis en " dur " [[Link]()]. Un utilisateur est
ici défini avec le login [user], le mot de passe [password] et le rôle [USER]. On peut accorder les mêmes droits à des
utilisateurs ayant le même rôle ;
1. package hello;
2.
3. import [Link];
4. import [Link];
5. import [Link];
6. import [Link];
7.
8. @EnableAutoConfiguration
9. @Configuration
10. @ComponentScan
11. public class Application {
12.
13. public static void main(String[] args) throws Throwable {
14. [Link]([Link], args);
15. }
16.
17. }
• ligne 8 : l'annotation [@EnableAutoConfiguration] demande à Spring Boot (ligne 3) de faire la configuration que le
développeur n'aura pas fait explicitement ;
• ligne 9 : fait de la classe [Application] une classe de configuration Spring ;
• ligne 10 : demande le scan du dossier de la classe [Application] afin de rechercher des composants Spring. Les deux classes
[MvcConfig] et [WebSecurityConfig] vont être ainsi découvertes car elles ont l'annotation [@Configuration] ;
• ligne 13 : la méthode [main] de la classe exécutable ;
• ligne 14 : la méthode statique [[Link]] est exécutée avec comme paramètre la classe de configuration
[Application]. Nous avons déjà rencontré ce processus et nous savons que le serveur Tomcat embarqué dans les
dépendances Maven du projet va être lancé et le projet déployé dessus. Nous avons vu que quatre URL étaient gérées
[/, /home, /login, /hello] et que certaines étaient protégées par des droits d'accès.
[Link]
111/315
L'URL demandée [/] est accessible à tous. C'est pourquoi nous l'avons obtenue. Le lien [here] est le suivant :
L'URL [/hello] va être demandée lorsqu'on va cliquer sur le lien. Celle-ci est protégée :
Il faut être authentifié pour l'obtenir. Spring Security va alors rediriger le navigateur client vers la page d'authentification. D'après la
configuration vue, c'est la page d'URL [/login]. Celle-ci est accessible à tous :
[Link]().loginPage("/login").permitAll().and().logout().permitAll();
3
2
1. <!DOCTYPE html>
2.
3. <html xmlns="[Link] xmlns:sec="[Link]
springsecurity3">
4. ...
5. <form method="post" action="/login">
6. ...
7. <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
8. </form>
9. </body>
10. </html>
• ligne 7, un champ caché apparaît qui n'est pas dans la page [[Link]] d'origine. C'est Thymeleaf qui l'a ajouté. Ce code
appelé CSRF (Cross Site Request Forgery) vise à éliminer une faille de sécurité. Ce jeton doit être renvoyé à Spring
Security avec l'authentification pour que cette dernière soit acceptée ;
Nous nous souvenons que seul l'utilisateur user/password est reconnu par Spring Security. Si nous entons autre chose en [2], nous
obtenons la même page avec un message d'erreur en [3]. Spring Security a redirigé le navigateur vers l'URL
[[Link] La présence du paramètre [error] a déclenché l'affichage de la balise :
[Link]
112/315
<div th:if="${[Link]}">Invalid username and password.</div>
5
4
Lorsqu'on clique sur le bouton [Sign Out], un POST va être fait sur l'URL [/logout]. Celle-ci comme l'URL [/login] est accessible à
tous :
[Link]().loginPage("/login").permitAll().and().logout().permitAll();
Dans notre association URL / vues, nous n'avons rien défini pour l'URL [/logout]. Que va-t-il se passer ? Essayons :
2.13.7 Conclusion
Dans l'exemple précédent, nous aurions pu écrire l'application web d'abord puis la sécuriser ensuite. Spring Security n'est pas
intrusif. On peut mettre en place la sécurité d'une application web déjà écrite. Par ailleurs, nous avons découvert les points suivants :
[Link]
113/315
• si l'authentification échoue, on est redirigé vers la page d'authentification avec de plus un paramètre error dans l'URL ;
• si l'authentification réussit, on est redirigé vers la page demandée lorsque l'autentification a eu lieu. Si on demande
directement la page d'authentification sans passer par une page intermédiaire, alors Spring Security nous redirige vers
l'URL [/] (ce cas n'a pas été présenté) ;
• on se déconnecte en demandant l'URL [/logout] avec un POST. Spring Security nous redirige alors vers la page
d'authentification avec le paramètre logout dans l'URL ;
Toutes ces conclusions reposent sur des comportements par défaut de Spring Security. Ces comportements peuvent être changés
par configuration en redéfinissant certaines méthodes de la classe [WebSecurityConfigurerAdapter].
Le tutoriel précédent nous aidera peu dans la suite. Nous allons en effet utiliser :
• une base de données pour stocker les utilisateurs, leurs mots de passe et leurs rôles ;
• une authentification par entête HTTP ;
On trouve assez peu de tutoriels pour ce qu'on veut faire ici. La solution qui va être proposée est un assemblage de codes trouvés ici
et là.
Dans la table USERS, les mots de passe ne sont pas stockés en clair :
• ID : clé primaire ;
• VERSION : colonne de versioning de la ligne ;
• NAME : nom du rôle. Par défaut, Spring Security attend des noms de la forme ROLE_XX, par exemple ROLE_ADMIN
ou ROLE_GUEST ;
[Link]
114/315
Table [USERS_ROLES] : table de jointure USERS / ROLES
Un utilisateur peut avoir plusieurs rôles, un rôle peut rassembler plusieurs utilisateurs. On a une relation plusieurs à plusieurs
matérialisée par la table [USERS_ROLES].
• ID : clé primaire ;
• VERSION : colonne de versioning de la ligne ;
• USER_ID : identifiant d'un utilisateur ;
• ROLE_ID : identifiant d'un rôle ;
Parce que nous modifions la base de données, l'ensemble des couches du projet [métier, DAO, JPA] doit être modifié :
Spring
7 4
[Link]
115/315
2.14.3 Les nouvelles entités [JPA]
Spring
7 4
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6.
7. @Entity
8. @Table(name = "USERS")
9. public class User extends AbstractEntity {
10. private static final long serialVersionUID = 1L;
11.
12. // propriétés
13. private String identity;
14. private String login;
15. private String password;
16.
17. // constructeur
18. public User() {
19. }
20.
21. public User(String identity, String login, String password) {
22. [Link] = identity;
23. [Link] = login;
24. [Link] = password;
25. }
26.
27. // identité
28. @Override
29. public String toString() {
30. return [Link]("User[%s,%s,%s]", identity, login, password);
31. }
32.
33. // getters et setters
34. ....
35. }
• ligne 9 : la classe étend la classe [AbstractEntity] déjà utilisée pour les autres entités ;
• lignes 13-15 : on ne précise pas de nom pour les colonnes parce qu'elles portent le même nom que les champs qui leur
sont associés ;
[Link]
116/315
La classe [Role] est l'image de la table [ROLES] :
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6.
7. @Entity
8. @Table(name = "ROLES")
9. public class Role extends AbstractEntity {
10.
11. private static final long serialVersionUID = 1L;
12.
13. // propriétés
14. private String name;
15.
16. // constructeurs
17. public Role() {
18. }
19.
20. public Role(String name) {
21. [Link] = name;
22. }
23.
24. // identité
25. @Override
26. public String toString() {
27. return [Link]("Role[%s]", name);
28. }
29.
30. // getters et setters
31. ...
32. }
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6. import [Link];
7.
8. @Entity
9. @Table(name = "USERS_ROLES")
10. public class UserRole extends AbstractEntity {
11.
12. private static final long serialVersionUID = 1L;
13.
14. // un UserRole référence un User
15. @ManyToOne
16. @JoinColumn(name = "USER_ID")
17. private User user;
18. // un UserRole référence un Role
19. @ManyToOne
20. @JoinColumn(name = "ROLE_ID")
21. private Role role;
22.
23. // getters et setters
24. ...
25. }
• lignes 15-17 : matérialisent la clé étrangère de la table [USERS_ROLES] vers la table [USERS] ;
• lignes 19-21 : matérialisent la clé étrangère de la table [USERS_ROLES] vers la table [ROLES] ;
1. package [Link];
2.
3. import [Link];
4. import [Link];
5.
6. import [Link];
7. import [Link];
8.
9. public interface UserRepository extends CrudRepository<User, Long> {
10.
11. // liste des rôles d'un utilisateur identifié par son id
12. @Query("select [Link] from UserRole ur where [Link]=?1")
13. Iterable<Role> getRoles(long id);
14.
15. // liste des rôles d'un utilisateur identifié par son login et son mot de passe
16. @Query("select [Link] from UserRole ur where [Link]=?1 and [Link]=?2")
17. Iterable<Role> getRoles(String login, String password);
18.
19. // recherche d'un utilisateur via son login
20. User findUserByLogin(String login);
21. }
1. package [Link];
2.
3. import [Link];
4.
5. public interface RoleRepository extends CrudRepository<Role, Long> {
6.
7. // recherche d'un rôle via son nom
8. Role findRoleByName(String name);
9.
10. }
1. package [Link];
2.
3. import [Link];
4.
5. public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
6.
7. }
[Link]
118/315
• ligne 5 : l'interface [UserRoleRepository] se contente d'étendre l'interface [CrudRepository] sans lui ajouter de nouvelles
méthodes ;
Spring Security impose la création d'une classe implémentant l'interface [UsersDetail] suivante :
1. package [Link];
2.
3. import [Link];
4. import [Link];
5.
6. import [Link];
7. import [Link];
8. import [Link];
9.
10. public class AppUserDetails implements UserDetails {
11.
12. private static final long serialVersionUID = 1L;
13.
14. // propriétés
15. private User user;
16. private UserRepository userRepository;
17.
18. // constructeurs
19. public AppUserDetails() {
20. }
21.
22. public AppUserDetails(User user, UserRepository userRepository) {
23. [Link] = user;
[Link]
119/315
24. [Link] = userRepository;
25. }
26.
27. // -------------------------interface
28. @Override
29. public Collection<? extends GrantedAuthority> getAuthorities() {
30. Collection<GrantedAuthority> authorities = new ArrayList<>();
31. for (Role role : [Link]([Link]())) {
32. [Link](new SimpleGrantedAuthority([Link]()));
33. }
34. return authorities;
35. }
36.
37. @Override
38. public String getPassword() {
39. return [Link]();
40. }
41.
42. @Override
43. public String getUsername() {
44. return [Link]();
45. }
46.
47. @Override
48. public boolean isAccountNonExpired() {
49. return true;
50. }
51.
52. @Override
53. public boolean isAccountNonLocked() {
54. return true;
55. }
56.
57. @Override
58. public boolean isCredentialsNonExpired() {
59. return true;
60. }
61.
62. @Override
63. public boolean isEnabled() {
64. return true;
65. }
66.
67. // getters et setters
68. ...
69. }
Spring Security impose également l'existence d'une classe implémentant l'interface [AppUserDetailsService] :
[Link]
120/315
Cette interface est implémentée par la classe [AppUserDetails] suivante :
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6. import [Link];
7. import [Link];
8.
9. @Service
10. public class AppUserDetailsService implements UserDetailsService {
11.
12. @Autowired
13. private UserRepository userRepository;
14.
15. @Override
16. public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
17. // on cherche l'utilisateur via son login
18. User user = [Link](login);
19. // trouvé ?
20. if (user == null) {
21. throw new UsernameNotFoundException([Link]("login [%s] inexistant", login));
22. }
23. // on rend les détails de l'utilsateur
24. return new AppUserDetails(user, userRepository);
25. }
26.
27. }
• ligne 9 : la classe sera un composant Spring, donc disponible dans son contexte ;
• lignes 12-13 : le composant [UserRepository] sera injecté ici ;
• lignes 16-25 : implémentation de la méthode [loadUserByUsername] de l'interface [UserDetailsService] (ligne 10). Le
paramètre est le login de l'utilisateur ;
• ligne 18 : l'utilisateur est recherché via son login ;
• lignes 20-22 : s'il n'est pas trouvé, une exception est lancée ;
• ligne 24 : un objet [AppUserDetails] est construit et rendu. Il est bien de type [UserDetails] (ligne 16) ;
Tout d'abord, nous créons une classe exécutable [CreateUser] capable de créer un utilisateur avec un rôle :
1. package [Link];
2.
3. import [Link];
4. import [Link];
5.
6. import [Link];
7. import [Link];
8. import [Link];
9. import [Link];
[Link]
121/315
10. import [Link];
11. import [Link];
12. import [Link];
13.
14. public class CreateUser {
15.
16. public static void main(String[] args) {
17. // syntaxe : login password roleName
18.
19. // il faut trois paramètres
20. if ([Link] != 3) {
21. [Link]("Syntaxe : [pg] user password role");
22. [Link](0);
23. }
24. // on récupère les paramètres
25. String login = args[0];
26. String password = args[1];
27. String roleName = [Link]("ROLE_%s", args[2].toUpperCase());
28. // contexte Spring
29. AnnotationConfigApplicationContext context = new
AnnotationConfigApplicationContext([Link]);
30. UserRepository userRepository = [Link]([Link]);
31. RoleRepository roleRepository = [Link]([Link]);
32. UserRoleRepository userRoleRepository = [Link]([Link]);
33. // le rôle existe-t-il déjà ?
34. Role role = [Link](roleName);
35. // s'il n'existe pas on le crée
36. if (role == null) {
37. role = [Link](new Role(roleName));
38. }
39. // l'utilisateur existe-t-il déjà ?
40. User user = [Link](login);
41. // s'il n'existe pas on le crée
42. if (user == null) {
43. // on hashe le mot de passe avec bcrypt
44. String crypt = [Link](password, [Link]());
45. // on sauvegarde l'utilisateur
46. user = [Link](new User(login, login, crypt));
47. // on crée la relation avec le rôle
48. [Link](new UserRole(user, role));
49. } else {
50. // l'utilisateur existe déjà- a-t-il le rôle demandé ?
51. boolean trouvé = false;
52. for (Role r : [Link]([Link]())) {
53. if ([Link]().equals(roleName)) {
54. trouvé = true;
55. break;
56. }
57. }
58. // si pas trouvé, on crée la relation avec le rôle
59. if (!trouvé) {
60. [Link](new UserRole(user, role));
61. }
62. }
63.
64. // fermeture contexte Spring
65. [Link]();
66. }
67.
68. }
• ligne 17 : la classe attend trois arguments définissant un utilisateur : son login, son mot de passe, son rôle ;
• lignes 25-27 : les trois paramètres sont récupérés ;
• ligne 29 : le contexte Spring est construit à partir de la classe de configuration [DomainAndPersistenceConfig]. Cette
classe existait déjà dans le projet précédent. Elle doit évoluer de la façon suivante :
[Link]
122/315
8. }
• ligne 1 : il faut indiquer qu'il y a maintenant des composants [Repository] dans le paquetage
[[Link]] ;
• ligne 4 : il faut indiquer qu'il y a maintenant des entités JPA dans le paquetage [[Link]] ;
• lignes 30-32 : on récupère les références des trois [Repository] qui peuent nous être utiles pour créer l'utilisateur ;
• ligne 34 : on regarde si le rôle existe déjà ;
• lignes 36-38 : si ce n'est pas le cas, on le crée en base. Il aura un nom du type [ROLE_XX] ;
• ligne 40 : on regarde si le login existe déjà ;
• lignes 42-49 : si le login n'existe pas, on le crée en base ;
• ligne 44 : on crypte le mot de passe. On utilise ici, la classe [BCrypt] de Spring Security (ligne 4). On a donc besoin des
archives de ce framework. Le fichier [[Link]] inclut une nouvelle dépendance :
1. <dependency>
2. <groupId>[Link]</groupId>
3. <artifactId>spring-boot-starter-security</artifactId>
4. </dependency>
Lorsqu'on exécute la classe avec les arguments [x x guest], on obtient en base les résultats suivants :
Table [USERS]
Table [ROLES]
Table [USERS_ROLES]
[Link]
123/315
1. package [Link];
2.
3. import [Link];
4.
5. import [Link];
6. import [Link];
7. import [Link];
8. import [Link];
9. import [Link];
10. import [Link];
11. import [Link];
12. import [Link].junit4.SpringJUnit4ClassRunner;
13.
14. import [Link];
15.
16. import [Link];
17.
18. @SpringApplicationConfiguration(classes = [Link])
19. @RunWith([Link])
20. public class UsersTest {
21.
22. @Autowired
23. private UserRepository userRepository;
24. @Autowired
25. private AppUserDetailsService appUserDetailsService;
26.
27. @Test
28. public void findAllUsersWithTheirRoles() {
29. Iterable<User> users = [Link]();
30. for (User user : users) {
31. [Link](user);
32. display("Roles :", [Link]([Link]()));
33. }
34. }
35.
36. @Test
37. public void findUserByLogin() {
38. // on récupère l'utilisateur [admin]
39. User user = [Link]("admin");
40. // on vérifie que son mot de passe est [admin]
41. [Link]([Link]("admin", [Link]()));
42. // on vérifie le rôle de admin / admin
43. List<Role> roles = [Link]([Link]("admin", [Link]()));
44. [Link](1L, [Link]());
45. [Link]("ROLE_ADMIN", [Link](0).getName());
46. }
47.
48. @Test
49. public void loadUserByUsername() {
50. // on récupère l'utilisateur [admin]
51. AppUserDetails userDetails = (AppUserDetails) [Link]("admin");
52. // on vérifie que son mot de passe est [admin]
53. [Link]([Link]("admin", [Link]()));
54. // on vérifie le rôle de admin / admin
55. @SuppressWarnings("unchecked")
56. List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>)
[Link]();
57. [Link](1L, [Link]());
58. [Link]("ROLE_ADMIN", [Link](0).getAuthority());
59. }
60.
61. // méthode utilitaire - affiche les éléments d'une collection
62. private void display(String message, Iterable<?> elements) {
63. [Link](message);
[Link]
124/315
64. for (Object element : elements) {
65. [Link](element);
66. }
67. }
68. }
• lignes 27-34 : test visuel. On affiche tous les utilisateurs avec leurs rôles ;
• lignes 36-46 : on vérifie que l'utilisateur [admin] a le mot de passe [admin] et le rôle [ROLE_ADMIN] en utilisant le
repository [UserRepository] ;
• ligne 41 : [admin] est le mot de passe en clair. En base, il est crypté selon l'algorithme BCrypt. La méthode
[ [Link]] permet de vérifier que le mot de passe en clair une fois crypté est bien égal à celui qui est en base ;
• lignes 48-59 : on vérifie que l'utilisateur [admin] a le mot de passe [admin] et le rôle [ROLE_ADMIN] en utilisant le
service [appUserDetailsService] ;
1. User[guest,guest,$2a$10$[Link]/Ql50PRXLf2FkolMTs7fr6A2J2]
2. Roles :
3. Role[ROLE_GUEST]
4. User[admin,admin,$2a$10$m79V6MKt9GPDdpjSulyqReqUioqYwXy8ollt/.ia15FhX2fym3AE6]
5. Roles :
6. Role[ROLE_ADMIN]
7. User[user,user,$2a$10$ph5y/1H89YC11oGVLB49fON.dZwnu44bAOKMK1FFl//xjAvsr/Ese]
8. Roles :
9. Role[ROLE_USER]
10. User[x,x,$2a$10$dAKd2SuQplR1iFhoBUUFs.XiA0lYxNqOmrkv97Gbr5KBoHzEi/5HG]
11. Roles :
12. Role[ROLE_GUEST]
Ce cas très favorable découle du fait que les trois tables ajoutées dans la base de données sont indépendantes des tables existantes.
On aurait même pu les mettre dans une base de données séparée. Ceci a été possible parce qu'on a décidé qu'un utilisateur avait une
existence indépendante des médecins et des clients. Si ces derniers avaient été des utilisateurs potentiels, il aurait fallu créer des liens
entre la table [USERS] et les tables [MEDECINS] et [CLIENTS]. Cela aurait eu alors un impact important sur le projet existant.
Spring
7 4
[Link]
125/315
1
Les seules modifications sont à faire dans le package [[Link]] où il faut configurer Spring Security. Nous avons
déjà rencontré une classe de configuration de Spring Security :
1. package hello;
2.
3. import [Link];
4. import
[Link];
5. import [Link];
6. import [Link];
7. import [Link];
8.
9. @Configuration
10. @EnableWebMvcSecurity
11. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
12. @Override
13. protected void configure(HttpSecurity http) throws Exception {
14. [Link]().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
15. [Link]().loginPage("/login").permitAll().and().logout().permitAll();
16. }
17.
18. @Override
19. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
20. [Link]().withUser("user").password("password").roles("USER");
21. }
22. }
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6. import
[Link];
7. import [Link];
8. import [Link];
9. import [Link];
10. import [Link];
11.
12. import [Link];
13.
14. @EnableAutoConfiguration
15. @EnableWebSecurity
16. public class SecurityConfig extends WebSecurityConfigurerAdapter {
17. @Autowired
18. private AppUserDetailsService appUserDetailsService;
19.
20. @Override
21. protected void configure(AuthenticationManagerBuilder registry) throws Exception {
22. // l'authentification est faite par le bean [appUserDetailsService]
[Link]
126/315
23. // le mot de passe est crypté par l'algorithme de hachage Bcrypt
24. [Link](appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
25. }
26.
27. @Override
28. protected void configure(HttpSecurity http) throws Exception {
29. // CSRF
30. [Link]().disable();
31. // le mot de passe est transmis par le header Authorization: Basic xxxx
32. [Link]();
33. // seul le rôle ADMIN peut utiliser l'application
34. [Link]() //
35. .antMatchers("/", "/**") // toutes les URL
36. .hasRole("ADMIN");
37. }
38. }
Authorization:Basic code
où code est le codage de la chaîne login:password par l'algorithme Base64. Par exemple, le codage Base64 de la chaîne
admin:admin est YWRtaW46YWRtaW4=. Donc l'utilisateur de login [admin] et de mot de passe [admin] enverra l'entête
HTTP suivant pour s'authentifier :
Authorization:Basic YWRtaW46YWRtaW4=
• lignes 34-36 : indiquent que toutes les URL du service web sont accessibles aux utilisateurs ayant le rôle [ROLE_ADMIN].
Cela veut dire qu'un utilisateur n'ayant pas ce rôle ne peut accéder au service web ;
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6.
7. import [Link];
8.
9. @EnableAutoConfiguration
10. @ComponentScan(basePackages = { "[Link]" })
11. @Import({ [Link], [Link] })
12. public class AppConfig {
13.
[Link]
127/315
14. }
• la modification a lieu ligne 11 : on indique qu'il y a maintennat deux fichiers de configuration à exploiter
[DomainAndPersistenceConfig] et [SecurityConfig].
Authorization:Basic code
où [code] est le code Base64 de la chaîne [login:password]. Pour générer ce code, on peut utiliser le programme suivant :
1. package [Link];
2.
3. import [Link].Base64;
4.
5. public class Base64Encoder {
6.
7. public static void main(String[] args) {
8. // on attend deux arguments : login password
9. if ([Link] != 2) {
10. [Link]("Syntaxe : login password");
11. [Link](0);
12. }
13. // on récupère les deux arguments
14. String chaîne = [Link]("%s:%s", args[0], args[1]);
15. // on encode la chaîne
16. byte[] data = [Link](chaî[Link]());
17. // on affiche son encodage Base64
18. [Link](new String(data));
19. }
20.
21. }
YWRtaW46YWRtaW4=
Maintenant que nous savons générer l'entête HTTP d'authentification, nous lançons le service web maintenant sécurisé. Puis avec le
client Chrome [Advanced Rest Client], nous demandons la liste des tous les médecins :
[Link]
128/315
1
2
[Link]
129/315
• en [2], le serveur renvoie une réponse JSON ;
• en [3], la liste des médecins.
Tentons maintenant une requête HTTP avec un entête d'authentification incorrect. La réponse est alors la suivante :
Maintenant, essayons l'utilisateur user / user. Il existe mais n'a pas accès au service web. Si nous exécutons le programme
d'encodage Base64 avec les deux arguments [user user] :
dXNlcjp1c2Vy
[Link]
130/315
1
2.15 Conclusion
Rapelons l'architecture globale de notre application client / serveur :
[Link]
131/315
Un service web sécurisé est maintenant opérationnel. On verra qu'il devra être modifié suite à des problèmes qui vont se révéler à la
construction du client Angular JS. Mais nous attendrons de rencontrer le problème pour le résoudre. Nous allons maintenant
construire le client Angular qui va offrir une interface web pour gérer les rendez-vous des médecins.
[Link]
132/315
3 Le client Angular JS
• [ref1] : le livre " Pro AngularJS " écrit par Adam Freeman aux éditions Apress. C'est un excellent livre. Les codes
source des exemples de ce livre sont disponibles gratuitement à l'URL
[[Link]
• [ref2] : la documentation officielle d'Angular JS [[Link]
Angular JS mérite un livre à lui tout seul. Celui d'Adam Freeman a plus de 600 pages et elles ne sont pas gaspillées. Nous allons
décrire une application Angular et au cours de cette description nous serons amenés à parler des fondamentaux de ce framework.
Néanmoins, nous nous en tiendrons aux seules explications nécessaires à la compréhension de la solution proposée. Angular est un
framework extrêmement riche et il existe de nombreuses solutions pour arriver au même résultat. C'est une difficulté car lorsqu'on
débute, on ne sait pas si on utilise une solution moins bonne ou meilleure qu'une autre. C'est le cas de la solution proposée ici. Elle
pourrait être écrite différemment et peut-être avec de meilleures pratiques.
L'architecture de notre client Angular sera analogue avec une terminologie un peu différente. Tout d'abord les applications Angular
sont généralement des applications web à page unique (APU) ou Single Page Application (SPA) :
[Link]
133/315
• l'utilisateur demande l'URL initiale de l'application sous la forme : [Link] Le navigateur va interroger
un serveur web pour obtenir le document demandé. Celui-ci est une page HTML stylisée par du CSS et rendue dynamique
par du Javascript ;
• ensuite l'utilisateur va interagir avec les vues qui lui sont présentées. On peut distinguer diverses sortes d'interactions :
◦ celles qui ne nécessitent aucune interaction avec l'extérieur, par exemple cacher / montrer des éléments de la vue. Elle
sont traitées par le Javascript embarqué ;
◦ celles qui nécessitent des données provenant d'un service web distant. Elles vont être récupérées par un appel AJAX
(Asynchronous Javascript And Xml), un modèle va être construit et une vue affichée ;
◦ celles qui nécessitent une autre vue que la vue initiale. Elle va être demandée par un appel Ajax au serveur qui a
délivré la page initiale. Puis le processus précédent va se répéter. La page obtenue va être mise en cache dans le
navigateur. Au prochain appel, elle ne sera pas demandée au serveur HTML distant ;
Au final, le navigateur ne fait qu'un seul appel HTTP, celui qui obtient la page initiale. Les appels HTTP suivants, vers le serveur de
pages HTML ou des services web distants, sont faits par le Javascript embarqué dans les pages.
Nous présentons maintenant l'architecture de l'application au sein du navigateur. Nous oublions le serveur HTML qui délivre les
pages HTML de l'application. Pour l'explication, on peut considérer qu'elles sont toutes présentes au sein du cache du navigateur.
Utilisateur V1 C1 Service 1
4 5 6
2
8
9 Service 2 3
M1
Données
11
réseau
Vn Cn 7
DAO
Mn
[Link]
134/315
L'utilisateur interagit avec des vues : il remplit des formulaires et les valide. Explicitons ce processus avec la vue V1 ci-dessus. On
supposera que c'est la vue initiale de l'application. Elle a été obtenue de la façon suivante :
L'utilisateur a maintenant une vue V1 devant lui. Imaginons que c'est un formulaire. Il le remplit puis le valide :
• en [4], l'utilisateur valide le formulaire ;
• en [5], cet événement va être traité par l'une des méthodes du contrôleur C1 ;
Si l'événement n'entraîne qu'un simple changement de la vue V1 (cacher / montrer des zones), le contrôleur C1 va modifier le
modèle M1 de la vue V1 puis afficher de nouveau la vue V1. Il peut pour ce faire avoir besoin de l'un des services de la couche
[services] [6].
Si l'événement entraîne un changement de vue, dans les deux cas précédents, au lieu d'afficher la vue V1, le contrôleur C1 va
demander une nouvelle URL [10]. C'est une URL interne au navigateur. Elle ne se traduit pas immédiatement par un appel HTTP
au serveur de pages HTML. Ce changement d'URL est traité par un routeur configuré de telle sorte qu'à chaque URL interne
correspond une vue V et son contrôleur C. Le routeur fait alors afficher la nouvelle vue Vn. Avant l'affichage, son contrôleur Cn
prend la main, contruit le modèle Mn puis fait afficher la vue Vn [11]. Si la page HTML de la vue Vn n'était pas en cache dans le
navigateur, alors elle sera demandée au serveur de pages HTML.
La couche [Présentation] de cette achitecture est proche de l'architecture JSF (Java Server Faces) :
La couche [Services] est différente des couches [Services] auxquelles on est habitué. En développement web, côté serveur, on a le
plus souvent l'architecture en couches suivante :
Ci-dessus, la couche [web] ne communique avec la couche [DAO] qu'au travers de la couche [métier]. Rien ne nous empêcherait
d'injecter dans la couche [web] une référence sur la couche [DAO] qui permettrait cette communication. Mais on se l'interdit.
[Link]
135/315
Application web / navigateur
couche [présentation] couche [services]
Service 1
Service ...
1 2
Métier
Données
DAO réseau
• en [1], la couche [présentation] peut communiquer directement avec n'importe quel service ;
• en [2], les services se connaissent entre-eux. Un service peut en utiliser un ou plusieurs autres.
7 9
10 11 12 13 14
• en [6], la page d'entrée de l'application. Il s'agit d'une application de prise de rendez-vous pour des médecins ;
• en [7], une case à cocher qui permet d'être ou non en mode [debug]. Ce dernier se caractérise par la présence du cadre [8]
qui affiche le modèle de la vue courante ;
• en [9], une durée d'attente artificielle en millisecondes. Elle vaut 0 par défaut (pas d'attente). Si N est la valeur de ce temps
d'attente, toute action de l'utilisateur sera exécutée après un temps d'attente de N millisecondes. Cela permet de voir la
gestion de l'attente mise en place par l'application ;
• en [10], l'URL du serveur Spring 4. Si on suit ce qui a précédé, c'est [[Link]
• en [11] et [12], l'identifiant et le mot de passe de celui qui veut utiliser l'application. Il y a deux utilisateurs : admin/admin
(login/password) avec un rôle (ADMIN) et user/user avec un rôle (USER). Seul le rôle ADMIN a le droit d'utiliser
l'application. Le rôle USER n'est là que pour montrer ce que répond le serveur dans ce cas d'utilisation ;
[Link]
136/315
• en [13], le bouton qui permet de se connecter au serveur ;
• en [14], la langue de l'application. Il y en a deux : le français par défaut et l'anglais.
• en [1], on se connecte ;
2 3
• une fois connecté, on peut choisir le médecin avec lequel on veut un rendez-vous [2] et le jour de celui-ci [3] ;
• on demande en [4] à voir l'agenda du médecin choisi pour le jour choisi ;
[Link]
137/315
5
[Link]
138/315
7
Une fois le rendez-vous validé, on est ramené automatiquement à l'agenda où le nouveau rendez-vous est désormais inscrit. Ce
rendez-vous pourra être ultérieurement supprimé [7].
Les principales fonctionnalités ont été décrites. Elles sont simples. Celles qui n'ont pas été décrites sont des fonctions de navigation
pour revenir à une vue précédente. Terminons par la gestion de la langue :
[Link]
139/315
• en [1], on passe du français à l'anglais ;
[Link]
140/315
6
7
5
10
8
9
12 11
[Link]
141/315
13
15
14
• en [14], on configure la propriété [Javascript / Bower] qui va nous permettre de déclarer les bibliothèques Javascript dont
nous avons besoin ;
• en [15], désigner le fichier [[Link]] que nous venons de créer ;
18
16
19
17
20
[Link]
142/315
• en [20], on la télécharge ;
21 22 23
24
• en [24], en suivant la même démarche que précédemment, on télécharge les bibliothèques suivantes :
[Link]
143/315
25
26
• en [25], les bibliothèques téléchargées ont été installées dans le dossier [bower_components] ;
• en [26], on voit que la bibliothèque JQuery a été téléchargée. C'est parce que Bootstrap l'utilise. Le système d'installation
des dépendances Javascript d'un projet est analogue à celui de Maven pour le monde Java : si une bibliothèque téléchargée
a elle-même des dépendances, celles-ci sont automatiquement téléchargées ;
1. {
2. "name": "rdvmedecins-angular",
3. "version": "0.0.1",
4. "dependencies": {
5. "angular": "~1.2.18",
6. "angular-base64": "~2.0.2",
7. "angular-route": "~1.2.18",
8. "angular-translate": "~2.2.0",
9. "bootstrap": "~3.1.1",
10. "footable": "~2.0.1",
11. "angular-ui-bootstrap-bower": "~0.11.0",
12. "bootstrap-select": "~1.5.2"
13. }
14. }
3
1
• en [1] et [2], nous créons un fichier HTML nommé [app-01] [3] et [4] ;
[Link]
144/315
Le fichier [[Link]] va être notre page principale pendant un moment. Nous allons y configurer l'importation des fichiers CSS
et JS dont l'application a besoin :
1. <!DOCTYPE html>
2. <html>
3. <head>
4. <title>RdvMedecins</title>
5. <!-- META -->
6. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
7. <meta name="viewport" content="width=device-width, initial-scale=1.0">
8. <meta name="description" content="Angular client for RdvMedecins">
9. <meta name="author" content="Serge Tahé">
10. <!-- le CSS -->
11. <link href="bower_components/bootstrap/dist/css/[Link]" rel="stylesheet" />
12. <link href="bower_components/bootstrap/dist/css/[Link]" rel="stylesheet"/>
13. <link href="bower_components/bootstrap-select/[Link]" rel="stylesheet"/>
14. <link href="bower_components/footable/css/[Link]" rel="stylesheet"/>
15. </head>
16. <body>
17. <div class="container">
18. <h1>Rdvmedecins - v1</h1>
19. </div>
20. <!-- Bootstrap core JavaScript ================================================== -->
21. <script type="text/javascript" src="bower_components/jquery/dist/[Link]"></script>
22. <script type="text/javascript" src="bower_components/bootstrap/dist/js/[Link]"></script>
23. <script type="text/javascript" src="bower_components/bootstrap-select/[Link]"></script>
24. <script type="text/javascript" src="bower_components/footable/dist/[Link]"></script>
25. <!-- angular js -->
26. <script type="text/javascript" src="bower_components/angular/[Link]"></script>
27. <script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-
[Link]"></script>
28. <script type="text/javascript" src="bower_components/angular-route/[Link]"></script>
29. <script type="text/javascript" src="bower_components/angular-translate/[Link]"></script>
30. <script type="text/javascript" src="bower_components/angular-base64/[Link]"></script>
31. </body>
32. </html>
[Link]
145/315
2
Cette inspection systématique du code avant son exécution est conseillée. Ici, cette détection permet de détecter toute erreur de
référence des fichiers CSS et JS. Si un chemin est incorrect, l'inspecteur de code le signalera.
• en [3], la page peut être chargée dans un navigateur par un débogueur. On obtient le résultat suivant dans le navigateur :
• en [4], la page [[Link]] a été délivrée par un serveur interne à Webstorm opérant ici sur le port 63342 ;
• en [5], la console du débogueur. Si des erreurs s'étaient produites, elles seraient apparues ici. C'est également là que vont
les affichages écran produits par l'instruction [[Link](expression)] du Javascript. Nous utiliserons abondamment
cette possibilité ;
Le mode débogage permet de modifier la page dans Webstorm et de voir les résultats de ces modifications dans le navigateur sans
avoir à recharger la page. Ainsi si nous ajoutons la ligne 3 ci-dessous :
1. <div class="container">
2. <h1>Rdvmedecins - v1</h1>
3. <h2>Version 1</h2>
4. </div>
[Link]
146/315
3.6 Découverte de Bootstrap
Nous allons illustrer maintenant certaines des caractéristiques de Bootstrap utilisées dans l'application. Je n'ai qu'une connaissance
limitée de ce framework, obtenue par des copier / coller de codes trouvés sur Internet. J'expliquerai le rôle des classes CSS que je
crois comprendre. Je m'abstiendrai de commenter les autres.
3.6.1 Exemple 1
Dans Angular, les opérations qui vont chercher de l'information à l'extérieur sont asynchrones. Cela signifie que l'opération est
lancée et qu'il y a retour immédiat à la vue avec laquelle l'utilisateur peut continuer à interagir. L'application est avertie de la fin de
l'opération par un événement. Cet événement est traité par une fonction JS qui peut alors enrichir la vue actuelle ou en changer. Si
l'opération est susceptible d'être longue, il est utile d'offrir à l'utilisateur la possibilité de l'annuler. Nous la lui offrirons
systématiquement. Pour cela, nous utiliserons un bandeau Bootstrap :
Pour obtenir ce résultat, nous dupliquons [[Link]] dans [[Link]] et nous modifions les ligne suivantes :
1. <div class="container">
2. <h1>Rdvmedecins - v1</h1>
3. <div class="alert alert-warning">
4. <h1>Opération en cours. Veuillez patienter...
5. <button class="btn btn-primary pull-right">Annuler</button>
6. <img src="assets/images/[Link]" alt=""/>
7. </h1>
8. </div>
9. </div>
• ligne 1 : la classe CSS [container] définit une zone d'affichage à l'intérieur du navigateur ;
• ligne 3 : la classe CSS [alert] affiche une zone colorée. La classe [alert-warning] utilise une couleur prédéfinie ;
• ligne 5 : la classe [btn] habille un bouton. La classe [btn-primary] lui donne une certaine couleur. La classe [pull-right]
l'envoie sur la droite du bandeau d'alerte ;
• ligne 6 : une image animée d'attente ;
3.6.2 Exemple 2
Les différentes vues de l'application auront un titre commun :
[Link]
147/315
Pour obtenir ce résultat, nous dupliquons [[Link]] dans [[Link]] et nous modifions les ligne suivantes :
1. <div class="container">
2. <h1>Rdvmedecins - v1</h1>
3. <!-- Bootstrap Jumbotron -->
4. <div class="jumbotron">
5. <div class="row">
6. <div class="col-md-2">
7. <img src="assets/images/[Link]" alt="RvMedecins"/>
8. </div>
9. <div class="col-md-10">
10. <h1>Les Médecins associés</h1>
11. </div>
12. </div>
13. </div>
14. </div>
3.6.3 Exemple 3
Les vues auront un bandeau haut de commande. On y trouvera des options de commande, liens ou boutons. On y trouvera
égelement des éléments de formulaire. Par exemple :
1 2 3
Pour obtenir ce résultat, nous dupliquons [[Link]] dans [[Link]] et nous modifions les lignes suivantes :
1. <div class="container">
2. <h1>Rdvmedecins - v1</h1>
3.
4. <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
5. <div class="container">
6. <div class="navbar-header">
7. <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
8. <span class="sr-only">Toggle navigation</span>
9. <span class="icon-bar"></span>
[Link]
148/315
10. <span class="icon-bar"></span>
11. <span class="icon-bar"></span>
12. </button>
13. <a class="navbar-brand" href="#">RdvMedecins</a>
14. </div>
15. <div class="navbar-collapse collapse">
16. <form class="navbar-form navbar-right">
17. <!-- mode debug -->
18. <label style="width: 100px">
19. <input type="checkbox">
20. <span style="color: white">Debug</span>
21. </label>
22. <!-- formulaire d'identification -->
23. <div class="form-group">
24. <input type="text" class="form-control" placeholder="Temps d'attente"
25. style="width: 150px"/>
26. <input type="text" class="form-control" placeholder="URL du service web"
27. style="width: 200px"/>
28. <input type="text" class="form-control" placeholder="Login"
29. style="width: 100px"/>
30. <input type="password" class="form-control" placeholder="Mot de passe"
31. style="width: 100px"/>
32. </div>
33. <button class="btn btn-success">
34. Connexion
35. </button>
36. </form>
37. </div>
38. <button class="btn btn-success">
39. Connexion
40. </button>
41. </form>
42. </div>
43. </div>
44. </div>
45. </div>
• ligne 4 : la classe [navbar] va styler la barre de navigation. La classe [navbar-inverse] lui donne le fond noir. La classe
[navbar-fixed-top] va faire en sorte que lorsqu'on 'scrolle' la page affichée par le navigateur, la barre de navigation va
rester en haut de l'écran ;
• lignes 6-14 : définissent la zone [1]. C'est typiquement une série de classes que je ne comprends pas. J'utilise le composant
tel quel ;
• ligne 15 : définissent une zone 'responsive' de la barre de commande. Sur un smartphone, cette zone disparaît dans une
zone de menu ;
• ligne 16 : la classe [navbar-form] habille un formulaire de la barre de commande. La classe [navbar-right] le rejette à
droite de celle-ci ;
• lignes 23-32 : les quatre zones de saisie du formulaire de la ligne 17 [3]. Elles sont à l'intérieur d'une classe [ form-group]
qui habille les éléments d'un formulaire et chacune d'elles a la classe [form-control] ;
• ligne 33 : la classe [btn] qu'on a déjà rencontrée, enrichie de la classe [btn-success] qui lui donne sa couleur verte ;
3.6.4 Exemple 4
Le bandeau de commande permettra de changer de langue grâce à une liste déroulante :
Pour obtenir ce résultat, nous dupliquons [[Link]] dans [[Link]] et nous ajoutons les ligne suivantes à la barre de
commande :
[Link]
149/315
2. Connexion
3. </button>
4. <!-- langues -->
5. <div class="btn-group">
6. <button type="button" class="btn btn-danger">
7. Langues
8. </button>
9. <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
10. <span class="caret"></span>
11. <span class="sr-only">Toggle Dropdown</span>
12. </button>
13. <ul class="dropdown-menu" role="menu">
14. <li>
15. <a href="">Français</a>
16. </li>
17. <li>
18. <a href="">English</a>
19. </li>
20. </ul>
21. </div>
22. </form>
3.6.5 Exemple 5
Pour valider un formulaire ou pour naviguer, l'utilisateur disposera dans la barre de commande d'options ou de boutons comme ci-
dessous :
Des options de menu ont été installées en [1]. Pour obtenir ce résultat, nous dupliquons [[Link]] dans [[Link]] et nous
ajoutons les ligne suivantes :
[Link]
150/315
22. </a>
23. </li>
24. <li class="active">
25. <a href="">
26. <span>Annuler</span>
27. </a>
28. </li>
29. </ul>
30. <!-- boutons de droite -->
31. <form class="navbar-form navbar-right" role="form">
32. ...
33. </form>
34. </div>
35. </div>
36. </div>
37. </div>
• les options de menu sont obtenues par les lignes 8-29. Ce sont là encore des éléments d'une liste <ul>. La classe [active]
fait que le texte est brillant indiquant par là qu'on peut cliquer sur l'option.
3.6.6 Exemple 6
Nous présenterons les médecins et les clients dans des listes déroulantes comme ci-dessous :
La liste déroulante utilisée n'est pas un composant natif Bootstrap. C'est le composant [bootstrap-select]
([Link] Pour obtenir ce résultat, nous dupliquons [[Link]] dans [[Link]] et
nous ajoutons les ligne suivantes :
1. <!DOCTYPE html>
2. <html>
3. <head>
4. ...
5. <link href="bower_components/bootstrap-select/[Link]" rel="stylesheet"/>
6.
7. </head>
8. <body>
9. <div class="container">
10. <h1>Rdvmedecins - v1</h1>
11.
12. <h2><label for="medecins">Médecins</label></h2>
13. <select id="medecins" data-style="btn btn-primary" class="selectpicker">
14. <option value="1">Mme Marie PELISSIER</option>
15. <option value="1">Mr Jacques BROMARD</option>
16. <option value="1">Mr Philippe JANDOT</option>
17. <option value="1">Mme Justine JACQUEMOT</option>
18. </select>
19. </div>
20. <!-- Bootstrap core JavaScript ================================================== -->
21. ...
22. <script type="text/javascript" src="bower_components/bootstrap-select/[Link]"></script>
23. <!-- script local -->
[Link]
151/315
24. <script>
25. $('.selectpicker').selectpicker();
26. </script>
27. </body>
28. </html>
3.6.7 Exemple 7
Pour afficher l'agenda d'un médecin, nous allons utiliser un tableau 'responsive' fourni par la bibliothèque JS [footable] :
1 2
Nous dupliquons [[Link]] dans [[Link]] et nous ajoutons les ligne suivantes :
1. ...
2. <link href="bower_components/footable/css/[Link]" rel="stylesheet"/>
3. <link href="assets/css/[Link]" rel="stylesheet"/>
4. ...
5. <div class="container">
6. <h1>Rdvmedecins - v1</h1>
7.
8. <div class="row alert alert-warning">
9. <div class="col-md-6">
10. <table id="creneaux" class="table">
11. <thead>
12. <tr>
13. <th data-toggle="true">
14. <span>Créneau horaire</span>
15. </th>
16. <th>
17. <span>Client</span>
18. </th>
19. <th data-hide="phone">
20. <span>Action</span>
21. </th>
[Link]
152/315
22. </thead>
23. <tbody>
24. <tr>
25. <td>
26. <span class='status-metro status-active'>
27. 9h00-9h20
28. </span>
29. </td>
30. <td>
31. <span></span>
32. </td>
33. <td>
34. <a href="" class="status-metro status-active">
35. Réserver
36. </a>
37. </td>
38. </tr>
39. <tr>
40. <td>
41. <span class='status-metro status-suspended'>
42. 9h20-9h40
43. </span>
44. </td>
45. <td>
46. <span>Mme Paule MARTIN</span>
47. </td>
48. <td>
49. <a href="" class="status-metro status-suspended">
50. Supprimer
51. </a>
52. </td>
53. </tr>
54. </tbody>
55. </table>
56. </div>
57. </div>
58. </div>
59. ...
60. <script src="bower_components/footable/dist/[Link]" type="text/javascript"></script>
• les lignes 2 et 60 sont déjà présentes dans [[Link]]. Ce sont les fichiers CSS et JS fournis par la bibliothèque
[footable] ;
• la ligne 3 référence le fichier CSS suivant :
@CHARSET "UTF-8";
#creneaux th {
text-align: center;
}
#creneaux td {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.[Link]-active {
background: #43c83c;
}
.[Link]-suspended {
background: #fa3031;
}
Les styles [status-*] proviennent d'un exemple d'utilisation de la table [footable] trouvé sur le site de la bibliothèque.
• ligne 8 : installe la table dans une ligne [row] et un encadré coloré [alert alert-warning] ;
• ligne 9 : la table va occuper 6 colonnes [col-md-6] ;
[Link]
153/315
• ligne 10 : la table HTML est formatée par Bootstrap [class='table'] ;
• ligne 13 : l'attribut [data-toggle] indique la colonne qui héberge le symbole [+/-] qui déplie / replie la ligne ;
• ligne 19 : l'attribut [data-hide='phone'] indique que la colonne doit être cachée si l'écran a la taille d'un écran de téléphone.
On peut également utiliser la valeur 'tablet' ;
3.6.8 Exemple 8
Pour aider l'utilisateur, nous allons créer des bulles d'aide (tooltip) autour des principaux composants des vues :
Pour obtenir ce résultat, nous dupliquons [[Link]] dans [[Link]] et nous ajoutons les ligne suivantes :
1. <!DOCTYPE html>
2. <html ng-app="rdvmedecins">
3. <head>
4. ...
5. </head>
6. <body>
7. <div class="container">
8. <h1>Rdvmedecins - v1</h1>
9. <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
10. <div class="container">
11. <div class="navbar-header">
12. <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
13. <span class="sr-only">Toggle navigation</span>
14. <span class="icon-bar"></span>
15. <span class="icon-bar"></span>
16. <span class="icon-bar"></span>
17. </button>
18. <a class="navbar-brand" href="#">RdvMedecins</a>
19. </div>
20. <!-- options de menu -->
21. <div class="collapse navbar-collapse">
22. <ul class="nav navbar-nav">
23. <li class="active">
24. <a href="">
25. <span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
26. </a>
27. </li>
28. <li class="active">
29. <a href="">
30. <span tooltip="Affiche l'agenda" tooltip-placement="top">Agenda</span>
31. </a>
32. </li>
33. <li class="active">
34. <a href="">
35. <span tooltip="Valide le rendez-vous" tooltip-placement="right">Valider</span>
36. </a>
37. </li>
38. <li class="active">
39. <a href="">
40. <span tooltip="Annule l'opération en cours" tooltip-placement="left">Annuler</span>
41. </a>
42. </li>
43. </ul>
44. </div>
45. </div>
[Link]
154/315
46. </div>
47. </div>
48. <!-- Bootstrap core JavaScript ================================================== -->
49. <...
50. <script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-
[Link]"></script>
51. <!-- script local -->
52. <script>
53. // --------------------- module Angular
54. [Link]("rdvmedecins", ['[Link]']);
55. </script>
56. </body>
57. </html>
Les bulles d'aide sont fournis par la bibliothèque [angular-ui-bootstrap] qui s'appuie elle-même sur la bibliothèque [angular]. La ligne
50 importe la bibliothèque [angular-ui-bootstrap]. Pour mettre en oeuvre les composants de la bibliothèque [angular-ui-bootstrap], il
nous faut créer un module Angular. Ceci est fait aux lignes 52-55. Ces lignes définissent un module Angular nommé [rdvmedecins]
(1er paramètre). Un module Angular peut utiliser d'autres modules Angular. C'est ce qu'on appelle des dépendances du module.
Elles sont fournies dans un tableau comme second paramètre de la fonction [[Link]]. Ici, le module nommé [[Link]]
est fourni par la bibliothèque [angular-ui-bootstrap]. C'est ce module qui va nous fournir les bulles d'aide.
La ligne 54 définit un module Angular. Par défaut, cela n'a aucun effet sur la page. On indique que la page doit être gérée par
Angular, en la rattachant à un module Angular. C'est ce qui est fait, ligne 2. L'attribut [ng-app='rdvmedecins'] rattache la page au
module créé ligne 54. La page va alors être analysée par Angular. Les attributs [tooltip] vont être découverts et traités par le module
[[Link]].
Angular JS permet d'ajouter de nouvelles balises ou nouveaux attributs à ceux existant déjà dans le langage HTML. Cette extension
du langage HTML est faite au moyen de directives Angular. Ici les attributs [tooltip] et [tooltip-placement] sont des attributs créés
par [angular-ui-bootstrap].
3.6.9 Exemple 9
Pour aider l'utilisateur à choisir le jour d'un rendez-vous, nous allons lui proposer un calendrier :
[Link]
155/315
Comme pour les bulles d'aide, ce calendrier est fourni par la bibliothèque [angular-ui-bootstrap]. Pour obtenir ce résultat, nous
dupliquons [[Link]] dans [[Link]] et nous ajoutons les ligne suivantes :
1. <!DOCTYPE html>
2. <html ng-app="rdvmedecins">
3. <head>
4. ...
5. <body>
6. <div class="container">
7. <h1>Rdvmedecins - v1</h1>
8.
9. <div>
10. <pre>Date <em>{{jour | date:'fullDate'}}</em></pre>
11. <div class="row">
12. <div class="col-md-2">
13. <h4>Calendrier</h4>
14.
15. <div style="display:inline-block; min-height:290px;">
16. <datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
17. </div>
18.
19. </div>
20. </div>
21. </div>
22. </div>
23. </div>
24. ...
25. <!-- script local -->
26. <script>
27. // --------------------- module Angular
28. [Link]("rdvmedecins", ['[Link]'])
29. </script>
30.
31. </body>
32. </html>
Comme précédemment, la page est associée à un module Angular (lignes 2 et 28). Le calendrier est défini par la balise
<datepicker> de la ligne 16 définie par la bibliothèque [angular-ui-bootstrap] :
[Link]
156/315
• [show-weeks='true'] : pour afficher les n°s des semaines ;
• [class='well'] : pour entourer le calendrier d'une zone grise à coins arrondis ;
• [ng-model='jour'] : les attrinuts [ng-*] sont des attributs Angular. L'attribut [ng-model] désigne une donnée qui va être
placée dans le modèle de la vue. Lorsque l'utilisateur va cliquer sur une date, celle-ci sera placée dans la variable [jour] du
modèle. Cette variable est utilisée ligne 10. La syntaxe {{expression}} permet d'évaluer une expression composée
d'éléments du modèle. Ici {{jour}} va afficher la valeur de la variable [jour] du modèle. Une caractéristique forte
d'Angular est que la vue va suivre automatiquement les changements de la variable [jour]. Ainsi, lorsque l'utilisateur va
changer les dates, ces changements seront immédiatement affichés ligne 10. De façon générale, le fonctionnement est le
suivant :
◦ une vue V est associée à un modèle M ;
◦ Angular observe le modèle M et met automatiquement à jour la vue V lorsqu'il y a un changement de son modèle
M;
La syntaxe {{jour|date}} est appelée un filtre. Ce n'est pas la valeur de [jour] qui est affichée mais la valeur de [jour]
filtrée par un filtre appelé [date]. Ce filtre est prédéfini dans Angular. Il sert à formater des dates. Il admet des paramètres
précisant le format désiré. Ainsi l'expression {{jour | date:'fullDate'}} indique qu'on veut le format complet de la date,
ici [Friday, June 20, 2014] parce que le calendrier est en anglais par défaut. Nous allons aborder son
internationalisation prochainement.
3.6.10 Conclusion
Nous avons présenté les éléments du framework CSS Bootstrap que nous serons amenés à utiliser. C'étaient des composants
passifs : leurs événements n'étaient pas gérés. Ainsi un clic sur les boutons ou les liens ne faisait rien. Ces événements seront gérés
en Javascript. Il est possible d'utiliser ce langage sans l'aide de frameworks mais comme ce fut le cas côté serveur, certains
frameworks s'imposent côté client. C'est le cas du framework Angular JS qui amène avec lui une nouvelle façon d'aborder le
développement des applications Javascript exécutées par un navigateur. Nous le présentons maintenant.
<html ng-app="rdvmedecins">
• Angular permet de créer de nouvelles balises et de nouveaux attributs HTML via des directives :
{{jour|date:'fullDate'}}
• une vue V affiche un modèle M. Angular observe le modèle M et met automatiquement à jour la vue V lorsqu'il y a un
changement de son modèle M. La valeur d'une variable du modèle M est affichée dans la vue V par :
{{variable}}
Nous allons commencer par approfondir l'implémentation du Design Pattern Modèle – Vue – Contrôleur dans Angular. Rappelons
les liens qui existent entre-eux d'un point de vue architecture :
[Link]
157/315
Application web / navigateur 1
Utilisateur V1 C1 Service 1
4 5 6
2
8
9 Service 2 3
M1
Données
11
réseau
Vn Cn 7
DAO
Mn
• la vue V1 affiche le modèle M1 construit par le contrôleur C1. Ce dernier contient non seulement le modèle M1 mais
également les gestionnaires des événements de la vue V1. On est dans le cycle 5, 8, 9 :
◦ [5] : un événement se produit dans la vue V1. Il est traité par le contrôleur C1 ;
◦ celui-ci fait son travail [6-7] puis construit le modèle M1 [8] ;
◦ [9] : la vue V1 affiche le nouveau modèle M1. Comme nous l'avons dit, cette dernière étape est automatique. Il n'y
pas comme dans d'autres frameworks MVC, un push explicite (C1 pousse le modèle M1 dans V1) ou un pull
explicite (la vue V1 va chercher le modèle M1 dans C1). Il y a un push implicite que le développeur ne voit pas ;
◦ puis le cycle 5, 8, 9 reprend ;
Cette directive admet d'autres attributs que ceux présentés ci-dessus, entre-autres l'attribut [min-date] qui fixe la date minimale
qu'on peut choisir dans le calendrier. Ce sera utile pour nous. Lorsque l'utilisateur choisit une date de rendez-vous, celle-ci doit être
égale ou supérieure à celle du jour courant. Nous écrirons alors :
où [dateMin] sera une variable du modèle de la page qui aura pour valeur la date du jour. Cela donnera la page suivante :
[Link]
158/315
2
1
• en [1], nous sommes le 19 Juin 2014. Le curseur indique qu'on peut sélectionner le 19 juin ;
• en [2], le curseur indique qu'on ne peut pas sélectionner le 18 juin ;
Nous dupliquons [[Link]] dans [[Link]] et nous faisons les modifications suivantes :
1. <!DOCTYPE html>
2. <html ng-app="rdvmedecins">
3. <head>
4. ...
5. </head>
6. <body ng-controller="rdvMedecinsCtrl">
7. <div class="container">
8. <h1>Rdvmedecins - v1</h1>
9.
10. <div>
11. <pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
12. <div class="row">
13. <div class="col-md-2">
14. <h4>Calendrier</h4>
15.
16. <div style="display:inline-block; min-height:290px;">
17. <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
18. </div>
19. </div>
20. </div>
21. </div>
22. </div>
23. <!-- Bootstrap core JavaScript ================================================== -->
24. ...
25. <!-- script local -->
26. <script>
27. // --------------------- module Angular
28. [Link]("rdvmedecins", ['[Link]']);
29. // contrôleur
30. [Link]("rdvmedecins")
31. .controller('rdvMedecinsCtrl', ['$scope',
32. function ($scope) {
33. // date minimale
34. $[Link] = new Date();
35. }]);
36.
37. </script>
38.
39. </body>
40. </html>
[Link]
159/315
Examinons d'abord le script local des lignes 26-37 :
• ligne 28 : création du module [rdvmedecins] avec sa dépendance sur le module [[Link]] qui fournit le calendrier ;
• lignes 30-35 : création d'un contrôleur. C'est lui qui va détenir le modèle de notre page. Il n'y aura pas de gestionnaire
d'événement ici ;
• lignes 30-31 : le contrôleur [rdvMedecinsCtrl] appartient au module [rdvmedecins]. On peut ajouter autant de contrôleurs
que l'on veut à un module. Dans notre application on aura :
◦ un module de gestion de l'application ;
◦ un contrôleur par vue ;
• le second paramètre de la fonction [controller] est un tableau de la forme ['O1', 'O2', ..., 'On', function(O1, O2, ...,
On)]. Le dernier paramètre est la fonction qui implémente le contrôleur. Ses paramètres sont des objets que Angular JS
va fournir à la fonction.
Utilisateur V1 C1 Service 1
4 5 6
2
8
9 Service 2 3
M1
Données
11
réseau
Vn Cn 7
DAO
Mn
Ci-dessus, le contrôleur C1 contient l'ensemble des gestionnaires d'événement de la vue V1 ainsi que le modèle M1 de cette
dernière. Les gestionnaires d'événement peuvent avoir besoin d'un ou plusieurs services [6] pour faire leur travail. On passe
l'ensemble de ceux-ci comme paramètres de la fonction de construction du contrôleur :
Les services Si sont des singletons. Angular les crée en un unique exemplaire. Ils sont identifiés par un nom Si. Pourquoi sont-ils
présents deux fois dans le tableau ci-dessus ? En exploitation, les scripts JS sont minifiés. Dans ce processus de minification, le
tableau ci-dessus devient :
Les paramètres perdent leur nom. Or c'est le nom de services. Il est donc important de garder ces noms. C'est pourquoi ils sont
passés en tant que chaînes de caractères comme paramètres précédant la fonction. Les chaînes de caractères ne sont pas changées
dans le processus de minification. Lorsqu'Angular va construire le contrôleur avec le nouveau tableau, il va remplacer a1 par S1, a2
par S2, ... L'ordre des paramètres est donc important. Il doit correspondre à l'ordre des services qui précèdent la définition de la
fonction.
1. // contrôleur
2. [Link]("rdvmedecins")
3. .controller('rdvMedecinsCtrl', ['$scope',
4. function ($scope) {
5. // date minimale
6. $[Link] = new Date();
7. }]);
[Link]
160/315
• lignes 3-4 : l'unique objet injecté dans le contrôleur est l'objet $scope. C'est un objet prédéfini qui représente le modèle M
des vues associées au contrôleur. Pour enrichir le modèle d'une vue, il suffit d'ajouter des champs à l'objet $scope ;
• c'est ce qui est fait ligne 6. On crée le champ [minDate] avec pour valeur la date du jour ;
1. <body ng-controller="rdvMedecinsCtrl">
2. <div class="container">
3. ...
4. <div style="display:inline-block; min-height:290px;">
5. <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
6. </div>
7. ...
8. </div>
9. ...
• ligne 1 : le corps de la page est associé au contrôleur [rdvMedecinsCtrl] grâce à l'attribut [ng-controller]. Cela signifie que
tout ce qui est dans la balise <body> va utiliser le contrôleur [rdvMedecinsCtrl] pour gérer ses événements et obtenir son
modèle M. Une page HTML peut dépendre de plusieurs contrôleurs imbriqués ou pas les uns dans les autres :
Ci-dessus :
• le contenu de [div1] (lignes 1-10) affiche le modèle M1 géré par le contrôleur c1. Les balises de cette zone
peuvent référencer des gestionnaires d'événement du contrôleur c1 ;
• le contenu de [div11] (lignes 3-4) affiche le modèle M11 géré par le contrôleur c11 mais également le modèle M1.
Il y a héritage des modèles. Les balises de cette zone peuvent référencer aussi bien des gestionnaires d'événement
du contrôleur c11 que des gestionnaires d'événement du contrôleur c1. Elles ne peuvent référencer ni le modèle
M12 du contrôleur c12 ni les gestionnaires d'événement de celui-ci. Le contrôleur c12 n'est en effet pas connu
entre les lignes 3-5 ;
• lignes 7-9 : on peut tenir un raisonnement analogue à celui tenu précédemment ;
L'attribut [min-date] est initialisé avec la valeur [minDate] du modèle. Implicitement [$[Link]]. Le champ est toujours
cherché dans l'objet $scope.
[Link]
161/315
3
1
Nous dupliquons la page [[Link]] dans [[Link]] puis nous modifions cette dernière de la façon suivante :
1. <!DOCTYPE html>
2. <html ng-app="rdvmedecins">
3. <head>
4. ...
5. </head>
6. <body ng-controller="rdvMedecinsCtrl">
7. <div class="container">
8. <h1>Rdvmedecins - v1</h1>
9.
10. <pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
11. <div class="row">
12. <!-- le calendrier-->
13. <div class="col-md-4">
14. <h4>Calendrier</h4>
15.
16. <div style="display:inline-block; min-height:290px;">
17. <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
18. </div>
19. </div>
20. <!-- les langues -->
21. <div class="col-md-2">
22. <div class="btn-group" dropdown is-open="isopen">
23. <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
24. Langues<span class="caret"></span>
25. </button>
26. <ul class="dropdown-menu" role="menu">
27. <li><a href="" ng-click="setLang('fr')">Français</a></li>
28. <li><a href="" ng-click="setLang('en')">English</a></li>
29. </ul>
30. </div>
31. </div>
32. </div>
33. </div>
34. ...
35. <script type="text/javascript" src="[Link]"></script>
36. </body>
37. </html>
Il y a peu de modifications. Il y a simplement l'ajout lignes 21-31 de la liste déroulante des langues. Pour la première fois, nous
rencontrons un gestionnaire d'événement aux lignes 27-28 :
[Link]
162/315
• ligne 27 : l'attribut [ng-click] est un attribut Angular qui indique le gestionnaire d'événement à exécuter lorsqu'on clique sur
l'élément ayant cet attribut. Ici, la fonction [$[Link]('fr')] sera exécutée. Elle mettra le calendrier en français ;
• ligne 28 : ici, on met le calendrier en anglais ;
• ligne 35 : le Javascript du contrôleur étant assez conséquent, nous le plaçons dans un fichier [[Link]] ;
Angular gère la localisation des vues avec un module appelé [ngLocale]. La définition de notre module [rdvmedecins] sera donc la
suivante :
Ligne 2, il ne faut pas oublier les dépendances car Angular est parfois peu précis dans ses messages d'erreur. L'oubli d'une
dépendance est ainsi particulièrement difficile à détecter. Ici on a une nouvelle dépendance sur le module [ngLocale].
Par défaut, Angular ne gère que la localisation des dates, nombres, ... qui ont des variantes locales. Il ne gère pas l'internationalisation
de textes. On utilisera pour cela la bibliothèque [angular-translate]. La gestion de la localisation est faite par la bibliothèque [angular-
i18n]. Cette bibliothèque amène avec elle autant de fichiers qu'il y a de variantes pour les dates, nombres, ...
Pour le calendrier français, nous utiliserons le fichier [angular-locale_fr-[Link]] et pour le calendrier anglais le fichier [angular-
locale_en-[Link]]. Regardons ce qu'il y a par exemple dans le fichier [angular-locale_fr-[Link]] :
1. 'use strict';
2. [Link]("ngLocale", [], ["$provide", function($provide) {
3. var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many",
OTHER: "other"};
4. $[Link]("$locale", {
5. "DATETIME_FORMATS": {
6. "AMPMS": [
7. "AM",
8. "PM"
9. ],
10. "DAY": [
11. "dimanche",
12. "lundi",
13. "mardi",
14. "mercredi",
15. "jeudi",
16. "vendredi",
17. "samedi"
18. ],
19. "MONTH": [
[Link]
163/315
20. "janvier",
21. "f\u00e9vrier",
22. "mars",
23. "avril",
24. "mai",
25. "juin",
26. "juillet",
27. "ao\u00fbt",
28. "septembre",
29. "octobre",
30. "novembre",
31. "d\u00e9cembre"
32. ],
33. "SHORTDAY": [
34. "dim.",
35. "lun.",
36. "mar.",
37. "mer.",
38. "jeu.",
39. "ven.",
40. "sam."
41. ],
42. "SHORTMONTH": [
43. "janv.",
44. "f\u00e9vr.",
45. "mars",
46. "avr.",
47. "mai",
48. "juin",
49. "juil.",
50. "ao\u00fbt",
51. "sept.",
52. "oct.",
53. "nov.",
54. "d\u00e9c."
55. ],
56. "fullDate": "EEEE d MMMM y",
57. "longDate": "d MMMM y",
58. "medium": "d MMM y HH:mm:ss",
59. "mediumDate": "d MMM y",
60. "mediumTime": "HH:mm:ss",
61. "short": "dd/MM/yy HH:mm",
62. "shortDate": "dd/MM/yy",
63. "shortTime": "HH:mm"
64. },
65. "NUMBER_FORMATS": {
66. "CURRENCY_SYM": "\u20ac",
67. "DECIMAL_SEP": ",",
68. "GROUP_SEP": "\u00a0",
69. "PATTERNS": [
70. {
71. "gSize": 3,
72. "lgSize": 3,
73. "macFrac": 0,
74. "maxFrac": 3,
75. "minFrac": 0,
76. "minInt": 1,
77. "negPre": "-",
78. "negSuf": "",
79. "posPre": "",
80. "posSuf": ""
81. },
82. {
[Link]
164/315
83. "gSize": 3,
84. "lgSize": 3,
85. "macFrac": 0,
86. "maxFrac": 2,
87. "minFrac": 2,
88. "minInt": 1,
89. "negPre": "(",
90. "negSuf": "\u00a0\u00a4)",
91. "posPre": "",
92. "posSuf": "\u00a0\u00a4"
93. }
94. ]
95. },
96. "id": "fr-fr",
97. "pluralCat": function (n) { if (n >= 0 && n <= 2 && n != 2) { return
PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
98. });
99. }]);
Dans le fichier [angular-locale_en-[Link]], on a exactement la même chose mais cette fois ci pour l'anglais des USA (en-us).
Le code ci-dessus n'est pas très simple à lire. En lisant attentivement, on découvre que tout ce code définit la variable [$ locale] de la
ligne 4. C'est en changeant la valeur de cette variable qu'on obtient l'internationalisation des dates, nombres, monnaie, ...
Curieusement, Angular n'a pas prévu qu'on change la variable [$locale] en cours d'exécution. On la définit une bonne fois pour
toutes en important le fichier de la locale désirée :
Cela ne sert à rien d'importer tous les fichiers des locales désirées, car chaque fichier, on l'a vu, ne fait qu'une chose : définir la
variable [$locale]. C'est le dernier fichier importé qui gagne et il n'y a ensuite plus moyen de changer la locale.
En naviguant sur la toile à la recherche d'une solution à ce problème, je n'en ai pas trouvé. J'en propose une ici
[[Link] L'idée est de mettre les différentes
locales dont nous avons besoin dans un dictionnaire. C'est là que nous irons les chercher lorsqu'il faudra en changer. Le code
Javascript de [[Link]] a l'architecture suivante :
[Link]
165/315
Si on enlève la définition des locales qui prend 200 lignes (lignes 15-215 ci-dessus), le code est simple :
pour définir un gestionnaire d'événement qui s'appellerait [nom_fonction] et qui admettrait les paramètres [param1,
param2, ...] ;
[Link]
166/315
9. <li><a href="" ng-click="setLang('en')">English</a></li>
10. </ul>
11. </div>
12. </div>
• ligne 225 : on change la valeur de la variable [$locale] avec la valeur du dictionnaire [locales] qui convient ;
• ligne 227 : on a dit que lorsque le modèle M d'une vue V change, la vue V est automatiquement rafraîchie avec le nouveau
modèle. Ligne 225, on a changé la valeur de la variable [$locale] qui ne fait pas partie du modèle M affiché par la vue V. Il
faut trouver un moyen de changer ce modèle M afin que le calendrier se rafraîchisse et utilise sa nouvelle locale. Ici, on
change la variable [jour] du modèle du calendrier. On l'initialise avec un nouveau pointeur (new) qui pointe sur une date
identique à celle qui est affichée. [$[Link]()] est le nombre de millisecondes écoulée entre le 1er janvier 1970 et
la date affichée par le calendrier. Avec ce nombre, on reconstruit une nouvelle date. On va bien sûr retrouver la même date
et le calendrier restera positionné sur la date qu'il affichait. Mais la valeur de [$[Link]] qui est en réalité un pointeur
aura elle changé et le calendrier va se rafraîchir ;
• ligne 229 : on positionne à false la valeur de la variable [isopen] du modèle. Cette variable contrôle un des attributs de la
liste déroulante :
Ligne 1 ci-dessus, l'attribut [is-open] va passer à false, ce qui va avoir pour effet de fermer la liste déroulante.
3
1
En [3], nous voyons que le calendrier est en anglais mais pas les textes [Calendrier, Langues]. Par défaut, Angular n'offre pas d'outil
pour l'internationalisation des messages. Nous allons utiliser ici la bibliothèque [angular-translate] ([Link]
translate/angular-translate).
[Link]
167/315
2
1
Voyons la configuration nécessaire à l'internationalisation. Le script [[Link]] est modifié de la façon suivante :
[Link]
168/315
• ligne 2 : la première modification est l'ajout d'une nouvelle dépendance. L'internationalisation de l'application nécessite le
module Angular [[Link]] ;
• lignes 5-26 : définissent la fonction [config] du module [rdvmedecins]. Au démarrage d'une application Angular, le
framework instancie tous les services nécessaires à l'application, ceux prédéfinis d'Angular et ceux définis par l'utilisateur.
Pour l'instant, nous n'avons pas défini de services. La fonction [config] du module d'une application est exécutée avant
toute instanciation de service. Elle peut être utilisée pour définir des informations de configuration des services qui vont
être ensuite instanciés. Ici, la fonction [config] va être utilisée pour définir les messages internationalisés de l'application ;
• ligne 5 : le paramètre de la fonction [config] est un tableau ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)] où Oi est un
objet connu et fourni par Angular. Ici, l'objet [$translateProvider] est fourni par le module [[Link]].
[function] est la fonction exécutée pour configurer l'application ;
• lignes 7-14 : la fonction [$[Link]] admet deux paramètres :
◦ le premier paramètre est la clé d'une langue. On peut mettre ce qu'on veut. Ici, on a mis 'fr' pour les traductions
françaises (ligne 7) et 'en' pour les traductions anglaises (ligne 16),
◦ le second est la liste des traductions sous la forme d'un dictionnaire {'cle1':'msg1', 'cle2':'msg2', ...} ;
• lignes 7-14 : les messages français ;
• lignes 16-23 : les messages anglais ;
• ligne 25 : la méthode [preferredLanguage] fixe la langue par défaut. Son paramètre est l'un des arguments utilisés comme
premier paramètre de la fonction [$[Link]] donc ici soit 'fr' (ligne 7), soit 'en' (ligne 16) ;
• notons qu'il y a trois sortes de messages :
◦ des messages sans paramètres ni éléments HTML (lignes 9, 11, 12, ...),
◦ des messages avec des éléments HTML (lignes 8, 10, ...),
◦ des messages avec des paramètres (lignes 10, 19) ;
Nous dupliquons maintenant [[Link]] dans [[Link]] et nous faisons les modifications suivantes :
1. <div class="container">
2. <!-- un premier texte avec des éléments HTML dedans -->
3. <h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
4. <!-- un second texte avec paramètres -->
5. <h3 class="alert alert-warning" translate="{{[Link]}}" translate-
values="{{[Link]}}"></h3>
6. <!-- un troisième texte traduit par le contrôleur -->
7. <h3 class="alert alert-danger">{{msg2}}</h3>
8.
9. <pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
10. <div class="row">
11. <!-- le calendrier-->
12. <div class="col-md-4">
13. <h4>{{'msg_calendrier'|translate}}</h4>
14.
15. <div style="display:inline-block; min-height:290px;">
16. <datepicker ng-model="jour" show-weeks="true" class="well" min-
date="minDate"></datepicker>
17. </div>
18. </div>
19. <!-- les langues -->
20. <div class="col-md-2">
21. <div class="btn-group" dropdown is-open="isopen">
22. <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
23. {{'msg_langues'|translate}}<span class="caret"></span>
24. </button>
25. <ul class="dropdown-menu" role="menu">
26. <li><a href="" ng-click="setLang('fr')">Français</a></li>
27. <li><a href="" ng-click="setLang('en')">English</a></li>
28. </ul>
29. </div>
30. </div>
31. </div>
32. </div>
[Link]
169/315
◦ la syntaxe [translate={{'msg_key'}} translate-values={{dictionnaire]}}] (ligne 5), convient aux messages avec ou
sans éléments HTML et avec paramètres ;
◦ la syntaxe [{{'msg_key'|translate}}] (lignes 9, 13, 23), convient aux messages sans paramètres et sans éléments
HTML ;
On notera que [[Link]] et [[Link]] ne sont pas entourés d'apostrophes. Ce ne sont pas des chaînes de caractères mais des
éléments du modèle :
• [Link] : définit la clé du message paramétré à utiliser ;
• [Link] : est le dictionnaire fournissant les valeurs des paramètres ;
Les noms des champs [text, model] peuvent être quelconques. Dans le contrôleur [rdvMedecinsCtrl] de la vue, l'objet [msg] est
défini de la façon suivante :
[Link]
170/315
1. <!-- un troisième texte traduit par le contrôleur -->
2. <h3 class="alert alert-danger">{{msg2}}</h3>
Toutes les traductions précédentes se sont faites dans la vue au moyen d'attributs du module [[Link]]. On peut
décider également de faire cette traduction côté serveur. C'est ce qui est fait ici. On a dans le contrôleur (ligne 247 dans la copie
d'écran ci-dessus) le code suivant :
$scope.msg2 = $filter('translate')('msg_meteo');
On utilise la même syntaxe que pour le filtre 'date' car 'translate' est lui aussi un filtre. On demande ici le message de clé
'msg_meteo'.
Examinons le mécanisme des changements de langues. On a vu que la fonction [config] de configuration du module [rdvmedecins]
avait désigné le français comme langue par défaut (ligne 9 ci-dessous) :
1. // configuration i18n
2. [Link]("rdvmedecins")
3. .config(['$translateProvider', function ($translateProvider) {
4. // messages français
5. $[Link]("fr", {...});
6. // messages anglais
7. $[Link]("en", {...});
8. // langue par défaut
9. $[Link]("fr");
10. }]);
On rappelle également que la locale par défaut était également le français. Dans l'initialisation du contrôleur [rdvmedecins] on a
écrit :
Il n'y a aucun lien entre l'internationalisation des messages amenée par le module [[Link]] et la localisation des dates
que nous avons mise en place. Cette dernière utilise une variable $locale qui n'est pas utilisée par le module [[Link]].
Ce sont deux processus qui s'ignorent.
Il est maintenant temps de regarder ce qui se passe lorsque l'utilisateur change de langue :
• ligne 251 : lors d'un changement de langue, la fonction [setLang] est appelée avec l'un des deux paramètres ['fr','en'] ;
• lignes 252-257 : ont été déjà expliquées – elles changent la variable [$locale] du calendrier. Cela n'a aucune incidence sur la
langue des traductions ;
• ligne 259 : on change la langue des traductions. On utilise l'objet [$translate] fourni par le module [ [Link]].
Pour cela, il faut l'injecter dans le contrôleur :
1. // contrôleur
2. [Link]("rdvmedecins")
3. .controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
4. function ($scope, $locale, $translate, $filter) {
[Link]
171/315
• le paramètre lang de la fonction [$[Link](lang)] doit avoir pour valeur l'une des clés utilisées dans la
configuration comme 1er paramètre de la fonction [$[Link]], ç-à-d soit 'fr', soit 'en'.
C'est bien le cas ;
• ligne 261 : on recalcule la valeur de msg2. Pourquoi ? Dans la vue, après le changement de langue opéré par la ligne 259,
tous les attributs [translate] présents vont être réévalués. Ce ne sera pas le cas de l'expression {{msg2}} qui n'a pas cet
attribut. Donc on calcule sa nouvelle valeur dans le contrôleur. Cela doit être fait après le changement de langue de la ligne
259 pour que la nouvelle langue soit utilisée pour le calcul de [msg2] ;
1. en [1], le jour est resté en français alors que le reste de la vue est en anglais ;
2. en [2] et [3], le jour sélectionné est le 24 juin alors qu'en [1], le jour reste fixé sur le 20 juin ;
Tentons des explications avant de trouver des solutions. Le message [1] est construit dans le contrôleur avec le code suivant :
$[Link] = {'text': 'msg_agenda', 'model': {'titre': 'Mme', 'prenom': 'Laure', 'nom': 'PELISSIER', 'jour':
$filter('date')($[Link], 'fullDate')}};
L'anomalie [1] (le jour est resté en français alors que le reste de la vue est en anglais) semble montré que si l'attribut [translate] est
réévalué lors d'un changement de langue, ce n'a pas été le cas de l'attribut [translate-values]. On peut alors forcer cette évaluation
dans le contrôleur :
A chaque changement de langue, la ligne 8 ci-dessus réévalue le jour affiché. Cela règle effectivement le premier problème mais pas
le second (le jour affiché dans le message ne change pas lorsqu'on sélectionne un autre jour dans le calendrier). La raison de ce
comportement est la suivante. Le message est affiché dans la vue avec le code suivant :
[Link]
172/315
La vue affichée V ne change que si son modèle M change. Or ici, le choix d'un nouveau jour dans le calendrier déclenche un
événement qui n'est pas géré, ce qui fait que le modèle [msg] ne change pas et que la vue donc ne change pas. Nous faisons évoluer
dans la vue, la définition du calendrier :
Ci-dessus, nous indiquons que le clic sur le calendrier doit être géré par la fonction [$[Link]]. Celle-ci est la suivante :
Utilisateur V1 C1 Service 1
4 5 6
2
8
9 Service 2 3
M1
Données
11
réseau
Vn Cn 7
DAO
Mn
Nous allons nous intéresser ici à la notion de service. C'est une notion assez large. Si ci-dessus, la couche [DAO] est clairement un
service, tout objet Angular peut devenir un service :
• un service suit une syntaxe particulière. Il a un nom et Angular le connaît via ce nom ;
• un service peut être injecté par Angular dans les contrôleurs et les autres services ;
Certains des services que nous allons configurer dans le module [rdvmedecins] auront besoin d'être configurés. Comme un service
peut être injecté dans un autre service, il est tentant de faire la configuration dans un service que nous nommerons [config] et
d'injecter celui-ci dans les services et contrôleurs à configurer. Nous décrivons maintenant ce processus.
1. <div class="container">
2. <!-- contrôle du msg d'attente -->
3. <label>
[Link]
173/315
4. <input type="checkbox" ng-model="[Link]">
5. <span>Voir le message d'attente</span>
6. </label>
7.
8. <!-- le message d'attente -->
9. <div class="alert alert-warning" ng-show="[Link]">
10. <h1>{{ [Link] | translate}}
11. <button class="btn btn-primary pull-right" ng-click="[Link]()">
12. {{'msg_cancel'|translate}}</button>
13. <img src="assets/images/[Link]" alt=""/>
14. </h1>
15. </div>
16. ...
17. </div>
18. ...
19. <script type="text/javascript" src="[Link]"></script>
• lignes 3-6 : une case à cocher qui contrôle l'affichage ou non du message d'attente des lignes 9-15. La valeur de la case à
cocher est placée dans la variable [[Link]] du modèle M de la vue V. Cette valeur est true si la case est cochée et false
sinon. Cela marche dans les deux sens. Si nous donnons la valeur true à variable [[Link]], la case sera cochée. On a
une association bi-directionnelle entre la vue V et son modèle M ;
• ligne 9-15 : un message d'attente avec un bouton d'annulation de l'attente (ligne 11) ;
• ligne 9 : le message n'est visible que si la variable [[Link]] a la valeur true. Ainsi lorsqu'on va cocher la case de la
ligne 4 :
◦ la valeur true est affectée à la variable [[Link]] (ng-model, ligne 4) ;
◦ comme il y a eu changement du modèle M, la vue V est automatiquement réévaluée. Le message d'attente sera alors
rendu visible (ng-show, ligne 9) ;
◦ le raisonnement est analogue lorsqu'on décoche la case de la ligne 4 : le message d'attente est caché ;
• ligne 10 : le message d'attente est l'objet d'une traduction (filtre translate) ;
• ligne 11 : lorsqu'on clique sur le bouton, la méthode [[Link]()] est exécutée (atribut ng-click) ;
• ligne 12 : le libellé du bouton fait l'objet d'une traduction ;
• ligne 19 : on met le code Javascript de l'application dans un nouveau fichier JS [rdvmedecins-02] pour ne pas perdre le
code déjà écrit et qui doit être maintenant réorganisé ;
2
1
[Link]
174/315
• ligne 6 : le module [rdvmedecins] de l'application ;
• lignes 9-10 : la fonction de configuration de l'application ;
• lignes 38-39 : le service [config] ;
• lignes 283-284 : le contrôleur [rdvMedecinsCtrl] ;
Précédemment, nous avions défini dans le contrôleur le dictionnaire locales={'fr':..., 'en': ...} qui faisait 200 lignes. Ce dictionnaire
est clairement un élément de configuration, aussi le migre-t-on dans le service [config] des lignes 38-39. Ce service est défini de la
façon suivante :
[Link]
175/315
• lignes 38-39 : un service est créé avec la fonction [factory] de l'objet [[Link]]. La syntaxe de cette fonction est
comme pour les précédentes factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){...}]) où les Oi
sont les noms d'objets connus d'Angular (prédéfinis ou créés par le développeur) et qu'Angular injecte comme paramètre
de la fonction factory. Comme ici, la fonction n'a pas de paramètres, on a utilisé une syntaxe plus courte également
acceptée factory('nom_service', function (){...}]) ;
• ligne 40 : la fonction [factory] doit implémenter le service au moyen d'un objet qu'elle rend. C'est cet objet qui est le
service. C'est pourquoi la fonction est-elle appelée factory (usine de création d'objets) ;
1. [Link]('nom_module')
2. .factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
3. // préparation du service
4. ...
5. // on rend l'objet implémentant le service
6. return {
7. // champs
8. ...
9. // méthodes
10. ...
11. }
12. });
• ligne 6 : on rend un objet JS qui peut contenir à la fois des champs et des méthodes. Ce sont ces dernières qui assurent le
service ;
Ici le service [config] ne définit que des champs et aucune méthode. On y mettra tout ce qui peut être paramétré dans l'application :
[Link]
176/315
• lignes 59-62 : les URL de l'application ;
• lignes 64-69 : les URL du service web distant ;
• ligne 71 : un appel HTTP vers un service web qui ne répond pas, peut être long. On fixe ici à 1 seconde, le temps d'attente
maximum de la réponse du service web. Passé ce délai, l'appel HTTP échoue et une exception JS est lancée ;
• ligne 73 : avant chaque appel au serveur, on va simuler une attente dont la durée est fixée ici en millisecondes. Une attente
de 0 fait qu'il n'y a pas d'attente. L'application va être construite de telle façon que l'utilisateur puisse annuler une opération
qu'il a lancée. Pour qu'elle puisse être annulée il faut qu'elle dure au moins quelques secondes. On utilisera cette attente
artificielle pour simuler des opérations longues ;
• ligne 75 : en mode [debug=true], des informations complémentaires sont affichées dans la vue courante. Par défaut, ce
mode est activé. En production, on mettrait ce champ à false ;
• lignes 77-278 : le dictionnaire des deux locales 'fr' et 'en'. Il était auparavant dans le contrôleur [rdvMedecinsCtrl] ;
Lorsque l'utilisateur clique sur le bouton d'annulation, la méthode [$[Link]()] est appelée. C'est au final la
fonction privée cancel de la ligne 316 qui est exécutée. Elle se contente de cacher le message d'attente en mettant à false, la
varible du modèle [[Link]] (ligne 318) ;
[Link]
177/315
Application web / navigateur
couche [présentation] couche [services]
Routeur
Utilisateur V C config
Données
utils
réseau
M
dao
2
3
• il s'agit de faire apparaître le bandeau [2] pendant un temps fixé par [1]. L'attente peut être annulée par [3].
1. <!DOCTYPE html>
2. <html ng-app="rdvmedecins">
3. <head>
4. <title>RdvMedecins</title>
5. ...
6. </head>
7. <body ng-controller="rdvMedecinsCtrl">
8. <div class="container">
9.
10. <!-- le message d'attente -->
11. <div class="alert alert-warning" ng-show="[Link]" ng-cloak="">
12. <h1>{{ [Link] | translate}}
13. <button class="btn btn-primary pull-right" ng-click="[Link]()">{{'msg_cancel'|
translate}}</button>
14. <img src="assets/images/[Link]" alt=""/>
15. </h1>
16. </div>
17.
18. <!-- le formulaire -->
19. <div class="alert alert-info" ng-hide="[Link]">
20. <div class="form-group">
21. <label for="waitingTime">{{waitingTimeText | translate}}</label>
[Link]
178/315
22. <input type="text" id="waitingTime" ng-model="[Link]"/>
23. </div>
24. <button class="btn btn-primary" ng-click="execute()">Exécuter</button>
25. </div>
26. </div>
27. ..
28. <script type="text/javascript" src="[Link]"></script>
29. </body>
30. </html>
• ligne 11 : l'attribut [ng-cloak] empêche l'affichage de la zone avant que les expressions Angular de celle-ci n'aient été
calculées. Cela évite un affichage bref de la zone avant l'évaluation de l'attribut [ng-show] qui va en fait provoquer sa
dissimulation ;
• ligne 22 : la saisie de l'utilisateur (temps d'attente) va être mémorisée dans le modèle [[Link]] (attribut ng-model) ;
• ligne 28 : la page utilise un nouveau script [rdvmedecins-03] ;
Nous ajoutons à la fonction [config], une nouvelle clé de message (lignes 6, 11) :
1. [Link]("rdvmedecins")
2. .config(['$translateProvider', function ($translateProvider) {
3. // messages français
4. $[Link]("fr", {
5. ...
6. 'msg_waiting_time_text': "Temps d'attente : "
7. });
8. // messages anglais
9. $[Link]("en", {
10. ...
11. 'msg_waiting_time_text': "Waiting time:"
12. });
13. // langue par défaut
14. $[Link]("fr");
15. }]);
Nous ajoutons au service [config] une nouvelle ligne (ligne 6) pour cette clé de message :
1. [Link]("rdvmedecins")
2. .factory('config', function () {
3. return {
4. // messages à internationaliser
5. ...
6. waitingTimeText: 'msg_waiting_time_text',
[Link]
179/315
Le service [utils] contient deux méthodes (lignes 4, 12) :
1. [Link]("rdvmedecins")
2. .factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
3. // affichage de la représentation Json d'un objet
4. function debug(message, data) {
5. if ([Link]) {
6. var text = data ? message + " : " + [Link](data) : message;
7. [Link](text);
8. }
9. }
10.
11. // attente
12. function waitForSomeTime(milliseconds) {
13. // attente asynchrone de milliseconds milli-secondes
14. var task = $[Link]();
15. $timeout(function () {
16. [Link]();
17. }, milliseconds);
18. // on retourne la tâche
19. return task;
20. };
21.
22. // instance du service
23. return {
24. debug: debug,
25. waitForSomeTime: waitForSomeTime
26. }
27. }]);
• ligne 2 : le service s'appelle [utils] (1er paramètre). Il a des dépendances sur trois services, deux services Angular prédéfinis
$timeout, $q et le service config. Le service [$timeout] permet d'exécuter une fonction après qu'un certain temps se soit
écoulé. Le service [$q] permet de créer des tâches asynchrones ;
• ligne 4 : une fonction locale [debug] ;
• ligne 12 : une fonction locale [waitForSomeTime] ;
• lignes 23-26 : l'instance du service [utils]. C'est un objet qui expose deux méthodes, celles des lignes 4 et 12. Notez que les
champs de l'objet peuvent porter des noms quelconques. Par cohérence, on leur a donné les noms des fonctions qu'ils
référencent ;
• lignes 4-9 : la méthode [debug] écrit sur la console un message [message] et éventuellement la représentation JSON d'un
objet [data]. Cela permet d'afficher des objets de n'importe quelle complexité ;
• lignes 12-20 : la méthode [waitForSomeTime] crée une tâche asynchrone qui dure [milliseconds] milli-secondes ;
• ligne 14 : création d'une tâche grâce à l'objet prédéfini [$q] ([Link] Ci-dessous,
l'API de la tâche appelée [deferred] dans la documentation Angular :
[Link]
180/315
◦ [[Link](value)] : qui termine la tâche avec échec et renvoie la valeur [value] à ceux qui attendent la fin de la
tâche ;
La tâche [task] peut régulièrement donner des informations à ceux qui attendent sa fin :
◦ [[Link](value)] : envoie la valeur [value] à ceux qui attendent la fin de la tâche. La tâche continue à s'exécuter ;
Ceux qui veulent attendre la fin de la tâche utilisent le champ [promise] de celle-ci :
var promise=[task].promise ;
1. var promise=[task].promise;
2. [Link](successCallback, errorCallBack);
3. promise['finally'](finallyCallback);
1. // attente
2. function waitForSomeTime(milliseconds) {
3. // attente asynchrone de milliseconds millisecondes
4. var task = $[Link]();
5. $timeout(function () {
6. [Link]();
7. }, milliseconds);
8. // on retourne la tâche
9. return task;
10. };
[Link]
181/315
◦ une seconde fois lorsque le délai [milliseconds] est écoulé ;
On a là, une fonction asynchrone : son résultat est obtenu à un moment ultérieur à celui de son exécution.
1. // contrôleur
2. [Link]("rdvmedecins")
3. .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
4. function ($scope, utils, config, $filter) {
5. // ------------------- initialisation modèle
6. // message d'attente
7. $[Link] = {text: [Link], visible: false, cancel: cancel, time: undefined};
8. $[Link] = [Link];
9. // tâche d'attente
10. var task;
11. // logs
12. [Link]("libellé temps d'attente", $filter('translate')($[Link]));
13. [Link]("locales['fr']=", [Link]['fr']);
14.
15. // exécution action
16. $[Link] = function () {
17. // log
18. [Link]('début', new Date());
19. // on affiche le msg d'attente
20. $[Link] = true;
21. // attente simulée
22. task = [Link]($[Link]);
23. // fin d'attente
24. [Link](function () {
25. // succès
26. [Link]('fin', new Date());
27. }, function () {
28. // échec
29. [Link]('Opération annulée')
30. });
31. [Link]['finally'](function () {
32. // fin d'attente dans tous les cas
33. $[Link] = false;
34. });
35.
36. };
37.
38. // annulation attente
39. function cancel() {
40. // on termine la tâche
41. [Link]();
42. }
43. }]);
[Link]
182/315
Ligne 2, on obtient la notation JSON de l'objet locales['fr'].
Il est important de comprendre les séquences d'exécution de ce code. Dans le cas où l'utilisateur met un délai de 3 secondes et
n'annule pas l'attente :
• lorsqu'il clique sur le bouton [Exécuter], la fonction [$[Link]] s'exécute. Les lignes 16-34 sont exécutées sans attente
des 3 secondes. A la fin de cette exécution, la vue V est synchronisée avec le modèle M. Le message d'attente est affiché
(ng-show=$[Link]=true, ligne 20) et le formulaire est caché (ng-hide=$[Link]=true, ligne 20) ;
• à partir de ce moment l'utilisateur peut interagir de nouveau avec la vue. Il peut notamment cliquer sur le bouton
[Annuler] ;
• s'il ne le fait pas, au bout de 3 secondes, la fonction du [$timeout] (cf lignes 5-7 ci-dessous) s'exécute :
1. // attente
2. function waitForSomeTime(milliseconds) {
3. // attente asynchrone de milliseconds millisecondes
4. var task = $[Link]();
5. $timeout(function () {
6. [Link]();
7. }, milliseconds);
8. // on retourne la tâche
9. return task;
10. };
• au bout de 3 secondes donc, du code est exécuté. Ce code termine la tâche [task] avec un code de succès (resolve). Cela va
déclencher l'exécution de tous les codes qui attendaient cette fin (ligne 4 ci-dessous) :
1. // attente simulée
2. task = [Link]($[Link]);
3. // fin d'attente
4. [Link](function () {
5. // succès
6. [Link]('fin', new Date());
7. }, function () {
8. // échec
9. [Link]('Opération annulée')
10. });
11. [Link]['finally'](function () {
12. // fin d'attente dans tous les cas
13. $[Link] = false;
14. });
15.
• la ligne 6 ci-dessus (fin avec succès) va donc être exécutée. Puis ce sera le tour des lignes 11-14. Une fois ce code exécuté,
on revient à la vue V qui va alors être synchronisée avec son modèle M. Le message d'attente est caché (ng-
show=$[Link]=false, ligne 13) et le formulaire est affiché (ng-hide=$[Link]=false, ligne 13) ;
début : "2014-06-23T[Link].480Z"
fin : "2014-06-23T[Link].481Z"
On voit ci-dessus, le délai de 3 secondes (06:01-05:58) entre le début et la fin de l'attente. Si on contraire, l'utilisateur annule l'attente
avant les 3 secondes, on a l'affichage suivant :
début : "2014-06-23T[Link].564Z"
Opération annulée
[Link]
183/315
Pour terminer, il est important de comprendre qu'à tout moment il n'y a qu'un tread d'exécution appelé le thread de l'UI (User
Interface). La fin d'une tâche asynchrone est signalée par un événement exactement comme l'est le clic sur un bouton. Cet
événement n'est pas traité immédiatement. Il est mis dans la file d'attente des événements qui attendent leur exécution. Lorsque
vient son tour, il est traité. Ce traitement utilise le thread de l'UI et donc pendant ce temps, l'interface est gelée. Elle ne réagit pas
aux sollicitations de l'utilisateur. Pour cela, il est important que le traitement d'un événement soit rapide. Parce que chaque
événement est traité par le thread de l'UI, on n'a jamais à régler des problèmes de synchronisation entre threads s'exécutant en
même temps. Il n'y a, à chaque instant, que le thread de l'UI qui s'exécute.
Utilisateur V C config
Données
utils
réseau
M
dao
[Link] La vue V
Utilisateur V C config
Données
utils
réseau
M
dao
[Link]
184/315
Nous dupliquons [[Link]] dans [[Link]] que nous modifions ensuite de la façon suivante :
[Link]
185/315
47. </div>
48.
49. </div>
50. ...
51. <script type="text/javascript" src="[Link]"></script>
• lignes 13-31 : implémentent le formulaire. Celui-ci n'est pas visible lorsque le message d'attente est affiché ( ng-
hide="[Link]"). On retiendra que les quatre saisies sont mémorisées dans (attributs ng-model) [[Link] (ligne
16), [Link] (ligne 20), [Link] (ligne 24), [Link] (ligne 28)] ;
• lignes 34-39 : affichent la liste des médecins. Cette liste n'est pas toujours visible (ng-show="[Link]").
• ligne 35 : une alternative à la syntaxe <div ... translate="{{[Link]}}" translate-values="{{[Link]}}">
déjà rencontrée ;
• ligne 36 : une liste non ordonnée ;
• ligne 37 : la liste des médecins sera trouvée dans le modèle [[Link]]. La directive Angular [ng-repeat] permet de
parcourir une liste. La syntaxe ng-repeat="medecin in [Link]" demande à ce que la balise <li> soit répétée pour chaque
élément de la liste [[Link]]. L'élément courant de la liste est appelée [medecin] ;
• ligne 37 : pour chaque <li>, on écrit le titre, le prénom et le nom du médecin courant désigné par la variable [medecin] ;
• lignes 42-47 : affichent la liste des erreurs. Cette liste n'est pas toujours visible (ng-show="[Link]"). Cet affichage suit le
même modèle que l'affichage de la liste des médecins. De façon générale, pour afficher une liste d'objets, on utilise la
directive Angular [ng-repeat] ;
• ligne 51 : le code Javascript est maintenant dans le fichier [rdvmedecins-04]
Utilisateur V C config
Données
utils
réseau
M
dao
[Link]
186/315
• lignes 6-9 : le module [rdvmedecins] déclare une dépendance sur le module [base64] fourni par la bibliothèque [angular-
base64] qui est l'une des dépendances du projet. Ce module sert à coder en Base64, la chaîne [login:password] envoyée au
service web pour s'authentifier ;
• lignes 12-13 : la fonction d'initialisation qui contient nos messages internationalisés. De nouveaux messages apparaissent.
Nous ne les présenterons plus ;
• lignes 69-70 : le service [config] qui paramètre notre application. De nouvelles clés de message y sont ajoutées. Nous ne les
présenterons plus ;
• lignes 318-319 : le service [utils] qui contient des méthodes utilitaires. De nouvelles y sont rajoutées. Nous les
présenterons ;
• lignes 385-386 : le service [dao] chargé des échanges avec le service web. C'est sur lui que nous allons nous concentrer ;
• lignes 467-468 : le contrôleur C de la vue V que nous venons de présenter. Nous allons le présenter maintenant car c'est lui
le chef d'orchestre qui réagit aux demandes de l'utilisateur ;
[Link] Le contrôleur C
1. [Link]("rdvmedecins")
2. .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
3. function ($scope, utils, config, dao, $translate) {
4. // ------------------- initialisation modèle
5. // modèle
6. $[Link] = {text: [Link], visible: false, cancel: cancel, time:
undefined};
7. $[Link] = [Link];
8. $[Link] = {url: undefined, login: undefined, password: undefined};
9. $[Link] = {title: [Link], show: false, model: {}};
10. $[Link] = {show: false, model: {}};
11. $[Link] = [Link];
12. $[Link] = [Link];
13. $[Link] = [Link];
14.
15. // tâche asynchrone
16. var task;
17.
18. // exécution action
19. $[Link] = function () {
20. // on met à jour l'UI
21. $[Link] = true;
22. $[Link] = false;
23. $[Link] = false;
24. // attente simulée
[Link]
187/315
25. task = [Link]($[Link]);
26. var promise = [Link];
27. // attente
28. promise = [Link](function () {
29. // on demande la liste des médecins;
30. task = [Link]($[Link], $[Link], $[Link],
[Link]);
31. return [Link];
32. });
33. // on analyse le résultat de l'appel précédent
34. [Link](function (result) {
35. // result={err: 0, data: [med1, med2, ...]}
36. // result={err: n, messages: [msg1, msg2, ...]}
37. if ([Link] == 0) {
38. // on met les données acquises dans le modèle
39. $[Link] = [Link];
40. // on met à jour l'UI
41. $[Link] = true;
42. $[Link] = false;
43. } else {
44. // il y a eu des erreurs pour obtenir la liste des médecins
45. $[Link] = { title: [Link], messages:
[Link](result), show: true, model: {}};
46. // on met à jour l'UI
47. $[Link] = false;
48. }
49. });
50. };
51.
52. // annulation attente
53. function cancel() {
54. // on termine la tâche
55. [Link]();
56. // on met à jour l'UI
57. $[Link] = false;
58. $[Link] = false;
59. $[Link] = false;
60. }
61.
62. }
63. ])
64. ;
L'attribut [[Link]] sera le titre du bandeau. Il est défini dans le service [config]. L'attribut [[Link]] va
contrôler l'affichage ou non du bandeau (attribut ng-show="[Link]"). L'attribut [[Link]] est un
dictionnaire vide et le restera. Il sert simplement à illustrer l'utilisation de la variante de traduction utilisée ligne 3. Non
défini encore, l'attribut [[Link]] qui contiendra la liste des médecins (ligne 5).
• ligne 10 : [$[Link]] va rassembler les informations nécessaires à l'affichage de la liste des erreurs :
[Link]
188/315
3. {{[Link]|translate:[Link]}}
4. <ul>
5. <li ng-repeat="message in [Link]">{{message|translate}}</li>
6. </ul>
7. </div>
L'attribut [[Link]] sera le titre du bandeau. Il est défini dans le service [config]. L'attribut [[Link]] va contrôler
l'affichage ou non du bandeau (attribut ng-show="errors .show"). L'attribut [[Link]] est un dictionnaire vide
et le restera. Il sert simplement à illustrer l'utilisation de la variante de traduction utilisée ligne 3. Non défini encore,
l'attribut [[Link]] qui contiendra la liste des messages d'erreur à afficher (ligne 5).
• ligne 16 : la tâche asynchrone. Le contrôleur va lancer successivement deux tâches asynchrones. Les références sur ces
tâches successives seront placées dans la variable [task]. Cela permettra de les annuler (ligne 55) ;
• ligne 19 : la méthode exécutée lorsque l'utilisateur clique sur le bouton [Liste des médecins] :
• lignes 21-23 : l'interface visuelle est mise à jour : le message d'attente est affiché, tout le reste est caché ;
• ligne 25 : on crée la tâche asynchrone de l'attente. On recevra un signal (tâche réalisée) au bout du temps saisi par
l'utilisateur dans le formulaire ;
• ligne 26 : on récupère la promesse de la tâche asynchrone. C'est avec elle que le programme qui lance la tâche travaille. Il
faut cependant avoir la référence de la tâche elle-même afin de pouvoir l'annuler (ligne 55) ;
• lignes 28-32 : on définit le travail à faire lorsque l'attente sera terminée ;
• ligne 30 : on utilise la méthode [[Link]] pour lancer une nouvelle tâche asynchrone. On lui passe les informations
dont elle a besoin :
◦ l'URL racine du service web [$[Link]], par exemple [[Link]
◦ le login [$[Link]] pour s'identifier, par exemple [admin];
◦ le mot de passe [$[Link]] pour s'identifier, par exemple [admin];
◦ l'URL qui rend le service demandé [[Link]], ici [/getAllMedecins]. Au total l'URL complète sera
[[Link] ;
• lignes 34-50 : de l'explication précédente, il découle que ces lignes se seront exécutée que lorsque la tâche [[Link]]
sera terminée. Le paramètre [result] passé à la fonction de la ligne 34 est construit par la méthode [[Link]] et transmis
au code appelant par l'opération [[Link](result)] où [result] est de la forme suivante :
{err: 0, data: [med1, med2, ...]} où [medi] est un objet représentant un médecin (titre, prenom, nom),
{err: n, messages: [msg1, msg2, ...]} où [msgi] est un message d'erreur et n est différent de 0 ;
• ligne 37 : on regarde le code d'erreur [[Link]] ;
• lignes 38-42 : s'il n'y a pas d'erreur ([Link]==0), alors on récupère la liste des médecins et on l'affiche ;
• lignes 44-47 : si au contraire il y a erreur ([Link] !=0), alors on récupère la liste des messages d'erreurs et on l'affiche ;
• lignes 53-56 : le message d'attente avec son bouton d'annulation est présent tant que les deux opérations asynchrones ne
sont pas terminées. Voyons ce qui se passe selon le moment de l'annulation :
◦ il faut tout d'abord comprendre que les lignes 19-50 sont exécutées d'une traite. Une seule tâche asynchrone a alors
été lancée, celle de la ligne 25,
◦ après cette première exécution, la vue V est mise à jour et donc le bandeau d'attente et son bouton d'annulation est
visible. Si l'utilisateur annule l'attente avant que la tâche de la ligne 25 ne soit terminée, la méthode de la ligne 53 est
alors exécutée et la tâche est annulée avec échec (ligne 55) ;
◦ lignes 56-59 : l'interface est mise à jour : on réaffiche le formulaire et tout le reste est caché,
◦ il y a alors retour à la vue V et le navigateur va traiter l'événement suivant. Puisqu'il y a eu fin de tâche, la promesse de
cette tâche est obtenue, ce qui crée un événement. Il est alors traité ;
◦ les lignes 28-32 sont ensuite exécutées. Il n'y a pas de fonction définie pour le cas d'échec, donc aucun code n'est
exécuté. On obtient une nouvelle promesse, celle toujours rendue par [[Link]] et toujours obtenue,
[Link]
189/315
◦ l'évenement ayant été traité, il y a retour à la vue V et le navigateur va traiter l'événement suivant. Puisque la [promise]
de la ligne 28 a été traitée, celle de la ligne 34 va être résolue, ce qui va provoquer un nouvel événement. Il est alors
traité ;
◦ les lignes 34-49 vont alors être exécutées à leur tour, car la promesse utilisée ligne 34 a été obtenue. De nouveau, parce
qu'il n'y a pas de fonction définie pour le cas d'échec, aucun code n'est exécuté,
◦ on arrive ainsi à la ligne 50. Il n'y a plus d'attente de tâche et la nouvelle vue V est affichée ;
◦ supposons maintenant que l'annulation intervient pendant que la seconde tâche asynchrone [[Link]] est en cours
d'exécution. Le raisonnement précédent peut être tenu de nouveau. La fin de la tâche va provoquer l'exécution des
lignes 34-50 avec une fin de tâche avec échec. On va découvrir bientôt que la méthode [[Link]] réalise un appel
HTTP asynchrone vers le service web. Cet appel ne sera pas annulé mais son résultat ne sera pas exploité.
Il est important de comprendre ce va et vient constant entre l'affichage de la vue V et le traitement des événements du navigateur.
Les événements sont provoqués par l'utilisateur (un clic) ou par des opérations système telles que la fin d'une opération asynchrone.
L'état de repos du navigateur est l'affichage de la vue V. Il est tiré de ce repos par un événement qui se produit et qu'il traite alors.
Dès que l'événement a été traité, il revient à son état de repos. La vue V est alors mise à jour si l'événement traité a modifié son
modèle M. Le navigateur est tiré de son état de repos par l'événement suivant.
Tout se passe dans un unique thread. Deux événements ne sont jamais traités simultanément . Leur exécution est
séquentielle. Le navigateur ne passe à l'événement suivant que lorsque le précédent lui laisse la main, en général parce qu'il a été
traité totalement.
Il nous reste un point à expliquer. Pour afficher les messages d'erreur, nous écrivons :
La liste des messages est fournie par la méthode [[Link]] définie dans le service [utils]. Cette méthode est la suivante :
[Link]
190/315
• lignes 2-3 : le paramètre [data] reçu est un objet avec deux attributs :
◦ [err] : un code d'erreur ;
◦ [messages] : une liste de messages ;
• ligne 5 : on va consruire un tableau de messages d'erreur. Ces messages sont internationalisés. Pour cette raison, ce ne sont
pas les messages eux-mêmes qu'on met dans le tableau, mais leurs clés d'internationalisation sauf à la ligne 27. Dans ce cas,
on utilise l'attribut [messages] du paramètre [data]. Ces messages sont de vrais messages et non des clés de message. La vue
V va cependant les traiter comme des clés de message qui ne seront alors pas trouvées. Dans ce cas, le module [translate]
affiche la clé de message qu'il n'a pas trouvée, donc ici un vrai message. C'est le résultat souhaité ;
• lignes 32-34 : traitent le cas ou [[Link]] ligne 27 vaut null. Cela arrive avec le service web écrit. Il aurait fallu éviter
ce cas.
Utilisateur V C config
Données
utils
réseau
M
dao
Le service [dao] assure les échanges HTTP avec le service web / JSON. Son code est le suivant :
1. [Link]("rdvmedecins")
2. .factory('dao', ['$http', '$q', 'config', '$base64', 'utils',
3. function ($http, $q, config, $base64, utils) {
4.
5. // logs
6. [Link]("[dao] init");
7.
8. // ----------------------------------méthodes privées
9. // obtenir des données auprès du service web
10. function getData(serverUrl, username, password, urlAction, info) {
11. // opération asynchrone
12. var task = $[Link]();
13. // url requête HTTP
14. var url = serverUrl + urlAction;
15. // authentification basique
16. var basic = "Basic " + $[Link](username + ":" + password);
17. // la réponse
18. var réponse;
19. // les requêtes http doivent être toutes authentifiées
20. var headers = $[Link];
21. [Link] = basic;
22. // on fait la requête HTTP
23. var promise;
24. if (info) {
25. promise = $[Link](url, info, {timeout: [Link]});
26. } else {
27. promise = $[Link](url, {timeout: [Link]});
28. }
29. [Link](success, failure);
30. // on retourne la tâche elle-même afin qu'elle puisse être annulée
31. return task;
32.
[Link]
191/315
33. // success
34. function success(response) {
35. // [Link]={status:0, data:[med1, med2, ...]} ou {status:x, data:[msg1,
msg2, ...]
36. [Link]("[dao] getData[" + urlAction + "] success réponse", response);
37. // réponse
38. var payLoad = [Link];
39. réponse = [Link] == 0 ? {err: 0, data: [Link]} : {err: 1, messages:
[Link]};
40. // on rend la réponse
41. [Link](réponse);
42. }
43.
44. // failure
45. function failure(response) {
46. [Link]("[dao] getData[" + urlAction + "] error réponse", response);
47. // on analyse le status
48. var status = [Link];
49. var error;
50. switch (status) {
51. case 401 :
52. // unauthorized
53. error = 2;
54. break;
55. case 403:
56. // forbidden
57. error = 3;
58. break;
59. case 404:
60. // not found
61. error = 6;
62. break;
63. case 0:
64. // erreur locale
65. error = 4;
66. break;
67. default:
68. // autre chose
69. error = 5;
70. }
71. // on rend la réponse
72. [Link]({err: error, messages: [[Link]]});
73. }
74. }
75.
76. // --------------------- instance du service [dao]
77. return {
78. getData: getData
79. }
80. }]);
• lignes 77-79 : le service n'a qu'un unique champ : la méthode [getData] qui permet d'obtenir des informations auprès du
service web / JSON ;
• ligne 2 : apparaît une dépendance [$http] que nous n'avions pas encore rencontrée. C'est un service prédéfini d'Angular qui
permet le dialogue HTTP avec une entité distante ;
• ligne 6 : un log pour voir à quel moment de la vie de l'application, le code est exécuté ;
• ligne 10 : la méthode [getData] admet cinq paramètres :
◦ [serverUrl] : l'URL racine du service web ([Link] ;
◦ [urlAction] : l'URL du service particulier demandé (/getAllMedecins) ;
◦ [username] : le login de l'utilisateur ;
◦ [password] : son mot de passe ;
◦ [info] : objet rassemblant des informations complémentaires lorsque l'URL du service particulier demandé est
demandé via une opération POST. Dans le cas de l'URL (/getAllMedecins), ce paramètre n'a pas été passé. Il est donc
[undefined] ;
• ligne 12 : on crée une tâche asynchrone ;
• ligne 14 : l'URL complète du service demandé ([Link]
[Link]
192/315
• ligne 16 : l'authentification se fait en envoyant l'entête HTTP suivant :
Authorization:Basic code
[Link] = 'x';
Authorization : x
• ligne 23 : les méthodes du service [$http] renvoient des promesses. Elle seront mémorisées dans la variable [promise] ;
• ligne 27 : parce qu'ici, le paramètre [info] a la valeur [undefined], c'est la ligne 27 qui est exécutée. L'URL
([Link] est demandée avec un GET. Pour ne pas attendre trop longtemps, on fixe un délai
d'attente maximum (timeout) pour obtenir la réponse du serveur. Par défaut, ce délai est d'une seconde ;
• ligne 29 : on définit les deux méthodes à exécuter lorsque la promesse est obtenue :
◦ [success] : définie ligne 34, est la méthode à exécuter lorsque la promesse est obtenue sur un succès de la tâche ;
◦ [failure] : définie ligne 45, est la méthode à exécuter lorsque la promesse est obtenue sur un échec de la tâche ;
◦ les deux méthodes (on devrait dire fonctions) sont définies à l'intérieur de la fonction [getData]. C'est possible en
Javascript. Les variables définies dans [getData] sont connues dans les deux fonctions internes [success, failure] ;
• ligne 31 : on retourne la tâche créée ligne 12. Il faut se rappeler ici le code appelant :
1. promise = [Link](function () {
2. // on demande la liste des médecins;
3. task = [Link]($[Link], $[Link], $[Link],
[Link]);
4. return [Link];
5. });
Le code est un texte de trois chiffres qui indique si l'appel a réussi ou non. Grossièrement, on peut dire que les codes 2xx
et 3xx sont des codes de réussite, les autres étant des codes d'échec. Le texte est un court texte d'explication. Voici deux
réponses possibles, l'un en cas de réussite, l'autre en cas d'échec :
HTTP/1.1 200 OK
• ligne 36 : on affiche sur la console la réponse du serveur. Dans l'erreur [404 Not Found], on obtient quelque chose
comme :
Dans cette réponse, nous n'utiliserons que les champs [data], [status] et [statusText].
• ligne 38 : on récupère le champ [data] de la réponse. Il aura l'une des formes suivantes :
• {status: 0, data: [med1, med2, ...]} où [medi] est un objet représentant un médecin (titre, prenom, nom),
• {status: n, data: [msg1, msg2, ...]} où [msgi] est un message d'erreur et n est différent de 0 ;
[Link]
193/315
• ligne 39 : on construit la réponse {0,data} ou {n,messages}. La première réponse contient les médecins dans le champ
[data]. La seconde signale une erreur qui s'est produite côté serveur. Celui-ci a géré celle-ci, généré un code d'erreur dans
[err] et une liste de messages d'erreur dans [data]. Dans les deux cas, il renvoie un code HTTP 200 indiquant que l'ordre
HTTP a été traité complètement. C'est pour cela que les deux cas sont traités dans la même fonction [success] ;
• ligne 41 : la tâche est terminée [[Link]] et on rend l'une des deux réponses :
◦ {err: 0, data: [med1, med2, ...]} où [medi] est un objet représentant un médecin (titre, prenom, nom),
◦ {err: n, messages: [msg1, msg2, ...]} où [msgi] est un message d'erreur et n est différent de 0 ;
Il faut relier ce code à la façon dont cette réponse est récupérée dans le code appelant du contrôleur :
• ligne 45 : la fonction [failure] lorsque la tâche asynchrone se termine sur un échec. Il y a deux cas possibles :
◦ le serveur signale cet échec en renvoyant un code qui n'est ni 2xx ni 3xx,
◦ Angular annule l'appel HTTP. Il n'y a alors pas d'appel. Il y a une exception Angular mais pas de code d'erreur HTTP
renvoyé par le serveur. C'est par exemple le cas, si on fournit une URL invalide qui ne peut être appelée ;
• ligne 46 : on affiche la réponse sur la console ;
• ligne 48 : on se rappelle que la réponse du serveur a la forme :
{"data":"...","status":404,"config":{...},"statusText":"Not Found"}
[Link]
194/315
◦ ligne 55 : l'erreur [403] correspond à un appel non autorisé. L'utilisateur s'est correctement authentifié mais il n'a pas
les droits suffisants pour demander l'URL qu'il a demandée. Cela arrivera avec l'utilisateur [user / user]. Celui-ci existe
bien en base de données mais n'a pas le droit d'utiliser l'application. Seul l'utilisateur [admin / admin] a ce droit ;
◦ ligne 59 : l'erreur [404] correspond à une URL non trouvée. L'erreur peut avoir plusieurs causes :
▪ l'utilisateur a fait une erreur de saisie dans l'URL du service ;
▪ le service web n'a pas été lancé ;
▪ le service web n'a pas répondu assez vite (délai d'une seconde par défaut) ;
◦ ligne 63 : le code d'erreur HTTP 0 n'existe pas. On est dans le cas où Angular n'a pas fait l'appel HTTP demandé
parce que l'URL saisie par l'utilisateur est invalide et ne peut être appelée. Nous allons par la suite rencontrer d'autres
cas où Angular est amené à ne pas exécuter l'appel HTTP demandé ;
• ligne 72 : on termine la tâche avec succès ([Link]) en renvoyant une réponse du type {err, messages} où le tableau
[messages] n'est formé que du seul message [[Link]]. Dans le cas où Angular n'a pas fait l'appel HTTP
demandé, on aura une chaîne vide ;
Maintenant qu'on a une vue à la fois globale et détaillée de l'application, nous pouvons commencer les tests.
[Link]
195/315
◦ utiliser la console de Webstorm ;
• ligne 1 : Angular signale une erreur sur laquelle nous allons revenir ;
• ligne 2 : le log de la méthode [[Link]]. On y trouve des choses intéressantes :
◦ [status] vaut 0, indiquant par là, qu'il n'y a pas eu d'appel HTTP. En conséquence [statusText] est vide,
◦ [url] vaut [[Link] ce qui est correct ;
◦ l'entête HTTP d'authentification [Authorization":"Basic YWRtaW46YWRtaW4=] est lui aussi correct ;
Bon alors pourquoi ça n'a pas marché ? La phrase clé des logs est [No 'Access-Control-Allow-Origin' header is present]. Pour la
comprendre, il faut faire une longue explication. Commençons par revenir sur l'architecture générale de l'application client /
serveur :
En fait, il est inexact de dire que le navigateur interdit à l'application Angular d'interroger le serveur [2]. Elle l'interroge en fait pour
lui demander s'il autorise un client qui ne vient pas de chez lui à l'interroger. On appelle cette technique de partage, le CORS
[Link]
196/315
(Cross-Origin Resource Sharing). Le serveur [2] donne son accord en envoyant des entêtes HTTP précis. C'est parce qu'ici, notre
serveur [2] ne les a pas envoyés que le navigateur a refusé de faire l'appel HTTP demandé par l'application.
Entrons maintenant dans les détails. Examinons les échanges réseau qui ont eu lieu lors de l'appel HTTP. Pour cela, dans le
navigateur Chrome, nous faisons [F12] pour obtenir les outils du développeur et nous sélectionnons l'onglet [Network] pour voir les
échanges réseau :
1 2
On peut voir dans [1], que le navigateur a envoyé une requête HTTP [OPTIONS] sur l'URL demandée. [OPTIONS] est une des
commandes HTTP possibles avec [GET] et [POST] plus connues. Elle permet de demander des informations à un serveur
notamment sur les options HTTP qu'il supporte, d'où le nom de la commande. Le serveur fait sa réponse en [2]. Pour indiquer qu'il
accepte des requêtes de clients qui ne sont pas dans son domaine, il doit renvoyer un entête particulier appelé [Access-Control-
Allow-Origin]. Et c'est parce qu'il ne l'a pas renvoyé qu'Angular n'a pas exécuté l'appel HTTP demandé et a renvoyé l'erreur :
[Link]
197/315
XMLHttpRequest cannot load [Link] No 'Access-Control-Allow-Origin'
header is present on the requested resource. Origin '[Link] is therefore not allowed
access.
Nous devons donc modifier notre serveur pour qu'il envoie l'entête HTTP attendu.
Nous revenons sous Eclipse. Afin de conserver l'acquis, nous dupliquons la version actuelle du serveur web / JSON [rdvmedecins-
webapi-v2] dans [rdvmedecins-webapi-v3] [1] :
Nous faisons une première modification dans [ApplicationModel] qui est l'un des éléments de configuration du service web :
1. package [Link];
2.
3. ...
4.
5. @Component
6. public class ApplicationModel implements IMetier {
7.
8. // la couche [métier]
9. @Autowired
10. private IMetier métier;
11.
12. // données provenant de la couche [métier]
13. private List<Medecin> médecins;
14. private List<Client> clients;
15. private List<String> messages;
16. // données de configuration
17. private boolean CORSneeded = true;
18.
19. ...
20.
21. public boolean isCORSneeded() {
22. return CORSneeded;
23. }
24.
25. }
• ligne 17 : nous créons un booléen qui indique si on accepte ou non les clients étrangers au domaine du serveur ;
• lignes 21-23 : la méthode d'accès à cette information ;
[Link]
198/315
3
1. package [Link];
2.
3. import [Link];
4.
5. import [Link];
6. import [Link];
7. import [Link];
8. import [Link];
9.
10. import [Link];
11.
12. @Controller
13. public class RdvMedecinsCorsController {
14.
15. @Autowired
16. private ApplicationModel application;
17.
18. // envoi des options au client
19. private void sendOptions(HttpServletResponse response) {
20. if ([Link]()) {
21. // on fixe le header CORS
22. [Link]("Access-Control-Allow-Origin", "*");
23. }
24.
25. }
26.
27. // liste des médecins
28. @RequestMapping(value = "/getAllMedecins", method = [Link])
29. public void getAllMedecins(HttpServletResponse response) {
30. sendOptions(response);
31. }
32. }
• lignes 28-31 : définissent un contrôleur pour l'URL [/getAllMedecins] lorsqu'elle est demandée avec la commande HTTP
[OPTIONS] ;
• ligne 29 : la méthode [getAllMedecins] admet pour paramètre l'objet [HttpServletResponse] qui va être envoyé au client
qui a fait la demande. Cet objet est injecté par Spring ;
• ligne 30 : on délègue le traitement de la demande à la méthode privée des lignes 19-25 ;
• lignes 15-16 : l'objet [ApplicationModel] est injecté ;
• lignes 20-23 : si le serveur est configuré pour accepter les clients étrangers à son domaine, alors on envoie l'entête HTTP :
Access-Control-Allow-Origin: *
qui signifie que le serveur accepte les clients de tout domaine (*).
Nous sommes désormais prêts pour de nouveaux tests. Nous lançons la nouvelle version du service web et nous découvrons que le
problème reste entier. Rien n'a changé. Si ligne 30 ci-dessus, on met un affichage console, celui-ci n'est jamais affiché montrant par
là que la méthode [getAllMedecins] de la ligne 29 n'est jamais appelée.
Après quelques recherches, on découvre que Spring MVC traite lui-même les commandes HTTP [OPTIONS] avec un traitement
par défaut. Aussi c'est toujours Spring qui répond et jamais la méthode [getAllMedecins] de la ligne 29. Ce comportement par
défaut de Spring MVC peut être changé. Nous introduisons une nouvelle classe de configuration, pour configurer le nouveau
comportement :
[Link]
199/315
La nouvelle classe de configuration [WebConfig] est la suivante :
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6. import [Link];
7.
8. @Configuration
9. public class WebConfig extends WebMvcConfigurerAdapter {
10.
11. // configuration dispatcherservlet pour les headers CORS
12. @Bean
13. public DispatcherServlet dispatcherServlet() {
14. DispatcherServlet servlet = new DispatcherServlet();
15. [Link](true);
16. return servlet;
17. }
18. }
• ligne 8 : la classe est une classe de configuration Spring. Elle déclare des beans qui seront placés dans le contexte de
Spring ;
• ligne 12 : le bean [dispatcherServlet] sert à définir la servlet qui gère les demandes des clients. Elle est de type
[DispatcherServlet]. Cette servlet est normalement créée par défaut. Si on la crée nous-mêmes, on peut alors la
configurer ;
• ligne 14 : on crée une instance de type [DispatcherServlet] ;
• ligne 15 : on demande à ce que la servlet fasse suivre à l'application les commandes HTTP [OPTIONS] ;
• ligne 16 : on rend la servlet ainsi configurée ;
1. package [Link];
2.
3. import [Link];
4. import [Link];
5. import [Link];
6.
7. import [Link];
8.
9. @EnableAutoConfiguration
10. @ComponentScan(basePackages = { "[Link]" })
11. @Import({ [Link], [Link], [Link] })
12. public class AppConfig {
13.
14. }
[Link]
200/315
2
• en [1], on peut constater que l'entête HTTP [Access-Control-Allow-Origin: *] est désormais présent dans la réponse du
serveur. Et pourtant ça ne marche toujours pas. Nous examinons en [2], les logs de la console. On y trouve le log suivant :
XMLHttpRequest cannot load [Link] Request header field Authorization is not allowed
by Access-Control-Allow-Headers
On voit que le navigateur attend un nouvel entête HTTP [Access-Control-Allow-Headers] qui lui dirait qu'on a le droit de
lui envoyer l'entête d'authentification :
Authorization:Basic code
Cela peut être bon signe. Angular a peut être voulu envoyer la commande HTTP GET. Mais comme celle-ci est accompagnée d'un
entête d'authentification, il demande si le serveur accepte celui-ci.
Nous modifions notre serveur web / JSON pour envoyer cet entête. La classe [RdvMedecinsCorsController] évolue comme
suit :
Nous relançons le serveur et redemandons la liste des médecins avec le client Angular :
[Link]
201/315
Cette fois, c'est bon. Les logs console montrent la réponse reçue par la méthode [[Link]] :
On voit que :
• le serveur a renvoyé un code d'erreur [status=200] avec le message [statusText=OK]. C'est pourquoi on est dans la
fonction [success] ;
• le serveur a renvoyé un objet [data] avec deux champs :
◦ [status] : (à ne pas confondre avce le code d'erreur HTTP [status]). Ici [status=0] indique que l'URL
[/getAllMedecins] a été traitée sans erreur ;
◦ [data] : qui contient la liste JSON des médecins ;
[Link]
202/315
On se connecte sous l'identité [user / user] qui n'a pas accès à l'application (seul [admin] y a accès) :
Cette fois-ci, l'erreur n'est plus [Erreur d'authentification] mais [Accès refusé].
[Link] La vue V
La vue initiale sera la suivante :
[Link]
203/315
Pour obtenir la vue V, nous dupliquons le code [[Link]] dans [[Link]] et le modifions de la façon suivante :
[Link]
204/315
Quasiment rien ne change, sauf dans le contrôleur qui est désormais adapté pour fournir la liste des clients :
1. [Link]("rdvmedecins")
2. .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
3. function ($scope, utils, config, dao, $translate) {
4. // ------------------- initialisation modèle
5. // modèle
6. $[Link] = {text: [Link], visible: false, cancel: cancel, time: undefined};
7. $[Link] = [Link];
8. $[Link] = {url: undefined, login: undefined, password: undefined};
9. $[Link] = {title: [Link], show: false, model: {}};
10. $[Link] = {show: false, model: {}};
11. $[Link] = [Link];
12. $[Link] = [Link];
13. $[Link] = [Link];
14.
15. // tâche asynchrone
16. var task;
17.
18. // exécution action
19. $[Link] = function () {
20. // on met à jour l'UI
21. $[Link] = true;
22. $[Link] = false;
23. $[Link] = false;
24. // attente simulée
25. task = [Link]($[Link]);
26. var promise = [Link];
27. // attente
28. promise = [Link](function () {
29. // on demande la liste des clients;
30. task = [Link]($[Link], $[Link], $[Link],
[Link]);
31. return [Link];
32. });
33. // on analyse le résultat de l'appel précédent
34. [Link](function (result) {
35. // result={err: 0, data: [client1, client2, ...]}
36. // result={err: n, messages: [msg1, msg2, ...]}
37. if ([Link] == 0) {
38. // on met les données acquises dans le modèle
39. $[Link] = [Link];
40. // on met à jour l'UI
41. $[Link] = true;
42. $[Link] = false;
43. // on style la liste déroulante
[Link]
205/315
44. $('.selectpicker').selectpicker();
45. } else {
46. // il y a eu des erreurs pour obtenir la liste des clients
47. $[Link] = { title: [Link], messages: [Link](result), show:
true, model: {}};
48. // on met à jour l'UI
49. $[Link] = false;
50. }
51. });
52. };
53.
54. // annulation attente
55. function cancel() {
56. // on termine la tâche
57. [Link]();
58. // on met à jour l'UI
59. $[Link] = false;
60. $[Link] = false;
61. $[Link] = false;
62. }
63. }
64. ])
65. ;
• très peu de choses changent dans le contrôleur. Il fournissait une liste de médecins. Il fournit désormais une liste de
clients ;
• ligne 9 : [$[Link]] sera le modèle du bandeau des clients dans la vue V ;
• ligne 30 : c'est l'URL [/getAllClients] qui est désormais utilisée ;
• lignes 35-36 : les deux formes de réponse rendue par la méthode [[Link]]. On a maintenant des clients au lieu de
médecins ;
• ligne 44 : une instruction assez rare dans un code Angular. On manipule directement le DOM (Document Object Model).
Ici on veut appliquer la méthode [selectpicker] (fait partie de [[Link]]) aux éléments du DOM qui ont la
classe [selectpicker] [$('.selectpicker')]. Il n'y en a qu'un, la liste déroulante :
Au paragraphe 3.6.6, page 151, il a été montré que cela stylisait la liste déroulante de la façon suivante :
Comme il a été fait pour les médecins, nous sommes amenés à modifier le service web également.
[Link]
206/315
La classe [RdvMedecinsController] s'enrichit d'une nouvelle méthode :
1. package [Link];
2.
3. ...
4.
5. @Controller
6. public class RdvMedecinsCorsController {
7.
8. @Autowired
9. private ApplicationModel application;
10.
11. // envoi des options au client
12. private void sendOptions(HttpServletResponse response) {
13. if ([Link]()) {
14. // on fixe le header CORS
15. [Link]("Access-Control-Allow-Origin", "*");
16. // on autorise le header [Authorization]
17. [Link]("Access-Control-Allow-Headers", "Authorization");
18. }
19.
20. }
21.
22. // liste des médecins
23. @RequestMapping(value = "/getAllMedecins", method = [Link])
24. public void getAllMedecins(HttpServletResponse response) {
25. sendOptions(response);
26. }
27.
28. // liste des clients
29. @RequestMapping(value = "/getAllClients", method = [Link])
30. public void getAllClients(HttpServletResponse response) {
31. sendOptions(response);
32. }
33. }
• lignes 29-32 : la méthode [getAllClients] va gérer la demande HTTP [OPTIONS] que va lui envoyer le navigateur ;
[Link]
207/315
Ce message d'erreur est affiché lorsqu'Angular n'a pu faire la requête HTTP demandée. Il faut en chercher alors les causes dans les
logs de la console. On y trouve le message suivant :
Un problème qu'on croyait résolu. On va alors voir les échanges réseau qui se sont produits :
On voit que l'opération [getAllClients] avec la méthode HTTP [OPTIONS] s'est bien passée mais que l'opération [getAllClients]
avec la méthode HTTP [GET] a été annulée. La réponse à la demande [OPTIONS] a été la suivante :
Les entêtes HTTP du CORS sont bien là. Examinons maintenant les échanges HTTP lors du GET :
[Link]
208/315
La requête HTTP semble correcte. On voit notamment l'entête d'authentification.
Outre le message d'erreur précédent, on trouve dans les logs console, le message suivant :
C'est le log que fait systématiquement la méthode [[Link]] à la réception de la réponse à sa demande HTTP. On peut
remarquer deux choses :
• [status=0] : cela veut dire que c'est Angular qui a annulé la requête HTTP ;
• [method=GET] : et c'est la requête GET qui a été annulée ;
Mis bout à bout avec le premier message, cela veut dire que pour la requête GET également, Angular attend ici des entêtes CORS.
Or actuellement, notre service web ne les envoie que pour la requête HTTP [OPTIONS]. Il est très étrange de rencontrer cette
erreur maintenant et pas pour la liste des médecins. Je n'ai pas d'explications.
Les méthodes [GET] et [POST] sont traitées dans la classe [RdvMedecinsController]. Nous devons la modifier pour que ces
méthodes envoient les entêtes CORS. Nous le faisons de la façon suivante :
1. @RestController
2. public class RdvMedecinsController {
3.
4. @Autowired
5. private ApplicationModel application;
6.
7. @Autowired
8. private RdvMedecinsCorsController rdvMedecinsCorsController;
9.
10. ...
11.
12. // liste des clients
13. @RequestMapping(value = "/getAllClients", method = [Link])
14. public Reponse getAllClients(HttpServletResponse response) {
15. // entêtes CORS
16. [Link](response);
17. // état de l'application
18. if (messages != null) {
[Link]
209/315
19. return new Reponse(-1, messages);
20. }
21. // liste des clients
22. try {
23. return new Reponse(0, [Link]());
24. } catch (Exception e) {
25. return new Reponse(1, [Link](e));
26. }
27. }
28. ...
• ligne 8 : nous voulons réutiliser le code que nous avons placé dans le contrôleur [RdvMedecinsCorsController]. Aussi
injectons-nous celui-ci ici ;
• ligne 14 : la méthode qui traite la demande [GET /getAllClients]. Nous faisons deux modifications :
◦ ligne 14 : nous injectons l'objet [HttpServletResponse] dans les paramètres de la méthode,
◦ ligne 16 : nous utilisons les méthodes de la classe [RdvMedecinsCorsController] pour mettre dans cet objet les entêtes
CORS ;
Dans les logs console, la méthode [[Link]] a affiché la réponse qu'elle a reçue :
Donc la méthode a bien reçu la liste des clients. Une fois le code vérifié, on en vient à suspecter l'instruction suivante qu'on ne
maîtrise pas très bien :
[Link]
210/315
1. // on style la liste déroulante
2. $('.selectpicker').selectpicker();
On a donc localisé le problème. C'est l'application de la méthode [selectpicker] à la liste déroulante qui pose problème. Lorsqu'on
regarde le code source de la page erronée, on a la chose suivante :
[Link]
211/315
• on découvre qu'en [1], la liste déroulante est bien présente avec ses éléments mais qu'elle n'est pas affichée
[style='display:none'] ;
• en [2], on voit le bouton [bootstrap select] affiché. Les éléments de la liste déroulante devraient apparaître dans la liste <ul
role='menu'>. Ils n'y sont pas et on a donc une liste vide. Il semble que lorsque la méthode [selectpicker] a été appliquée à
la liste déroulante, son contenu était vide à ce moment là ;
par le suivant :
Le style [bootstrap-select] est appliqué au travers d'une fonction [$timeout]. Nous avons déjà rencontré cette fonction qui permet
d'exécuter une fonction passé un certain délai. Ici, l'absence de délai vaut un délai nul. Les lignes précédentes mettent un événement
dans la liste d'attente des événements du navigateur. Lorsque le traitement de l'événement en cours (clic sur le bouton [Liste des
clients]) va être terminé, la vue V va être affichée. Puis aussitôt après, le navigateur va consulter sa liste d'événements. A cause de
son délai nul, l'événement [$timeout] va être en tête de liste et traité. Le style [bootstrap-select] est alors appliqué à une liste
déroulante remplie. Voyons le résultat :
[Link]
212/315
1
Le bouton [bootstrap-select] qui précédemment était vide contient désormais la liste des clients.
On manipule un objet du DOM. Nombre de développeurs Angular sont allergiques à la manipulation du DOM dans le code d'un
contrôleur. Pour eux, celle-ci doit être faite dans une directive. Une directive Angular peut être vue comme une extension du
langage HTML. Il est ainsi possible de créer de nouveaux éléments ou attributs HTML. Voyons un premier exemple :
La directive appartient au module [rvmedecins]. C'est une fonction qui accepte deux paramètres :
• le premier est le nom de la directive [selectEnable] ;
[Link]
213/315
• le second est un tableau ['obj1','obj2',..., function(obj1, obj2,...)] où les [obj] sont les objets à injecter dans la
fonction. Ici le seul objet injecté est l'objet prédéfini [$timeout] ;
• la fonction [directive] retourne un objet qui peut avoir divers attributs. Ici le seul attribut est l'attribut [link] (ligne 3). Sa
valeur est ici une fonction admettant trois paramètres :
◦ scope : le modèle de la vue dans laquelle est utilisée la directive ;
◦ element : l'élément de la vue, objet de la directive ;
◦ attrs : les attributs de cet élément ;
Prenons un exemple. La directive [selectEnable] pourrait être utilisée dans le contexte suivant :
<div select-enable="data"></div>
Ci-dessus, l'attribut [select-enable] applique la directive [selectEnable] à l'élement HTML <div>. Une directive [doSomething] peut
être appliquée à n'importe quel élément HTML en lui ajoutant l'attribut [do-something]. On fera attention au changement d'écriture
entre le nom de la directive et l'attribut qui lui est associé. On passe d'une écriture [camelCase] à une écriture [camel-case].
Ici la directive [doSomething] est appliquée sous la forme d'une balise HTML <do-something>.
Revenons à l'écriture
<div select-enable="data"></div>
• ligne 14 : on retrouve le code que nous avions placé auparavant dans le contrôleur. Celui-ci est exécuté lors de la rencontre
de la directive [select-enable] (sous forme d'élément ou d'attribut) lors de l'affichage de la vue V.
Pour mettre en oeuvre cette directive, nous copions le fichier [[Link]] dans [[Link]] et le modifions de la façon
suivante :
• ligne 1 : on applique la directive [selectEnable] à l'élément HTML [select]. Comme il n'y a pas d'informations à passer à la
directive, nous écrivons simplement [select-enable=""] ;
Nous modifions également le contrôleur en dupliquant le fichier JS [[Link]] dans [[Link]] et nous
référençons le nouveau fichier JS dans le fichier [[Link]] et le fichier [[Link]] de directive. Il ne faut pas oublier ce
dernier point. Si le fichier de la directive est absent, l'attribut [select-enable=""] ne sera pas géré mais Angular ne signalera aucune
erreur.
[Link]
214/315
1. <script type="text/javascript" src="[Link]"></script>
2. <script type="text/javascript" src="[Link]"></script>
1. [dao] init
2. directive selectEnable
3. [dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},
{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},
{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},
{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":
{"method":"GET","transformRequest":[null],"transformResponse":
[null],"timeout":1000,"url":"[Link]
text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
1. $('.selectpicker').selectpicker();
[Link]
215/315
ne s'est pas déroulée au bon moment. On peut essayer de résoudre le problème de diverses façons. Au bout de nombreux tests
infructueux, on se rend compte que l'opération ci-dessus ne doit se dérouler qu'une fois et uniquement lorsque la liste
déroulante a été remplie. Pour obtenir ce résultat, on réécrit la balise <select> de la façon suivante :
Ligne 1, la balise <select> n'est générée que si [[Link]] existe. Ce n'est pas le cas lors de l'affichage initial de la vue V. La balise
<select> ne sera donc pas générée et la directive [selectEnable] pas évaluée. Lorsque l'utilisateur va cliquer sur le bouton [Liste des
clients], [[Link]] aura une nouvelle valeur dans le modèle M. Parce que le modèle M a changé, la balise <select> va être
réévaluée et ici générée. La directive [selectEnable] va donc être évaluée également. Lorsqu'elle est évaluée, les lignes 2-4 de la balise
<select> n'ont pas encore été évaluées. On a donc une liste de clients vide. Si on écrit la directive [selectEnable] de la façon
suivante :
la ligne 5 va être exécutée avec une liste vide et on aura alors une liste déroulante vide à l'affichage. Il faut alors écrire :
pour avoir le résultat attendu. A cause du [$timeout] de la ligne 5, la ligne 6 ne sera exécutée qu'après évaluation complète de la vue
V, donc à un moment ou la balise <select> aura tous ses éléments.
[Link]
216/315
2
[Link] Le formulaire
Nous dupliquons le fichier [[Link]] dans [[Link]] puis nous modifions le code de la façon suivante :
1. <div class="container">
2. <h1>Rdvmedecins - v1</h1>
3.
4. <!-- le message d'attente -->
5. <div class="alert alert-warning" ng-show="[Link]">
6. ...
7. </div>
8.
9. <!-- la demande -->
10. <div class="alert alert-info" ng-hide="[Link]">
11. <div class="row" style="margin-bottom: 20px">
12. <div class="col-md-3">
13. <h2 translate="{{[Link]}}"></h2>
14. <select data-style="btn-primary" class="selectpicker">
15. <option ng-repeat="medecin in [Link]" value="{{[Link]}}">
16. {{[Link]}} {{[Link]}} {{[Link]}}
[Link]
217/315
17. </option>
18. </select>
19. </div>
20. <div class="col-md-3">
21. <h2 translate="{{[Link]}}"></h2>
22. <div style="display:inline-block; min-height:290px;">
23. <datepicker ng-model="[Link]" min-date="[Link]" show-weeks="true"
24. class="well well-sm"></datepicker>
25. </div>
26. </div>
27. </div>
28. <button class="btn btn-primary" ng-click="execute()">{{[Link]|translate}}</button>
29. </div>
30.
31. <!-- la liste d'erreurs -->
32. <div class="alert alert-danger" ng-show="[Link]">
33. ...
34. </div>
35.
36. <!-- l'agenda -->
37. <div id="agenda" ng-show="[Link]">
38. ...
39. </div>
40. </div>
41. ...
42. <script type="text/javascript" src="[Link]"></script>
[Link] Le contrôleur C
Le code JS de l'application devient le suivant :
Seuls le service [utils] et le contrôleur [rdvMedecinsCtrl] vont être impactés par les modifications.
1. // contrôleur
[Link]
218/315
2. [Link]("rdvmedecins")
3. .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter',
'$locale',
4. function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
5. // ------------------- initialisation modèle
6. // modèle
7. $[Link] = {text: [Link], visible: false, cancel: cancel, time: 3000};
8. $[Link] = {url: '[Link] login: 'admin', password: 'admin'};
9. $[Link] = {show: false, model: {}};
10. $[Link] = {
11. data: [
12. {id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
13. {id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
14. {id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
15. {id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
16. ],
17. title: [Link]};
18. $[Link] = {title: [Link], data: undefined, show: false};
19. $[Link] = {title: [Link], minDate: new Date(), jour: new Date()};
20. // on style la liste déroulante
21. $timeout(function () {
22. $('.selectpicker').selectpicker();
23. });
24. // locale française pour le calendrier
25. [Link]([Link]['fr'], $locale);
26. ...
27. }
28. ])
29. ;
1. // exécution action
2. $[Link] = function () {
3. // les infos du formulaire
4. var idMedecin = $('.selectpicker').selectpicker('val');
5.
6. // vérification
7. [Link]("[homeCtrl] idMedecin", idMedecin);
8. [Link]("[homeCtrl] jour", $[Link]);
9.
10. // on met le jour au format yyyy-MM-dd
11. var formattedJour = $filter('date')($[Link], 'yyyy-MM-dd');
12. // mise à jour de la vue
13. $[Link] = true;
14. $[Link] = false;
15. $[Link] = false;
16. ...
17. };
• ligne 4 : on récupère l'attribut [value] du médecin sélectionné. On utilise ici de nouveau la méthode [selectpicker] qui
provient du fichier [[Link]]. Il faut se souvenir de la forme des options de la liste déroulante :
• ligne 11 : on met le jour choisi par l'utilisateur au format [aaaa-mm-jj] qui est le format de date attendu par le serveur web ;
• lignes 13-15 : lorsque la méthode [execute] sera terminée, le bandeau d'attente sera affiché et tout le reste caché ;
[Link]
219/315
Le code se poursuit de la façon suivante :
1. // attente simulée
2. var task = [Link]($[Link]);
3. // on demande l'agenda du médecin
4. var promise = [Link](function () {
5. // le chemin de l'URL de service
6. var path = [Link] + "/" + idMedecin + "/" + formattedJour;
7. // on demande l'agenda
8. task = [Link]($[Link], $[Link], $[Link],
path);
9. // on retourne la promesse d'achèvement de la tâche
10. return [Link];
11. });
12. // on analyse le résultat de l'appel au service [dao]
13. [Link](function (result) {
14. // fin d'attente
15. $[Link] = false;
16. // erreur ?
17. if ([Link] == 0) {
18. // on prépare le modèle de l'agenda
19. $[Link] = [Link];
20. $[Link] = true;
21. // mise en forme de l'affichage des horaires
22. [Link]($[Link], function (creneauMedecin) {
23. [Link] = [Link]([Link]);
24. });
25. // on crée un evt pour styler la table après l'affichage de la vue
26. $timeout(function () {
27. $("#creneaux").footable();
28. });
29. } else {
30. // il y a eu des erreurs pour obtenir l'agenda
31. $[Link] = {
32. title: [Link],
33. messages: [Link](result),
34. show: true
35. };
36. }
[Link]
220/315
1 2
Le paramètre [[Link]] de la ligne 19 est l'attribut [data] [1] ci-dessus. Cet attribut contient à son tour l'attribut [creneauxMedecin]
[2] ci-dessus. Celui-ci est un tableau de créneaux avec pour chacun d'eux les deux informations :
• [rv] : la forme JSON d'un rendez-vous ou [null] s'il n'y a pas de rendez-vous pris sur ce créneau ;
• [hDeb, mDeb, hFin, mFin] : les informations horaires du créneau ;
• ligne 27 : pour rendre la table 'responsive', il faut lui appliquer la méthode [footable]. On retrouve ici la même difficulté
que celle rencontrée pour le composant [bootstrap-select]. Si on écrit simplement la ligne 17, on constate que la table n'est
pas 'responsive'. On résoud ce problème de la même façon avec la fonction [$timeout] (ligne 26) ;
• lignes 31-34 : le cas où l'appel HTTP a échoué. On affiche alors les messages d'erreur ;
[Link]
221/315
[Link] Affichage de l'agenda
Nous revenons maintenant au code de l'agenda dans le fichier [[Link]]. C'est le suivant :
• lignes 4-5 : on se rappelle que [[Link]] est l'agenda, que [[Link]] est un tableau d'objets de
type [CreneauMedecin]. Chaque élément de ce dernier type a un attribut [CreneauMedecin].creneau qui est un créneau
horaire. Chaque créneau horaire a deux éléments qui nous intéressent :
◦ [[Link]] qui est l'éventuel RV (rv!=null) pris sur le créneau ;
◦ [[Link]] qui est le texte [début:fin] du créneau horaire ;
• ligne 4 : affiche un message spécial si le médecin n'a pas de créneaux horaires. C'est improbable mais il se trouve que notre
base de données est incomplète et ce cas existe. La génération HTML ou non du message est contrôlée par la directive
[ng-if] ;
[Link]
222/315
La directive [ng-if] est différente des directives [ng-show, ng-hide]. Ces dernières se contentent de cacher une zone
présente dans le document. Si [ng-if='false'], alors la zone est enlevée du document. On l'a utilisée ici pour illustration ;
• ligne 9 : l'attribut [id='creneaux'] est important. C'est lui qui est utilisé dans l'instruction :
$("#creneaux").footable();
4
3
[Link]
223/315
Dans la classe [RdvMedecinsCorsController] on ajoute ue nouvelle méthode :
1. // agenda du médecin
2. @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method =
[Link])
3. public void getAgendaMedecinJour(HttpServletResponse response) {
4. sendOptions(response);
5. }
Cette méthode va envoyer les entêtes CORS pour la requête HTTP [OPTIONS]. On doit faire la même chose pour la requête
HTTP [GET] dans la classe [RdvMedecinsController] :
Pour le 1er cas, nous allons utiliser la directive [selectEnable] déjà présentée. Pour le second cas, nous créons la directive [footable]
dans le fichier JS [[Link]] suivant :
[Link]
224/315
On utilise donc la même technique que pour la directive [selectEnable].
Le code HTML [[Link]] est dupliqué dans [[Link]]. Puis on le fait évoluer de la façon suivante :
• ligne 1 : on applique la directive [selectEnable] (via l'attribut [select-enable]) à la balise <select> des médecins ;
• ligne 3 : on applique la directive [footable] (via l'attribut [footable]) à la table HTML de l'agenda ;
Le fichier [[Link]] est identique au fichier [[Link]] à deux détails près. Les lignes manipulant le DOM
disparaissent :
Cei fait, l'exécution de l'application [[Link]] donne les mêmes résultats que celle de [[Link]].
[Link]
225/315
2
• en [1], on pourra réserver. La réservation qui sera faite le sera pour un client aléatoire ;
• en [2], on pourra supprimer les réservations que nous aurons faites ;
Nous dupliquons le fichier [[Link]] dans [[Link]] puis nous modifions le code de la façon suivante :
1. <div class="container">
2. <h1>Rdvmedecins - v1</h1>
3.
4. <!-- le message d'attente -->
5. <div class="alert alert-warning" ng-show="[Link]">
6. ...
7. </div>
8.
9. <!-- la liste d'erreurs -->
10. <div class="alert alert-danger" ng-show="[Link]">
11. ...
12. </div>
13.
14. <!-- l'agenda -->
15. <div id="agenda" ng-show="[Link]">
16. ..
17. <!-- agenda du médecin -->
18. <div class="row tab-content alert alert-warning" ng-if="[Link]!=0">
19. <div class="tab-pane active col-md-6">
20. <table id="creneaux" class="table" footable="">
21. ...
22. <tbody>
23. <tr ng-repeat="creneauMedecin in [Link]">
24. ...
25. <td>
26. <a href="" ng-if="![Link]" translate="agenda_reserver" class="status-metro
status-active" ng-click="reserver([Link])">
27. </a>
28. <a href="" ng-if="[Link]" translate="agenda_supprimer" class="status-metro
status-suspended" ng-click="supprimer([Link])">
29. </a>
30. </td>
31. </tr>
32. </tbody>
33. </table>
34. </div>
35. </div>
36. </div>
37. </div>
38. ....
[Link]
226/315
39. <script type="text/javascript" src="[Link]"></script>
40. <script type="text/javascript" src="[Link]"></script>
[Link] Le contrôleur C
Le code JS de [[Link]] est d'abord obtenu par recopie du fichier [[Link]]. Puis il est modifié. On a toujours les
grands blocs de code habituels. Les modifications se font essentiellement dans le contrôleur :
1. [Link]("rdvmedecins")
2. .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout',
'$filter', '$locale',
3. function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
4. // ------------------- initialisation modèle
5. // modèle
6. $[Link] = {text: [Link], visible: false, cancel: cancel, time: 3000};
7. $[Link] = {url: '[Link] login: 'admin', password: 'admin'};
8. $[Link] = {show: false, model: {}};
9. $[Link] = {
10. data: [
11. {id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
12. {id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
13. {id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
14. {id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
15. ],
16. title: [Link]
17. };
18. var médecin = $[Link][0];
19. var clients = [
20. {id: 1, version: 1, titre: "Mr", nom: "MARTIN", prenom: "Jules"},
21. {id: 2, version: 1, titre: "Mme", nom: "GERMAN", prenom: "Christine"},
[Link]
227/315
22. {id: 3, version: 1, titre: "Mr", nom: "JACQUARD", prenom: "Maurice"},
23. {id: 4, version: 1, titre: "Melle", nom: "BISTROU", prenom: "Brigitte"}
24. ];
25. // locale française pour la date
26. [Link]([Link]['fr'], $locale);
27. var today = new Date();
28. var formattedDay = $filter('date')(today, 'yyyy-MM-dd');
29. var fullDay = $filter('date')(today, 'fullDate');
30. $[Link] = {title: [Link], data: undefined, show: false, model: {titre:
mé[Link], prenom: mé[Link], nom: mé[Link], jour: fullDay}};
31.
32.
33. // ---------------------------------------------------------------- agenda initial
34. // la tâche asynchrone globale
35. var task;
36. // on demande l'agenda
37. getAgenda();
38.
39. // ------------------------------------------------------------------ réservation
40. $[Link] = function (creneauId) {
41. ....
42. };
43.
44. // ------------------------------------------------------------ suppression RV
45. $[Link] = function (idRv) {
46. ...
47. };
48.
49. // obtention de l'agenda
50. function getAgenda() {
51. ...
52. }
53.
54. // annulation attente
55. function cancel() {
56. ...
57. }
58. } ]);
• ligne 6 : configuration du message d'attente. Par défaut, on attendra 3 secondes avant de faire un appel HTTP ;
• ligne 7 : les informations nécessaires aux appels HTTP ;
• ligne 8 : configuration du message d'erreurs ;
• lignes 9-17 : les médecins en dur ;
• ligne 18 : un médecin particulier. C'est pour ses créneaux qu'on fera des réservations ;
• lignes 19-24 : les clients en dur ;
• ligne 26 : on veut manipuler des dates françaises ;
• ligne 27 : les rendez-vous seront pris à la date d'aujourd'hui ;
• ligne 28 : le service web de réservation attend des dates au format 'aaaa-mm-jj' ;
• ligne 29 : la date d'aujourd'hui sous la forme [jeudi 26 juin 2014] ;
• ligne 30 : configuration de l'agenda. L'attribut [model] transporte les paramètres du message internationalisé qui va être
affiché :
• ligne 35 : la variable globale [task] représente à un moment donné la tâche asynchrone en cours d'exécution ;
• ligne 37 : on demande l'agenda initial ;
C'est tout ce qui est fait lors du chargement initial de la page. Si tout se passe bien, la vue affiche l'agenda du jour de Mme
PELISSIER.
[Link]
228/315
[Link] Obtention de l'agenda
L'agenda est obtenu avec la méthode [getAgenda] suivante :
1. // obtention de l'agenda
2. function getAgenda() {
3. // le chemin de l'URL de service
4. var path = [Link] + "/" + mé[Link] + "/" + formattedDay;
5. // on demande l'agenda
6. task = [Link]($[Link], $[Link], $[Link], path);
7. // msg d'attente
8. $[Link] = true;
9. // on analyse le résultat de l'appel au service [dao]
10. [Link](function (result) {
11. // fin d'attente
12. $[Link] = false;
13. // erreur ?
14. if ([Link] == 0) {
15. // on prépare le modèle de l'agenda
16. $[Link] = [Link];
17. $[Link] = true;
18. // mise en forme de l'affichage des horaires
19. [Link]($[Link], function (creneauMedecin) {
20. [Link] = [Link]([Link]);
21. });
22. } else {
23. // il y a eu des erreurs pour obtenir l'agenda
24. $[Link] = {title: [Link], messages: [Link](result), show:
true};
25. }
26. });
27. }
Ce code a été isolé dans une fonction car il est également utilisé par les fonctions [reserver] et [supprimer].
[Link]
229/315
On rappelle que les clients sont choisis de façon aléatoire.
• ligne 1 : on rappelle que le paramètre de la fonction [reserver] est le n° du créneau (attribut id) ;
• ligne 4 : un client est choisi de façon aléatoire dans la liste des clients définie en dur dans le code d'initialisation. On retient
de lui son identifiant [id] ;
• lignes 7-8 : l'attente de 3 secondes ;
• lignes 11-18 : ces lignes ne sont exécutées qu'à la fin des 3 secondes ;
• ligne 12 : l'URL du service de réservation [/ajouterRv]. Cette URL est particulière vis à vis de celles qu'on a rencontrées
jusqu'à maintenant. Elle est définie comme suit dans le service web :
• ligne 1 : l'URL n'a pas de paramètres et elle est demandée avec un POST ;
• ligne 2 : les paramètres postés le sont sous la forme d'un objet JSON. Celui-ci sera désérialisé dans le paramètre
[post] (@RequestBody) ;
[Link]
230/315
Nous avons vu un exemple de ce POST(page 74) :
• ligne 14 : on crée la valeur à poster sous la forme d'un objet JS. Angular le sérialisera en JSON lorsqu'il sera posté ;
• ligne 16 : l'appel HTTP est fait. La valeur à poster est le dernier paramètre de la fonction [[Link]]. Lorsque ce
paramètre est présent, fonction [[Link]] fait un POST au lieu d'un GET (revoir le code page 191, paragraphe
[Link]) ;
• ligne 18 : on retourne la promesse de l'appel HTTP ;
• lignes 23-29 : ne sont exécutées que lorsque l'appel HTTP a rendu sa réponse ;
• ligne 23 : le paramètre [result] est de la forme [err,data] ou [err,messages] où [err] est un code d'erreur ;
• lignes 23-26 : s'il y a eu des erreurs, on rend visible le message d'erreur ;
• ligne 28 : si la réservation s'est bien passée, on réaffiche le nouvel agenda ;
[Link]
231/315
3. if ([Link]()) {
4. // on fixe le header CORS
5. [Link]("Access-Control-Allow-Origin", "*");
6. // on autorise le header [authorization]
7. [Link]("Access-Control-Allow-Headers", "authorization");
8. }
9.
10. @RequestMapping(value = "/ajouterRv", method = [Link])
11. public void ajouterRv(HttpServletResponse response) {
12. sendOptions(response);
13. }
L'ajout est fait lignes 10-13. Les entêtes des lignes 2-8 seront envoyés pour l'URL [/ajouterRv] (ligne 10) et la méthode HTTP
[OPTIONS] (ligne 10).
Pour la méthode [POST] (ligne 1) et l'URL [/ajouterRv] (ligne 1), la méthode que nous venons d'ajouter dans
[RdvMedecinsCorsController] est appelée (ligne 4), renvoyant donc les mêmes entêtes HTTP que pour la méthode HTTP
[OPTIONS].
[Link] Tests
Faisons un premier test où nous réservons un créneau quelconque :
Comme toujours dans ces cas là, il faut regarder les logs de la console :
La méthode [[Link]] a échoué avec [status=0], ce qui signifie que c'est Angular qui a annulé la requête. On a la cause de
l'erreur dans les logs :
XMLHttpRequest cannot load [Link] Request header field Content-Type is not allowed by
Access-Control-Allow-Headers.
[Link]
232/315
1
2
La nouveauté est donc que sur une opération POST, le client Angular demande davantage d'autorisations au serveur. Il faut donc
modifier celui-ci pour qu'il les lui donne :
Dans la classe [RdvMedecinsCorsController], nous modifions la méthode privée qui génère les entêtes HTTP envoyés pour les
commandes OPTIONS, GET et POST :
• ligne 7 : on a ajouté une autorisation pour les entêtes HTTP [accept, content-type] ;
[Link]
233/315
• ligne 9 : on a ajouté une autorisation pour la méthode POST ;
[Link]
234/315
• ligne 1 : il faut se rappeler que le paramètre de la fonction est le n° du rendez-vous à supprimer. On a là un code très
similaire à celui de la réservation. Nous ne commentons que les différences ;
• ligne 9 : l'URL du service est ici [/supprimerRV] et là également elle est accédée via un POST :
Le paramètre posté est là encore transmis sous une forme JSON. A la page 99, nous avons montré la nature du POST
réalisé à la main :
[Link]
235/315
1. // envoi des options au client
2. private void sendOptions(HttpServletResponse response) {
3. if ([Link]()) {
4. // on fixe le header CORS
5. [Link]("Access-Control-Allow-Origin", "*");
6. // on autorise certains headers
7. [Link]("Access-Control-Allow-Headers", "accept, authorization, content-
type");
8. // on autorise le POST
9. [Link]("Access-Control-Allow-Methods", "POST");
10. }
11. }
12. ...
13. @RequestMapping(value = "/supprimerRv", method = [Link])
14. public void supprimerRv(HttpServletResponse response) {
15. sendOptions(response);
16. }
L'ajout est fait lignes 13-16. Les entêtes des lignes 2-10 seront envoyés pour l'URL [/supprimerRv] (ligne 13) et la méthode HTTP
[OPTIONS] (ligne 13).
Pour la méthode [POST] (ligne 1) et l'URL [/supprimerRv] (ligne 1), la méthode que nous venons d'ajouter dans
[RdvMedecinsCorsController] est appelée (ligne 4), renvoyant donc les mêmes entêtes HTTP que pour la méthode HTTP
[OPTIONS].
[Link]
236/315
1
Le code est similaire à celui de l'application précédente, aussi ne présentons-nous que les principales différences.
Nous dupliquons le fichier [[Link]] dans [[Link]] puis nous créons le code de la liste déroulante des clients [1] :
[Link] Le contrôleur C
Le code du contrôleur C évolue de la façon suivante :
[Link]
237/315
1. // contrôleur
2. [Link]("rdvmedecins")
3. .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter',
'$locale',
4. function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
5. // ------------------- initialisation modèle
6. ...
7. // les clients
8. $[Link] = {title: [Link], show: false, model: {}};
9.
10. //------------------------------------------- initilisation vue
11. // la tâche asynchrone globale
12. var task;
13. // on demande les clients puis l'agenda
14. getClients().then(function () {
15. getAgenda();
16. });
17. ...
18.
19. // exécution action
20. function getClients() {
21. ....
22. };
23. } ]);
• ligne 8 : l'objet [$[Link]] configure la liste déroulante des clients dans la vue V ;
• lignes 14-16 : de façon asynchrone, on demande d'abord la liste des clients, puis une fois celle-ci obtenue, on demande
l'agenda de Mme PELISSIER pour le jour d'aujourd'hui. La syntaxe utilisée ici ne fonctionne que parce que la fonction
[getClients] rend une promesse (promise) ;
1. function getClients() {
2. // on met à jour l'UI
3. $[Link] = true;
4. $[Link] = false;
5. $[Link] = false;
6. // on demande la liste des clients;
7. task = [Link]($[Link], $[Link], $[Link],
[Link]);
8. var promise = [Link];
9. // on analyse le résultat de l'appel précédent
10. promise = [Link](function (result) {
11. // result={err: 0, data: [client1, client2, ...]}
12. // result={err: n, messages: [msg1, msg2, ...]}
13. if ([Link] == 0) {
14. // on met les données acquises dans le modèle
15. $[Link] = [Link];
16. // on met à jour l'UI
17. $[Link] = true;
18. $[Link] = false;
19. } else {
20. // il y a eu des erreurs pour obtenir la liste des clients
21. $[Link] = { title: [Link], messages: [Link](result),
show: true, model: {}};
22. // on met à jour l'UI
23. $[Link] = false;
24. }
25. });
26. // on rend la promesse
27. return promise;
28. };
C'est un code que nous avons déjà rencontré et commenté. L'élément important à noter est ligne 31 :
• ligne 27 : on rend la promesse de la ligne 10, ç-à-d la dernière promesse obtenue dans le code. Cette promesse ne sera
obtenue que lorsque l'appel HTTP aura rendu sa réponse ;
[Link]
238/315
1. $[Link] = function (creneauId) {
2. [Link]("réservation du créneau", creneauId);
3. // on crée un RV pour le client sélectionné
4. var idClient = $(".selectpicker").selectpicker('val');
5. ...
6. });
• ligne 4 : on ne réserve plus pour un client aléatoire mais pour le client sélectionné dans la liste des clients.
[Link] La vue V
L'application affiche la vue suivante :
1. <div class="container">
2. <h1>Rdvmedecins - v1</h1>
3.
4. <!-- le message d'attente -->
5. <div class="alert alert-warning" ng-show="[Link]">
6. ...
7. </div>
8.
9. <!-- la liste d'erreurs -->
10. <div class="alert alert-danger" ng-show="[Link]">
11. ...
12. </div>
13.
14. <!-- la liste des clients -->
15. <div class="alert alert-info">
16. <div class="row" ng-show="[Link]">
17. <div class="col-md-4">
18. <h2 translate="{{[Link]}}"></h2>
19. <select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="[Link]">
20. <option ng-repeat="client in [Link]" value="{{[Link]}}">
21. {{[Link]}} {{[Link]}} {{[Link]}}
22. </option>
[Link]
239/315
23. </select>
24. </div>
25. </div>
26. </div>
27.
28. <!-- la liste des médecins -->
29. <div class="alert alert-info">
30. <div class="row" ng-show="[Link]">
31. <div class="col-md-4">
32. <h2 translate="{{[Link]}}"></h2>
33. <select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-
if="[Link]">
34. <option ng-repeat="medecin in [Link]" value="{{[Link]}}">
35. {{[Link]}} {{[Link]}} {{[Link]}}
36. </option>
37. </select>
38. </div>
39. </div>
40. </div>
41. </div>
42. ...
43. <script type="text/javascript" src="[Link]"></script>
44. <!-- directives -->
45. <script type="text/javascript" src="[Link]"></script>
• ligne 4 : on fait afficher la valeur du paramètres [attrs] afin de faire comprendre le fonctionnement du code. On va
découvrir que attrs['id']='selectpickerClients' pour la liste des clients ;
• ligne 6 : pour localiser dans le DOM un élément d'[id='x'], on écrit [$('#x')]. Donc on doit écrire [$('#selectpickerClients')]
pour localiser la liste des clients. Ceci est obtenu avec la syntaxe [$('#' + attrs['id'])] ;
La directive [selectEnable2] utilise donc l'information transportée par l'un des attributs de l'élement HTML auquel elle est
appliquée.
[Link] Le contrôleur C
Le contrôleur C se trouve dans le fichier JS [[Link]] et a la structure suivante :
1. // contrôleur
2. [Link]("rdvmedecins")
3. .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
4. function ($scope, utils, config, dao) {
[Link]
240/315
5. // ------------------- initialisation modèle
6. // le msg d'attente
7. $[Link] = {text: [Link], visible: false, cancel: cancel, time: 3000};
8. // les informations de connexion
9. $[Link] = {url: '[Link] login: 'admin', password: 'admin'};
10. // les erreurs
11. $[Link] = {show: false, model: {}};
12. // les médecins
13. $[Link] = {title: [Link], show: false, model: {}};
14. // les clients
15. $[Link] = {title: [Link], show: false, model: {}};
16.
17. // la tâche asynchrone globale
18. var task;
19. // ---------------------------------------------------- initialisation vue
20. // on met à jour l'UI
21. $[Link] = true;
22. $[Link] = false;
23. $[Link] = false;
24. $[Link] = false;
25. // on demande les clients puis les médecins
26. getClients().then(function () {
27. getMedecins();
28. });
29.
30. // liste des clients
31. function getClients() {
32. ...
33. }
34.
35. // liste des médecins
36. function getMedecins() {
37. ...
38. }
39.
40. // annulation attente
41. function cancel() {
42. ...
43. }
44. } ]);
[Link]
241/315
21. {{[Link]}} {{[Link]}} {{[Link]}}
22. </option>
23. </select>
24. </div>
25. </div>
26. </div>
Les lignes 14-26 sont identiques aux lignes 1-13. Elles s'appliquent à des médecins au lieu des clients. Nous voudrions pouvoir écrire
la chose suivante :
Ce code fait intervenir une nouvelle directive [list] que nous allons créer maintenant.
1. [Link]("rdvmedecins")
2. .directive("list", ['utils', '$timeout', function (utils, $timeout) {
3. // instance de la directive retournée
4. return {
5. // élément HTML
6. restrict: "E",
7. // url du fragment
8. templateUrl: "[Link]",
9. // scope unique à chaque instance de la directive
10. scope: true,
11. // fonction lien avec le document
12. link: function (scope, element, attrs) {
13. [Link]("directive list attrs", attrs);
14. [Link] = scope[attrs['model']];
15. [Link]("directive list model", [Link]);
16. $timeout(function () {
17. $('#' + [Link]).selectpicker();
18. })
19. }
20. }
21. }]);
Pour comprendre le code ci-dessus, il faut se rappeler l'utilisation qui va être faite de la directive :
La directive [list] est utilisée comme élément HTML <list>. Cet élément a deux attributs :
• [model] : qui va avoir pour valeur, l'élément du modèle M de la vue V dans laquelle se trouve la directive [list]. Cet élément
va alimenter le modèle de la directive ;
• [ng-if] : qui va faire en sorte que le code HTML de la directive ne soit pas généré s'il n'y a rien à visualiser ;
[Link]
242/315
3. [Link] = scope[attrs['model']];
4. [Link]("directive list model", [Link]);
5. $timeout(function () {
6. $('#' + [Link]).selectpicker();
7. })
8. }
Maintenant, regardons le code HTML généré par la directive. A cause de l'attribut [templateUrl: "[Link]"] de la directive, il faut le
chercher dans le fichier [[Link]] :
• la première chose qu'il faut se rappeler pour lire ce code est que la directive a créé un objet [[Link]] de la forme
[{id :'...', data:[client1, client2, ...], show : ..., title :'...'}]. Cet objet [model] (scope est implicite dans le code HTML) est
utilisé par le code HTML de la directive ;
• ligne 2 : utilisation de [[Link]] pour montrer / cacher la vue générée par la directive ;
• ligne 5 : utilisation de [[Link]] pour mettre un titre ;
• ligne 6 : utilisation de [[Link]] pour mettre un id à la balise <select>. Cet id est utilisé par le code JS de la directive ;
• ligne 6 : utilisation de [[Link]] pour générer le <select> uniquement s'il y a des données à afficher ;
• lignes 7-9 : utilisation de [[Link]] pour générer les éléments de la liste déroulante ;
1. <div class="container">
2. <h1>Rdvmedecins - v1</h1>
3.
4. <!-- le message d'attente -->
5. <div class="alert alert-warning" ng-show="[Link]">
6. ...
7. </div>
8.
9. <!-- la liste d'erreurs -->
10. <div class="alert alert-danger" ng-show="[Link]">
11. ...
12. </div>
13.
14. <!-- la liste des clients -->
15. <list model="clients" ng-if="[Link]"></list>
16. <!-- la liste des médecins -->
17. <list model="medecins" ng-if="[Link]"></list>
18. </div>
[Link]
243/315
19. ...
20. <script type="text/javascript" src="[Link]"></script>
21. <!-- directives -->
22. <script type="text/javascript" src="[Link]"></script>
[Link] Le contrôleur C
Le contrôleur C évolue très peu :
1. [Link]("rdvmedecins")
2. .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
3. function ($scope, utils, config, dao) {
4. // ------------------- initialisation modèle
5. ...
6. // les médecins
7. $[Link] = {title: [Link], show: false, id: 'medecins'};
8. // les clients
9. $[Link] = {title: [Link], show: false, id: 'clients'};
10. ...
• lignes 7 et 9, nous ajoutons l'attribut [id] aux modèles des médecins et des clients ;
[Link]
244/315
2
• en [2], on demande une seconde fois la liste des clients. Cette seconde liste est alors cumulée à la première [3]. C'est la mise
à jour du composant [Bootstrap select] qu'on veut étudier dans cet exemple.
1. <div class="container">
2. <h1>Rdvmedecins - v1</h1>
3.
4. <!-- le message d'attente -->
5. <div class="alert alert-warning" ng-show="[Link]">
6. ...
7. </div>
8.
9. <!-- la liste d'erreurs -->
10. <div class="alert alert-danger" ng-show="[Link]">
11. ...
12. </div>
13.
14. <!-- le bouton -->
15. <div class="alert alert-warning">
16. <button class="btn btn-primary" ng-click="getClients()">{{[Link]|translate}}</button>
17. </div>
18.
19. <!-- la liste des clients -->
20. <list2 model="clients" ng-if="[Link]"></list2>
21. </div>
22. ...
23. <script type="text/javascript" src="[Link]"></script>
24. <!-- directives -->
25. <script type="text/javascript" src="[Link]"></script>
[Link]
245/315
[Link] La directive [list2]
La directive [list2] dans [[Link]] est la suivante :
1. [Link]("rdvmedecins")
2. .directive("list2", ['utils', '$timeout', function (utils, $timeout) {
3. // instance de la directive retournée
4. return {
5. // élément HTML
6. restrict: "E",
7. // url du fragment
8. templateUrl: "[Link]",
9. // scope unique à chaque instance de la directive
10. scope: true,
11. // fonction lien avec le document
12. link: function (scope, element, attrs) {
13. [Link]('directive list2');
14. [Link] = scope[attrs['model']];
15. $timeout(function () {
16. $('#' + [Link]).selectpicker('refresh');
17. })
18. }
19. }
20. }]);
La seule différence avec la directive [list] est ligne 16 : avec la méthode [selectpicker('refresh')], on demande au composant [Bootstrap-
select] de se rafraîchir. L'idée derrière cela est qu'à chaque fois que l'utilisateur va demander une nouvelle liste de clients, on va
rafraîchir la liste déroulante. Ca ne va pas marcher mais c'est l'idée de base.
[Link] Le contrôleur C
Le contrôleur est dans le fichier [[Link]] obtenu par recopie du fichier [[Link]] :
1. // les clients
2. $[Link] = {title: [Link], show: false, id: 'clients', data: []};
3. ...
4. // liste des clients
5. $[Link] = function getClients() {
6. // on met à jour l'UI
7. $[Link] = true;
8. $[Link] = false;
9. // on demande la liste des clients;
10. task = [Link]($[Link], $[Link], $[Link],
[Link]);
11. var promise = [Link];
12. // on analyse le résultat de l'appel précédent
13. promise = [Link](function (result) {
14. // result={err: 0, data: [client1, client2, ...]}
15. // result={err: n, messages: [msg1, msg2, ...]}
16. if ([Link] == 0) {
17. // on met les données acquises dans un nouveau modèle pour forcer la vue à se rafraîchir
18. $[Link] = {title: $[Link], data: $[Link]([Link]),
show: $[Link], id: $[Link]};
19. // on met à jour l'UI
20. $[Link] = true;
21. $[Link] = false;
22. } else {
23. // il y a eu des erreurs pour obtenir la liste des clients
24. $[Link] = { title: [Link], messages: [Link](result), show:
true, model: {}};
25. // on met à jour l'UI
26. $[Link] = false;
27. }
28. });
29. }
• ligne 1 : afin de permettre la concaténation de tableaux dans [[Link]], cet objet est initialisé avec un tableau vide ;
• ligne 18 : on concatène la nouvelle liste de clients avec celles déjà présentes dans le tableau [[Link]] ;
[Link]
246/315
Avant on avait écrit :
Maintenant on écrit :
1. // on met les données acquises dans un nouveau modèle pour forcer la vue à se rafraîchir
2. $[Link] = {title: $[Link], data: $[Link]([Link]), show:
$[Link], id: $[Link]};
Pour comprendre ce code, il faut se rappeler comment le modèle M est utilisé dans la vue V dans le cas de la directive [list2] :
Le modèle utilisé par la directive [list2] est [clients]. Elle ne sera réévaluée dans la vue V, que si [clients] change dans le modèle M de
la vue. La première idée qui vient pour la modification est d'écrire :
$[Link]=$[Link]([Link]) ;
pour tenir compte du fait que la nouvelle liste de clients doit être ajoutée aux précédentes. Ce faisant, on modifie [[Link]] mais
pas [clients]. Je ne connais pas les arcanes de Javascript mais il ne serait pas étonnant que [clients] soit un pointeur, ainsi que
[[Link]]. Le pointeur [clients] ne change pas lorsqu'on change le pointeur [[Link]]. La directive [list2] n'est alors pas
réévaluée. C'est effectivement ce qu'on constate lorsqu'on débogue l'application (F12 dans Chrome).
En écrivant :
On s'assure que [$[Link]] reçoit bien une nouvelle valeur. Le pointeur [$[Link]] pointe sur un nouvel objet. La directive
[list2] devrait alors être réévaluée. Mais pourtant, on n'a pas le résultat cherché. Examinons les copies d'écran lorsqu'on demande
deux fois la liste des clients :
[Link]
247/315
3
• en [3], on retrouve les quatre clients dans une autre architecture HTML et c'est celle-ci que l'utilisateur voit lorsqu'il clique
sur la liste déroulante ;
1. [dao] init
2. [dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},
{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},
{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},
{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":
{"method":"GET","transformRequest":[null],"transformResponse":
[null],"timeout":1000,"url":"[Link]
text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
3. directive list2
4. [dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},
{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},
{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},
{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":
{"method":"GET","transformRequest":[null],"transformResponse":
[null],"timeout":1000,"url":"[Link]
text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
[Link]
248/315
• entre les lignes 1 et 2, elle n'est pas activée alors que la vue a été affichée une première fois. C'est dû à son attribut [ ng-
if="[Link]"] dans la vue V :
• ligne 3 : après l'obtention de la première liste de médecins, [[Link]] passe à true et la directive est activée ;
• après l'obtention de la seconde liste de clients, on voit que le code de la directive [list2] n'est pas appelé. C'est pourquoi, on
ne voit pas la seconde liste ;
1. [Link]("rdvmedecins")
2. .directive("list2", ['utils', '$timeout', function (utils, $timeout) {
3. // instance de la directive retournée
4. return {
5. // élément HTML
6. restrict: "E",
7. // url du fragment
8. templateUrl: "[Link]",
9. // scope unique à chaque instance de la directive
10. scope: true,
11. // fonction lien avec le document
12. link: function (scope, element, attrs) {
13. // à chaque fois que attrs["model"] change, le modèle de la directive doit changer également
14. scope.$watch(attrs["model"], function (newValue) {
15. [Link]("directive list2 newValue", newValue);
16. // on met à jour le modèle de la directive
17. [Link] = newValue;
18. $timeout(function () {
19. $('#' + [Link]).selectpicker('refresh');
20. })
21. });
22. }
23. }
24. }]);
• ligne 14 : la fonction [scope.$watch] permet d'observer une valeur du modèle. Sa syntaxe est [scope.$watch('var'), f] où
[var] est l'identifiant d'une variable du modèle et f la fonction à exécuter lorsque cette variable change de valeur. Ici, nous
voulons observer la variable [clients]. Donc on doit écrire [scope.$watch('clients')]. Comme on a attrs['model']='clients', on
écrit [scope.$watch(attrs["model"], function (newValue)] ;
• ligne 14 : le second paramètre de la fonction [scope.$watch] est la fonction à exécuter lorsque la variable observée change
de valeur. Le paramètre [newValue] est la nouvelle valeur de la variable, donc pour nous la nouvelle valeur de la variable
[clients] du modèle ;
• ligne 17 : cette nouvelle valeur est affectée au champ [model] du modèle de la directive ;
[Link]
249/315
Ci-dessus, on voit qu'après avoir obtenu la seconde liste de clients, la directive [list2] est bien exécutée de nouveau, ce que confirme
le résultat [2].
1. <div class="container">
2. <h1>Rdvmedecins - v1</h1>
3.
4. <!-- le message d'attente -->
5. <div class="alert alert-warning" ng-show="[Link]">
6. ...
7. </div>
8.
9. <!-- la liste d'erreurs -->
10. <div class="alert alert-danger" ng-show="[Link]">
11. ...
12. </div>
13.
14. <!-- le bouton -->
15. <div class="alert alert-warning">
16. <button class="btn btn-primary" ng-click="getClients()">{{[Link]|translate}}</button>
17. </div>
18.
19. <!-- la liste des clients -->
20. <list2 model="clients" ng-if="[Link]"></list2>
21. </div>
Nous décidons de mettre les codes HTML de ces deux messages dans des directives.
1. <div class="container">
2. <h1>Rdvmedecins - v1</h1>
3.
4. <!-- le message d'attente -->
5. <waiting model="waiting"></waiting>
6.
7. <!-- la liste d'erreurs -->
8. <errors model="errors"></errors>
9.
10. <!-- le bouton -->
11. <div class="alert alert-warning">
12. <button class="btn btn-primary" ng-click="getClients()">{{[Link]|translate}}</button>
13. </div>
14.
15. <!-- la liste des clients -->
16. <list2 model="clients" ng-if="[Link]"></list2>
17. </div>
18. ...
19. <script type="text/javascript" src="[Link]"></script>
20. <!-- directives -->
21. <script type="text/javascript" src="[Link]"></script>
22. <script type="text/javascript" src="[Link]"></script>
23. <script type="text/javascript" src="[Link]"></script>
[Link]
250/315
[Link] La directive [waiting]
Le code JS de la directive [waiting] est dans le fichier [[Link]] suivant :
1. [Link]("rdvmedecins")
2. .directive("waiting", ['utils', function (utils) {
3. // instance de la directive retournée
4. return {
5. // élément HTML
6. restrict: "E",
7. // url du fragment
8. templateUrl: "[Link]",
9. // scope unique à chaque instance de la directive
10. scope: true,
11. // fonction lien avec le document
12. link: function (scope, element, attrs) {
13. // à chaque fois que attr["model"] change, le modèle de la page doit changer également
14. scope.$watch(attrs["model"], function (newValue) {
15. [Link]("[waiting] watch newValue", newValue);
16. [Link] = newValue;
17. });
18. }
19. }
20. }]);
Ce code suit la même logique que celui de la directive [list2] déjà étudiée.
Dans le code JS de l'application, le modèle [$[Link]] de ce code HTML sera défini de la façon suivante :
// le msg d'attente
$[Link] = {title: {text: [Link], values: {}}, show: false, cancel: cancel, time: 3000};
1. [Link]("rdvmedecins")
2. .directive("errors", ['utils', function (utils) {
3. // instance de la directive retournée
4. return {
5. // élément HTML
6. restrict: "E",
7. // url du fragment
8. templateUrl: "[Link]",
9. // scope unique à chaque instance de la directive
10. scope: true,
11. // fonction lien avec le document
12. link: function (scope, element, attrs) {
13. // à chaque fois que attr["model"] change, le modèle de la page doit changer également
14. scope.$watch(attrs["model"], function (newValue) {
15. [Link]("[errors] watch newValue", newValue);
16. [Link] = newValue;
17. });
18. }
19. }
20. }]);
Ce code suit la même logique que celui de la directive [list2] déjà étudié.
[Link]
251/315
1. <div class="alert alert-danger" ng-show="[Link]">
2. {{[Link]|translate:[Link]}}
3. <ul>
4. <li ng-repeat="message in [Link]">{{message|translate}}</li>
5. </ul>
6. </div>
Dans le code JS de l'application, le modèle [$[Link]] de ce code HTML sera défini de la façon suivante :
1
5
4
3
2
8
6
• en [6], la vue n° 3 ;
• en [7], on passe à la page 1 ;
• en [8], on est revenu à la vue n° 1 ;
[Link]
252/315
[Link] Organisation du code
Nous commençons une nouvelle organisation du code :
1. <!DOCTYPE html>
2. <html ng-app="rdvmedecins">
3. <head>
4. ...
5. </head>
6. <body>
7. <div class="container" ng-controller="mainCtrl">
8. <!-- la barre de navigation -->
9. <ng-include src="'views/[Link]'"></ng-include>
10.
11. <!-- la vue courante -->
12. <ng-view></ng-view>
13. </div>
14.
15. ...
16. <!-- le module -->
17. <script type="text/javascript" src="modules/[Link]"></script>
18. <!-- les contrôleurs -->
19. <script type="text/javascript" src="controllers/[Link]"></script>
20. <script type="text/javascript" src="controllers/[Link]"></script>
21. <script type="text/javascript" src="controllers/[Link]"></script>
22. <script type="text/javascript" src="controllers/[Link]"></script>
23. </body>
24. </html>
[Link]
253/315
• ligne 12 : les différentes vues affichées par le conteneur le sont à l'intérieur de la directive [ng-view]. Au final, on a un
conteneur qui affiche :
◦ toujours la même barre de navigation (ligne 9) ;
◦ des vues différentes en ligne 12 ;
• lignes 16-22 : on importe les fichiers JS du module de l'application [[Link]] et de ses contrôleurs ;
• ligne 1 : on définit le module [rdvmedecins]. Il a une dépendance sur le module [ngRoute] fourni par la bibliothèque
[[Link]]. C'est ce module qui permet le routage défini aux lignes 6-24 ;
• ligne 4 : définit la fonction [config] du module [rdvmedecins]. On rappelle que cette fonction est exécutée avant toute
instanciation de service. C'est une fonction de configuration du module. Ici, c'est son routage qui est configuré. Ceci est
fait au moyen de l'objet [$routeProvider] fourni par le module [ngRoute] ;
• lignes 6-10 : définissent la vue à afficher lorsque l'utilisateur demande l'URL [/page1]. C'est un routage interne à
l'application. L'URL est en fait [/rdvmedecins-angular-v1/[Link]#/page1]. On voit que c'est toujours l'URL du
conteneur [/rdvmedecins-angular-v1/[Link]] qui est utilisée mais avec une information supplémentaire derrière un
caractère #. C'est cette information supplémentaire que le routage Angular gère ;
• ligne 8 : indique le fragment HTML à insérer dans la directive [ng-view] du conteneur :
• ligne 9 : indique le nom du contrôleur de ce fragment ;
• lignes 11-15 : définissent la vue à afficher lorsque l'utilisateur demande l'URL [/page2] ;
• lignes 16-20 : définissent la vue à afficher lorsque l'utilisateur demande l'URL [/page3] ;
• lignes 21-24 : définissent le routage à exercer lorsque l'URL demandée n'est pas l'une des trois précédentes (otherwise,
ligne 21) ;
• ligne 23 : redirection vers l'URL [/page1], donc vers la vue définie aux lignes 6-10 ;
1. // contrôleur
2. [Link]("rdvmedecins")
3. .controller('mainCtrl', ['$scope', '$location',
4. function ($scope, $location) {
[Link]
254/315
5.
6. // modèles des pages
7. $scope.page1 = {};
8. $scope.page2 = {};
9. $scope.page3 = {};
10. // modèle global
11. var main = $[Link] = {};
12. [Link] = "[Modèle global]";
13.
14. // méthodes exposées à la vue
15. main.showPage1 = function () {
16. $[Link]("/page1");
17. };
18. main.showPage2 = function () {
19. $[Link]("/page2");
20. };
21. main.showPage3 = function () {
22. $[Link]("/page3");
23. }
24. }]);
• ligne 3 : le contrôleur [mainCtrl] a besoin de l'objet [$location] fourni par le module de routage [ngRoute]. Cet objet
permet de changer de vue (lignes 16, 19, 22) ;
Il y a héritage des modèles. Dans la vue affichée ligne 6, les modèles des contrôleurs [mainCtrl] et [pagexCtrl] sont
visibles tous les deux. Si deux variables de ces modèles portent le même nom, l'une va cacher l'autre. Pour éviter cette
collision des noms, nous créons quatre modèles sous quatre noms :
Les lignes 7-11 ont une conséquence très particulière : elles définissent le [$scope] du contrôleur [mainCtrl] et dans celui-ci, elles
créent quatre variables [main, page1, page2, page3]. Ces quatre variables vont être utilisées comme modèles respectifs du conteneur
et des trois vues qu'il va contenir tour à tour.
[Link]
255/315
4.
5. <!-- la vue courante -->
6. <ng-view></ng-view>
7. </div>
La barre de navigation est définie ligne 3. Cela signifie qu'elle ne connaît que le modèle [main]. Son code est le suivant :
• aux lignes 16, 21, 26, ce sont des méthodes du modèle [main] qui sont utilisées ;
• ligne 16 : un clic sur le lien [Page1] va lancer l'exécution de la méthode [$[Link].showPage1]. Celle-ci est définie
dans le contrôleur [mainCtrl] de la façon suivante :
1. // modèle global
2. var main = $[Link] = {};
3. [Link] = "[Modèle global]";
4.
5. // méthodes exposées à la vue
6. main.showPage1 = function () {
7. $[Link]("/page1");
8. };
• ligne 6 : du code qui précède, on voit que la méthode [main.showPage1] est en réalité la méthode
[$[Link].showPage1]. C'est donc bien celle-ci qui va s'exécuter ;
• ligne 7 : on change l'URL de l'application qui devient [/page1]. Revenons au routage qui a été défini dans le module
principal :
1. $[Link]("/page1",
2. {
3. templateUrl: "views/[Link]",
4. controller: 'page1Ctrl'
5. });
on voit que le fragment [views/[Link]] va être inséré dans le conteneur et que son contrôleur est [page1Ctrl].
[Link]
256/315
[Link] La vue [/page1] et son contrôleur
Le fragment [views/[Link]] est le suivant :
1. <h1>Page 1</h1>
2. <div class="alert alert-info">
3. <ul>
4. <li>Modèle global : {{[Link]}}</li>
5. <li>Modèle local : {{[Link]}}</li>
6. </ul>
7. </div>
On se rappelle que dans la vue insérée dans le conteneur, le modèle [main] est visible. C'est ce qu'on veut vérifier ligne 4. Par
ailleurs, le contrôleur [page1Ctrl] du fragment [views/[Link]] définit un modèle [page1]. C'est lui qui est utilisé ligne 5.
1. [Link]("rdvmedecins")
2. .controller('page1Ctrl', ['$scope',
3. function ($scope) {
4.
5. // modèle de la page 1
6. var page1=$scope.page1;
7. [Link]="[Modèle local dans page 1]";
8. }]);
• ligne 2 : le [$scope] injecté ici n'est pas vide. Puisque le contrôleur [page1Ctrl] contrôle une zone insérée dans un
conteneur contrôlé par [mainCtrl], le [$scope] de la ligne 2 contient les éléments du [$scope] défini par le contrôleur
[mainCtrl]. Il est important de le comprendre. Le [$scope] défini par le contrôleur [mainCtrl] contient les éléments suivants
[main, page1, page2, page3]. Cela signifie qu'on a accès aux modèles de toutes les vues. Ce n'est pas forcément
désirable mais c'est le cas ici. Dans la version finale du client Angular, nous utiliserons cette particularité pour stocker dans
le modèle [main] les informations qui doivent être partagées entre vues. On aura là, un concept analogue au concept de
'session' côté serveur ;
• ligne 6 : on récupère dans le [$scope] le modèle [page1] de la page 1 et ensuite on travaille avec (ligne 7). On obtient alors
l'affichage suivant :
Les vues [/page2] et [/page3] sont construits sur le même modèle que la vue [/page1] (voir les copies d'écran page 252).
Pour obtenir ce résultat, nous modifions les contrôleurs des pages de la façon suivante :
[Link]
257/315
1. [Link]("rdvmedecins")
2. .controller('page1Ctrl', ['$scope', '$location',
3. function ($scope, $location) {
4. // navigation autorisée ?
5. var main = $[Link];
6. if ([Link] && [Link] != '/page3') {
7. // on revient à la dernière URL
8. $[Link]([Link]);
9. return;
10. }
11. // on mémorise l'URL de la page
12. [Link] = '/page1';
13. // modèle de la page
14. var page1 = $scope.page1;
15. [Link] = "[Modèle local dans page 1]";
16. }]);
• ligne 12 : lorsqu'une page sera affichée, on mémorisera son URL dans le modèle [[Link]]. Nous utilisons ici le
concept dont nous avons parlé précédemment : utiliser le modèle [main] pour stocker des informations partagées par
toutes les vues. Ici, c'est la dernière URL consultée ;
• le code des lignes 4-12 est dupliqué et adapté aux trois vues. Ici on est dans la vue [/page1] ;
• ligne 5 : on récupère le modèle [main] ;
• ligne 6 : si le modèle [[Link]] existe et s'il est différent de [/page3] alors la navigation est interdite (la dernière URL
visitée existe et n'est pas /page3) ;
• ligne 8 : on revient alors sur la dernière URL visitée ;
Faisons un essai :
3
2
3.7.16 Conclusion
Nous avons balayé tous les cas d'utilisation que nous allons rencontrer dans la version finale du client Angular. Lorsque nous allons
présenter celui-ci, nous commenterons davantage les fonctionnalités de l'application que ses détails d'implémentation. Pour ces
derniers, nous nous contenterons de faire référence à l'exemple illustrant le cas d'utilisation alors étudié.
[Link]
258/315
3.8 Le client final Angular
1
2
4 5
[Link]
259/315
Le rôle de ces différents éléments a été expliqué au paragraphe 3.4, page 140.
1. <!DOCTYPE html>
2. <html ng-app="rdvmedecins">
3. <head>
4. <title>RdvMedecins</title>
5. <!-- META -->
6. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
7. <meta name="viewport" content="width=device-width, initial-scale=1.0">
8. <meta name="description" content="Angular client for RdvMedecins">
9. <meta name="author" content="Serge Tahé">
10. <!-- le CSS -->
11. <link rel="stylesheet" href="bower_components/bootstrap/dist/css/[Link]"/>
12. <link href="bower_components/bootstrap/dist/css/[Link]" rel="stylesheet"/>
13. <link href="bower_components/bootstrap-select/[Link]" rel="stylesheet"/>
14. <link href="assets/css/[Link]" rel="stylesheet"/>
15. <link href="assets/css/[Link]" rel="stylesheet"/>
16. </head>
17. <!-- contrôleur [appCtrl], modèle [app] -->
18. <body ng-controller="appCtrl">
19. <div class="container">
20. ...
21. </div>
22. <!-- Bootstrap core JavaScript ================================================== -->
23. <script type="text/javascript" src="bower_components/jquery/dist/[Link]"></script>
24. <script type="text/javascript" src="bower_components/bootstrap/dist/js/[Link]"></script>
25. <script type="text/javascript" src="bower_components/bootstrap-select/[Link]"></script>
26. <script src="bower_components/footable/js/[Link]" type="text/javascript"></script>
27. <!-- angular js -->
28. <script type="text/javascript" src="bower_components/angular/[Link]"></script>
29. <script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-
[Link]"></script>
30. <script type="text/javascript" src="bower_components/angular-route/[Link]"></script>
31. <script type="text/javascript" src="bower_components/angular-translate/[Link]"></script>
32. <script type="text/javascript" src="bower_components/angular-base64/[Link]"></script>
33. <!-- modules -->
34. <script type="text/javascript" src="modules/[Link]"></script>
35. <!-- services -->
36. <script type="text/javascript" src="services/[Link]"></script>
37. <script type="text/javascript" src="services/[Link]"></script>
38. <script type="text/javascript" src="services/[Link]"></script>
39. <!-- directives -->
40. <script type="text/javascript" src="directives/[Link]"></script>
41. <script type="text/javascript" src="directives/[Link]"></script>
42. <script type="text/javascript" src="directives/[Link]"></script>
43. <script type="text/javascript" src="directives/[Link]"></script>
44. <script type="text/javascript" src="directives/[Link]"></script>
45. <!-- controllers -->
[Link]
260/315
46. <script type="text/javascript" src="controllers/[Link]"></script>
47. <script type="text/javascript" src="controllers/[Link]"></script>
48. <script type="text/javascript" src="controllers/[Link]"></script>
49. <script type="text/javascript" src="controllers/[Link]"></script>
50. <script type="text/javascript" src="controllers/[Link]"></script>
51. </body>
52. </html>
1. <div class="container">
2. <!-- les barres de navigation -->
3. <ng-include src="'views/[Link]'" ng-show="[Link]"></ng-include>
4. <ng-include src="'views/[Link]'" ng-show="[Link]"></ng-include>
5. <!-- le jumbotron -->
6. <ng-include src="'views/[Link]'"></ng-include>
7. <!-- le titre de la page -->
8. <div class="alert alert-info" ng-show="[Link]" translate="{{[Link]}}"
9. translate-values="{{[Link]}}"></div>
10. <!-- les erreurs de la page -->
11. <errors model="[Link]" ng-show="[Link]"></errors>
12. <!-- le message d'attente -->
13. <waiting model="[Link]" ng-show="[Link]"></waiting>
14. <!-- la vue courante -->
15. <ng-view></ng-view>
16. <!-- debug -->
17. <debug model="app" ng-show="[Link]"></debug>
18. </div>
Quelque soit la vue affichée, elle aura toujours les éléments suivants :
• lignes 3-4 : une barre de commande. Les deux barres des lignes 3 et 4 sont exclusives l'une de l'autre ;
• ligne 8 : un titre
[Link]
261/315
• ligne 11 : un message d'erreurs :
Tous les éléments précédents sont contrôlés par une directive [ng-show / ng-hide] qui fait que s'ils sont bien présents, ils ne sont pas
toujours visibles.
1. <div class="container">
2. ...
3. <!-- la vue courante -->
4. <ng-view></ng-view>
5. ...
6. </div>
[Link]
262/315
La ligne 4 reçoit les différentes vues de l'application. Celles-ci sont définies dans le module [[Link]] :
Le rôle de la configuration des différentes routes a été expliqué au paragraphe [Link], page 254.
La vue [[Link]] est vide, ç-à-d qu'elle ne rajoute aucun élément à ceux déjà présents dans la page maître.
[Link]
263/315
La vue [[Link]] rajoute l'élément suivant à la page maître :
[Link]
264/315
7 9
10 11 12 13 14
• en [6], la page d'entrée de l'application. Il s'agit d'une application de prise de rendez-vous pour des médecins ;
• en [7], une case à cocher qui permet d'être ou non en mode [debug]. Ce dernier se caractérise par la présence du cadre [8]
qui affiche le modèle de la vue courante ;
• en [9], une durée d'attente artificielle en millisecondes. Elle vaut 0 par défaut (pas d'attente). Si N est la valeur de ce temps
d'attente, toute action de l'utilisateur sera exécutée après un temps d'attente de N millisecondes. Cela permet de voir la
gestion de l'attente mise en place par l'application ;
• en [10], l'URL du serveur Spring 4. Si on suit ce qui a précédé, c'est [[Link]
• en [11] et [12], l'identifiant et le mot de passe de celui qui veut utiliser l'application. Il y a deux utilisateurs : admin/admin
(login/password) avec un rôle (ADMIN) et user/user avec un rôle (USER). Seul le rôle ADMIN a le droit d'utiliser
l'application. Le rôle USER n'est là que pour montrer ce que répond le serveur dans ce cas d'utilisation ;
• en [13], le bouton qui permet de se connecter au serveur ;
• en [14], la langue de l'application. Il y en a deux : le français par défaut et l'anglais.
• en [1], on se connecte ;
[Link]
265/315
4
2 3
• une fois connecté, on peut choisir le médecin avec lequel on veut un rendez-vous [2] et le jour de celui-ci [3] ;
• on demande en [4] à voir l'agenda du médecin choisi pour le jour choisi ;
[Link]
266/315
5
[Link]
267/315
7
Une fois le rendez-vous validé, on est ramené automatiquement à l'agenda où le nouveau rendez-vous est désormais inscrit. Ce
rendez-vous pourra être ultérieurement supprimé [7].
Les principales fonctionnalités ont été décrites. Elles sont simples. Celles qui n'ont pas été décrites sont des fonctions de navigation
pour revenir à une vue précédente. Terminons par la gestion de la langue :
[Link]
268/315
• en [1], on passe du français à l'anglais ;
[Link]
269/315
3.8.7 Le contrôleur de la page maître
Rappelons le code HTML de la page maître [[Link]] :
1. <body ng-controller="appCtrl">
2. <div class="container">
3. ...
Ligne 1, tout le corps (body) de la page maître est contrôlé par le contrôleur [appCtrl]. De par sa position, cela en fait un contrôleur
général et principal de l'application. Comme il a été expliqué au paragraphe 3.7.15, page 252, le modèle construit par ce contrôleur
est hérité par toutes les vues qui viendront s'insérer dans la page maître.
1. [Link]("rdvmedecins")
2. .controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
3. function ($scope, config, utils, $location, $locale) {
4.
5. // debug
6. [Link]("[app] init");
7.
8. // ----------------------------------------initialisation page
9. // les modèles des # pages
10. $[Link] = {waitingTimeBeforeTask: [Link]};
11. $[Link] = {};
12. $[Link] = {};
13. $[Link] = {};
14. $[Link] = {};
15. // modèle de la page courante
16. var app = $[Link];
17. ...
18.
19. // ---------------------------------- méthodes
20.
21. // annulation tâche courante
22. [Link] = function () {
23. ...
24. };
25.
26. // déconnexion
27. [Link] = function () {
28. ...
29. };
30.
31. // ce code doit rester là car il référence la fonction [cancel] qui précède
32. [Link] = {title: {text: [Link], values: {}}, cancel: [Link], show: true};
33. }])
34. ;
Les lignes 10-14 définissent les cinq modèles qui sont utilisés dans l'application :
Ce qu'il est important de comprendre est que l'objet [$scope] étant le modèle du contrôleur de la page maître, est hérité par toutes
les vues et contrôleurs. Ainsi le contrôleur [loginCtrl] a accès aux éléments [$[Link], $[Link], $[Link], $[Link],
$[Link]]. Dit autrement un contrôleur a accès aux modèles des autres contrôleurs. L'application étudiée évite
soigneusement d'utiliser cette possibilité. Ainsi, par exemple, le contrôleur [loginCtrl] travaille avec deux modèles seulement :
• le sien [$[Link]] ;
• et celui du contrôleur parent [$[Link]] ;
Il en est de même pour tous les autres contrôleurs. Le modèle [$[Link]] sera utilisé comme mémoire partagée entre les différents
contrôleurs. Lorsqu'un contrôleur C1 devra transmettre de l'information au contrôleur C2, on procèdera ainsi :
[Link]
270/315
Dans [C1] :
$[Link]=value ;
Dans [C2] :
var value=$[Link] ;
Dans les deux cas, $scope est hérité du contrôleur [appCtrl] et est donc identique (c'est un pointeur) dans [C1] et [C2]. L'objet
[$[Link]] qui sert de mémoire partagée entre les contrôleurs sera souvent appelé session dans les commentaires, par mimétisme
avec la session utilisée dans les applications web classiques qui désigne la mémoire partagée entre requêtes HTTP successives.
• ligne 8 : [$[Link]] sera le modèle de la page maître. Ce sera aussi la mémoire partagée entre les différents contrôleurs.
Plutôt que d'écrire partout [$[Link]=value], le pointeur [$[Link]] est affecté à la variable [app] et on écrira
alors [[Link]=value]. Il faut simplement se souvenir que [app] est le modèle exposé à la page maître ;
• ligne 11 : [[Link]] est un booléen qui contrôle le mode debug de l'application. Par défaut il est à true. Sa valeur est liée
à la case à cocher [debug] des barres de navigation ;
• ligne 15 : [[Link]] contrôle l'affichage de la barre de navigation suivante :
[Link]
271/315
• ligne 22 : [[Link]] contiendra des informations sur la vue courante, celle qui est actuellement affichée par la balise [ng-
view] de la page maître. Nous y noterons les information suivantes :
◦ [url] : l'URL de la vue courante, par exemple [/agenda] ;
◦ [model] : le modèle de la vue courante, par exemple [$[Link]] ;
◦ [done] : à vrai, indique que la vue courante a terminé son travail et qu'on est en train de passer à une autre vue ;
• ligne 24 : lance une tâche asynchrone, une attente simulée. La tâche asynchrone est référencée par deux pointeurs
[[Link]] et [[Link]] ;
• ligne 2 : la fonction [[Link]] sert à annuler la tâche courante pour laquelle un message d'attente est actuellement
affiché. Toutes les vues offrent ce message et donc l'annulation de la tâche se fera ici ;
• ligne 7 : la fonction [[Link]] ramène l'utilisateur à la page d'authentification. Toutes les vues, sauf la vue
[/login] offrent cette possibilité ;
1. // déconnexion
2. [Link] = function () {
3. // on revient à la page de login
4. $[Link]([Link]);
5. };
La tâche en cours d'exécution est annulée avec le code suivant dans le contrôleur [appCtrl] :
• ligne 5 : la tâche est cherchée dans [[Link]]. Aussi, tous les contrôleurs feront en sorte que leurs tâches
asynchrones soient référencées par cet objet ;
[Link]
272/315
• ligne 6 : pour indiquer que la tâche est finie ;
• ligne 7 : pour terminer la tâche avec un échec. Cette notation est différente de celle utilisée dans les exemples Angular
étudiés :
◦ dans les exemples, l'objet [task] était un objet [$[Link]()] qu'on pouvait terminer ;
◦ dans la version finale, l'objet [task] est un objet avec les champs [action, isFinished] où [action] est l'objet [$[Link]()]
qu'on peut terminer et [isFinished] un booléen qui indique que l'action est terminée ;
Examinons le cycle de vie de l'objet [task] sur un exemple. Au démarrage, après le contrôleur [appCtrl], c'est le contrôleur
[loginCtrl] qui prend la main pour afficher la vue [views/[Link]]. Son code d'initilisation est le suivant :
Ligne 5, on a [model=login]. Ceci signifie que lorsqu'on modifie l'objet [login], on modifie l'objet [[Link]] donc
[$[Link]]. Lorsque dans le contrôleur [loginCtrl], on veut faire une attente simulée, on écrit :
// attente simulée
var task = [Link] = {action: [Link]([Link]), isFinished: false};
En ajoutant le champ [task] à l'objet [login], c'est donc à l'objet [$[Link]] qu'il a été ajouté. Si l'utilisateur annule
l'attente, le code dans [[Link]] :
Pour [agendaCtrl] :
[Link]
273/315
• lignes 11-20 : implémentation de la règle de navigation ;
• ligne 26 : nouvelle vue courante ;
Pour [resaCtrl] :
Pour [loginCtrl] :
• il n'y a ici aucun contrôle de navigation puisque la règle dit qu'on peut venir à l'URL [/login] de n'importe où. Donc si
l'utilisateur tape cette URL dans son navigateur, cela marchera quelque soit la vue courante du moment ;
• ligne 16 : la nouvelle vue courante ;
[Link]
274/315
Le code pour le contrôleur [homeCtrl] est le suivant :
/agenda /home oui si le contrôleur [homeCtrl] a indiqué qu'il avait fini son travail
voici un exemple de code qui fait passer de l'URL [/home] à l'URL [/agenda] :
Ci-dessus, on est dans la méthode [afficherAgenda] du contrôleur [homeCtrl]. L'utilisateur a demandé l'agenda d'un médecin.
[Link]
275/315
• ligne 113 : le résultat [[Link]] est mis dans le modèle [app] ;
• ligne 116 : le contrôleur [homeCtrl] va passer la main au contrôleur [agendaCtrl]. Il lui indique qu'il a terminé son travail
avec le code de la ligne 115. Ce code va être exploité par le contrôleur [agendaCtrl] de la façon suivante :
Les services [config, utils, dao] sont ceux déjà décrits lors de la présentation d'Angular :
• le service [config] a été introduit au paragraphe 3.7.4, page 173 ;
• le service [utils] a été introduit au paragraphe 3.7.5, page 177 ;
• le service [dao] a été introduit au paragraphe 3.7.6, page 184 ;
Service [config]
• en [1] : on voit que le code fait environ 250 lignes. L'essentiel de ce code est l'externalisation des clés des messages
internatiomalisés [2]. On évite de mettre ces clés en dur dans le code ;
[Link]
276/315
Service [utils]
• ligne 8 : nous n'avions pas encore rencontré la variable [verbose]. Elle contrôle la fonction [debug] de la façon suivante :
• lignes 22-25 : la fonction [[Link]] ne fait rien si [[Link]] est évalué à false. Cette variable est liée à une variable du
contrôleur [appCtrl] :
• ligne 21 : [[Link]] prend la valeur du pointeur [[Link]]. Donc toute modification faite sur [[Link]] sera faite
également sur [[Link]] ;
• ligne 22 : la valeur initiale de [[Link]] est prise dans le fichier de configuration. Par défaut, c'est la valeur true. Cette
valeur peut changer dans le temps. L'utilisateur a en effet la possibilité de la changer dans les barres de navigation :
[Link]
277/315
• ligne 45 : une case à cocher (type=checkbox) permet de changer la valeur de [[Link]] (attribut ng-model) ;
Service [dao]
Les directives [errors, footable, list, waiting] sont celles déjà décrites lors de la présentation d'Angular :
[Link]
278/315
• la directive [footable] a été introduite page 224;
• la directive [list] a été introduite au paragraphe 3.7.12, page 241 ;
• les directives [errors] et [waiting] ont été introduites au paragraphe 3.7.14, page 250 ;
• ligne 2 : la directive [debug] affiche son modèle au format JSON dans un bandeau Bootstrap (ligne 1) ;
• la directive [debug] est utilisée ligne 35. Elle affiche donc la forme JSON du modèle [$[Link]] lorsqu'on est en mode
debug (attribut ng-show). Cela donne des choses comme celle-ci :
[Link]
279/315
Cela nécessite une bonne connaissance du code pour être interprété mais lorsque celle-ci est acquise, l'information ci-dessus devient
utile pour le débogage. On a surligné ici les éléments du modèle [$[Link]] affiché. On rappelle que [$[Link]] est la mémoire
partagée par les contrôleurs ;
[Link]
280/315
Les options de menu sont aux lignes 16, 23, 29 et 36.
Le contrôleur [loginCtrl] est associé à la vue [views/[Link]] qui associée à la page maître produit la page suivante :
[Link]
281/315
Le contrôleur [loginCtrl] est le suivant :
Ce code d'initialisation se retrouvera dans chaque contrôleur. Pour le contrôleur C1 d'une vue V1 ayant le modèle M1 on aura le
code d'initialisation suivant :
1. var app=$[Link];
2. var M1=$scope.M1;
3. [Link]={url: config.urlV1, model:M1, done:false};
• ligne 18 : on se rappelle peut-être que [appCtrl] a lancé une attente simulée référencée par l'objet [[Link]]. On
utilise la [promise] de cette tâche pour attendre sa fin ;
• ligne 39 : la méthode [[Link]] gère le changement de langues ;
• ligne 47 : la méthode [[Link]] gère l'authentification de l'utilisateur ;
[Link]
282/315
• lignes 50-51 : [[Link]] est le modèle du bandeau d'attente ;
• ligne 53 : [[Link]] est le modèle du bandeau d'erreurs ;
• ligne 55 : une attente simulée est lancée. L'objet [action, isFinished] est référencé par [[Link]] et donc puisque
[[Link]=login], par [[Link]]. On rappelle que c'est la condition pour que la tâche puisse être
annulée ;
• ligne 57 : après la fin de l'attente simulée, on charge les médecins ;
• ligne 62 : lorsque la demande des médecins a été obtenue, on analyse cette demande. Si les médecins ont été obtenus, on
demande alors les clients ;
• ligne 83 : on analyse la réponse obtenue et on affiche la vue finale. Cela se fait avec le code suivant :
• ligne 87 : le booléen [[Link]] est positionné à true dans les cas suivants :
◦ l'utilisateur a annulé l'attente ;
◦ la demande des médecins s'est terminée avec une erreur ;
• lignes 91-98 : le cas où on a eu les clients ;
• ligne 93 : [[Link]] est le modèle de la directive [list] qui va afficher les clients dans une liste déroulante ;
• lignes 97-98 : on se prépare à changer de vue (ligne 98) mais auparavant on indique que le contrôleur a terminé son travail
(ligne 97). On rappelle que [$[Link]] est utilisé pour le contrôle de navigation ;
Le point important à noter ici est que les médecins et les clients ont été mis en cache sur le navigateur. Ils ne seront désormais plus
demandés au service web.
[Link]
283/315
3.8.13 Le contrôleur [homeCtrl]
Le contrôleur [homeCtrl] est associé à la vue [views/[Link]] qui associée à la page maître produit la page suivante :
[Link]
284/315
• lignes 12-20 : c'est le contrôle de navigation. Tous les contrôleurs l'ont sauf [loginCtrl] car la page [/[Link]] est
accessible sans conditions ;
• lignes 25-28 : on retrouve là des lignes analogues à celles rencontrées dans le contrôleur [loginCtrl]. [home] est ainsi le
modèle de la vue associée au contrôleur ;
• ligne 33 : un attribut que nous n'avions pas encore rencontré. C'est le modèle du bandeau de titre de la vue :
L'affichage de l'agenda (ligne 51) a été traité au paragraphe 3.7.8, page 216.
[Link]
285/315
3.8.14 Le contrôleur [agendaCtrl]
Le contrôleur [agendaCtrl] est associé à la vue [views/[Link]] qui associée à la page maître produit la page suivante :
[Link]
286/315
• lignes 23-26 : [agenda] sera le modèle de la vue associée au contrôleur [agendaCtrl] ;
• lignes 36-44 : [[Link]] est le modèle du bandeau de titre suivant :
[Link]
287/315
La méthode [[Link]] est la suivante :
Le contrôleur [resaCtrl] est associé à la vue [views/[Link]] qui associée à la page maître produit la page suivante :
[Link]
288/315
La structure du contrôleur [resaCtrl] est la suivante :
[Link]
289/315
• lignes 24-27 : [resa] sera le modèle de la vue courante ;
• lignes 38-45 : [[Link]] est le modèle du bandeau de titre suivant :
[Link]
290/315
4 Exploitation de l'application
Nous souhaitons maintenant exploiter l'application en-dehors des IDE STS (pour le serveur) et Webstorm (pour le client).
1. <modelVersion>4.0.0</modelVersion>
2. <groupId>[Link]</groupId>
3. <artifactId>rdvmedecins-webapi-v4</artifactId>
4. <version>0.0.1-SNAPSHOT</version>
5. <packaging>war</packaging>
6.
7. <name>rdvmedecins-webapi-v3</name>
8. <description>Gestion de RV Médecins</description>
9. <parent>
10. <groupId>[Link]</groupId>
11. <artifactId>spring-boot-starter-parent</artifactId>
12. <version>[Link]</version>
13. </parent>
14. <dependencies>
15. <dependency>
16. <groupId>[Link]</groupId>
17. <artifactId>spring-boot-starter-web</artifactId>
18. </dependency>
19. <dependency>
20. <groupId>[Link]</groupId>
21. <artifactId>spring-boot-starter-security</artifactId>
22. </dependency>
23. <dependency>
24. <groupId>[Link]</groupId>
25. <artifactId>spring-boot-starter-tomcat</artifactId>
26. <scope>provided</scope>
27. </dependency>
28. <dependency>
29. <groupId>[Link]</groupId>
30. <artifactId>rdvmedecins-metier-dao-v2</artifactId>
31. <version>0.0.1-SNAPSHOT</version>
32. </dependency>
33. </dependencies>
• ligne 5 : il faut indiquer qu'on va générer une archive war (Web ARchive) ;
• lignes 23-27 : il faut ajouter une dépendance sur l'artifact [spring-boot-starter-tomcat]. Cet artifact amène toutes les classes
de Tomcat dans les dépendances du projet ;
• ligne 26 : cet artifact est [provided], ç-à-d que les archives correspondantes ne seront pas placées dans le war généré. En
effet, ces archives seront trouvées sur le serveur Tomcat sur lequel s'exécutera l'application ;
Il faut par ailleurs configurer l'application web. En l'absence de fichier [[Link]], cela se fait avec une classe héritant de
[SpringBootServletInitializer] :
[Link]
291/315
La classe [ApplicationInitializer] est la suivante :
1. package [Link];
2.
3. import [Link];
4. import [Link];
5.
6. public class ApplicationInitializer extends SpringBootServletInitializer {
7. @Override
8. protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
9. return [Link]([Link]);
10. }
11.
12. }
Ceci fait, il peut être nécessaire de mettre à jour le projet Maven (j'ai du le faire) : [clic droit sur projet / Maven / Update project] ou
[Alt-F5].
• en [1], on exécute le projet sur l'un des serveurs enregistrés dans l'IDE Eclipse ;
• en [2], on choisit [tc Server Developer] qui est présent par défaut. C'est une variante de Tomcat ;
[Link]
292/315
C'est normal. Rappelons que le service web n'a pas l'URL [/] dans ses méthodes. Lorsqu'on essaie l'URL [/getAllMedecins], on a la
réponse suivante :
En [1], on met l'URL du nouveau service web [[Link] On obtient le résultat suivant :
[Link]
293/315
Pour exécuter l'application en-dehors de l'IDE STS, il existe diverses solutions. En voici une.
On choisit en [1] une version zippée qu'on dézippe en [2]. On revient dans STS :
[Link]
294/315
3 4
• dans l'onglet [Servers], on clique droit sur l'application [rdvmedecins-webapi-v4] et on sélectionne l'option [Browse
Deployment Location] ;
• en [4] : on copie le dossier [rdvmedecins-webapi-v4] ;
En [1], on met l'URL du nouveau service web [[Link] On obtient le résultat suivant :
[Link]
295/315
4.2 Déploiement du client Angular sur le serveur Tomcat
Maintenant que le service web a été déployé sur Tomcat, nous allons maintenant déployer le client Angular sur un serveur lui aussi.
Ce peut très bien être le serveur qui héberge déjà le service web. Nous prenons cette voie.
Tout d'abord nous dupliquons le client [rdvmedecins-angular-v2] dans [rdvmedecins-angular-v3] et nous faisons les modifications
suivantes :
[Link]
296/315
Le fichier [[Link]] a été modifié pour prendre en compte les changements de chemin des ressources utilisées :
1. <!DOCTYPE html>
2. <html ng-app="rdvmedecins">
3. <head>
4. <title>RdvMedecins</title>
5. ...
6. <!-- le CSS -->
7. ...
8. <link href="lib/[Link]" rel="stylesheet"/>
9. <link href="lib/[Link]" rel="stylesheet"/>
10. </head>
11. <!-- contrôleur [appCtrl], modèle [app] -->
12. <body ng-controller="appCtrl">
13. <div class="container">
14. ...
15. </div>
16. <!-- Bootstrap core JavaScript ================================================== -->
17. <script type="text/javascript" src="lib/[Link]"></script>
18. <script type="text/javascript" src="lib/[Link]"></script>
19. <script type="text/javascript" src="lib/[Link]"></script>
20. <script type="text/javascript" src="lib/[Link]"></script>
21. <!-- angular js -->
22. <script type="text/javascript" src="lib/[Link]"></script>
23. <script type="text/javascript" src="lib/[Link]"></script>
24. <script type="text/javascript" src="lib/[Link]"></script>
25. <script type="text/javascript" src="lib/[Link]"></script>
26. <script type="text/javascript" src="lib/[Link]"></script>
27. <!-- modules -->
28. ...
29. <!-- services -->
30. ...
31. <!-- directives -->
32. ...
33. <!-- controllers -->
34. ....
35. </body>
36. </html>
Par ailleurs, le contrôleur [loginCtrl] a été modifié pour pointer sur le bon serveur afin d'éviter à l'utilisateur de taper son URL :
1. // credentials
2. [Link] = "[Link]
3. [Link] = "admin";
4. [Link] = "admin";
[Link]
297/315
Puis connectons-nous au service web. Ca doit marcher. Ceci vérifié, arrêtons le serveur Tomcat. Nous allons réutiliser le serveur
intégré de STS.
Dans STS, copions tout le contenu du dossier [rdvmedecins-angular-v3/app] dans le dossier [webapp] du projet [rdvmedecins-
webapi-v4] (onglet Navigator) [1] :
2
1
Ceci fait, lançons [2], le serveur VMware de STS, puis demandons l'URL [[Link]
v4/app/[Link]] :
On a un problème de droits en [3]. Ce n'est pas étonnant car on a protégé le service web. Il faut qu'on déclare que l'accès au fichier
[/app/[Link]] est libre. Revenons dans Eclipse :
[Link]
298/315
On se rappelle que les droits d'accès ont été déclarés dans la classe [SecurityConfig]. Modifions celle-ci de la façon suivante :
1. @Override
2. protected void configure(HttpSecurity http) throws Exception {
3. // CSRF
4. [Link]().disable();
5. // le mot de passe est transmis par le header Authorization: Basic xxxx
6. [Link]();
7. // la méthode HTTP OPTIONS doit être autorisée pour tous
8. [Link]() //
9. .antMatchers([Link], "/", "/**").permitAll();
10. // le dossier [app] est accessible à tous
11. [Link]() //
12. .antMatchers([Link], "/app", "/app/**").permitAll();
13. // seul le rôle ADMIN peut utiliser l'application
14. [Link]() //
15. .antMatchers("/", "/**") // toutes les URL
16. .hasRole("ADMIN");
17. }
• lignes 11-12 : on autorise tout le monde à lire le dossier [app] et son contenu. On s'inspire, pour le faire, des lignes
précédentes.
Maintenant, relançons le serveur Tomcat de STS puis demandons de nouveau l'URL [[Link]
v4/app/[Link]] :
[Link]
299/315
Le client HTML et le service web sont donc sur le même serveur [[Link] Il n'y a alors pas de conflits CORS car
ceux-ci n'interviennent que lorsque le client et le serveur ne sont pas dans le même domaine. On devrait pouvoir le vérifier. Nous
revenons dans STS :
La génération ou non des entêtes CORS est contrôlée par un booléen défini dans la classe [ApplicationModel] :
// données de configuration
private boolean CORSneeded = true;
Nous passons le booléen ci-dessus à false, nous relançons le service web et nous redemandons l'URL
[[Link] On constate que l'application fonctionne.
[Link]
300/315
• en [3], on télécharge l'application zippée [4] (le dossier [app] créé page 296 est zippé) ;
7 8
• seuls les binaires Android [7] et Windows [8] ont été générés ;
• on clique sur [7] pour télécharger le binaire d'Android ;
Lancez un émulateur [GenyMotion] pour une tablette Android (voir paragraphe 6.4, page 313) :
[Link]
301/315
Ci-dessus, on lance un émulateur de tablette avec l'API 16 d'Android. Une fois l'émulateur lancé,
• déverrouillez-le en tirant le verrou (s'il est présent) sur le côté puis en le lâchant ;
• avec la souris, tirez le fichier [[Link]] que vous avez téléchargé et déposez-le sur l'émulateur. Il va être
alors installé et exécuté ;
Il faut changer l'URL en [1]. Pour cela, dans une fenêtre de commandes, tapez la commande [ipconfig] (ligne 1 ci-dessous) qui va
afficher les différentes adresses IP de votre machine :
1. C:\Users\Serge Tahé>ipconfig
2.
3. Configuration IP de Windows
4.
5.
6. Carte réseau sans fil Connexion au réseau local* 15 :
7.
8. Statut du média. . . . . . . . . . . . : Média déconnecté
9. Suffixe DNS propre à la connexion. . . :
10.
11. Carte Ethernet Connexion au réseau local :
12.
13. Suffixe DNS propre à la connexion. . . : [Link]
14. Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
15. Adresse IPv4. . . . . . . . . . . . . .: [Link]
16. Masque de sous-réseau. . . . . . . . . : [Link]
17. Passerelle par défaut. . . . . . . . . : [Link]
18.
19. Carte réseau sans fil Wi-Fi :
20.
[Link]
302/315
21. Statut du média. . . . . . . . . . . . : Média déconnecté
22. Suffixe DNS propre à la connexion. . . :
23.
24. ...
Notez soit l'adresse IP Wifi (lignes 6-9), soit l'adresse IP sur le réseau local (lignes 11-17). Puis utilisez cette adresse IP dans l'URL
du serveur web :
Testez l'application sur l'émulateur. Elle doit fonctionner. Côté serveur, on peut ou non autoriser les entêtes CORS dans la classe
[ApplicationModel] :
// données de configuration
private boolean CORSneeded = false;
[Link]
303/315
Cela n'a pas d'importance pour l'application Android. Celle-ci ne s'exécute pas dans un navigateur. Or l'exigence des entêtes CORS
vient du navigateur et non pas du serveur.
• en [4], on se connecte ;
• en [5], la liste et le calendrier sont l'un sous l'autre au lieu d'être l'un à côté de l'autre ;
[Link]
304/315
6
Au final, notre application s'adapte plutôt bien au smartphone. Cela pourrait être sûrement mieux mais cela reste utilisable.
[Link]
305/315
5 Conclusion
Nous avons construit l'application client / serveur suivante :
Pour arriver à la version finale du code, nous avons du expliquer de nombreux points des frameworks AngularJS et Spring 4. Ce
document peut donc être utilisé pour se former à l'utilisation de ces deux frameworks. Le paragraphe 1.3, page 7, explique où
trouver les codes et comment les exploiter.
Nous avons montré que l'application client / serveur était utilisable dans divers environnements :
Encore une fois, ce tutoriel n'est pas exhaustif quant à l'étude des deux frameworks. Pour Angular, il faudrait certainement
présenter les outils de tests qui l'accompagnent. Les tests sont des étapes indispensables lors de l'écriture d'une application. Les
outils qui gravitent autour d'Angular permettent de les automatiser et des inclure dans un processus d'intégration continue.
• l'écriture du service web Spring a été moyennement compliquée. Dès le départ, je connaissais bien les concepts de Spring.
Je n'ai rencontré de difficultés qu'avec la sécurisation du service web puis plus tard avec la gestion des entêtes HTTP
CORS, deux domaines que je ne connaissais pas ;
• l'écriture du client Angular a été beaucoup plus complexe pour différentes raisons :
◦ j'avais une connaissance insuffisante du langage Javascript et de ses possibilités ;
◦ j'ai eu du mal à comprendre comment fonctionnait la programmation asynchrone au sein du navigateur. Je raisonnais
comme sur un serveur où cet asynchronisme est obtenu avec l'utilisation simultanée de plusieurs threads. Dans le
navigateur, il n'y a qu'un thread, et les tâches asynchrones sont traitées successivement et non pas en parallèle. Plus
précisément, des tâches asynchrones peuvent s'exécuter en parallèle (requêtes HTTP multiples par exemple) mais les
événements qu'elles produisent lorsqu'elles sont terminées, sont eux traités séquentiellement. Il n'y a donc pas
d'excécution concurrente à gérer avec les nombreux problèmes qui vont avec ;
[Link]
306/315
◦ Angular est un framework riche avec de nombreuses notions (MVC, directives, services, portée des modèles, ...). Son
apprentissage est long ;
◦ Angular n'impose pas de méthode de développement. Ainsi pour arriver à un même résultat, on peut utiliser
différentes architectures. C'est déroutant. Je suis plus à l'aise avec des frameworks fermés où tout le monde utilise les
mêmes patrons de conception (design pattern). J'ai donc constamment cherché à reproduire les modèles de
conception que j'utilise côte serveur. Je suis satisfait du résultat car je pense qu'il est reproductible. C'est ce que je
cherchais. Mais je ne sais pas du tout si je me suis écarté ou non des « bonnes pratiques » d'Angular ;
[Link]
307/315
6 Annexes
Nous présentons ici comment installer les outils utilisés dans ce document sur des machines Windows 7.
2A
• aller sur le site de SpringSource Tool Suite (STS) [1], pour télécharger la version courante de STS [2A] [2B],
2B
3A
[Link]
308/315
3B
5
6
4
• le fichier téléchargé est un installateur qui crée l'arborescence de fichiers [3A] [3B]. En [4], on lance l'exécutable,
• en [5], la fenêtre de travail de l'IDE après avoir fermé la fenêtre de bienvenue. En [6], on fait afficher la fenêtre des
serveurs d'applications,
• en [7], la fenêtre des serveurs. Un serveur est enregistré. C'est un serveur VMware compatible Tomcat.
L'utilisation de STS dans le cadre de l'application est expliquée au paragraphe 1.3.2, page 9.
3
2
[Link]
309/315
4
5
• en [4], l'icône de [WampServer] s'installe dans la barre des tâches en bas et à droite de l'écran [4],
• lorsqu'on clique dessus, le menu [5] s'affiche. Il permet de gérer le serveur Apache et le SGBD MySQL. Pour gérer celui-
ci, on utiliser l'option [PhpPmyAdmin],
• on obtient alors la fenêtre ci-dessous,
Nous donnerons peu de détails sur l'utilisation de [PhpMyAdmin]. Nous montrons au paragraphe 1.3.1, page 8, comment l'utiliser
pour créer la base de données de l'application.
Son utilisation dans le cadre de l'application est décrite au paragraphe 1.3.3, page 11. Pour installer des bibliothèques JS au sein
d'une application, WS utilise un outil appelé [bower]. Cet outil est un module de [[Link]], un ensemble de bibliothèques JS. Par
ailleurs, les bibliothèques JS sont cherchées sur un site Git, nécessitant un client Git sur le poste qui télécharge.
[Link]
310/315
6.3.1 Installation de [[Link]]
Le site de téléchargement de [[Link]] est [[Link] Téléchargez l'installateur puis exécutez-le. Il n'y a rien de plus à faire
pour le moment.
• ligne 1 : la commande [[Link]] qui installe le module [bower]. Pour que la commande marche, il faut que l'exécutable
[npm] soit dans le PATH de la machine (voir paragraphe ci-après) ;
Pour les autres étapes de l'installation, vous pouvez accepter les valeurs par défaut proposées.
Une fois, l'installation de Git terminée, vérifiez que l'exécutable est dans le PATH de votre machine : [Panneau de configuration /
Système et sécurité / Système / Paramètres systèmes avancés] :
[Link]
311/315
La variable PATH ressemble à ceci :
D:\Programs\devjava\java\jdk1.7.0\bin;D:\Programs\ActivePerl\Perl64\site\bin;D:\Programs\ActivePerl\Perl64\bin;D:\
Programs\sgbd\OracleXE\app\oracle\product\11.2.0\client;D:\Programs\sgbd\OracleXE\app\oracle\product\11.2.0\client
\bin;D:\Programs\sgbd\OracleXE\app\oracle\product\11.2.0\server\bin;...;D:\Programs\javascript\[Link]\;D:\Program
s\utilitaires\Git\cmd
Vérifiez que :
• le chemin du dossier d'installation de [[Link]] est bien présent (ici D:\Programs\javascript\[Link]) ;
• le chemin de l'excéutable du client Git est bien présent (ici D:\Programs\utilitaires\Git\cmd) ;
[Link]
312/315
1 3
Ci-dessus, sélectionnez l'option [1]. La liste des modules [[Link]] déjà installés apparaît en [2]. Cette liste ne devrait contenir que la
ligne [3] du module [bower] si vous avez suivi le processus d'installation précédent.
Vous aurez à vous enregistrer pour obtenir une version à usage personnel. Téléchargez le produit [Genymotion] avec la machine
virtuelle VirtualBox ;
Installez puis lancez [Genymotion]. Téléchargez ensuite une image pour une tablette ou un téléphone :
1 3
4
[Link]
313/315
5
• une fois le téléchargement terminé, vous obtenez en [5] la liste des terminaux virtuels dont vous disposez pour tester vos
applications Android ;
• aller sur le site de [Google Web store] ([Link] avec le navigateur Chrome ;
• chercher l'application [Advanced Rest Client] :
• pour l'obtenir, il vous faudra créer un compte Google. [Google Web Store] demande ensuite confirmation [1] :
[Link]
314/315
2
• en [2], l'extension ajoutée est disponible dans l'option [Applications] [3]. Cette option est affichée sur chaque nouvel onglet
que vous créez (CTRL-T) dans le navigateur.
[Link]
315/315