Beruflich Dokumente
Kultur Dokumente
AngularJS / Spring 4
http://tahe.developpez.com
1/325
http://tahe.developpez.com
2/325
http://tahe.developpez.com
3/325
http://tahe.developpez.com
4/325
http://tahe.developpez.com
5/325
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 ;
La comprhension de ce document ncessite certains pr-requis :
la connaissance de JPA (Java Persistence Api) qui sera utilis pour accder une base de donnes ;
la connaissance d'au moins une version prcdente de Spring pour connatre la philosophie de ce framework ;
une connaissance de base des changes HTTP dans une application web ;
[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
[http://www.apress.com/downloadable/download/sample/sample_id/1527/] ;
[ref2] : la documentation officielle d'Angular JS [https://docs.angularjs.org/guide]
[ref3] : le livre " Spring Data " chez o'Reilly [http://shop.oreilly.com/product/0636920024767.do] qui prsente
l'utilisation du framework [Spring Data] pour l'accs aux donnes que ce soit des bases de donnes relationnelles ou pas
(NoSQL) ;
[ref4] : le livre " Pro Spring3 " aux ditions Apress. C'est la version prcdente de Spring 4 mais les principaux concepts
sont dj l ;
[ref5] : la documentation de rfrence de Spring 4 [http://docs.spring.io/spring/docs/current/spring-frameworkreference/pdf/spring-framework-reference.pdf].
Les sources qui ont nourri ce document sont celles cites ci-dessus plus l'indispensable [ http://stackoverflow.com/] pour les trs
nombreuses sances de dbogage.
1.1
L'architecture de l'application
http://tahe.developpez.com
6/325
1.2
en [1], un serveur web dlivre des pages statiques un navigateur. Ces pages contiennent une application AngularJS
construite sur le modle MVC (Modle Vue Contrleur). Le modle ici est la fois celui des vues et celui du domaine
reprsent ici par la couche [Services] ;
l'utilisateur va interagir avec les vues qui lui sont prsentes dans le navigateur. Ses actions vont parfois ncessiter
l'interrogation du serveur Spring 4 [2]. Celui-ci traitera la demande et rendra une rponse JSON (JavaScript Object
Notation) [3]. Celle-ci sera utilise pour mettre jour la vue prsente l'utilisateur.
1.3
Le code de l'exemple est disponible l'URL [http://tahe.developpez.com/java/angularjs-spring4] sous la forme d'un fichier zip
tlcharger.
http://tahe.developpez.com
7/325
1.3.1
Pour tester l'application, nous crons d'abord la base de donnes avec le script SQL [dbrdvmedecins.sql]. Nous utilisons l'outil
[PhpMyAdmin] de WampServer :
1
2
3
5
4
http://tahe.developpez.com
8/325
1.3.2
Avec Spring Tool Suite (STS), on importe les deux projets Maven du serveur Spring 4 :
3
2
3
5
4
6
en [3], les projets imports. Il se peut que les projets prsentent des erreurs. Il faut que chacun d'eux utilise un compilateur
>=1.7 :
http://tahe.developpez.com
9/325
en [4], [5] et [6] nous excutons le projet [rdvmedecins-webapi-v3] comme une application Spring Boot ;
http://tahe.developpez.com
10/325
1.3.3
http://tahe.developpez.com
11/325
9
10
11
12
13
14
en [6], la page d'entre de l'application. Il s'agit d'une application de prise de rendez-vous pour des mdecins. Cette
application a dj t traite 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 caractrise par la prsence du cadre [8]
qui affiche le modle de la vue courante ;
en [9], une dure d'attente artificielle en millisecondes. Elle vaut 0 par dfaut (pas d'attente). Si N est la valeur de ce temps
d'attente, toute action de l'utilisateur sera excute aprs 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 prcd, c'est [http://localhost:8080];
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 rle (ADMIN) et user/user avec un rle (USER). Seul le rle ADMIN a le droit d'utiliser
l'application. Le rle USER n'est l que pour montrer ce que rpond 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 franais par dfaut et l'anglais.
en [1], on se connecte ;
http://tahe.developpez.com
12/325
une fois connect, on peut choisir le mdecin avec lequel on veut un rendez-vous [2] et le jour de celui-ci [3] ;
on demande en [4] voir l'agenda du mdecin choisi pour le jour choisi ;
http://tahe.developpez.com
13/325
http://tahe.developpez.com
14/325
Une fois le rendez-vous valid, on est ramen automatiquement l'agenda o le nouveau rendez-vous est dsormais inscrit. Ce
rendez-vous pourra tre ultrieurement supprim [7].
Les principales fonctionnalits ont t dcrites. Elles sont simples. Celles qui n'ont pas t dcrites sont des fonctions de navigation
pour revenir une vue prcdente. Terminons par la gestion de la langue :
http://tahe.developpez.com
15/325
http://tahe.developpez.com
16/325
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 [mtier] et [DAO] (Data Access Object). Nous utiliserons ici Spring Data ;
puis le service web JSON sans authentification. Nous utiliserons ici Spring MVC ;
2.1
La base de donnes
Couche
[web /
JSON]
Couche
[mtier]
Spring 4
Couche
[DAO]
SGBD
La base de donnes appele par la suite [dbrdvmedecins] est une base de donnes MySQL5 avec les tables suivantes :
http://tahe.developpez.com
17/325
2.1.1
La table [MEDECINS]
Elle contient des informations sur les mdecins grs par l'application [RdvMedecins].
http://tahe.developpez.com
18/325
2.1.2
La table [CLIENTS]
Les clients des diffrents mdecins sont enregistrs dans la table [CLIENTS] :
2.1.3
La table [CRENEAUX]
http://tahe.developpez.com
19/325
La seconde ligne de la table [CRENEAUX] (cf [1] ci-dessus) indique, par exemple, que le crneau n 2 commence 8 h 20 et se
termine 8 h 40 et appartient au mdecin n 1 (Mme Marie PELISSIER).
2.1.4
La table [RV]
Cette table a une contrainte d'unicit sur les valeurs des colonnes jointes (JOUR, ID_CRENEAU) :
ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (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 mme moment pour le mme mdecin. 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 crneau n 20 et le client n 4 le 23/08/2006. La table
[CRENEAUX] nous apprend que le crneau n 20 correspond au crneau horaire 16 h 20 - 16 h 40 et appartient au mdecin n 1
(Mme Marie PELISSIER). La table [CLIENTS] nous apprend que le client n 4 est Melle Brigitte BISTROU.
2.2
Nous allons implmenter la couche [DAO] du projet avec Spring Data, une branche de l'cosystme Spring.
Couche
[web /
JSON]
Couche
[mtier]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7 4
Spring
Sur le site de Spring existent de nombreux tutoriels pour dmarrer avec Spring [ http://spring.io/guides]. Nous allons utiliser l'un
d'eux pour introduire Spring Data. Nous utilisons pour cela Spring Tool Suite (STS).
http://tahe.developpez.com
20/325
3
4
5
2.2.1
en [2], on choisit le tutoriel [Accessing Data Jpa] qui montre comment accder une base de donnes avec Spring Data ;
en [3], on choisit un projet configur par Maven ;
en [4], le tutoriel peut tre dlivr 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 dernire ;
en [5], on peut choisir de visualiser le tutoriel dans un navigateur ;
en [6], le projet final.
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.2.RELEASE</version>
</parent>
http://tahe.developpez.com
21/325
11.
<dependencies>
12.
<dependency>
13.
<groupId>org.springframework.boot</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.
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
25.
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
26.
<start-class>hello.Application</start-class>
27. </properties>
lignes 5-9 : dfinissent un projet Maven parent. C'est lui qui dfinit l'essentiel des dpendances du projet. Elles peuvent
tre suffisantes, auquel cas on n'en rajoute pas, ou pas, auquel cas on rajoute les dpendances manquantes ;
lignes 12-15 : dfinissent une dpendance sur [spring-boot-starter-data-jpa]. Cet artifact contient les classes de Spring
Data ;
lignes 16-19 : dfinissent une dpendance sur le SGBD H2 qui permet de crer et grer des bases de donnes en mmoire.
d'autres appartiennent l'cosystme Hibernate (hibernate, jboss) dont on utilise ici l'implmentation JPA ;
http://tahe.developpez.com
22/325
6.
7.
8.
9.
10.
11.
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Lignes 6-9, le plugin [spring-boot-maven-plugin] permet de gnrer le jar excutable de l'application. La ligne 26 du fichier
[pom.xml] dsigne alors la classe excutable de ce jar.
2.2.2
La couche [JPA]
L'accs la base de donnes se fait au travers d'une couche [JPA], Java Persistence API :
Couche
[console]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7
Spring
4
L'application est basique et gre des clients [Customer]. La classe [Customer] fait partie de la couche [JPA] et est la suivante :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
package hello;
import
import
import
import
javax.persistence.Entity;
javax.persistence.GeneratedValue;
javax.persistence.GenerationType;
javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
protected Customer() {
}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName,
lastName);
http://tahe.developpez.com
23/325
28.
29.
30. }
Un client a un identifiant [id], un prnom [firstName] et un nom [lastName]. Chaque instance [Customer] reprsente une ligne
d'une table de la base de donnes.
ligne 8 : annotation JPA qui fait que la persistence des instances [Customer] (Create, Read, Update, Delete) va tre gre
par une implmentation JPA. D'aprs les dpendances Maven, on voit que c'est l'implmentation JPA / Hibernate qui est
utilise ;
lignes 11-12 : annotations JPA qui associent le champ [id] la cl primaire de la table des [Customer]. La ligne 12, indique
que l'implmentation JPA utilisera la mthode de gnration de cl primaire propre au SGBD utilis, ici H2 ;
Il n'y a pas d'autres annotations JPA. Des valeurs par dfaut seront alors utilises :
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 ;
On notera qu' aucun moment, l'implmentation JPA utilise n'est nomme.
2.2.3
La couche [DAO]
Couche
[console]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7
Spring
4
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
}
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 paramtre par deux types : le premier est le type des lments grs, ici le type [Customer], le second le type de
la cl primaire des lments grs, ici un type [Long]. L'interface [CrudRepository] est la suivante :
1.
2.
3.
4.
5.
6.
7.
package org.springframework.data.repository;
import java.io.Serializable;
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
http://tahe.developpez.com
24/325
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29. }
Cette interface dfinit les opration CRUD (Create Read Update Delete) qu'on peut faire sur un type JPA T :
ligne 8 : la mthode save permet de persister une entit T en base. Elle rend l'entit persiste avec la cl primaire que lui a
donne le SGBD. Elle permet galement de mettre jour une entit T identifie 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'opration de persistence qui a lieu,
sinon c'est l'opration de mise jour ;
ligne 10 : idem mais pour une liste d'entits ;
ligne 12 : la mthode findOne permet de retrouver une entit T identifie par sa cl primaire id ;
ligne 22 : la mthode delete permet de supprimer une entit T identifie par sa cl primaire id ;
lignes 24-28 : des variantes de la mthode [delete] ;
ligne 16 : la mthode [findAll] permet de retrouver toutes les entits persistes T ;
ligne 18 : idem mais limite aux entits dont on a pass la liste des identifiants ;
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
}
Et c'est tout pour la couche [DAO]. Il n'y a pas de classe d'implmentation de l'interface prcdente. Celle-ci est gnre
l'excution par [Spring Data]. Les mthodes de l'interface [CrudRepository] sont automatiquement implmentes. Pour les
mthodes rajoutes dans l'interface [CustomerRepository], a dpend. Revenons la dfinition de [Customer] :
1.
private long id;
2.
private String firstName;
3. private String lastName;
La mthode de la ligne 9 est implmente automatiquement par [Spring Data] parce qu'elle rfrence le champ [lastName] (ligne 3)
de [Customer]. Lorsqu'il rencontre une mthode [findBySomething] dans l'interface implmenter, Spring Data l'implmente par la
requte JPQL (Java Persistence Query Language) suivante :
select t from T t where t.something=:value
http://tahe.developpez.com
25/325
Il faut donc que le type T ait un champ nomm [something]. Ainsi la mthode
List<Customer> findByLastName(String lastName);
o [em] dsigne 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'implmenter la couche [DAO] avec une simple interface.
2.2.4
La couche [console]
Couche
[console]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7
Spring
4
package hello;
import java.util.List;
import
import
import
import
org.springframework.boot.SpringApplication;
org.springframework.boot.autoconfigure.EnableAutoConfiguration;
org.springframework.context.ConfigurableApplicationContext;
org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
// save a couple of
repository.save(new
repository.save(new
repository.save(new
repository.save(new
repository.save(new
customers
Customer("Jack", "Bauer"));
Customer("Chloe", "O'Brian"));
Customer("Kim", "Bauer"));
Customer("David", "Palmer"));
Customer("Michelle", "Dessler"));
http://tahe.developpez.com
26/325
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53. }
context.close();
la ligne 10 : indique que la classe sert configurer Spring. Les versions rcentes de Spring peuvent en effet tre configures
en Java plutt qu'en XML. Les deux mthodes peuvent tre utilises simultanment. Dans le code d'une classe ayant
l'annotation [Configuration] on trouve normalement des beans Spring, --d des dfinitions de classe instancier. Ici
aucun bean n'est dfini. Il faut rappeler ici que lorsqu'on travaille avec un SGBD, divers beans Spring doivent tre dfinis :
un [EntityManagerFactory] qui dfinit l'implmentation JPA utiliser,
un [DataSource] qui dfinit la source de donnes utiliser,
un [TransactionManager] qui dfinit 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 bibliothques trouves dans son Classpath. Parce que les bibliothques Hibernate sont dans le Classpath, le bean
[entityManagerFactory] sera implment avec Hibernate. Parce que la bibliothque du SGBD H2 est dans le Classpath, le
bean [dataSource] sera implment avec H2. Dans le bean [dataSource], on doit dfinir galement l'utilisateur et son mot
de passe. Ici Spring Boot utilisera l'administrateur par dfaut de H2, sa sans mot de passe. Parce que la bibliothque
[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 dfinis explicitement par des annotations Spring. Ainsi les classes [Customer] et
[CustomerRepository] vont-elles tre inspectes. Parce que la premire a l'annotation [@Entity] elle sera catalogue
comme entit grer par Hibernate. Parce que la seconde tend l'interface [CrudRepository] elle sera enregistre comme
bean Spring.
ligne 1 : la mthode statique [run] de la classe [SpringApplication] du projet Spring Boot est excute. Son paramtre est la
classe qui a une annotation [Configuration] ou [EnableAutoConfiguration]. Tout ce qui a t expliqu prcdemment va
alors se drouler. Le rsultat est un contexte d'application Spring, --d un ensemble de beans grs par Spring ;
ligne 17 : on demande ce contexte Spring, un bean implmentant l'interface [CustomerRepository]. Nous rcuprons ici,
la classe gnre par Spring Data pour implmenter cette interface.
Les oprations qui suivent ne font qu'utiliser les mthodes du bean implmentant l'interface [CustomerRepository]. On notera ligne
50, que le contexte est ferm. Les rsultats console sont les suivants :
http://tahe.developpez.com
27/325
1. .
____
_
__ _ _
2. /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
3. ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
4. \\/ ___)| |_)| | | | | || (_| | ) ) ) )
5.
' |____| .__|_| |_|_| |_\__, | / / / /
6. =========|_|==============|___/=/_/_/_/
7. :: Spring Boot ::
(v1.0.2.RELEASE)
8.
9. 2014-06-05 16:23:13.877 INFO 11664 --- [
main] hello.Application
: Starting Application on Gportpers3 with PID 11664 (D:\Temp\wksSTS\gs-accessing-data-jpacomplete\target\classes started by ST in D:\Temp\wksSTS\gs-accessing-data-jpa-complete)
10. 2014-06-05 16:23:13.936 INFO 11664 --- [
main]
s.c.a.AnnotationConfigApplicationContext : Refreshing
org.springframework.context.annotation.AnnotationConfigApplicationContext@331a8fa0: startup date
[Thu Jun 05 16:23:13 CEST 2014]; root of context hierarchy
11. 2014-06-05 16:23:15.424 INFO 11664 --- [
main]
j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for
persistence unit 'default'
12. 2014-06-05 16:23:15.518 INFO 11664 --- [
main]
o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [
13.
name: default
14.
...]
15. 2014-06-05 16:23:15.690 INFO 11664 --- [
main] org.hibernate.Version
: HHH000412: Hibernate Core {4.3.1.Final}
16. 2014-06-05 16:23:15.692 INFO 11664 --- [
main] org.hibernate.cfg.Environment
: HHH000206: hibernate.properties not found
17. 2014-06-05 16:23:15.694 INFO 11664 --- [
main] org.hibernate.cfg.Environment
: HHH000021: Bytecode provider name : javassist
18. 2014-06-05 16:23:15.988 INFO 11664 --- [
main] o.hibernate.annotations.common.Version
: HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
19. 2014-06-05 16:23:16.078 INFO 11664 --- [
main] org.hibernate.dialect.Dialect
: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
20. 2014-06-05 16:23:16.300 INFO 11664 --- [
main] o.h.h.i.ast.ASTQueryTranslatorFactory
: HHH000397: Using ASTQueryTranslatorFactory
21. 2014-06-05 16:23:16.613 INFO 11664 --- [
main]
org.hibernate.tool.hbm2ddl.SchemaExport : 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 16:23:16.619 INFO 11664 --- [
main]
org.hibernate.tool.hbm2ddl.SchemaExport : HHH000230: Schema export complete
25. 2014-06-05 16:23:17.074 INFO 11664 --- [
main] o.s.j.e.a.AnnotationMBeanExporter
: Registering beans for JMX exposure on startup
26. 2014-06-05 16:23:17.094 INFO 11664 --- [
main] hello.Application
: 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.
http://tahe.developpez.com
28/325
2.2.5
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 dfaut ne nous conviennent pas.
http://tahe.developpez.com
29/325
Tout d'abord, nous allons expliciter les dpendances ncessaires dans le fichier [pom.xml] :
1. <dependencies>
2.
<!-- Spring Core -->
3.
<dependency>
4.
<groupId>org.springframework</groupId>
5.
<artifactId>spring-core</artifactId>
6.
<version>4.0.5.RELEASE</version>
7.
</dependency>
8.
<dependency>
9.
<groupId>org.springframework</groupId>
10.
<artifactId>spring-context</artifactId>
11.
<version>4.0.5.RELEASE</version>
12.
</dependency>
13.
<dependency>
14.
<groupId>org.springframework</groupId>
15.
<artifactId>spring-beans</artifactId>
16.
<version>4.0.5.RELEASE</version>
17.
</dependency>
18.
<!-- Spring transactions -->
19.
<dependency>
20.
<groupId>org.springframework</groupId>
21.
<artifactId>spring-aop</artifactId>
22.
<version>4.0.5.RELEASE</version>
23.
</dependency>
24.
<dependency>
25.
<groupId>org.springframework</groupId>
26.
<artifactId>spring-tx</artifactId>
27.
<version>4.0.5.RELEASE</version>
28.
</dependency>
29.
<!-- Spring Data -->
30.
<dependency>
31.
<groupId>org.springframework.data</groupId>
32.
<artifactId>spring-data-jpa</artifactId>
33.
<version>1.5.2.RELEASE</version>
34.
</dependency>
35.
<!-- Spring Boot -->
36.
<dependency>
37.
<groupId>org.springframework.boot</groupId>
38.
<artifactId>spring-boot</artifactId>
39.
<version>1.0.2.RELEASE</version>
40.
</dependency>
41.
<!-- Hibernate -->
42.
<dependency>
43.
<groupId>org.hibernate</groupId>
44.
<artifactId>hibernate-entitymanager</artifactId>
45.
<version>4.3.4.Final</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>
http://tahe.developpez.com
30/325
Dans le nouveau projet, l'entit [Customer] et l'interface [CustomerRepository] ne changent pas. On va changer la classe
[Application] qui va tre scinde en deux classes :
La classe excutable [Main] est la mme que prcdemment sans les annotations de configuration :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
package demo.console;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
...
}
context.close();
http://tahe.developpez.com
31/325
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
package demo.config;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import
import
import
import
import
import
import
import
import
import
import
org.apache.commons.dbcp.BasicDataSource;
org.springframework.context.annotation.Bean;
org.springframework.context.annotation.Configuration;
org.springframework.data.jpa.repository.config.EnableJpaRepositories;
org.springframework.orm.jpa.JpaTransactionManager;
org.springframework.orm.jpa.JpaVendorAdapter;
org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
org.springframework.orm.jpa.vendor.Database;
org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
org.springframework.transaction.PlatformTransactionManager;
org.springframework.transaction.annotation.EnableTransactionManagement;
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
// la source de donnes H2
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
// le provider JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.H2);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter,
DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new
LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan("demo.entities");
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Transaction manager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory
entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
59.
60.
61.
62.
63.
64. }
http://tahe.developpez.com
32/325
ligne 22 : l'annotation [@Configuration] fait de la classe [Config] une classe de configuration Spring ;
ligne 21 : l'annotation [@EnableJpaRepositories] permet de dsigner 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 mthodes des interfaces [CrudRepository]
doivent se drouler l'intrieur d'une transaction ;
ligne 19 : l'annotation [@EntityScan] permet de nommer les dossiers o doivent tre cherches les entits JPA. Ici elle a
t mise en commentaires, parce que cette information a t donne explicitement ligne 50. Cette annotation devrait tre
prsente si on utilise le mode [@EnableAutoConfiguration] et que les entits JPA ne sont pas dans le mme dossier que la
classe de configuration ;
ligne 18 : l'annotation [@ComponentScan] permet de lister les dossiers o les composants Spring doivent tre recherchs.
Les composants Spring sont des classes tagues avec des annotations Spring telles que @Service, @Component,
@Controller, ... Ici il n'y en a pas d'autres que ceux qui sont dfinis au sein de la classe [Config], aussi l'annotation a-t-elle
t mise en commentaires ;
lignes 25-33 : dfinissent la source de donnes, la base de donnes H2. C'est l'annotation @Bean de la ligne 25 qui fait de
l'objet cr par cette mthode un composant gr par Spring. Le nom de la mthode peut tre ici quelconque. Cependant
elle doit tre appele [dataSource] si l'EntityManagerFactory de la ligne 47 est absent et dfini par autoconfiguration ;
ligne 29 : la base de donnes s'appellera [demo] et sera gnre dans le dossier du projet ;
lignes 36-43 : dfinissent l'implmentation JPA utilise, ici une implmentation Hibernate. Le nom de la mthode peut tre
ici quelconque ;
ligne 39 : pas de logs SQL ;
ligne 30 : la base de donnes sera cre si elle n'existe pas ;
lignes 46-54 : dfinissent l'EntityManagerFactory qui va grer la persistance JPA. La mthode doit s'appeler
obligatoirement [entityManagerFactory] ;
ligne 47 : la mthode reoit deux paramtres ayant le type des deux beans dfinis prcdemment. Ceux-ci seront alors
construits puis injects par Spring comme paramtres de la mthode ;
ligne 49 : fixe l'implmentation JPA utilise ;
ligne 50 : fixent les dossiers o trouver les entits JPA ;
ligne 51 : fixe la source de donne grer ;
lignes 57-62 : le gestionnaire de transactions. La mthode doit s'appeler obligatoirement [transactionManager]. Elle
reoit pour paramtre le bean des lignes 46-54 ;
ligne 60 : le gestionnaire de transactions est associ l'EntityManagerFactory ;
Enfin, on peut se passer de Spring Boot. On cre une seconde classe excutable [Main2] :
http://tahe.developpez.com
33/325
package demo.console;
ligne 15 : la classe de configuration [Config] est dsormais exploite par la classe Spring
[AnnotationConfigApplicationContext]. On peut voir ligne 5 qu'il n'y a maintenant plus de dpendance vis vis de Spring
Boot.
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
....
context.close();
}
}
2.2.6
http://tahe.developpez.com
34/325
5
2
3
4
9
8
7
Ceci fait, on ouvre une console dans le dossier contenant l'archive excutable :
.....\dist>dir
12/06/2014 09:11
http://tahe.developpez.com
35/325
2.2.7
Pour crer un squelette de projet Spring Data, on peut procder de la faon suivante :
http://tahe.developpez.com
36/325
6
2
3
4
1
5
en [8] : le projet cr ;
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
http://tahe.developpez.com
37/325
10.
<groupId>org.springframework.boot</groupId>
11.
<artifactId>spring-boot-starter-data-jpa</artifactId>
12.
</dependency>
13.
<dependency>
14.
<groupId>org.springframework.boot</groupId>
15.
<artifactId>spring-boot-starter-test</artifactId>
16.
<scope>test</scope>
17.
</dependency>
18. </dependencies>
lignes 9-12 : les dpendances ncessaires JPA vont inclure [Spring Data] ;
lignes 13-17 : les dpendances ncessaires aux test JUnit intgrs avec Spring ;
package istia.st;
import
import
import
import
org.springframework.boot.SpringApplication;
org.springframework.boot.autoconfigure.EnableAutoConfiguration;
org.springframework.context.annotation.ComponentScan;
org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
package istia.st;
import
import
import
import
org.junit.Test;
org.junit.runner.RunWith;
org.springframework.boot.test.SpringApplicationConfiguration;
org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Test
public void contextLoads() {
}
}
Maintenant que nous avons un squelette d'application JPA, nous pouvons le complter pour crire le projet de la couche de
persistance du serveur de notre application de gestion de rendez-vous.
http://tahe.developpez.com
38/325
2.3
Couche
[web /
JSON]
Couche
[metier]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7 4
Spring
2.4
La configuration Maven
http://tahe.developpez.com
39/325
http://tahe.developpez.com
40/325
43.
</dependency>
44.
</dependencies>
45.
<properties>
46.
<!-- use UTF-8 for everything -->
47.
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
48.
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
49.
<start-class>istia.st.spring.data.main.Application</start-class>
50.
</properties>
51.
<build>
52.
<plugins>
53.
<plugin>
54.
<artifactId>maven-compiler-plugin</artifactId>
55.
</plugin>
56.
<plugin>
57.
<groupId>org.springframework.boot</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>http://repo.spring.io/libs-milestone</url>
67.
<snapshots>
68.
<enabled>false</enabled>
69.
</snapshots>
70.
</repository>
71.
<repository>
72.
<id>org.jboss.repository.releases</id>
73.
<name>JBoss Maven Release Repository</name>
74.
<url>https://repository.jboss.org/nexus/content/repositories/releases</url>
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>http://repo.spring.io/libs-milestone</url>
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 dpendances dj prsentes dans le
projet parent, on ne prcise pas de version. C'est la version dfinie dans le parent qui sera utilise. Pour les autres
dpendances, on les dclare 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 : bibliothque Jackson de gestion du JSON ;
lignes 39-43 : bibliothque Google de gestion des collections ;
http://tahe.developpez.com
41/325
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
<commons-dbcp.version>1.4</commons-dbcp.version>
<commons-digester.version>2.1</commons-digester.version>
<commons-pool.version>1.6</commons-pool.version>
<commons-pool2.version>2.2</commons-pool2.version>
<crashub.version>1.3.0-beta20</crashub.version>
<flyway.version>3.0</flyway.version>
<freemarker.version>2.3.20</freemarker.version>
<gemfire.version>7.0.2</gemfire.version>
<gradle.version>1.6</gradle.version>
<groovy.version>2.3.2</groovy.version>
<h2.version>1.3.175</h2.version>
<hamcrest.version>1.3</hamcrest.version>
<hibernate-entitymanager.version>4.3.1.Final</hibernate-entitymanager.version>
<hibernate-jpa-api.version>1.0.1.Final</hibernate-jpa-api.version>
<hibernate-validator.version>5.0.3.Final</hibernate-validator.version>
<hibernate.version>4.3.1.Final</hibernate.version>
<hikaricp.version>1.3.8</hikaricp.version>
<hornetq.version>2.4.1.Final</hornetq.version>
<hsqldb.version>2.3.2</hsqldb.version>
<httpasyncclient.version>4.0.1</httpasyncclient.version>
<httpclient.version>4.3.3</httpclient.version>
<jackson.version>2.3.3</jackson.version>
<java.version>1.6</java.version>
<javassist.version>3.18.1-GA</javassist.version>
<jedis.version>2.4.1</jedis.version>
<jetty-jsp.version>2.2.0.v201112011158</jetty-jsp.version>
<jetty.version>8.1.14.v20131031</jetty.version>
<joda-time.version>2.3</joda-time.version>
<jolokia.version>1.2.0</jolokia.version>
<jstl.version>1.2</jstl.version>
<junit.version>4.11</junit.version>
<liquibase.version>3.0.8</liquibase.version>
<log4j.version>1.2.17</log4j.version>
<logback.version>1.1.2</logback.version>
<mockito.version>1.9.5</mockito.version>
<mongodb.version>2.12.1</mongodb.version>
<mysql.version>5.1.30</mysql.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<reactor.version>1.1.1.RELEASE</reactor.version>
<servlet-api.version>3.0.1</servlet-api.version>
<slf4j.version>1.7.7</slf4j.version>
<snakeyaml.version>1.13</snakeyaml.version>
<solr.version>4.7.2</solr.version>
<spock.version>0.7-groovy-2.0</spock.version>
<spring-amqp.version>1.3.4.RELEASE</spring-amqp.version>
<spring-batch.version>3.0.0.RELEASE</spring-batch.version>
<spring-boot.version>1.1.0.RC1</spring-boot.version>
<spring-data-releasetrain.version>Dijkstra-RELEASE</spring-data-releasetrain.version>
<spring-hateoas.version>0.12.0.RELEASE</spring-hateoas.version>
<spring-integration.version>4.0.2.RELEASE</spring-integration.version>
<spring-loaded.version>1.2.0.RELEASE</spring-loaded.version>
<spring-mobile.version>1.1.1.RELEASE</spring-mobile.version>
<spring-security-jwt.version>1.0.2.RELEASE</spring-security-jwt.version>
<spring-security.version>3.2.4.RELEASE</spring-security.version>
<spring-social-facebook.version>1.1.1.RELEASE</spring-social-facebook.version>
<spring-social-linkedin.version>1.0.1.RELEASE</spring-social-linkedin.version>
<spring-social-twitter.version>1.1.0.RELEASE</spring-social-twitter.version>
<spring-social.version>1.1.0.RELEASE</spring-social.version>
<spring.version>4.0.5.RELEASE</spring.version>
<thymeleaf-extras-springsecurity3.version>2.1.1.RELEASE</thymeleaf-extrasspringsecurity3.version>
<thymeleaf-layout-dialect.version>1.2.4</thymeleaf-layout-dialect.version>
<thymeleaf.version>2.1.3.RELEASE</thymeleaf.version>
<tomcat.version>7.0.54</tomcat.version>
<velocity-tools.version>2.0</velocity-tools.version>
<velocity.version>1.7</velocity.version>
http://tahe.developpez.com
42/325
2.5
Couche
[web /
JSON]
Couche
[metier]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7 4
Spring
Les entits JPA sont les objets qui vont encapsuler les lignes des tables de la base de donnes.
La classe [AbstractEntity] est la classe parent des entits [Personne, Creneau, Rv]. Sa dfinition est la suivante :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
package rdvmedecins.entities;
import java.io.Serializable;
import
import
import
import
import
javax.persistence.GeneratedValue;
javax.persistence.GenerationType;
javax.persistence.Id;
javax.persistence.MappedSuperclass;
javax.persistence.Version;
@MappedSuperclass
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Long id;
@Version
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// initialisation
public AbstractEntity build(Long id, Long version) {
this.id = id;
http://tahe.developpez.com
43/325
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48. }
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id == other.id;
}
// getters et setters
..
ligne 11 : l'annotation [@MappedSuperclass] indique que la classe annote est parente d'entits JPA [@Entity] ;
lignes 15-17 : dfinissent la cl primaire [id] de chaque entit. C'est l'annotation [@Id] qui fait du champ [id] une cl
primaire. L'annotation [@GeneratedValue(strategy = GenerationType.AUTO)] indique que la valeur de cette cl primaire
est gnre par le SGBD et qu'aucun mode de gnration n'est impos ;
lignes 18-19 : dfinissent la version de chaque entit. L'implmentation JPA va incrmenter ce n de version chaque fois
que l'entit sera modifie. Ce n sert empcher la mise jour simultane de l'entit par deux utilisateur diffrents : 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 possde une version (V1) diffrente de celle en base (V1+1) ;
lignes 29-33 : la mthode [build] permet d'initialiser les deux champs de [AbstractEntity]. Cette mthode rend la rfrence
de l'instance [AbstractEntity] ainsi initialise ;
lignes 36-44 : la mthode [equals] de la classe est redfinie : deux entits seront dites gales si elles ont le mme nom de
classe et le mme identifiant id ;
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public class Personne extends AbstractEntity {
private static final long serialVersionUID = 1L;
// attributs d'une personne
@Column(length = 5)
private String titre;
@Column(length = 20)
private String nom;
@Column(length = 20)
private String prenom;
// constructeur par dfaut
public Personne() {
}
// constructeur avec paramtres
public Personne(String titre, String nom, String prenom) {
this.titre = titre;
this.nom = nom;
this.prenom = prenom;
}
// toString
public String toString() {
return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
}
http://tahe.developpez.com
44/325
32.
33.
34.
35. }
// getters et setters
...
ligne 6 : l'annotation [@MappedSuperclass] indique que la classe annote est parente d'entits JPA [@Entity] ;
lignes 10-15 : une personne a un titre (Melle), un prnom (Jacqueline), un nom (Tatou). aucune information n'est donne
sur les colonnes de la table. Elles porteront donc par dfaut les mmes noms que les champs ;
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
private static final long serialVersionUID = 1L;
// constructeur par dfaut
public Medecin() {
}
// constructeur avec paramtres
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "clients")
public class Client extends Personne {
private static final long serialVersionUID = 1L;
// constructeur par dfaut
public Client() {
http://tahe.developpez.com
45/325
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26. }
}
// constructeur avec paramtres
public Client(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
// identit
public String toString() {
return String.format("Client[%s]", super.toString());
}
package rdvmedecins.entities;
import
import
import
import
import
import
javax.persistence.Column;
javax.persistence.Entity;
javax.persistence.FetchType;
javax.persistence.JoinColumn;
javax.persistence.ManyToOne;
javax.persistence.Table;
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// caractristiques d'un crneau de RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// un crneau est li un mdecin
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// cl trangre
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
// constructeur par dfaut
public Creneau() {
}
// constructeur avec paramtres
public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
this.medecin = medecin;
this.hdebut = hdebut;
this.mdebut = mdebut;
this.hfin = hfin;
this.mfin = mfin;
}
// toString
public String toString() {
return String.format("Crneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut,
mdebut, hfin, mfin);
46.
}
http://tahe.developpez.com
46/325
47.
48.
49.
50.
51.
52.
53.
54.
55. }
// cl trangre
public long getIdMedecin() {
return idMedecin;
}
// setters - getters
...
package rdvmedecins.entities;
import java.util.Date;
import
import
import
import
import
import
import
import
javax.persistence.Column;
javax.persistence.Entity;
javax.persistence.FetchType;
javax.persistence.JoinColumn;
javax.persistence.ManyToOne;
javax.persistence.Table;
javax.persistence.Temporal;
javax.persistence.TemporalType;
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// caractristiques d'un Rv
@Temporal(TemporalType.DATE)
private Date jour;
// un rv est li un client
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// un rv est li un crneau
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// cls trangres
http://tahe.developpez.com
47/325
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 dfaut
40.
public Rv() {
41.
}
42.
43.
// avec paramtres
44.
public Rv(Date jour, Client client, Creneau creneau) {
45.
this.jour = jour;
46.
this.client = client;
47.
this.creneau = creneau;
48.
}
49.
50.
// toString
51.
public String toString() {
52.
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
53.
}
54.
55.
// cls trangres
56.
public long getIdCreneau() {
57.
return idCreneau;
58.
}
59.
60.
public long getIdClient() {
61.
return idClient;
62.
}
63.
64.
// getters et setters
65. ...
66. }
2.6
La couche [DAO]
Couche
[web /
JSON]
Couche
[metier]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7 4
Spring
Nous allons implmenter la couche [DAO] avec Spring Data :
http://tahe.developpez.com
48/325
package rdvmedecins.repositories;
ligne 7 : l'interface [MedecinRepository] se contente d'hriter des mthodes de l'interface [CrudRepository] sans en ajouter
d'autres ;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Medecin;
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
package rdvmedecins.repositories;
ligne 7 : l'interface [ClientRepository] se contente d'hriter des mthodes de l'interface [CrudRepository] sans en ajouter
d'autres ;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Client;
public interface ClientRepository extends CrudRepository<Client, Long> {
}
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Creneau;
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
// liste des crneaux horaires d'un mdecin
@Query("select c from Creneau c where c.medecin.id=?1")
Iterable<Creneau> getAllCreneaux(long idMedecin);
}
http://tahe.developpez.com
49/325
ligne 11 : le paramtre est l'identifiant du mdecin. Le rsultat est une liste de crneaux horaires sous la forme d'un objet
[Iterable<Creneau>] ;
ligne 10 : l'annotation [@Query] permet de spcifier la requte JPQL (Java Persistence Query Language) qui implmente la
mthode. Le paramtre [?1] sera remplac par le paramtre [idMedecin] de la mthode ;
package rdvmedecins.repositories;
import java.util.Date;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Rv;
public interface RvRepository extends CrudRepository<Rv, Long> {
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where
cr.medecin.id=?1 and rv.jour=?2")
13.
Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
14. }
2.7
La couche [mtier]
Couche
[web /
JSON]
Couche
[mtier]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7 4
Spring
http://tahe.developpez.com
50/325
2.7.1
Les entits
L'entit [CreneauMedecinJour] associe un crneau horaire et le rendez-vous ventuel pris dans ce crneau :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
package rdvmedecins.domain;
import java.io.Serializable;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// champs
private Creneau creneau;
private Rv rv;
// constructeurs
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau, rv);
}
// getters et setters
...
}
L'entit [AgendaMedecinJour] est l'agenda d'un mdecin pour un jour donn, --d la liste de ses rendez-vous :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
package rdvmedecins.domain;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.entities.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// champs
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// constructeurs
public AgendaMedecinJour() {
}
http://tahe.developpez.com
51/325
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
2.7.2
ligne 13 : le mdecin ;
ligne 14 : le jour dans l'agenda ;
ligne 15 : ses crneaux horaires avec ou sans rendez-vous ;
Le service
package rdvmedecins.metier;
import java.util.Date;
import java.util.List;
import
import
import
import
import
rdvmedecins.domain.AgendaMedecinJour;
rdvmedecins.entities.Client;
rdvmedecins.entities.Creneau;
rdvmedecins.entities.Medecin;
rdvmedecins.entities.Rv;
http://tahe.developpez.com
52/325
42.
43.
44.
45.
46.
47. }
package rdvmedecins.metier;
import
import
import
import
java.util.Date;
java.util.Hashtable;
java.util.List;
java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import
import
import
import
import
import
import
import
import
import
rdvmedecins.domain.AgendaMedecinJour;
rdvmedecins.domain.CreneauMedecinJour;
rdvmedecins.entities.Client;
rdvmedecins.entities.Creneau;
rdvmedecins.entities.Medecin;
rdvmedecins.entities.Rv;
rdvmedecins.repositories.ClientRepository;
rdvmedecins.repositories.CreneauRepository;
rdvmedecins.repositories.MedecinRepository;
rdvmedecins.repositories.RvRepository;
import com.google.common.collect.Lists;
@Service("mtier")
public class Metier implements IMetier {
// rpositories
@Autowired
private MedecinRepository medecinRepository;
@Autowired
private ClientRepository clientRepository;
@Autowired
private CreneauRepository creneauRepository;
@Autowired
private RvRepository rvRepository;
// implmentation interface
@Override
public List<Client> getAllClients() {
return Lists.newArrayList(clientRepository.findAll());
}
@Override
public List<Medecin> getAllMedecins() {
return Lists.newArrayList(medecinRepository.findAll());
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
}
http://tahe.developpez.com
53/325
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92. }
@Override
public Client getClientById(long id) {
return clientRepository.findOne(id);
}
@Override
public Medecin getMedecinById(long id) {
return medecinRepository.findOne(id);
}
@Override
public Rv getRvById(long id) {
return rvRepository.findOne(id);
}
@Override
public Creneau getCreneauById(long id) {
return creneauRepository.findOne(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau crneau, Client client) {
return rvRepository.save(new Rv(jour, client, crneau));
}
@Override
public void supprimerRv(Rv rv) {
rvRepository.delete(rv.getId());
}
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
...
}
ligne 24 : l'annotation [@Service] est une annotation Spring qui fait de la classe annote un composant gr par Spring. On
peut ou non donner un nom un composant. Celui-ci est nomm [mtier] ;
ligne 25 : la classe [Metier] implmente l'interface [IMetier] ;
ligne 28 : l'annotation [@Autowired] est une annotation Spring. La valeur du champ ainsi annot sera initialise (injecte)
par Spring avec la rfrence d'un composant Spring du type ou du nom prciss. Ici l'annotation [@Autowired] ne prcise
pas de nom. Ce sera donc une injection par type qui sera faite ;
ligne 29 : le champ [medecinRepository] sera initialis avec la rfrence d'un composant Spring de type
[MedecinRepository]. Ce sera la rfrence de la classe gnre par Spring Data pour implmenter l'interface
[MedecinRepository] que nous avons dj prsente ;
lignes 30-35 : ce processus est rpt pour les trois autres interfaces tudies ;
lignes 39-41 : implmentation de la mthode [getAllClients] ;
ligne 40 : nous utilisons la mthode [findAll] de l'interface [ClientRepository]. Cette mthode rend un type
[Iterable<Client>] que nous transformons en [List<Client>] avec la mthode statique [Lists.newArrayList]. La classe
[Lists] est dfinie dans la bibliothque Google Guava. Dans [pom.xml] cette dpendance a t importe :
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
lignes 38-86 : les mthodes de l'interface [IMetier] sont implmentes avec l'aide des classes de la couche [DAO] ;
Seule la mthode de la ligne 88 est spcifique la couche [mtier]. Elle a t place ici parce qu'elle fait un traitement mtier qui
n'est pas qu'un simple accs aux donnes. Sans cette mthode, il n'y avait pas de raison de crer une couche [mtier]. La mthode
[getAgendaMedecinJour] est la suivante :
1.
2.
http://tahe.developpez.com
54/325
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
2.8
La configuration du projet
package rdvmedecins.config;
import javax.sql.DataSource;
import
import
import
import
import
import
import
http://tahe.developpez.com
org.apache.commons.dbcp.BasicDataSource;
org.springframework.boot.autoconfigure.EnableAutoConfiguration;
org.springframework.boot.orm.jpa.EntityScan;
org.springframework.context.annotation.Bean;
org.springframework.context.annotation.ComponentScan;
org.springframework.data.jpa.repository.config.EnableJpaRepositories;
org.springframework.orm.jpa.JpaVendorAdapter;
55/325
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
// la source de donnes MySQL
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
// le provider JPA - n'est pas ncessaire si on est satisfait des valeurs par dfaut
utilises par Spring boot
// ici on le dfinit pour activer / dsactiver les logs SQL
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// l'EntityManagerFactory et le TransactionManager sont dfinis avec des valeurs par dfaut
par Spring boot
46.
47. }
lignes 45 : nous n'allons pas dfinir les beans [EntityManagerFactory] et [TransactionManager]. Nous allons pour cela nous
appuyer sur l'annotation [@EnableAutoConfiguration] de Spring Boot (ligne 17) ;
lignes 24-32 : dfinissent la source de donnes MySQL5. C'est un bean qui en gnral ne peut tre devin par Spring
Boot ;
lignes 36-43 : nous configurons galement l'implmentation JPA pour mettre l'attribut [showSql] d'Hibernate faux (ligne
39). Par dfaut, il est vrai ;
pour l'instant, les seuls composants grs 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 [mtier]
et [DAO] ;
la ligne 16 ajoute au contexte de Spring les interfaces du package [rdvmdecins.repositories] qui hritent 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 [rdvmdecins.metier], la classe [Metier] avec son annotation [@Service] va tre trouve
et ajoute au contexte Spring ;
ligne 45 : un bean [entityManagerFactory] va tre dfini par dfaut par Spring Boot. On doit indiquer ce bean o se
trouvent les entits JPA qu'il doit grer. C'est la ligne 19 qui fait cela ;
ligne 20 : indique que les mthodes des interfaces hritant de l'interface [CrudRepository] doivent tre excutes au sein
d'une transaction ;
http://tahe.developpez.com
56/325
2.9
package rdvmedecins.tests;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import
import
import
import
import
import
org.junit.Assert;
org.junit.Test;
org.junit.runner.RunWith;
org.springframework.beans.factory.annotation.Autowired;
org.springframework.boot.test.SpringApplicationConfiguration;
org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import
import
import
import
import
import
import
rdvmedecins.config.DomainAndPersistenceConfig;
rdvmedecins.domain.AgendaMedecinJour;
rdvmedecins.entities.Client;
rdvmedecins.entities.Creneau;
rdvmedecins.entities.Medecin;
rdvmedecins.entities.Rv;
rdvmedecins.metier.IMetier;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
@Autowired
private IMetier mtier;
@Test
public void test1(){
// affichage clients
List<Client> clients = mtier.getAllClients();
display("Liste des clients :", clients);
// affichage mdecins
List<Medecin> medecins = mtier.getAllMedecins();
display("Liste des mdecins :", medecins);
// affichage crneaux d'un mdecin
Medecin mdecin = medecins.get(0);
List<Creneau> creneaux = mtier.getAllCreneaux(mdecin.getId());
display(String.format("Liste des crneaux du mdecin %s", mdecin), creneaux);
// liste des Rv d'un mdecin, un jour donn
Date jour = new Date();
display(String.format("Liste des rv du mdecin %s, le [%s]", mdecin, jour),
mtier.getRvMedecinJour(mdecin.getId(), jour));
// ajouter un RV
Rv rv = null;
Creneau crneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le crneau %s pour le client
%s", jour, crneau,
client));
rv = mtier.ajouterRv(jour, crneau, client);
// vrification
Rv rv2 = mtier.getRvById(rv.getId());
Assert.assertEquals(rv, rv2);
http://tahe.developpez.com
57/325
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97. }
http://tahe.developpez.com
58/325
ligne 54 : on vrifie que le rendez-vous cherch et le rendez-vous trouv sont les mmes. On rappelle que la mthode
[equals] de l'entit [Rv] a t redfinie : deux rendez-vous sont gaux s'ils ont le mme id. Ici, cela nous montre que le
rendez-vous ajout a bien t mis en base ;
lignes 61-73 : on essaie d'ajouter une deuxime fois le mme rendez-vous. Cela doit tre rejet par le SGBD car on a une
contrainte d'unicit :
1. CREATE TABLE IF NOT EXISTS `rv` (
2.
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
3.
`JOUR` date NOT NULL,
4.
`ID_CLIENT` bigint(20) NOT NULL,
5.
`ID_CRENEAU` bigint(20) NOT NULL,
6.
`VERSION` int(11) NOT NULL DEFAULT '0',
7.
PRIMARY KEY (`ID`),
8.
UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
9.
KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
10.
KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
11.
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;
La ligne 8 ci-dessus indique que la combinaison [JOUR, ID_CRENEAU] doit tre unique, ce qui empche de mettre deux
rendez-vous le mme jour dans le mme crneau horaire.
ligne 73 : on vrifie qu'une exception s'est bien produite ;
ligne 77 : on demande l'agenda du mdecin pour lequel on vient d'ajouter un rendez-vous ;
ligne 79 : on vrifie que le rendez-vous ajout est bien prsent 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 vrifie qu'on a rcupr un pointeur null, montrant par l que le rendez-vous cherch n'existe pas ;
2.10
Le programme console
Couche
[console]
Couche
[mtier]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7 4
Spring
http://tahe.developpez.com
59/325
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
package rdvmedecins.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import
import
import
import
import
rdvmedecins.config.DomainAndPersistenceConfig;
rdvmedecins.entities.Client;
rdvmedecins.entities.Creneau;
rdvmedecins.entities.Rv;
rdvmedecins.metier.IMetier;
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52. }
http://tahe.developpez.com
60/325
ligne 34 : on veut connatre le mdecin ayant le crneau n 1. Pour cela on a besoin d'aller en base chercher le crneau n
1. Parce qu'on est en mode [FetchType.LAZY], le mdecin n'est pas ramen avec le crneau. Cependant, on a pris soin de
prvoir un champ [idMedecin] dans l'entit [Creneau] pour rcuprer la cl primaire du mdecin ;
ligne 35 : on rcupre la primaire du mdecin ;
ligne 36 : on affiche la liste des rendez-vous du mdecin ;
2.11
Couche
[web /
JSON]
Couche
[DAO]
Couche
[metier]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7 4
Spring
Nous abordons maintenant la construction de la couche web. Celle-ci est principalement constitue de mthodes qui traitent des
URL prcises et rpondent 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 implmenter cette interface avec Spring MVC, une autre branche de
l'cosystme Spring. Nous commenons par tudier l'un des guides trouvs sur [http://spring.io].
2.11.1
Le projet de dmonstration
http://tahe.developpez.com
61/325
2
6
3
4
Les services web accessibles via des URL standard et qui dlivrent du texte JSON sont souvent appels 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 rgles. Je n'ai pas cherch respecter celles-ci.
Examinons maintenant le projet import, d'abord sa configuration Maven.
2.11.2
Configuration Maven
http://tahe.developpez.com
62/325
20.
</dependency>
21.
<dependency>
22.
<groupId>com.fasterxml.jackson.core</groupId>
23.
<artifactId>jackson-databind</artifactId>
24.
</dependency>
25.
</dependencies>
26.
27.
<properties>
28.
<start-class>hello.Application</start-class>
29.
</properties>
30.
31.
<build>
32.
<plugins>
33.
<plugin>
34.
<artifactId>maven-compiler-plugin</artifactId>
35.
</plugin>
36.
<plugin>
37.
<groupId>org.springframework.boot</groupId>
38.
<artifactId>spring-boot-maven-plugin</artifactId>
39.
</plugin>
40.
</plugins>
41.
</build>
42.
43.
<repositories>
44.
<repository>
45.
<id>spring-releases</id>
46.
<url>http://repo.spring.io/release</url>
47.
</repository>
48.
</repositories>
49.
<pluginRepositories>
50.
<pluginRepository>
51.
<id>spring-releases</id>
52.
<url>http://repo.spring.io/release</url>
53.
</pluginRepository>
54.
</pluginRepositories>
55. </project>
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] amne avec lui les bibliothques ncessaires un projet spring MVC. Il
amne en particulier avec lui un serveur Tomcat embarqu. C'est sur ce serveur que l'application sera excute ;
lignes 21-24 : la bibliothque Jackson gre le JSON : transformation d'un objet Java en chane JSON et inversement ;
http://tahe.developpez.com
63/325
2.11.3
Spring MVC implmente le modle d'architecture dit MVC (Modle Vue Contrleur) de la faon suivante :
Application web
couche [web]
2b
2a
Dispatcher
Servlet
Contrleurs/
Navigateur
4b
Actions
Vue1
Vue2
Modles
Vuen
couches
[mtier, DAO, JPA]
Donnes
2c
l'action choisie peut exploiter les paramtres parami que la servlet [Dispatcher Servlet] lui a transmis. Ceux-ci peuvent
provenir de plusieurs sources :
dans le traitement de la demande de l'utilisateur, l'action peut avoir besoin de la couche [metier] [2b]. Une fois la
demande du client traite, celle-ci peut appeler diverses rponses. Un exemple classique est :
l'action demande une certaine vue de s'afficher [3]. Cette vue va afficher des donnes qu'on appelle le modle de la
vue. C'est le M de MVC. L'action va crer ce modle M [2c] et demander une vue V de s'afficher [3] ;
3. rponse - la vue V choisie utilise le modle M construit par l'action pour initialiser les parties dynamiques de la rponse HTML
qu'elle doit envoyer au client puis envoie cette rponse.
Pour un service web / JSON, l'architecture prcdente est lgrement modifie :
Application web
couche [web]
2b
2a
Dispatcher
Servlet
Contrleurs/
Navigateur
4b
JSON
Modles
Actions
2c
couches
[mtier, DAO,
ORM]
Donnes
4a
2.11.4
en [4a], le modle qui est une classe Java est transform en chane JSON par une bibliothque JSON ;
en [4b], cette chane JSON est envoye au navigateur ;
Le contrleur C
http://tahe.developpez.com
64/325
package hello;
ligne 9 : l'annotation [@Controller] fait de la classe [GreetingController] un contrleur Spring, --d que ses mthodes
sont enregistres pour traiter des URL ;
ligne 15 : l'annotation [@RequestMapping] indique l'URL que traite la mthode, ici l'URL [/greeting]. Nous verrons
ultrieurement que cette URL peut tre paramtre et qu'il est possible de rcuprer ces paramtres ;
ligne 16 : l'annotation [@ResponseBody] indique que la mthode ne produit pas un modle pour une vue (JSP, JSF,
Thymeleaf, ...) qui sera envoye ensuite au navigateur client mais produit elle-mme la rponse faite au navigateur. Ici, elle
produit un objet de type [Greeting] (ligne 18). De faon non apparente ici, cet objet va d'abord tre transform en JSON
avant d'tre envoy au navigateur. C'est la prsence d'une bibliothque JSON dans les dpendances du projet qui fait que
Spring Boot va, par autoconfiguration, configurer le projet de cette faon ;
ligne 17 : la mthode [greeting] a un paramtre [String name]. L'annotation [@RequestParam(value = "name", required =
false, defaultValue = "World"] indique que ce paramtre doit tre initialis avec un paramtre nomm [name]
(@RequestParam(value = "name"). Celui-ci peut tre le paramtre d'un GET ou d'un POST. Ce paramtre n'est pas
obligatoire (required = false). Dans ce dernier cas, le paramtre [name] de la mthode sera initialis avec la valeur [World]
(defaultValue = "World").
import
import
import
import
import
java.util.concurrent.atomic.AtomicLong;
org.springframework.stereotype.Controller;
org.springframework.web.bind.annotation.RequestMapping;
org.springframework.web.bind.annotation.RequestParam;
org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public @ResponseBody
Greeting greeting(@RequestParam(value = "name", required = false, defaultValue = "World")
String name) {
18.
return new Greeting(counter.incrementAndGet(), String.format(template, name));
19.
}
20. }
2.11.5
Le modle M
1. package hello;
2.
3. public class Greeting {
4.
http://tahe.developpez.com
65/325
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20. }
La transformation JSON de cet objet crera la chane de caractres {"id":n,"content":"texte"}. Au final, la chane JSON
produite par la mthode du contrleur sera de la forme :
{"id":2,"content":"Hello, World!"}
ou
{"id":2,"content":"Hello, John!"}
2.11.6
Configuration du projet
package hello;
ligne 11 : curieusement cette classe est excutable avec une mthode [main] propre aux applications console. C'est bien le
cas. La classe [SpringApplication] de la ligne 12 va lancer le serveur Tomcat prsent dans les dpendances et dployer le
service REST dessus ;
ligne 4 : on voit que la classe [SpringApplication] appartient au projet [Spring Boot] ;
ligne 12 : le premier paramtre est la classe qui configure le projet, le second d'ventuels paramtres ;
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 ;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Application {
http://tahe.developpez.com
66/325
2.11.7
Excution du projet
Excutons le projet :
__ _ _
http://tahe.developpez.com
67/325
On reoit bien la chane JSON attendue. Il peut tre intressant de voir les enttes HTTP envoys par le serveur. Pour cela, on va
utiliser le plugin de Chrome appel [Advanced Rest Client] (cf Annexes) :
http://tahe.developpez.com
68/325
1
5
7
4
2.11.8
Il est possible de crer une archive excutable en-dehors d'Eclipse. La configuration ncessaire est dans le fichier [pom.xml] :
1.
2.
3.
4.
5.
6.
7.
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.Application</start-class>
<java.version>1.7</java.version>
</properties>
<build>
http://tahe.developpez.com
69/325
8.
<plugins>
9.
<plugin>
10.
<groupId>org.springframework.boot</groupId>
11.
<artifactId>spring-boot-maven-plugin</artifactId>
12.
</plugin>
13.
</plugins>
14. </build>
On procde ainsi :
3
1
Dans les logs qui apparaissent dans la console, il est important de voir apparatre le plugin [spring-boot-maven-plugin]. C'est
lui qui gnre l'archive excutable.
[INFO] --- spring-boot-maven-plugin:1.1.0.RELEASE:repackage (default) @ gs-rest-service ---
D:\Temp\wksSTS\gs-rest-service-complete\target>dir
...
11/06/2014 15:30
<DIR>
classes
11/06/2014 15:30
<DIR>
generated-sources
11/06/2014 15:30
11 073 572 gs-rest-service-0.1.0.jar
11/06/2014 15:30
3 690 gs-rest-service-0.1.0.jar.original
11/06/2014 15:30
<DIR>
maven-archiver
11/06/2014 15:30
<DIR>
maven-status
...
http://tahe.developpez.com
70/325
Maintenant que l'application web est lance, on peut l'interroger avec un navigateur :
2.11.9
Si Spring Boot s'avre trs pratique en mode dveloppement, il est probable qu'une application en production sera dploye sur un
vrai serveur Tomcat. Voici comment procder :
Modifier le fichier [pom.xml] de la faon suivante :
1. <?xml version="1.0" encoding="UTF-8"?>
2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchemainstance"
3.
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven4.0.0.xsd">
4.
<modelVersion>4.0.0</modelVersion>
5.
6.
<groupId>org.springframework</groupId>
7.
<artifactId>gs-rest-service</artifactId>
8.
<version>0.1.0</version>
9.
<packaging>war</packaging>
10.
11.
<parent>
12.
<groupId>org.springframework.boot</groupId>
13.
<artifactId>spring-boot-starter-parent</artifactId>
14.
<version>1.1.0.RELEASE</version>
15.
</parent>
16.
17.
<dependencies>
18.
<dependency>
19.
<groupId>org.springframework.boot</groupId>
20.
<artifactId>spring-boot-starter-web</artifactId>
21.
</dependency>
22.
<dependency>
23.
<groupId>com.fasterxml.jackson.core</groupId>
24.
<artifactId>jackson-databind</artifactId>
25.
</dependency>
http://tahe.developpez.com
71/325
26.
<dependency>
27.
<groupId>org.springframework.boot</groupId>
28.
<artifactId>spring-boot-starter-tomcat</artifactId>
29.
<scope>provided</scope>
30.
</dependency>
31.
</dependencies>
32.
33.
<properties>
34.
<start-class>hello.Application</start-class>
35.
</properties>
36. ....
37. </project>
ligne 9 : il faut indiquer qu'on va gnrer une archive war (Web ARchive) ;
lignes 26-30 : il faut ajouter une dpendance sur l'artifact [spring-boot-starter-tomcat]. Cet artifact amne toutes les classes
de Tomcat dans les dpendances du projet ;
ligne 29 : cet artifact est [provided], --d que les archives correspondantes ne seront pas places dans le war gnr. En
effet, ces archives seront trouves sur le serveur Tomcat sur lequel s'excutera l'application ;
Il faut par ailleurs configurer l'application web. En l'absence de fichier [web.xml], cela se fait avec une classe hritant de
[SpringBootServletInitializer] :
package hello;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
http://tahe.developpez.com
72/325
en [1], on excute le projet sur l'un des serveurs enregistrs dans l'IDE Eclipse ;
en [2], on choisit [tc Server Developer] qui est prsent par dfaut. C'est une variante de Tomcat ;
Nous savons dsormais gnrer une archive war. Par la suite, nous continuerons travailler avec Spring Boot et son archive jar
excutable.
2.11.10
6
1
3
4
5
2
http://tahe.developpez.com
73/325
2.12
La couche [web]
Application web
couche [web]
2b
2a
Dispatcher
Servlet
Contrleurs/
Navigateur
4b
JSON
Modles
Actions
couches
[mtier, DAO, JPA]
Donnes
2c
4a
2.12.1
Configuration Maven
http://tahe.developpez.com
74/325
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
2.12.2
<name>rdvmedecins-webapi-v1</name>
<description>Gestion de RV Mdecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
Navigateur
4b
JSON
Actions
Modles
couches
[mtier, DAO, JPA]
Donnes
2c
4a
en [1], ci-dessus, le navigateur ne peut demander qu'un nombre restreint d'URL avec une syntaxe prcise ;
en [4], il reoit une rponse JSON ;
Les rponses de notre service web auront toutes la mme forme correspondant la transformation JSON d'un objet de type
[Reponse] suivant :
1. package rdvmedecins.web.models;
2.
3. public class Reponse {
4.
5.
// ----------------- proprits
6.
// statut de l'opration
7.
private int status;
8.
// la rponse JSON
9.
private Object data;
10.
11.
// ---------------constructeurs
12.
public Reponse() {
13.
}
14.
15.
public Reponse(int status, Object data) {
16.
this.status = status;
17.
this.data = data;
18.
}
19.
20.
// mthodes
21.
public void incrStatusBy(int increment) {
22.
status += increment;
http://tahe.developpez.com
75/325
23.
}
24.
25.
// ----------------------getters et setters
26. ...
27. }
Nous prsentons maintenant les copies d'cran qui illustrent l'interface du service web / JSON :
Liste de tous les patients du cabinet mdical [/getAllClients]
http://tahe.developpez.com
76/325
http://tahe.developpez.com
77/325
Pour ajouter / supprimer un rendez-vous nous utilisons le complment Chrome [Advanced Rest Client] car ces oprations se font
avec un POST.
Ajouter un rendez-vous [/ajouterRv]
0
1
http://tahe.developpez.com
78/325
en [4] : le client envoie l'entte signifiant que les donnes qu'il envoie sont au format JSON ;
en [5] : le service web rpond qu'il envoie lui aussi du JSON ;
en [6] : la rponse JSON du service web. Le champ [data] contient la forme JSON du rendez-vous ajout ;
http://tahe.developpez.com
79/325
1
2
http://tahe.developpez.com
80/325
Toutes ces URL sont traites par le contrleur [RdvMedecinsController] que nous prsentons maintenant.
http://tahe.developpez.com
81/325
2.12.3
package rdvmedecins.web.controllers;
import java.text.ParseException;
...
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// messages d'erreur de l'application
messages = application.getMessages();
}
// liste des mdecins
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
...
}
// liste des clients
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients() {
...
}
// liste des crneaux d'un mdecin
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
}
// liste des rendez-vous d'un mdecin
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
...
http://tahe.developpez.com
82/325
52.
}
53.
54.
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
55.
public Reponse getRvById(@PathVariable("id") long id) {
56. ...
57.
}
58.
59.
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
60.
public Reponse getCreneauById(@PathVariable("id") long id) {
61. ...
62.
}
63.
64.
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes =
"application/json; charset=UTF-8")
65.
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
66. ...
67.
}
68.
69.
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes =
"application/json; charset=UTF-8")
70.
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
71. ...
72.
}
73.
74.
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method =
RequestMethod.GET)
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 contrleur Spring. Par ailleurs, elle
entrane galement que les mthodes traitant les URL vont gnrer une rponse qui sera automatiquement transforme en
JSON ;
lignes 9-10 : un objet de type [ApplicationModel] sera inject ici par Spring ;
ligne 13 : l'annotation [@PostConstruct] tague une mthode excuter juste aprs l'instanciation de la classe. Lorsqu'elle
celle-ci s'excute, les objets injects par Spring sont disponibles ;
toutes les mthodes rendent un objet de type [Reponse] suivant :
1. package rdvmedecins.web.models;
2.
3. public class Reponse {
4.
5.
// ----------------- proprits
6.
// statut de l'opration
7.
private int status;
8.
// la rponse
9.
private Object data;
10.
...
11.
}
Cet objet est srialis en JSON avant d'tre envoy au navigateur client ;
ligne 20 : l'annotation [@RequestMapping] fixe les conditions d'appel de la mthode. Ici la mthode traite une demande
GET de l'URL [/getAllMedecins]. Si cette URL tait demande par un POST, elle serait refuse et Spring MVC enverrait
un code HTTP d'erreur au client web ;
ligne 32 : l'URL est paramtre par {idMedecin}. Ce paramtre est rcupr avec l'annotation [@PathVariable] ligne 33 ;
ligne 33 : l'unique paramtre [long idMedecin] reoit sa valeur du paramtre {idMedecin} de l'URL
[@PathVariable("idMedecin")]. Le paramtre dans l'URL et celui de la mthode peuvent porter des noms diffrents. Il faut
noter ici que [@PathVariable("idMedecin")] est de type String (toute l'URL est un String) alors que le paramtre [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 ;
http://tahe.developpez.com
83/325
ligne 65 : l'annotation [@RequestBody] dsigne le corps de la requte. Dans une requte GET, il n'y a quasiment jamais
de corps (mais il est possible d'en mettre un). Dans une requte 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 chane JSON suivante :
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}
La syntaxe [@RequestBody PostAjouterRv post] (ligne 65) ajoute au fait que la mthode attend du JSON [consumes =
"application/json; charset=UTF-8"] ligne 64 va faire que la chane JSON envoye par le client web va tre dsrialise en
un objet de type [PostAjouter]. Celui-ci est le suivant :
1. package rdvmedecins.web.models;
2.
3. public class PostAjouterRv {
4.
5.
// donnes du post
6.
private String jour;
7.
private long idClient;
8.
private long idCreneau;
9.
10.
// getters et setters
11.
...
12.
}
2.12.4
Nous avons dj prsent les modles [Reponse, PostAjouterRv, PostSupprimerRv]. Le modle [ApplicationModel] est le suivant :
1.
2.
3.
4.
5.
6.
7.
package rdvmedecins.web.models;
import java.util.Date;
...
@Component
public class ApplicationModel implements IMetier {
http://tahe.developpez.com
84/325
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
// la couche [mtier]
@Autowired
private IMetier mtier;
// donnes provenant de la couche [mtier]
private List<Medecin> mdecins;
private List<Client> clients;
// messages d'erreur
private List<String> messages;
@PostConstruct
public void init() {
// on rcupre les mdecins et les clients
try {
mdecins = mtier.getAllMedecins();
clients = mtier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
// getter
public List<String> getMessages() {
return messages;
}
// ------------------------- interface couche [mtier]
@Override
public List<Client> getAllClients() {
return clients;
}
@Override
public List<Medecin> getAllMedecins() {
return mdecins;
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return mtier.getAllCreneaux(idMedecin);
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return mtier.getRvMedecinJour(idMedecin, jour);
}
@Override
public Client getClientById(long id) {
return mtier.getClientById(id);
}
@Override
public Medecin getMedecinById(long id) {
return mtier.getMedecinById(id);
}
@Override
public Rv getRvById(long id) {
return mtier.getRvById(id);
}
@Override
public Creneau getCreneauById(long id) {
return mtier.getCreneauById(id);
}
http://tahe.developpez.com
85/325
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91. }
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return mtier.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(Rv rv) {
mtier.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return mtier.getAgendaMedecinJour(idMedecin, jour);
}
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] implmente l'interface [IMetier] ;
lignes 10-11 : une rfrence sur la couche [mtier] est injecte par Spring ;
ligne 19 : l'annotation [@PostConstruct] fait que la mthode [init] va tre excute juste aprs l'instanciation de la classe
[ApplicationModel] ;
lignes 23-24 : on rcupre les listes de mdecins et de clients auprs de la couche [mtier] ;
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
2a
Dispatcher
Servlet
3
Navigateur
4b
JSON
Contrleurs/
Actions
Modles
2c
2b
couches
[Application [mtier,
DAO,
Model]
JPA]
Donnes
4a
Cette stratgie amne de la souplesse quant la gestion du cache. Actuellement les crneaux horaires des mdecins ne sont pas mis
en cache. Pour les y mettre, il suffit de modifier la classe [ApplicationModel]. Cela n'a aucun impact sur le contrleur qui continuera
utiliser la mthode [List<Creneau> getAllCreneaux(long idMedecin)] comme il le faisait auparavant. C'est l'implmentation de
cette mthode dans [ApplicationModel]qui sera change.
2.12.5
La classe Static
La classe [Static] regroupe un ensemble de mthodes statiques utilitaires qui n'ont pas d'aspect " mtier " ou " web " :
http://tahe.developpez.com
86/325
package rdvmedecins.web.helpers;
ligne 12 : la mthode [Static.getErreursForException] qui a t utilise (ligne 8 ci-dessous) dans la mthode [init] de la
classe [ApplicationModel] :
import java.text.SimpleDateFormat;
...
public class Static {
public Static() {
}
// liste des messages d'erreur d'une exception
public static List<String> getErreursForException(Exception exception) {
// on rcupre la liste des messages d'erreur de l'exception
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
erreurs.add(cause.getMessage());
cause = cause.getCause();
}
return erreurs;
}
// mappers Object --> Map
// -------------------------------------------------------....
}
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
@PostConstruct
public void init() {
// on rcupre les mdecins et les clients
try {
mdecins = mtier.getAllMedecins();
clients = mtier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
La mthode construit un objet [List<String>] avec les messages d'erreur [exception.getMessage()] d'une exception
[exception] et de celles qu'elle contient [exception.getCause()].
La classe [Static] contient d'autres mthodes utilitaires sur lesquelles nous reviendrons lorsqe nous les rencontrerons.
Nous allons maintenant dtailler le traitement des URL du service web. Trois classes principales sont en jeu dans ce traitement :
le contrleur [RdvMedecinsController] ;
http://tahe.developpez.com
87/325
2.12.6
Le contrleur [RdvMedecinsController] (cf page 82) a une mthode [init] qui est excute juste aprs son instanciation :
1.
2.
3.
4.
5.
6.
7.
8.
9. }
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// messages d'erreur de l'application
messages = application.getMessages();
ligne 8 : les messages d'erreur stocks dans l'application cache [ApplicationModel] sont mmoriss en local dans le champ
de la ligne 3. Cela va permettre aux mthodes de savoir si l'application s'est initialise correctement.
2.12.7
L'URL [/getAllMedecins]
ligne 5 : on regarde si l'application s'est correctement initialise (messages==null). Si ce n'est pas le cas, on renvoie une
rponse avec status=-1 et data=messages ;
ligne 10 : sinon on renvoie la liste des mdecins avec un status gal 0. La mthode [application.getAllMedecins()] ne lance
pas d'exception car elle se contente de rendre une liste qui est en cache. Nanmoins on gardera cette gestion d'exception
pour le cas o les mdecins ne seraient plus mis en cache ;
Nous n'avons pas encore illustr le cas o l'application s'est mal initialise. Arrtons le SGBD MySQL5, lanons le service web puis
demandons l'URL [/getAllMedecins] :
http://tahe.developpez.com
88/325
On obtient bien une erreur. Dans un contexte normal, on obtient la vue suivante :
2.12.8
L'URL [/getAllClients]
Elle est analogue la mthode [getAllMedecins] dj tudie. Les rsultats obtenus sont les suivants :
http://tahe.developpez.com
89/325
2.12.9
L'URL [/getAllCreneaux/{idMedecin}]
ligne 9 : le mdecin identifi par le paramtre [id] est demand une mthode locale :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15. }
On revient de cette mthode avec un status dans [0,1,2]. Revenons au code de la mthode [getAllCreneaux] :
lignes 10-12 : si status!=0, on rend immdiatement la rponse ;
ligne 13 : on rcupre le mdecin ;
ligne 17 : on rcupre les crneaux de ce mdecin ;
ligne 22 : on envoie comme rponse un objet [Static.getListMapForCreneaux(crneaux)] ;
http://tahe.developpez.com
90/325
13.
@ManyToOne(fetch = FetchType.LAZY)
14.
@JoinColumn(name = "id_medecin")
15.
private Medecin medecin;
16.
17.
// cl trangre
18.
@Column(name = "id_medecin", insertable = false, updatable = false)
19.
private long idMedecin;
20. ...
21. }
Rappelons la requte JPQL qui implmente la mthode [getAllCreneaux] dans la couche [DAO] :
@Query("select c from Creneau c where c.medecin.id=?1")
La notation [c.medecin.id] force la jointure entre les tables [CRENEAUX] et [MEDECINS]. Aussi la requte ramne-t-elle tous les
crneaux du mdecin avec dans chacun d'eux le mdecin. Lorsqu'on srialize en JSON ces crneaux, on voit apparatre la chane
JSON du mdecin dans chacun d'eux. C'est inutile. Aussi plutt que de srialiser un objet [Creneau], on va srialiser un objet [Map]
dans lequel on ne mettra que les champs dsirs.
Revenons au code tudi initialement :
1. // on rend la rponse
2. return new Reponse(0, Static.getListMapForCreneaux(crneaux));
http://tahe.developpez.com
91/325
2.12.10
L'URL [/getRvMedecinJour/{idMedecin}/{jour}]
http://tahe.developpez.com
92/325
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32. }
if (messages != null) {
return new Reponse(-1, messages);
}
// on vrifie la date
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, null);
}
// on rcupre le mdecin
Reponse rponse = getMedecin(idMedecin);
if (rponse.getStatus() != 0) {
return rponse;
}
Medecin mdecin = (Medecin) rponse.getData();
// liste de ses rendez-vous
List<Rv> rvs = null;
try {
rvs = application.getRvMedecinJour(mdecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// on rend la rponse
return new Reponse(0, Static.getListMapForRvs(rvs));
ligne 31 : on rend un objet List<Map<String,Object>> au lieu d'un objet List<Rv>. Rappelons la dfinition de la
classe [Rv] :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// caractristiques d'un Rv
@Temporal(TemporalType.DATE)
private Date jour;
// un rv est li un client
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// un rv est li un crneau
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// cls trangres
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
...
28. }
http://tahe.developpez.com
93/325
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where
cr.medecin.id=?1 and rv.jour=?2")
De jointures sont faites explicitement pour ramener les champs [client] et [creneau]. Par ailleurs cause de la jointure
[cr.medecin.id=?1], nous aurons galement le mdecin. Le mdecin va donc apparatre dans la chane JSON de chaque rendez-vous.
Or cette information duplique est en outre inutile. Revenons au code de la mthode :
// Rv --> Map
public static Map<String, Object> getMapForRv(Rv rv) {
// qq chose faire ?
if (rv == null) {
return null;
}
// dictionnaire <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("client", rv.getClient());
hash.put("creneau", getMapForCreneau(rv.getCreneau()));
// on rend le dictionnaire
return hash;
ligne 11 : nous reprenons le dictionnaire de l'objet [Creneau] que nous avons prsent prcdemment ;
http://tahe.developpez.com
94/325
2.12.11
L'URL [/getAgendaMedecinJour/{idMedecin}/{jour}]
L'URL [/getAgendaMedecinJour/{idMedecin}/{jour}]
[RdvMedecinsController] :
est
traite
par
la
mthode
suivante
du
contrleur
32. }
[medecin] : le mdecin propritaire de l'agenda. On a gard cette information car elle n'est prsente qu'une fois alors que
dans les cas prcdents, elle tait rpte dans chaque chane JSON ;
http://tahe.developpez.com
95/325
[creneauxMedecin] : la liste des crneaux du mdecin avec un ventuel rendez-vous sur ce crneau ;
lignes 9-10 : on utilise les dictionnaires dj tudis pour les types [Creneau] et [Rv] qui n'embarquent donc pas d'objet
[Medecin] ;
http://tahe.developpez.com
96/325
2.12.12
L'URL [/getMedecinById/{id}]
http://tahe.developpez.com
97/325
2.12.13
L'URL [/getClientById/{id}]
http://tahe.developpez.com
98/325
2.12.14
L'URL [/getCreneauById/{id}]
2.12.15
L'URL [/getRvById/{id}]
http://tahe.developpez.com
99/325
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14. }
http://tahe.developpez.com
100/325
2.12.16
L'URL [/ajouterRv]
Il n'y a l rien qui n'ait t dj vu. Ligne 41, on rend le rendez-vous qui a t ajout ligne 36.
Les rsultats obtenus ressemblent ceci avec le client [Advanced Rest Client] :
http://tahe.developpez.com
101/325
0
1
4
http://tahe.developpez.com
102/325
2.12.17
L'URL [/supprimerRv]
http://tahe.developpez.com
103/325
1
2
http://tahe.developpez.com
104/325
Nous en avons termin avec le contrleur. Nous voyons maintenant comment configurer le projet.
2.12.18
package rdvmedecins.web.config;
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 cherchs dans le package [rdvmedecins.web] et ses
descendants. C'est ainsi que seront dcouverts les composants :
[@RestController RdvMedecinsController] dans le package [rdvmedecins.web.controllers] ;
[@Component ApplicationModel] dans le package [rdvmedecins.web.models] ;
ligne 11 : on importe la classe [DomainAndPersistenceConfig] qui configure le projet [rdvmedecins-metier-dao] afin
d'avoir accs aux beans de ce projet ;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class })
public class AppConfig {
}
http://tahe.developpez.com
105/325
2.12.19
package rdvmedecins.web.boot;
import org.springframework.boot.SpringApplication;
import rdvmedecins.web.config.AppConfig;
public class Boot {
Ligne 10, la mthode statique [SpringApplication.run] est excute avec comme premier paramtre, la classe [AppConfig] de
configuration du projet. Cette mthode va procder l'auto-configuration du projet, lancer le serveur Tomcat embarqu dans les
dpendances et y dployer le contrleur [RdvMedecinsController].
Les logs l'excution sont les suivants :
1. .
____
_
__ _ _
2. /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
3. ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
4. \\/ ___)| |_)| | | | | || (_| | ) ) ) )
5.
' |____| .__|_| |_|_| |_\__, | / / / /
6. =========|_|==============|___/=/_/_/_/
7. :: Spring Boot ::
(v1.0.0.RELEASE)
8.
9. 2014-06-12 17:30:41.261 INFO 9388 --- [
main] rdvmedecins.web.boot.Boot
: Starting Boot on Gportpers3 with PID 9388 (D:\data\istia-1314\polys\istia\angularjsspring4\dvp\rdvmedecins-webapi\target\classes started by ST)
10. 2014-06-12 17:30:41.306 INFO 9388 --- [
main]
ationConfigEmbeddedWebApplicationContext : Refreshing
org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@a1e932
e: startup date [Thu Jun 12 17:30:41 CEST 2014]; root of context hierarchy
11. 2014-06-12 17:30:42.058 INFO 9388 --- [
main] o.s.b.f.s.DefaultListableBeanFactory
: Overriding bean definition for bean
'org.springframework.boot.autoconfigure.AutoConfigurationPackages': replacing [Generic bean:
class [org.springframework.boot.autoconfigure.AutoConfigurationPackages$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
[org.springframework.boot.autoconfigure.AutoConfigurationPackages$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 17:30:42.866 INFO 9388 --- [
main]
trationDelegate$BeanPostProcessorChecker : Bean
'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type
[class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$
http://tahe.developpez.com
106/325
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
http://tahe.developpez.com
107/325
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
rdvmedecins.web.models.Reponse
rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String)
2014-06-12 17:30:46.826 INFO 9388 --- [
main]
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped
"{[/getAllMedecins],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}"
onto public rdvmedecins.web.models.Reponse
rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins()
2014-06-12 17:30:46.826 INFO 9388 --- [
main]
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getMedecinById/
{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public
rdvmedecins.web.models.Reponse
rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long)
2014-06-12 17:30:46.827 INFO 9388 --- [
main]
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCreneauById/
{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public
rdvmedecins.web.models.Reponse
rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long)
2014-06-12 17:30:46.827 INFO 9388 --- [
main]
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getClientById/
{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public
rdvmedecins.web.models.Reponse
rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long)
2014-06-12 17:30:46.827 INFO 9388 --- [
main]
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getRvById/
{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public
rdvmedecins.web.models.Reponse
rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long)
2014-06-12 17:30:46.827 INFO 9388 --- [
main]
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped
"{[/getAllClients],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto
public rdvmedecins.web.models.Reponse
rdvmedecins.web.controllers.RdvMedecinsController.getAllClients()
2014-06-12 17:30:46.827 INFO 9388 --- [
main]
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped
"{[/ajouterRv],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF8],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse
rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouter
Rv)
2014-06-12 17:30:46.828 INFO 9388 --- [
main]
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped
"{[/supprimerRv],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF8],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse
rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSuppr
imerRv)
2014-06-12 17:30:46.851 INFO 9388 --- [
main]
o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class
org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-12 17:30:46.851 INFO 9388 --- [
main]
o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type
[class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-12 17:30:47.131 INFO 9388 --- [
main] o.s.j.e.a.AnnotationMBeanExporter
: Registering beans for JMX exposure on startup
2014-06-12 17:30:47.169 INFO 9388 --- [
main]
s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-06-12 17:30:47.170 INFO 9388 --- [
main] rdvmedecins.web.boot.Boot
: Started Boot in 6.302 seconds (JVM running for 6.906)
2014-06-12 17:30:55.520 INFO 9388 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]
: Initializing Spring FrameworkServlet 'dispatcherServlet'
2014-06-12 17:30:55.520 INFO 9388 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet
: FrameworkServlet 'dispatcherServlet': initialization started
2014-06-12 17:30:55.538 INFO 9388 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet
: FrameworkServlet 'dispatcherServlet': initialization completed in 18 ms
http://tahe.developpez.com
108/325
ligne 52 : la servlet de Spring MVC [DispatcherServlet] est prte rpondre aux demandes de clients web ;
Nous avons dsormais un service web oprationnel interrogeable avec un client web. Nous abordons maintenant la scurisation de
ce service : nous voulons que seules certaines personnes puissent grer les rendez-vous des mdecins. Nous allons utiliser pour cela
le framework Spring Security, une branche de l'cosystme Spring.
2.13
Nous allons de nouveau importer un guide Spring en suivant les tapes 1 3 ci-dessous :
2.13.1
Configuration Maven
Le projet [3] est un projet Maven. Examinons son fichier [pom.xml] pour connatre ses dpendances :
http://tahe.developpez.com
109/325
1.
<parent>
2.
<groupId>org.springframework.boot</groupId>
3.
<artifactId>spring-boot-starter-parent</artifactId>
4.
<version>1.1.1.RELEASE</version>
5.
</parent>
6.
7.
<dependencies>
8.
<dependency>
9.
<groupId>org.springframework.boot</groupId>
10.
<artifactId>spring-boot-starter-thymeleaf</artifactId>
11.
</dependency>
12.
<dependency>
13.
<groupId>org.springframework.boot</groupId>
14.
<artifactId>spring-boot-starter-security</artifactId>
15.
</dependency>
16. </dependencies>
2.13.2
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click <a th:href="@{/hello}">here</a> to see a greeting.
</p>
</body>
</html>
http://tahe.developpez.com
110/325
les attributs [th:xx] sont des attributs Thymeleaf. Ils sont interpts par Thymeleaf avant que la page HTML ne soit
envoye au client. Celui ne les voit pas ;
ligne 12 : l'attribut [th:href="@{/hello}"] va gnrer l'attribut [href] de la balise <a>. La valeur [@{/hello}] va gnrer le
chemin [<context>/hello] o [context] est le contexte de l'application web ;
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
ligne 9 : L'attribut [th:inline="text"] va gnrer le texte de la balise <h1>. Ce texte contient une expression $ qui doit tre
value. L'lment [[${#httpServletRequest.remoteUser}]] est la valeur de l'attribut [RemoteUser] de la requte HTTP
courante. C'est le nom de l'utilisateur connect ;
ligne 10 : un formulaire HTML. L'attribut [th:action="@{/logout}"] va gnrer l'attribut [action] de la balise [form]. La
valeur [@{/logout}] va gnrer le chemin [<context>/logout] o [context] est le contexte de l'application web ;
</html>
http://tahe.developpez.com
111/325
2.
3. <html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extrasspringsecurity3">
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>
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" />
</label>
</div>
<div>
<label> Password: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
</form>
</body>
ligne 9 : l'attribut [th:if="${param.error}"] fait que la balise <div> ne sera gnre que si l'URL qui affiche la page de login
contient le paramtre [error] (http://context/login?error);
ligne 10 : l'attribut [th:if="${param.logout}"] fait que la balise <div> ne sera gnre que si l'URL qui affiche la page de
login contient le paramtre [logout] (http://context/login?logout);
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 ;
</html>
http://tahe.developpez.com
112/325
2.13.3
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
http://tahe.developpez.com
113/325
URL
vue
/,
/home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html
Le suffixe [html] et le dossier [templates] sont les valeurs par dfaut utilises par Thymeleaf. Elles peuvent tre changes par
configuration. Le dossier [templates] doit tre la racine du Classpath du projet :
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.
2.13.4
package hello;
import org.springframework.context.annotation.Configuration;
import
org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
5.
6.
7.
8.
9. @Configuration
10. @EnableWebMvcSecurity
11. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
12.
@Override
13.
protected void configure(HttpSecurity http) throws Exception {
14.
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
15.
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
16.
}
http://tahe.developpez.com
114/325
17.
18.
19.
20.
21.
22. }
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
URL
rgle
/, /home
code
http.authorizeRequests().antMatchers("/", "/home").permitAll()
ligne 15 : dfinit la mthode d'authentification. L'authentification se fait via un formulaire d'URL [/login] accessible tous
[http.formLogin().loginPage("/login").permitAll()]. La dconnexion (logout) est galement accessible tous.
lignes 19-21 : redfinissent la mthode [configure(AuthenticationManagerBuilder auth)] qui gre les utilisateurs ;
ligne 20 : l'autentification se fait avec des utilisateurs dfinis en " dur " [auth.inMemoryAuthentication()]. Un utilisateur est
ici dfini avec le login [user], le mot de passe [password] et le rle [USER]. On peut accorder les mmes droits des
utilisateurs ayant le mme rle ;
2.13.5
Classe excutable
package hello;
ligne 8 : l'annotation [@EnableAutoConfiguration] demande Spring Boot (ligne 3) de faire la configuration que le
dveloppeur n'aura pas fait explicitement ;
import
import
import
import
org.springframework.boot.autoconfigure.EnableAutoConfiguration;
org.springframework.boot.SpringApplication;
org.springframework.context.annotation.ComponentScan;
org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
http://tahe.developpez.com
115/325
2.13.6
Tests de l'application
Commenons par demander l'URL [/] qui est l'une des quatre URL acceptes. Elle est associe la vue [/templates/home.html] :
L'URL demande [/] est accessible tous. C'est pourquoi nous l'avons obtenue. Le lien [here] est le suivant :
Click <a href="/hello">here</a> to see a greeting.
L'URL [/hello] va tre demande lorsqu'on va cliquer sur le lien. Celle-ci est protge :
URL
rgle
/, /home
code
http.authorizeRequests().antMatchers("/", "/home").permitAll()
Il faut tre authentifi pour l'obtenir. Spring Security va alors rediriger le navigateur client vers la page d'authentification. D'aprs la
configuration vue, c'est la page d'URL [/login]. Celle-ci est accessible tous :
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
http://tahe.developpez.com
116/325
ligne 7, un champ cach apparat qui n'est pas dans la page [login.html] d'origine. C'est Thymeleaf qui l'a ajout. Ce code
appel CSRF (Cross Site Request Forgery) vise liminer une faille de scurit. Ce jeton doit tre renvoy Spring
Security avec l'authentification pour que cette dernire soit accepte ;
Nous nous souvenons que seul l'utilisateur user/password est reconnu par Spring Security. Si nous entons autre chose en [2], nous
obtenons la mme page avec un message d'erreur en [3]. Spring Security a redirig le navigateur vers l'URL
[http://localhost:8080/login?error]. La prsence du paramtre [error] a dclench l'affichage de la balise :
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 :
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Dans notre association URL / vues, nous n'avons rien dfini pour l'URL [/logout]. Que va-t-il se passer ? Essayons :
http://tahe.developpez.com
117/325
2.13.7
Conclusion
Dans l'exemple prcdent, nous aurions pu crire l'application web d'abord puis la scuriser ensuite. Spring Security n'est pas
intrusif. On peut mettre en place la scurit d'une application web dj crite. Par ailleurs, nous avons dcouvert les points suivants :
Toutes ces conclusions reposent sur des comportements par dfaut de Spring Security. Ces comportements peuvent tre changs
par configuration en redfinissant certaines mthodes de la classe [WebSecurityConfigurerAdapter].
Le tutoriel prcdent nous aidera peu dans la suite. Nous allons en effet utiliser :
une base de donnes pour stocker les utilisateurs, leurs mots de passe et leurs rles ;
une authentification par entte HTTP ;
On trouve assez peu de tutoriels pour ce qu'on veut faire ici. La solution qui va tre propose est un assemblage de codes trouvs ici
et l.
2.14
2.14.1
La base de donnes [rdvmedecins] volue pour prendre en compte les utilisateurs, leurs mots de passe et leur rles. Trois nouvelles
tables apparaissent :
http://tahe.developpez.com
118/325
ID : cl primaire ;
ID : cl primaire ;
VERSION : colonne de versioning de la ligne ;
NAME : nom du rle. Par dfaut, Spring Security attend des noms de la forme ROLE_XX, par exemple ROLE_ADMIN
ou ROLE_GUEST ;
ID : cl primaire ;
Parce que nous modifions la base de donnes, l'ensemble des couches du projet [mtier, DAO, JPA] doit tre modifi :
Couche
[web /
JSON]
Couche
[metier]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7 4
Spring
http://tahe.developpez.com
119/325
2.14.2
2.14.3
Couche
[web /
JSON]
Couche
[metier]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7 4
Spring
La couche JPA dfinit trois nouvelles entits :
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "USERS")
http://tahe.developpez.com
120/325
ligne 9 : la classe tend la classe [AbstractEntity] dj utilise pour les autres entits ;
lignes 13-15 : on ne prcise pas de nom pour les colonnes parce qu'elles portent le mme nom que les champs qui leur
sont associs ;
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
private static final long serialVersionUID = 1L;
// proprits
private String name;
// constructeurs
public Role() {
}
public Role(String name) {
this.name = name;
}
// identit
@Override
public String toString() {
return String.format("Role[%s]", name);
}
// getters et setters
...
}
http://tahe.developpez.com
121/325
package rdvmedecins.entities;
2.14.4
import
import
import
import
javax.persistence.Entity;
javax.persistence.JoinColumn;
javax.persistence.ManyToOne;
javax.persistence.Table;
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
private static final long serialVersionUID = 1L;
// un UserRole rfrence un User
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// un UserRole rfrence un Role
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// getters et setters
...
}
Couche
[web /
JSON]
Couche
[metier]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7 4
Spring
La couche [DAO] s'enrichit de trois nouveaux [Repository] :
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Role;
http://tahe.developpez.com
122/325
7. import rdvmedecins.entities.User;
8.
9. public interface UserRepository extends CrudRepository<User, Long> {
10.
11.
// liste des rles d'un utilisateur identifi par son id
12.
@Query("select ur.role from UserRole ur where ur.user.id=?1")
13.
Iterable<Role> getRoles(long id);
14.
15.
// liste des rles d'un utilisateur identifi par son login et son mot de passe
16.
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
17.
Iterable<Role> getRoles(String login, String password);
18.
19.
// recherche d'un utilisateur via son login
20.
User findUserByLogin(String login);
21. }
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface RoleRepository extends CrudRepository<Role, Long> {
// recherche d'un rle via son nom
Role findRoleByName(String name);
}
package rdvmedecins.security;
ligne 5 : l'interface [UserRoleRepository] se contente d'tendre l'interface [CrudRepository] sans lui ajouter de nouvelles
mthodes ;
2.14.5
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
Spring Security impose la cration d'une classe implmentant l'interface [UsersDetail] suivante :
http://tahe.developpez.com
123/325
package rdvmedecins.security;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class AppUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
// proprits
private User user;
private UserRepository userRepository;
// constructeurs
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------interface
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : userRepository.getRoles(user.getId())) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
http://tahe.developpez.com
124/325
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69. }
Spring Security impose galement l'existence d'une classe implmentant l'interface [AppUserDetailsService] :
package rdvmedecins.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
http://tahe.developpez.com
125/325
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
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 : implmentation de la mthode [loadUserByUsername] de l'interface [UserDetailsService] (ligne 10). Le
paramtre 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 lance ;
ligne 24 : un objet [AppUserDetails] est construit et rendu. Il est bien de type [UserDetails] (ligne 16) ;
2.14.6
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// on cherche l'utilisateur via son login
User user = userRepository.findUserByLogin(login);
// trouv ?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// on rend les dtails de l'utilsateur
return new AppUserDetails(user, userRepository);
}
}
Tout d'abord, nous crons une classe excutable [CreateUser] capable de crer un utilisateur avec un rle :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
package rdvmedecins.security;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
import
import
import
import
import
import
import
rdvmedecins.config.DomainAndPersistenceConfig;
rdvmedecins.security.Role;
rdvmedecins.security.RoleRepository;
rdvmedecins.security.User;
rdvmedecins.security.UserRepository;
rdvmedecins.security.UserRole;
rdvmedecins.security.UserRoleRepository;
http://tahe.developpez.com
126/325
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68. }
ligne 17 : la classe attend trois arguments dfinissant un utilisateur : son login, son mot de passe, son rle ;
lignes 25-27 : les trois paramtres sont rcuprs ;
ligne 29 : le contexte Spring est construit partir de la classe de configuration [DomainAndPersistenceConfig]. Cette
classe existait dj dans le projet prcdent. Elle doit voluer de la faon suivante :
1.
2.
3.
4.
5.
6.
7.
8.
ligne 1 : il faut indiquer qu'il y a maintenant des composants [Repository] dans le paquetage
[rdvmedecins.security] ;
ligne 4 : il faut indiquer qu'il y a maintenant des entits JPA dans le paquetage [rdvmedecins.security] ;
http://tahe.developpez.com
127/325
lignes 30-32 : on rcupre les rfrences des trois [Repository] qui peuent nous tre utiles pour crer l'utilisateur ;
ligne 34 : on regarde si le rle existe dj ;
lignes 36-38 : si ce n'est pas le cas, on le cre en base. Il aura un nom du type [ROLE_XX] ;
ligne 40 : on regarde si le login existe dj ;
lignes 42-49 : si le login n'existe pas, on le cre 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 [pom.xml] inclut une nouvelle dpendance :
1.
<dependency>
2.
<groupId>org.springframework.boot</groupId>
3.
<artifactId>spring-boot-starter-security</artifactId>
4. </dependency>
Lorsqu'on excute la classe avec les arguments [x x guest], on obtient en base les rsultats suivants :
Table [USERS]
Table [ROLES]
Table [USERS_ROLES]
http://tahe.developpez.com
128/325
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
package rdvmedecins.security;
import java.util.List;
import
import
import
import
import
import
import
import
org.junit.Assert;
org.junit.Test;
org.junit.runner.RunWith;
org.springframework.beans.factory.annotation.Autowired;
org.springframework.boot.test.SpringApplicationConfiguration;
org.springframework.security.core.authority.SimpleGrantedAuthority;
org.springframework.security.crypto.bcrypt.BCrypt;
org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
@Autowired
private UserRepository userRepository;
@Autowired
private AppUserDetailsService appUserDetailsService;
@Test
public void findAllUsersWithTheirRoles() {
Iterable<User> users = userRepository.findAll();
for (User user : users) {
System.out.println(user);
display("Roles :", userRepository.getRoles(user.getId()));
}
}
@Test
public void findUserByLogin() {
// on rcupre l'utilisateur [admin]
User user = userRepository.findUserByLogin("admin");
// on vrifie que son mot de passe est [admin]
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// on vrifie le rle de admin / admin
List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin",
user.getPassword()));
44.
Assert.assertEquals(1L, roles.size());
45.
Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
46.
}
47.
48.
@Test
49.
public void loadUserByUsername() {
50.
// on rcupre l'utilisateur [admin]
51.
AppUserDetails userDetails = (AppUserDetails)
appUserDetailsService.loadUserByUsername("admin");
52.
// on vrifie que son mot de passe est [admin]
53.
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
54.
// on vrifie le rle de admin / admin
55.
@SuppressWarnings("unchecked")
http://tahe.developpez.com
129/325
56.
lignes 27-34 : test visuel. On affiche tous les utilisateurs avec leurs rles ;
lignes 36-46 : on vrifie que l'utilisateur [admin] a le mot de passe [admin] et le rle [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 mthode
[ BCrypt.checkpw] permet de vrifier que le mot de passe en clair une fois crypt est bien gal celui qui est en base ;
lignes 48-59 : on vrifie que l'utilisateur [admin] a le mot de passe [admin] et le rle [ROLE_ADMIN] en utilisant le
service [appUserDetailsService] ;
2.14.7
User[guest,guest,$2a$10$Gzyp54mvkgMH0SPQkXo.Zeu.DvJ/Ql50PRXLf2FkolMTs7fr6A2J2]
Roles :
Role[ROLE_GUEST]
User[admin,admin,$2a$10$m79V6MKt9GPDdpjSulyqReqUioqYwXy8ollt/.ia15FhX2fym3AE6]
Roles :
Role[ROLE_ADMIN]
User[user,user,$2a$10$ph5y/1H89YC11oGVLB49fON.dZwnu44bAOKMK1FFl//xjAvsr/Ese]
Roles :
Role[ROLE_USER]
User[x,x,$2a$10$dAKd2SuQplR1iFhoBUUFs.XiA0lYxNqOmrkv97Gbr5KBoHzEi/5HG]
Roles :
Role[ROLE_GUEST]
Conclusion intermdiaire
L'ajout des classes ncessaires Spring Security a pu se faire avec peu de modifications du projet originel. Rappelons-les :
Ce cas trs favorable dcoule du fait que les trois tables ajoutes dans la base de donnes sont indpendantes des tables existantes.
On aurait mme pu les mettre dans une base de donnes spare. Ceci a t possible parce qu'on a dcid qu'un utilisateur avait une
existence indpendante des mdecins et des clients. Si ces derniers avaient t des utilisateurs potentiels, il aurait fallu crer des liens
entre la table [USERS] et les tables [MEDECINS] et [CLIENTS]. Cela aurait eu alors un impact important sur le projet existant.
2.14.8
Couche
[web /
JSON]
Couche
[metier]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
7 4
Spring
http://tahe.developpez.com
130/325
Les seules modifications sont faire dans le package [rdvmedecins.web.config] o il faut configurer Spring Security. Nous avons
dj rencontr une classe de configuration de Spring Security :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
package hello;
import org.springframework.context.annotation.Configuration;
import
org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
package rdvmedecins.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpMethod;
import
org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
7. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
10. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
11.
12. import rdvmedecins.security.AppUserDetailsService;
13.
14. @EnableAutoConfiguration
15. @EnableWebSecurity
16. public class SecurityConfig extends WebSecurityConfigurerAdapter {
17.
@Autowired
18.
private AppUserDetailsService appUserDetailsService;
19.
http://tahe.developpez.com
131/325
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38. }
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// l'authentification est faite par le bean [appUserDetailsService]
// le mot de passe est crypt par l'algorithme de hachage Bcrypt
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// le mot de passe est transmis par le header Authorization: Basic xxxx
http.httpBasic();
// seul le rle ADMIN peut utiliser l'application
http.authorizeRequests() //
.antMatchers("/", "/**") // toutes les URL
.hasRole("ADMIN");
}
o code est le codage de la chane login:password par l'algorithme Base64. Par exemple, le codage Base64 de la chane
admin:admin est YWRtaW46YWRtaW4=. Donc l'utilisateur de login [admin] et de mot de passe [admin] enverra l'entte
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 rle [ROLE_ADMIN].
Cela veut dire qu'un utilisateur n'ayant pas ce rle ne peut accder au service web ;
1.
2.
3.
4.
5.
6.
7.
8.
9.
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
http://tahe.developpez.com
132/325
10.
11.
12.
13.
14.
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class })
public class AppConfig {
la modification a lieu ligne 11 : on indique qu'il y a maintennat deux fichiers de configuration exploiter
[DomainAndPersistenceConfig] et [SecurityConfig].
2.14.9
Nous allons tester le service web avec le client Chrome [Advanced Rest Client]. Nous allons avoir besoin de prciser l'entte HTTP
d'authentification :
Authorization:Basic code
o [code] est le code Base64 de la chane [login:password]. Pour gnrer ce code, on peut utiliser le programme suivant :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
package rdvmedecins.helpers;
import org.springframework.security.crypto.codec.Base64;
public class Base64Encoder {
public static void main(String[] args) {
// on attend deux arguments : login password
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// on rcupre les deux arguments
String chane = String.format("%s:%s", args[0], args[1]);
// on encode la chane
byte[] data = Base64.encode(chane.getBytes());
// on affiche son encodage Base64
System.out.println(new String(data));
}
}
http://tahe.developpez.com
133/325
Maintenant que nous savons gnrer l'entte HTTP d'authentification, nous lanons le service web maintenant scuris. Puis avec le
client Chrome [Advanced Rest Client], nous demandons la liste des tous les mdecins :
1
2
3
http://tahe.developpez.com
134/325
Tentons maintenant une requte HTTP avec un entte d'authentification incorrect. La rponse est alors la suivante :
http://tahe.developpez.com
135/325
Maintenant, essayons l'utilisateur user / user. Il existe mais n'a pas accs au service web. Si nous excutons le programme
d'encodage Base64 avec les deux arguments [user user] :
http://tahe.developpez.com
136/325
2.15
Conclusion
http://tahe.developpez.com
137/325
Un service web scuris est maintenant oprationnel. On verra qu'il devra tre modifi suite des problmes qui vont se rvler la
construction du client Angular JS. Mais nous attendrons de rencontrer le problme pour le rsoudre. Nous allons maintenant
construire le client Angular qui va offrir une interface web pour grer les rendez-vous des mdecins.
http://tahe.developpez.com
138/325
3 Le client Angular JS
3.1
Deux rfrences ont t donnes pour le framework Angular JS au dbut de ce document. Nous les redonnons ici :
[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
[http://www.apress.com/downloadable/download/sample/sample_id/1527/];
[ref2] : la documentation officielle d'Angular JS [https://docs.angularjs.org/guide];
Angular JS mrite un livre lui tout seul. Celui d'Adam Freeman a plus de 600 pages et elles ne sont pas gaspilles. Nous allons
dcrire une application Angular et au cours de cette description nous serons amens parler des fondamentaux de ce framework.
Nanmoins, nous nous en tiendrons aux seules explications ncessaires la comprhension de la solution propose. Angular est un
framework extrmement riche et il existe de nombreuses solutions pour arriver au mme rsultat. C'est une difficult car lorsqu'on
dbute, on ne sait pas si on utilise une solution moins bonne ou meilleure qu'une autre. C'est le cas de la solution propose ici. Elle
pourrait tre crite diffremment et peut-tre avec de meilleures pratiques.
3.2
L'architecture du client Angular ressemble celle d'une application web MVC classique avec quelques diffrences. Une application
web Spring MVC a par exemple l'architecture suivante :
Navigateur
4b
Vue1
Vue2
Vuen
Modles
Actions
couches
[mtier, DAO, JPA]
Donnes
2c
l'action choisie peut exploiter les paramtres parami que la servlet [Dispatcher Servlet] lui a transmis. Ceux-ci peuvent
provenir de plusieurs sources :
dans le traitement de la demande de l'utilisateur, l'action peut avoir besoin de la couche [metier] [2b]. Une fois la
demande du client traite, celle-ci peut appeler diverses rponses. Un exemple classique est :
l'action demande une certaine vue de s'afficher [3]. Cette vue va afficher des donnes qu'on appelle le modle de la
vue. C'est le M de MVC. L'action va crer ce modle M [2c] et demander une vue V de s'afficher [3] ;
3. rponse - la vue V choisie utilise le modle M construit par l'action pour initialiser les parties dynamiques de la rponse HTML
qu'elle doit envoyer au client puis envoie cette rponse.
L'architecture de notre client Angular sera analogue avec une terminologie un peu diffrente. Tout d'abord les applications Angular
sont gnralement des applications web page unique (APU) ou Single Page Application (SPA) :
http://tahe.developpez.com
139/325
l'utilisateur demande l'URL initiale de l'application sous la forme : http://machine:port/contexte. Le navigateur va interroger
un serveur web pour obtenir le document demand. Celui-ci est une page HTML stylise par du CSS et rendue dynamique
par du Javascript ;
ensuite l'utilisateur va interagir avec les vues qui lui sont prsentes. On peut distinguer diverses sortes d'interactions :
celles qui ne ncessitent aucune interaction avec l'extrieur, par exemple cacher / montrer des lments de la vue. Elle
sont traites par le Javascript embarqu ;
celles qui ncessitent des donnes provenant d'un service web distant. Elles vont tre rcupres par un appel AJAX
(Asynchronous Javascript And Xml), un modle va tre construit et une vue affiche ;
celles qui ncessitent une autre vue que la vue initiale. Elle va tre demande par un appel Ajax au serveur qui a
dlivr la page initiale. Puis le processus prcdent va se rpter. La page obtenue va tre mise en cache dans le
navigateur. Au prochain appel, elle ne sera pas demande 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 prsentons maintenant l'architecture de l'application au sein du navigateur. Nous oublions le serveur HTML qui dlivre les
pages HTML de l'application. Pour l'explication, on peut considrer qu'elles sont toutes prsentes au sein du cache du navigateur.
couche [services]
Routeur
10
Utilisateur
2
V1
C1
6
8
Service 1
Service 2
M1
Donnes
rseau
11
Vn
Cn
Mn
DAO
en [3], les donnes sont cherches sur le rseau, souvent auprs de services web ;
http://tahe.developpez.com
140/325
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 faon suivante :
L'utilisateur a maintenant une vue V1 devant lui. Imaginons que c'est un formulaire. Il le remplit puis le valide :
en [5], cet vnement va tre trait par l'une des mthodes du contrleur C1 ;
Si l'vnement n'entrane qu'un simple changement de la vue V1 (cacher / montrer des zones), le contrleur C1 va modifier le
modle 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'vnement ncessite des donnes externes :
La couche [Services] est diffrente des couches [Services] auxquelles on est habitu. En dveloppement web, ct serveur, on a le
plus souvent l'architecture en couches suivante :
Couche
[web]
Couche
[metier]
Couche
[DAO]
Couche
[JPA]
Pilote
[JDBC]
SGBD
Ci-dessus, la couche [web] ne communique avec la couche [DAO] qu'au travers de la couche [mtier]. Rien ne nous empcherait
d'injecter dans la couche [web] une rfrence sur la couche [DAO] qui permettrait cette communication. Mais on se l'interdit.
Avec Angular, on ne se l'interdit pas. L'architecture devient alors la suivante :
http://tahe.developpez.com
141/325
couche [services]
Service 1
Service ...
1
Mtier
Donnes
rseau
DAO
3.3
en [1], la couche [prsentation] 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.
Les vues du client Angular ont dj t prsentes au paragraphe 1.3.3, page 11. Pour faciliter la lecture de ce nouveau chapitre,
nous les redonnons ici. La premire vue est la suivante :
9
10
11
12
13
14
en [6], la page d'entre de l'application. Il s'agit d'une application de prise de rendez-vous pour des mdecins ;
en [7], une case cocher qui permet d'tre ou non en mode [debug]. Ce dernier se caractrise par la prsence du cadre [8]
qui affiche le modle de la vue courante ;
en [9], une dure d'attente artificielle en millisecondes. Elle vaut 0 par dfaut (pas d'attente). Si N est la valeur de ce temps
d'attente, toute action de l'utilisateur sera excute aprs 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 prcd, c'est [http://localhost:8080];
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 rle (ADMIN) et user/user avec un rle (USER). Seul le rle ADMIN a le droit d'utiliser
l'application. Le rle USER n'est l que pour montrer ce que rpond le serveur dans ce cas d'utilisation ;
http://tahe.developpez.com
142/325
en [1], on se connecte ;
une fois connect, on peut choisir le mdecin avec lequel on veut un rendez-vous [2] et le jour de celui-ci [3] ;
on demande en [4] voir l'agenda du mdecin choisi pour le jour choisi ;
http://tahe.developpez.com
143/325
http://tahe.developpez.com
144/325
Une fois le rendez-vous valid, on est ramen automatiquement l'agenda o le nouveau rendez-vous est dsormais inscrit. Ce
rendez-vous pourra tre ultrieurement supprim [7].
Les principales fonctionnalits ont t dcrites. Elles sont simples. Celles qui n'ont pas t dcrites sont des fonctions de navigation
pour revenir une vue prcdente. Terminons par la gestion de la langue :
http://tahe.developpez.com
145/325
3.4
Nous allons construire notre client Angular de faon progressive. Nous utilisons l'IDE Webstorm.
Crons un dossier vide [rdvmedecins-angular-v1] puis ouvrons-le avec Webstorm :
3
http://tahe.developpez.com
146/325
7
5
10
9
12
11
http://tahe.developpez.com
147/325
13
15
14
en [14], on configure la proprit [Javascript / Bower] qui va nous permettre de dclarer les bibliothques Javascript dont
nous avons besoin ;
en [15], dsigner le fichier [bower.json] que nous venons de crer ;
18
16
19
17
20
http://tahe.developpez.com
148/325
en [20], on la tlcharge ;
21
22
23
24
en [24], suivant la mme dmarche que prcdemment, on tlcharge les bibliothques suivantes :
angular-base64
angular-i18n
angular-route
angulartranslate
permet l'internationalisation des vues. C'est un projet https://github.com/angular-translate/angularindpendant d'Angular. Ici, deux langues seront translate
utilises : le franais et l'anglais ;
angular-uibootstrap-bower
bootstrap
footable
bootstrap-select
http://tahe.developpez.com
https://code.angularjs.org/1.2.18/i18n/
http://silviomoreto.github.io/bootstrap-select/
149/325
25
26
3.5
http://tahe.developpez.com
150/325
en [1] et [2], nous crons un fichier HTML nomm [app-01] [3] et [4] ;
Le fichier [app-01.html] 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
<!DOCTYPE html>
<html>
<head>
<title>RdvMedecins</title>
<!-- META -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Angular client for RdvMedecins">
<meta name="author" content="Serge Tah">
<!-- le CSS -->
<link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- angular js -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstraptpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
</body>
</html>
http://tahe.developpez.com
151/325
Cette inspection systmatique du code avant son excution est conseille. Ici, cette dtection permet de dtecter toute erreur de
rfrence des fichiers CSS et JS. Si un chemin est incorrect, l'inspecteur de code le signalera.
en [3], la page peut tre charge dans un navigateur par un dbogueur. On obtient le rsultat suivant dans le navigateur :
4
5
en [4], la page [app-01.html] a t dlivre par un serveur interne Webstorm oprant ici sur le port 63342 ;
en [5], la console du dbogueur. Si des erreurs s'taient produites, elles seraient apparues ici. C'est galement l que vont
les affichages cran produits par l'instruction [console.log(expression)] du Javascript. Nous utiliserons abondamment
cette possibilit ;
Le mode dbogage permet de modifier la page dans Webstorm et de voir les rsultats 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>
http://tahe.developpez.com
152/325
3.6
Dcouverte de Bootstrap
Nous allons illustrer maintenant certaines des caractristiques de Bootstrap utilises dans l'application. Je n'ai qu'une connaissance
limite de ce framework, obtenue par des copier / coller de codes trouvs sur Internet. J'expliquerai le rle des classes CSS que je
crois comprendre. Je m'abstiendrai de commenter les autres.
3.6.1
Exemple 1
Dans Angular, les oprations qui vont chercher de l'information l'extrieur sont asynchrones. Cela signifie que l'opration est
lance et qu'il y a retour immdiat la vue avec laquelle l'utilisateur peut continuer interagir. L'application est avertie de la fin de
l'opration par un vnement. Cet vnement est trait par une fonction JS qui peut alors enrichir la vue actuelle ou en changer. Si
l'opration est susceptible d'tre longue, il est utile d'offrir l'utilisateur la possibilit de l'annuler. Nous la lui offrirons
systmatiquement. Pour cela, nous utiliserons un bandeau Bootstrap :
Pour obtenir ce rsultat, nous dupliquons [app-01.html] dans [app-02.html] et nous modifions les ligne suivantes :
1. <div class="container">
2.
<h1>Rdvmedecins - v1</h1>
3.
<div class="alert alert-warning">
4.
<h1>Opration en cours. Veuillez patienter...
5.
<button class="btn btn-primary pull-right">Annuler</button>
6.
<img src="assets/images/waiting.gif" alt=""/>
7.
</h1>
8.
</div>
9. </div>
3.6.2
ligne 1 : la classe CSS [container] dfinit une zone d'affichage l'intrieur du navigateur ;
ligne 3 : la classe CSS [alert] affiche une zone colore. La classe [alert-warning] utilise une couleur prdfinie ;
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 anime d'attente ;
Exemple 2
http://tahe.developpez.com
153/325
Pour obtenir ce rsultat, nous dupliquons [app-01.html] dans [app-03.html] 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/caduceus.jpg" alt="RvMedecins"/>
8.
</div>
9.
<div class="col-md-10">
10.
<h1>Les Mdecins associs</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 lments de formulaire. Par exemple :
Pour obtenir ce rsultat, nous dupliquons [app-01.html] dans [app-04.html] et nous modifions les ligne 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">
http://tahe.developpez.com
154/325
6.
7.
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbarcollapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
8.
9.
10.
11.
12.
13.
14.
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>
3.6.4
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 affiche par le navigateur, la barre de navigation va
rester en haut de l'cran ;
lignes 6-14 : dfinissent la zone [1]. C'est typiquement une srie de classes que je ne comprends pas. J'utilise le composant
tel quel ;
ligne 15 : dfinissent une zone 'responsive' de la barre de commande. Sur un smartphone, cette zone disparat 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'intrieur d'une classe [ form-group]
qui habille les lments d'un formulaire et chacune d'elles a la classe [form-control] ;
ligne 33 : la classe [btn] qu'on a dj rencontre, enrichie de la classe [btn-success] qui lui donne sa couleur verte ;
Exemple 4
http://tahe.developpez.com
155/325
Pour obtenir ce rsultat, nous dupliquons [app-01.html] dans [app-05.html] et nous ajoutons les ligne suivantes la barre de
commande :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
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 cidessous :
1
Des options de menu ont t installes en [1]. Pour obtenir ce rsultat, nous dupliquons [app-01.html] dans [app-06.html] et nous
ajoutons les ligne suivantes :
1. <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
2.
<div class="container">
http://tahe.developpez.com
156/325
3.
<div class="navbar-header">
4. ...
5.
</div>
6.
<!-- options de menu -->
7.
<div class="collapse navbar-collapse">
8.
<ul class="nav navbar-nav">
9.
<li class="active">
10.
<a href="">
11.
<span>Home</span>
12.
</a>
13.
</li>
14.
<li class="active">
15.
<a href="">
16.
<span>Agenda</span>
17.
</a>
18.
</li>
19.
<li class="active">
20.
<a href="">
21.
<span>Valider</span>
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>
3.6.6
les options de menu sont obtenues par les lignes 8-29. Ce sont l encore des lments d'une liste <ul>. La classe [active]
fait que le texte est brillant indiquant par l qu'on peut cliquer sur l'option.
Exemple 6
Nous prsenterons les mdecins et les clients dans des listes droulantes comme ci-dessous :
La liste droulante utilise n'est pas un composant natif Bootstrap. C'est le composant [bootstrap-select]
(http://silviomoreto.github.io/bootstrap-select/). Pour obtenir ce rsultat, nous dupliquons [app-01.html] dans [app-07.html] et
nous ajoutons les ligne suivantes :
http://tahe.developpez.com
157/325
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
<!DOCTYPE html>
<html>
<head>
...
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2><label for="medecins">Mdecins</label></h2>
<select id="medecins" data-style="btn btn-primary" class="selectpicker">
<option value="1">Mme Marie PELISSIER</option>
<option value="1">Mr Jacques BROMARD</option>
<option value="1">Mr Philippe JANDOT</option>
<option value="1">Mme Justine JACQUEMOT</option>
</select>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrapselect.min.js"></script>
<!-- script local -->
<script>
$('.selectpicker').selectpicker();
</script>
</body>
23.
24.
25.
26.
27.
28. </html>
3.6.7
Exemple 7
Pour afficher l'agenda d'un mdecin, nous allons utiliser un tableau 'responsive' fourni par la bibliothque JS [footable] :
http://tahe.developpez.com
158/325
Nous dupliquons [app-01.html] dans [app-08.html] et nous ajoutons les ligne suivantes :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
...
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
...
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="row alert alert-warning">
<div class="col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span>Crneau horaire</span>
</th>
<th>
<span>Client</span>
</th>
<th data-hide="phone">
<span>Action</span>
</th>
</thead>
<tbody>
<tr>
<td>
<span class='status-metro status-active'>
9h00-9h20
</span>
</td>
<td>
<span></span>
</td>
<td>
<a href="" class="status-metro status-active">
Rserver
</a>
</td>
</tr>
<tr>
<td>
<span class='status-metro status-suspended'>
9h20-9h40
</span>
</td>
<td>
<span>Mme Paule MARTIN</span>
</td>
<td>
<a href="" class="status-metro status-suspended">
Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
...
<script src="bower_components/footable/dist/footable.min.js" type="text/javascript"></script>
http://tahe.developpez.com
159/325
les lignes 2 et 60 sont dj prsentes dans [app-01.html]. Ce sont les fichiers CSS et JS fournis par la bibliothque
[footable] ;
la ligne 3 rfrence 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;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
Les styles [status-*] proviennent d'un exemple d'utilisation de la table [footable] trouv sur le site de la bibliothque.
3.6.8
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] ;
ligne 10 : la table HTML est formate par Bootstrap [class='table'] ;
ligne 13 : l'attribut [data-toggle] indique la colonne qui hberge le symbole [+/-] qui dplie / replie la ligne ;
ligne 19 : l'attribut [data-hide='phone'] indique que la colonne doit tre cache si l'cran a la taille d'un cran de tlphone.
On peut galement utiliser la valeur 'tablet' ;
Exemple 8
Pour aider l'utilisateur, nous allons crer des bulles d'aide (tooltip) autour des principaux composants des vues :
Pour obtenir ce rsultat, nous dupliquons [app-01.html] dans [app-09.html] et nous ajoutons les ligne suivantes :
1.
2.
3.
4.
5.
6.
7.
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container">
http://tahe.developpez.com
160/325
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
<h1>Rdvmedecins - v1</h1>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbarcollapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<!-- options de menu -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span tooltip="Retourne la page d'accueil" tooltipplacement="bottom">Home</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Affiche l'agenda" tooltip-placement="top">Agenda</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Valide le rendez-vous" tooltip-placement="right">Valider</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Annule l'opration en cours" tooltipplacement="left">Annuler</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstraptpls.min.js"></script>
<!-- script local -->
<script>
// --------------------- module Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>
Les bulles d'aide sont fournis par la bibliothque [angular-ui-bootstrap] qui s'appuie elle-mme sur la bibliothque [angular]. La ligne
50 importe la bibliothque [angular-ui-bootstrap]. Pour mettre en oeuvre les composants de la bibliothque [angular-ui-bootstrap], il
nous faut crer un module Angular. Ceci est fait aux lignes 52-55. Ces lignes dfinissent un module Angular nomm [rdvmedecins]
(1er paramtre). Un module Angular peut utiliser d'autres modules Angular. C'est ce qu'on appelle des dpendances du module.
Elles sont fournies dans un tableau comme second paramtre de la fonction [angular.module]. Ici, le module nomm [ui.bootstrap]
est fourni par la bibliothque [angular-ui-bootstrap]. C'est ce module qui va nous fournir les bulles d'aide.
La ligne 54 dfinit un module Angular. Par dfaut, cela n'a aucun effet sur la page. On indique que la page doit tre gre 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 analyse par Angular. Les attributs [tooltip] vont tre dcouverts et traits par le module
[ui.bootstrap].
http://tahe.developpez.com
161/325
3.6.9
Exemple 9
Pour aider l'utilisateur choisir le jour d'un rendez-vous, nous allons lui proposer un calendrier :
Comme pour les bulles d'aide, ce calendrier est fourni par la bibliothque [angular-ui-bootstrap]. Pour obtenir ce rsultat, nous
dupliquons [app-01.html] dans [app-10.html] et nous ajoutons les ligne suivantes :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div>
<pre>Date <em>{{jour | date:'fullDate'}}</em></pre>
<div class="row">
<div class="col-md-2">
<h4>Calendrier</h4>
http://tahe.developpez.com
162/325
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
Comme prcdemment, la page est associe un module Angular (lignes 2 et 28). Le calendrier est dfini par la balise
<datepicker> de la ligne 16 dfinie par la bibliothque [angular-ui-bootstrap] :
[ng-model='jour'] : les attrinuts [ng-*] sont des attributs Angular. L'attribut [ng-model] dsigne une donne qui va tre
place dans le modle de la vue. Lorsque l'utilisateur va cliquer sur une date, celle-ci sera place dans la variable [jour] du
modle. Cette variable est utilise ligne 10. La syntaxe {{expression}} permet d'valuer une expression compose
d'lments du modle. Ici {{jour}} va afficher la valeur de la variable [jour] du modle. Une caractristique 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 immdiatement affichs ligne 10. De faon gnrale, le fonctionnement est le
suivant :
une vue V est associe un modle M ;
Angular observe le modle M et met automatiquement jour la vue V lorsqu'il y a un changement de son modle
M;
La syntaxe {{jour|date}} est appele un filtre. Ce n'est pas la valeur de [jour] qui est affiche mais la valeur de [jour]
filtre par un filtre appel [date]. Ce filtre est prdfini dans Angular. Il sert formater des dates. Il admet des paramtres
prcisant le format dsir. 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 dfaut. Nous allons aborder son
internationalisation prochainement.
3.6.10
Conclusion
Nous avons prsent les lments du framework CSS Bootstrap que nous serons amens utiliser. C'taient des composants
passifs : leurs vnements n'taient pas grs. Ainsi un clic sur les boutons ou les liens ne faisait rien. Ces vnements seront grs
en Javascript. Il est possible d'utiliser ce langage sans l'aide de frameworks mais comme ce fut le cas ct serveur, certains
frameworks s'imposent ct client. C'est le cas du framework Angular JS qui amne avec lui une nouvelle faon d'aborder le
dveloppement des applications Javascript excutes par un navigateur. Nous le prsentons maintenant.
3.7
Dcouverte d'Angular JS
Nous allons illustrer maintenant certaines des caractristiques du framework Angular JS utilises dans l'application. Nous en avons
dj rencontr quelques unes :
une page HTML est propulse par Angular JS si on lui rattache un module :
<html ng-app="rdvmedecins">
Angular permet de crer de nouvelles balises et de nouveaux attributs HTML via des directives :
attributs : ng-app, ng-model, tooltip-placement, tooltip
balises : datepicker
http://tahe.developpez.com
163/325
{{jour|date:'fullDate'}}
une vue V affiche un modle M. Angular observe le modle M et met automatiquement jour la vue V lorsqu'il y a un
changement de son modle M. La valeur d'une variable du modle M est affiche dans la vue V par :
{{variable}}
Nous allons commencer par approfondir l'implmentation du Design Pattern Modle Vue Contrleur dans Angular. Rappelons
les liens qui existent entre-eux d'un point de vue architecture :
couche [services]
Routeur
10
Utilisateur
2
V1
C1
6
8
Service 1
Service 2
M1
Donnes
rseau
11
Vn
Cn
Mn
3.7.1
DAO
la vue V1 affiche le modle M1 construit par le contrleur C1. Ce dernier contient non seulement le modle M1 mais
galement les gestionnaires des vnements de la vue V1. On est dans le cycle 5, 8, 9 :
[5] : un vnement se produit dans la vue V1. Il est trait par le contrleur C1 ;
celui-ci fait son travail [6-7] puis construit le modle M1 [8] ;
[9] : la vue V1 affiche le nouveau modle M1. Comme nous l'avons dit, cette dernire tape est automatique. Il n'y
pas comme dans d'autres frameworks MVC, un push explicite (C1 pousse le modle M1 dans V1) ou un pull
explicite (la vue V1 va chercher le modle M1 dans C1). Il y a un push implicite que le dveloppeur ne voit pas ;
puis le cycle 5, 8, 9 reprend ;
Nous allons reprendre l'exemple du calendrier. Nous avons vu la directive qui le gnre :
<datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
Cette directive admet d'autres attributs que ceux prsents 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 suprieure celle du jour courant. Nous crirons alors :
<datepicker ng-model="jour" ... min-date="dateMin"></datepicker>
o [dateMin] sera une variable du modle de la page qui aura pour valeur la date du jour. Cela donnera la page suivante :
http://tahe.developpez.com
164/325
2
1
en [1], nous sommes le 19 Juin 2014. Le curseur indique qu'on peut slectionner le 19 juin ;
en [2], le curseur indique qu'on ne peut pas slectionner le 18 juin ;
Nous dupliquons [app-10.html] dans [app-11.html] et nous faisons les modifications suivantes :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div>
<pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<div class="col-md-2">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" mindate="minDate"></datepicker>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
...
<!-- script local -->
<script>
// --------------------- module Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
// contrleur
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// date minimale
$scope.minDate = new Date();
}]);
http://tahe.developpez.com
165/325
36.
37. </script>
38.
39. </body>
40. </html>
ligne 28 : cration du module [rdvmedecins] avec sa dpendance sur le module [ui.bootstrap] qui fournit le calendrier ;
lignes 30-35 : cration d'un contrleur. C'est lui qui va dtenir le modle de notre page. Il n'y aura pas de gestionnaire
d'vnement ici ;
lignes 30-31 : le contrleur [rdvMedecinsCtrl] appartient au module [rdvmedecins]. On peut ajouter autant de contrleurs
que l'on veut un module. Dans notre application on aura :
un module de gestion de l'application ;
un contrleur par vue ;
le second paramtre de la fonction [controller] est un tableau de la forme ['O1', 'O2', ..., 'On', function(O1, O2, ...,
On)]. Le dernier paramtre est la fonction qui implmente le contrleur. Ses paramtres sont des objets que Angular JS
va fournir la fonction.
Revenons l'architecture d'une application Angular :
couche [services]
Routeur
10
Utilisateur
2
V1
C1
6
8
Service 1
Service 2
M1
Donnes
rseau
11
Vn
Cn
Mn
DAO
Ci-dessus, le contrleur C1 contient l'ensemble des gestionnaires d'vnement de la vue V1 ainsi que le modle M1 de cette
dernire. Les gestionnaires d'vnement peuvent avoir besoin d'un ou plusieurs services [6] pour faire leur travail. On passe
l'ensemble de ceux-ci comme paramtres de la fonction de construction du contrleur :
['S1', 'S2', ..., 'Sn', function(S1, S2, ..., Sn)]
Les services Si sont des singletons. Angular les cre en un unique exemplaire. Ils sont identifis par un nom Si. Pourquoi sont-ils
prsents deux fois dans le tableau ci-dessus ? En exploitation, les scripts JS sont minifis. Dans ce processus de minification, le
tableau ci-dessus devient :
['S1', 'S2', ..., 'Sn', function(a1, a2, ..., an)]
Les paramtres perdent leur nom. Or c'est le nom de services. Il est donc important de garder ces noms. C'est pourquoi ils sont
passs en tant que chanes de caractres comme paramtres prcdant la fonction. Les chanes de caractres ne sont pas changes
dans le processus de minification. Lorsqu'Angular va construire le contrleur avec le nouveau tableau, il va remplacer a1 par S1, a2
par S2, ... L'ordre des paramtres est donc important. Il doit correspondre l'ordre des services qui prcdent la dfinition de la
fonction.
Revenons la dfinition du contrleur [rdvMedecinsCtrl] :
1.
2.
// contrleur
angular.module("rdvmedecins")
http://tahe.developpez.com
166/325
3.
.controller('rdvMedecinsCtrl', ['$scope',
4.
function ($scope) {
5.
// date minimale
6.
$scope.minDate = new Date();
7. }]);
lignes 3-4 : l'unique objet inject dans le contrleur est l'objet $scope. C'est un objet prdfini qui reprsente le modle M
des vues associes au contrleur. Pour enrichir le modle d'une vue, il suffit d'ajouter des champs l'objet $scope ;
c'est ce qui est fait ligne 6. On cre le champ [minDate] avec pour valeur la date du jour ;
ligne 1 : le corps de la page est associ au contrleur [rdvMedecinsCtrl] grce l'attribut [ng-controller]. Cela signifie que
tout ce qui est dans la balise <body> va utiliser le contrleur [rdvMedecinsCtrl] pour grer ses vnements et obtenir son
modle M. Une page HTML peut dpendre de plusieurs contrleurs imbriqus ou pas les uns dans les autres :
1. <div id='div1' ng-controller='c1'>
2.
...
3.
<div id='div11' ng-controller='c11'>
4.
...
5.
</div>
6.
...
7.
<div id='div12' ng-controller='c12'>
8.
...
9.
</div>
10. </div>
Ci-dessus :
le contenu de [div1] (lignes 1-10) affiche le modle M1 gr par le contrleur c1. Les balises de cette zone
peuvent rfrencer des gestionnaires d'vnement du contrleur c1 ;
le contenu de [div11] (lignes 3-4) affiche le modle M11 gr par le contrleur c11 mais galement le modle M1.
Il y a hritage des modles. Les balises de cette zone peuvent rfrencer aussi bien des gestionnaires d'vnement
du contrleur c11 que des gestionnaires d'vnement du contrleur c1. Elles ne peuvent rfrencer ni le modle
M12 du contrleur c12 ni les gestionnaires d'vnement de celui-ci. Le contrleur c12 n'est en effet pas connu
entre les lignes 3-5 ;
L'attribut [min-date] est initialis avec la valeur [minDate] du modle. Implicitement [$scope.minDate]. Le champ est toujours
cherch dans l'objet $scope.
3.7.2
Pour l'instant le calendrier ne nous est gure utile puisque c'est un calendrier anglais. Il est possible de le localiser :
http://tahe.developpez.com
167/325
Nous dupliquons la page [app-11.html] dans [app-12.html] puis nous modifions cette dernire de la faon suivante :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<h1>Rdvmedecins - v1</h1>
<pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- le calendrier-->
<div class="col-md-4">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" mindate="minDate"></datepicker>
</div>
</div>
<!-- les langues -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Franais</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins.js"></script>
</body>
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37. </html>
http://tahe.developpez.com
168/325
Il y a peu de modifications. Il y a simplement l'ajout lignes 21-31 de la liste droulante des langues. Pour la premire fois, nous
rencontrons un gestionnaire d'vnement aux lignes 27-28 :
ligne 27 : l'attribut [ng-click] est un attribut Angular qui indique le gestionnaire d'vnement excuter lorsqu'on clique sur
l'lment ayant cet attribut. Ici, la fonction [$scope.setLang('fr')] sera excute. Elle mettra le calendrier en franais ;
ligne 35 : le Javascript du contrleur tant assez consquent, nous le plaons dans un fichier [rdvmedecins.js] ;
Angular gre la localisation des vues avec un module appel [ngLocale]. La dfinition de notre module [rdvmedecins] sera donc la
suivante :
1.
// --------------------- module Angular
2. angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);
Ligne 2, il ne faut pas oublier les dpendances car Angular est parfois peu prcis dans ses messages d'erreur. L'oubli d'une
dpendance est ainsi particulirement difficile dtecter. Ici on a une nouvelle dpendance sur le module [ngLocale].
Par dfaut, Angular ne gre que la localisation des dates, nombres, ... qui ont des variantes locales. Il ne gre pas l'internationalisation
de textes. On utilisera pour cela la bibliothque [angular-translate]. La gestion de la localisation est faite par la bibliothque [angulari18n]. Cette bibliothque amne avec elle autant de fichiers qu'il y a de variantes pour les dates, nombres, ...
Pour le calendrier franais, nous utiliserons le fichier [angular-locale_fr-fr.js] et pour le calendrier anglais le fichier [angularlocale_en-us.js]. Regardons ce qu'il y a par exemple dans le fichier [angular-locale_fr-fr.js] :
1. 'use strict';
2. angular.module("ngLocale", [], ["$provide", function($provide) {
3. var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many",
OTHER: "other"};
4. $provide.value("$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",
http://tahe.developpez.com
169/325
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
"samedi"
],
"MONTH": [
"janvier",
"f\u00e9vrier",
"mars",
"avril",
"mai",
"juin",
"juillet",
"ao\u00fbt",
"septembre",
"octobre",
"novembre",
"d\u00e9cembre"
],
"SHORTDAY": [
"dim.",
"lun.",
"mar.",
"mer.",
"jeu.",
"ven.",
"sam."
],
"SHORTMONTH": [
"janv.",
"f\u00e9vr.",
"mars",
"avr.",
"mai",
"juin",
"juil.",
"ao\u00fbt",
"sept.",
"oct.",
"nov.",
"d\u00e9c."
],
"fullDate": "EEEE d MMMM y",
"longDate": "d MMMM y",
"medium": "d MMM y HH:mm:ss",
"mediumDate": "d MMM y",
"mediumTime": "HH:mm:ss",
"short": "dd/MM/yy HH:mm",
"shortDate": "dd/MM/yy",
"shortTime": "HH:mm"
},
"NUMBER_FORMATS": {
"CURRENCY_SYM": "\u20ac",
"DECIMAL_SEP": ",",
"GROUP_SEP": "\u00a0",
"PATTERNS": [
{
"gSize": 3,
"lgSize": 3,
"macFrac": 0,
"maxFrac": 3,
"minFrac": 0,
"minInt": 1,
"negPre": "-",
"negSuf": "",
"posPre": "",
http://tahe.developpez.com
170/325
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
"posSuf": ""
},
{
"gSize": 3,
"lgSize": 3,
"macFrac": 0,
"maxFrac": 2,
"minFrac": 2,
"minInt": 1,
"negPre": "(",
"negSuf": "\u00a0\u00a4)",
"posPre": "",
"posSuf": "\u00a0\u00a4"
}
]
},
"id": "fr-fr",
"pluralCat": function (n) { if (n >= 0 && n <= 2 && n != 2) {
PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
98. });
99. }]);
return
Dans le fichier [angular-locale_en-us.js], on a exactement la mme chose mais cette fois ci pour l'anglais des USA (en-us).
Le code ci-dessus n'est pas trs simple lire. En lisant attentivement, on dcouvre que tout ce code dfinit 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 prvu qu'on change la variable [$locale] en cours d'excution. On la dfinit une bonne fois pour
toutes en important le fichier de la locale dsire :
<script type="text/javascript" src="bower_components/angular-i18n/angular-locale_fr-fr.js"></script>
Cela ne sert rien d'importer tous les fichiers des locales dsires, car chaque fichier, on l'a vu, ne fait qu'une chose : dfinir 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 problme, je n'en ai pas trouv. J'en propose une ici
[https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]. L'ide est de mettre les diffrentes
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 [rdvmedecins.js] a l'architecture suivante :
http://tahe.developpez.com
171/325
Si on enlve la dfinition des locales qui prend 200 lignes (lignes 15-215 ci-dessus), le code est simple :
pour dfinir un gestionnaire d'vnement qui s'appellerait [nom_fonction] et qui admettrait les paramtres [param1,
param2, ...] ;
Rappelons le code HTML de la liste droulante :
1.
<!-- les langues -->
2.
<div class="col-md-2">
3.
<div class="btn-group" dropdown is-open="isopen">
4.
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top:
30px">
5.
Langues<span class="caret"></span>
6.
</button>
7.
<ul class="dropdown-menu" role="menu">
8.
<li><a href="" ng-click="setLang('fr')">Franais</a></li>
http://tahe.developpez.com
172/325
9.
10.
11.
12.
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 modle M d'une vue V change, la vue V est automatiquement rafrachie avec le nouveau
modle. Ligne 225, on a chang la valeur de la variable [$locale] qui ne fait pas partie du modle M affich par la vue V. Il
faut trouver un moyen de changer ce modle M afin que le calendrier se rafrachisse et utilise sa nouvelle locale. Ici, on
change la variable [jour] du modle du calendrier. On l'initialise avec un nouveau pointeur (new) qui pointe sur une date
identique celle qui est affiche. [$scope.jour.getTime()] est le nombre de millisecondes coule entre le 1er janvier 1970 et
la date affiche par le calendrier. Avec ce nombre, on reconstruit une nouvelle date. On va bien sr retrouver la mme date
et le calendrier restera positionn sur la date qu'il affichait. Mais la valeur de [$scope.jour] qui est en ralit un pointeur
aura elle chang et le calendrier va se rafrachir ;
ligne 229 : on positionne false la valeur de la variable [isopen] du modle. Cette variable contrle un des attributs de la
liste droulante :
1. <div class="btn-group" dropdown is-open="isopen">
2.
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
3.
Langues<span class="caret"></span>
4.
</button>
5. ...
6. </div>
Ligne 1 ci-dessus, l'attribut [is-open] va passer false, ce qui va avoir pour effet de fermer la liste droulante.
3.7.3
En [3], nous voyons que le calendrier est en anglais mais pas les textes [Calendrier, Langues]. Par dfaut, Angular n'offre pas d'outil
pour l'internationalisation des messages. Nous allons utiliser ici la bibliothque [angular-translate] (https://github.com/angulartranslate/angular-translate).
Nous allons dvelopper l'exemple suivant :
http://tahe.developpez.com
173/325
http://tahe.developpez.com
174/325
2
1
Voyons la configuration ncessaire l'internationalisation. Le script [rdvmedecins.js] est modifi de la faon suivante :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
http://tahe.developpez.com
175/325
ligne 2 : la premire modification est l'ajout d'une nouvelle dpendance. L'internationalisation de l'application ncessite le
module Angular [pascalprecht.translate] ;
lignes 5-26 : dfinissent la fonction [config] du module [rdvmedecins]. Au dmarrage d'une application Angular, le
framework instancie tous les services ncessaires l'application, ceux prdfinis d'Angular et ceux dfinis par l'utilisateur.
Pour l'instant, nous n'avons pas dfini de services. La fonction [config] du module d'une application est excute avant
toute instanciation de service. Elle peut tre utilise pour dfinir des informations de configuration des services qui vont
tre ensuite instancis. Ici, la fonction [config] va tre utilise pour dfinir les messages internationaliss de l'application ;
ligne 5 : le paramtre 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 [pascalprecht.translate].
[function] est la fonction excute pour configurer l'application ;
lignes 7-14 : la fonction [$translateProvider.translations] admet deux paramtres :
le premier paramtre est la cl d'une langue. On peut mettre ce qu'on veut. Ici, on a mis 'fr' pour les traductions
franaises (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 franais ;
lignes 16-23 : les messages anglais ;
ligne 25 : la mthode [preferredLanguage] fixe la langue par dfaut. Son paramtre est l'un des arguments utiliss comme
premier paramtre de la fonction [$translateProvider.translations] donc ici soit 'fr' (ligne 7), soit 'en' (ligne 16) ;
notons qu'il y a trois sortes de messages :
des messages sans paramtres ni lments HTML (lignes 9, 11, 12, ...),
des messages avec des lments HTML (lignes 8, 10, ...),
des messages avec des paramtres (lignes 10, 19) ;
Nous dupliquons maintenant [app-11.html] dans [app-12.html] et nous faisons les modifications suivantes :
1. <div class="container">
2.
<!-- un premier texte avec des lments HTML dedans -->
3.
<h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
4.
<!-- un second texte avec paramtres -->
5.
<h3 class="alert alert-warning" translate="{{msg.text}}" translatevalues="{{msg.model}}"></h3>
6.
<!-- un troisime texte traduit par le contrleur -->
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" mindate="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')">Franais</a></li>
27.
<li><a href="" ng-click="setLang('en')">English</a></li>
28.
</ul>
29.
</div>
30.
</div>
31. </div>
32. </div>
http://tahe.developpez.com
176/325
anglais
13
Calendrier
Calendar
23
Langues
Languages
Jour slectionn :
Selected day:
On notera que [msg.text] et [msg.model] ne sont pas entours d'apostrophes. Ce ne sont pas des chanes de caractres mais des
lments du modle :
http://tahe.developpez.com
177/325
1.
<!-- un troisime texte traduit par le contrleur -->
2. <h3 class="alert alert-danger">{{msg2}}</h3>
Toutes les traductions prcdentes se sont faites dans la vue au moyen d'attributs du module [pascalprecht.translate]. On peut
dcider galement de faire cette traduction ct serveur. C'est ce qui est fait ici. On a dans le contrleur (ligne 247 dans la copie
d'cran ci-dessus) le code suivant :
$scope.msg2 = $filter('translate')('msg_meteo');
On utilise la mme syntaxe que pour le filtre 'date' car 'translate' est lui aussi un filtre. On demande ici le message de cl
'msg_meteo'.
Examinons le mcanisme des changements de langues. On a vu que la fonction [config] de configuration du module [rdvmedecins]
avait dsign le franais comme langue par dfaut (ligne 9 ci-dessous) :
1. // configuration i18n
2. angular.module("rdvmedecins")
3.
.config(['$translateProvider', function ($translateProvider) {
4.
// messages franais
5.
$translateProvider.translations("fr", {...});
6.
// messages anglais
7.
$translateProvider.translations("en", {...});
8.
// langue par dfaut
9.
$translateProvider.preferredLanguage("fr");
10. }]);
On rappelle galement que la locale par dfaut tait galement le franais. Dans l'initialisation du contrleur [rdvmedecins] on a
crit :
1. // on met la locale en franais
2. angular.copy(locales['fr'], $locale);
Il n'y a aucun lien entre l'internationalisation des messages amene par le module [pascalprecht.translate] et la localisation des dates
que nous avons mise en place. Cette dernire utilise une variable $locale qui n'est pas utilise par le module [pascalprecht.translate].
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 appele avec l'un des deux paramtres ['fr','en'] ;
lignes 252-257 : ont t dj expliques 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 [ pascalprecht.translate].
Pour cela, il faut l'injecter dans le contrleur :
1. // contrleur
2. angular.module("rdvmedecins")
3.
.controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
4. function ($scope, $locale, $translate, $filter) {
http://tahe.developpez.com
178/325
1.
2.
en [1], le jour est rest en franais alors que le reste de la vue est en anglais ;
en [2] et [3], le jour slectionn 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 contrleur avec le code suivant :
$scope.msg = {'text': 'msg_agenda', 'model': {'titre': 'Mme', 'prenom': 'Laure', 'nom':
'PELISSIER', 'jour': $filter('date')($scope.jour, 'fullDate')}};
L'anomalie [1] (le jour est rest en franais alors que le reste de la vue est en anglais) semble montr que si l'attribut [translate] est
rvalu 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 contrleur :
1.
2.
3.
4. ...
5.
6.
7.
8.
9. };
http://tahe.developpez.com
179/325
A chaque changement de langue, la ligne 8 ci-dessus rvalue le jour affich. Cela rgle effectivement le premier problme mais pas
le second (le jour affich dans le message ne change pas lorsqu'on slectionne 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 :
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
La vue affiche V ne change que si son modle M change. Or ici, le choix d'un nouveau jour dans le calendrier dclenche un
vnement qui n'est pas gr, ce qui fait que le modle [msg] ne change pas et que la vue donc ne change pas. Nous faisons voluer
dans la vue, la dfinition du calendrier :
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"
ng-click="calendarClick()"></datepicker>
Ci-dessus, nous indiquons que le clic sur le calendrier doit tre gr par la fonction [$scope.calendarClick]. Celle-ci est la suivante :
3.7.4
couche [services]
Routeur
10
Utilisateur
2
V1
C1
6
8
Service 1
Service 2
M1
Donnes
rseau
11
Vn
Cn
Mn
DAO
Nous allons nous intresser 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 particulire. Il a un nom et Angular le connat via ce nom ;
un service peut tre inject par Angular dans les contrleurs et les autres services ;
Certains des services que nous allons configurer dans le module [rdvmedecins] auront besoin d'tre configurs. 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 contrleurs configurer. Nous dcrivons maintenant ce processus.
http://tahe.developpez.com
180/325
<div class="container">
<!-- contrle du msg d'attente -->
<label>
<input type="checkbox" ng-model="waiting.visible">
<span>Voir le message d'attente</span>
</label>
lignes 3-6 : une case cocher qui contrle l'affichage ou non du message d'attente des lignes 9-15. La valeur de la case
cocher est place dans la variable [waiting.visible] du modle M de la vue V. Cette valeur est true si la case est coche et false
sinon. Cela marche dans les deux sens. Si nous donnons la valeur true variable [waiting.visible], la case sera coche. On a
une association bi-directionnelle entre la vue V et son modle 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 [waiting.visible] a la valeur true. Ainsi lorsqu'on va cocher la case de la
ligne 4 :
la valeur true est affecte la variable [waiting.visible] (ng-model, ligne 4) ;
comme il y a eu changement du modle M, la vue V est automatiquement rvalue. Le message d'attente sera alors
rendu visible (ng-show, ligne 9) ;
le raisonnement est analogue lorsqu'on dcoche 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 mthode [waiting.cancel()] est excute (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 dj crit et qui doit tre maintenant rorganis ;
http://tahe.developpez.com
181/325
Prcdemment, nous avions dfini dans le contrleur le dictionnaire locales={'fr':..., 'en': ...} qui faisait 200 lignes. Ce dictionnaire
est clairement un lment de configuration, aussi le migre-t-on dans le service [config] des lignes 38-39. Ce service est dfini de la
faon suivante :
http://tahe.developpez.com
182/325
lignes 38-39 : un service est cr avec la fonction [factory] de l'objet [angular.module]. La syntaxe de cette fonction est
comme pour les prcdentes factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){...}]) o les Oi
sont les noms d'objets connus d'Angular (prdfinis ou crs par le dveloppeur) et qu'Angular injecte comme paramtre
de la fonction factory. Comme ici, la fonction n'a pas de paramtres, on a utilis une syntaxe plus courte galement
accepte factory('nom_service', function (){...}]) ;
ligne 40 : la fonction [factory] doit implmenter 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 appele factory (usine de cration d'objets) ;
ligne 6 : on rend un objet JS qui peut contenir la fois des champs et des mthodes. Ce sont ces dernires qui assurent le
service ;
Ici le service [config] ne dfinit que des champs et aucune mthode. On y mettra tout ce qui peut tre paramtr dans l'application :
http://tahe.developpez.com
183/325
Lorsque l'utilisateur clique sur le bouton d'annulation, la mthode [$scope.waiting.cancel()] est appele. C'est au final la
fonction prive cancel de la ligne 316 qui est excute. Elle se contente de cacher le message d'attente en mettant false, la
varible du modle [waiting.visible] (ligne 318) ;
3.7.5
Nous prsentons maintenant un nouveau service avec une nouvelle notion, celle de la programmation asynchrone.
http://tahe.developpez.com
184/325
couche [services]
Routeur
Utilisateur
config
utils
Donnes
rseau
dao
[dao] : le service d'accs au service web de prise de rendez-vous. Nous allons le prsenter prochainement ;
Nous allons crire l'application suivante :
il s'agit de faire apparatre le bandeau [2] pendant un temps fix par [1]. L'attente peut tre annule par [3].
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
http://tahe.developpez.com
185/325
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
<div class="form-group">
<label for="waitingTime">{{waitingTimeText | translate}}</label>
<input type="text" id="waitingTime" ng-model="waiting.time"/>
</div>
<button class="btn btn-primary" ng-click="execute()">Excuter</button>
</div>
</div>
..
<script type="text/javascript" src="rdvmedecins-03.js"></script>
</body>
ligne 11 : l'attribut [ng-cloak] empche l'affichage de la zone avant que les expressions Angular de celle-ci n'aient t
calcules. 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 mmorise dans le modle [waiting.time] (attribut ng-model) ;
ligne 28 : la page utilise un nouveau script [rdvmedecins-03] ;
</html>
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// messages franais
$translateProvider.translations("fr", {
...
'msg_waiting_time_text': "Temps d'attente : "
});
// messages anglais
$translateProvider.translations("en", {
...
'msg_waiting_time_text': "Waiting time:"
});
// langue par dfaut
$translateProvider.preferredLanguage("fr");
}]);
Nous ajoutons au service [config] une nouvelle ligne (ligne 6) pour cette cl de message :
1. angular.module("rdvmedecins")
http://tahe.developpez.com
186/325
2.
.factory('config', function () {
3.
return {
4.
// messages internationaliser
5.
...
6. waitingTimeText: 'msg_waiting_time_text',
ligne 2 : le service s'appelle [utils] (1er paramtre). Il a des dpendances sur trois services, deux services Angular prdfinis
$timeout, $q et le service config. Le service [$timeout] permet d'excuter une fonction aprs qu'un certain temps se soit
coul. Le service [$q] permet de crer des tches 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 mthodes, celles des lignes 4 et 12. Notez que les
champs de l'objet peuvent porter des noms quelconques. Par cohrence, on leur a donn les noms des fonctions qu'ils
rfrencent ;
lignes 4-9 : la mthode [debug] crit sur la console un message [message] et ventuellement la reprsentation JSON d'un
objet [data]. Cela permet d'afficher des objets de n'importe quelle complexit ;
lignes 12-20 : la mthode [waitForSomeTime] cre une tche asynchrone qui dure [milliseconds] milli-secondes ;
ligne 14 : cration d'une tche grce l'objet prdfini [$q] (https://docs.angularjs.org/api/ng/service/$q). Ci-dessous,
l'API de la tche appele [deferred] dans la documentation Angular :
http://tahe.developpez.com
187/325
Ceux qui veulent attendre la fin de la tche utilisent le champ [promise] de celle-ci :
var promise=[task].promise ;
http://tahe.developpez.com
188/325
ligne 3 : on dfinit la fonction excuter aprs que l'une des deux fonction prcdentes se soit excute. On met ici, le
code commun aux deux fonctions [successCallback, errorCallBack].
// attente
function waitForSomeTime(milliseconds) {
// attente asynchrone de milliseconds millisecondes
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// on retourne la tche
return task;
http://tahe.developpez.com
189/325
35.
36.
37.
38.
39.
40.
41.
42.
43.
};
// annulation attente
function cancel() {
// on termine la tche
task.reject();
}
}]);
Il est important de comprendre les squences d'excution de ce code. Dans le cas o l'utilisateur met un dlai de 3 secondes et
n'annule pas l'attente :
lorsqu'il clique sur le bouton [Excuter], la fonction [$scope.execute] s'excute. Les lignes 16-34 sont excutes sans attente
des 3 secondes. A la fin de cette excution, la vue V est synchronise avec le modle M. Le message d'attente est affich
(ng-show=$scope.waiting.visible=true, ligne 20) et le formulaire est cach (ng-hide=$scope.waiting.visible=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'excute :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
http://tahe.developpez.com
// attente
function waitForSomeTime(milliseconds) {
// attente asynchrone de milliseconds millisecondes
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// on retourne la tche
return task;
};
190/325
au bout de 3 secondes donc, du code est excut. Ce code termine la tche [task] avec un code de succs (resolve). Cela va
dclencher l'excution de tous les codes qui attendaient cette fin (ligne 4 ci-dessous) :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
// attente simule
task = utils.waitForSomeTime($scope.waiting.time);
// fin d'attente
task.promise.then(function () {
// succs
utils.debug('fin', new Date());
}, function () {
// chec
utils.debug('Opration annule')
});
task.promise['finally'](function () {
// fin d'attente dans tous les cas
$scope.waiting.visible = false;
});
la ligne 6 ci-dessus (fin avec succs) va donc tre excute. Puis ce sera le tour des lignes 11-14. Une fois ce code excut,
on revient la vue V qui va alors tre synchronise avec son modle M. Le message d'attente est cach (ngshow=$scope.waiting.visible=false, ligne 13) et le formulaire est affich (ng-hide=$scope.waiting.visible=false, ligne 13) ;
On voit ci-dessus, le dlai de 3 secondes (06:01-05:58) entre le dbut et la fin de l'attente. Si on contraire, l'utilisateur annule l'attente
avant les 3 secondes, on a l'affichage suivant :
dbut : "2014-06-23T15:08:09.564Z"
Opration annule
Pour terminer, il est important de comprendre qu' tout moment il n'y a qu'un tread d'excution appel le thread de l'UI (User
Interface). La fin d'une tche asynchrone est signale par un vnement exactement comme l'est le clic sur un bouton. Cet
vnement n'est pas trait immdiatement. Il est mis dans la file d'attente des vnements qui attendent leur excution. Lorsque
vient son tour, il est trait. Ce traitement utilise le thread de l'UI et donc pendant ce temps, l'interface est gele. Elle ne ragit pas
aux sollicitations de l'utilisateur. Pour cela, il est important que le traitement d'un vnement soit rapide. Parce que chaque
vnement est trait par le thread de l'UI, on n'a jamais rgler des problmes de synchronisation entre threads s'excutant en
mme temps. Il n'y a, chaque instant, que le thread de l'UI qui s'excute.
3.7.6
Nous prsentons maintenant le service [dao] qui communique avec le serveur web :
couche [services]
Routeur
Utilisateur
config
utils
http://tahe.developpez.com
Donnes
rseau
dao
191/325
3.7.6.1
La vue V
Application web / navigateur
couche [prsentation]
couche [services]
Routeur
Utilisateur
config
utils
Donnes
rseau
dao
Nous dupliquons [app-01.html] dans [app-16.html] que nous modifions ensuite de la faon suivante :
1. <div class="container" ng-cloak="">
2.
<h1>Rdvmedecins - v1</h1>
3.
4.
<!-- le message d'attente -->
5.
<div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
6.
<h1>{{ waiting.text | translate}}
7.
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|
translate}}</button>
8.
<img src="assets/images/waiting.gif" alt=""/>
9.
</h1>
10. </div>
11.
12. <!-- la demande -->
13. <div class="alert alert-info" ng-hide="waiting.visible">
14.
<div class="form-group">
15.
<label for="waitingTime">{{waitingTimeText | translate}}</label>
16.
<input type="text" id="waitingTime" ng-model="waiting.time"/>
17.
</div>
18.
<div class="form-group">
19.
<label for="urlServer">{{urlServerLabel | translate}}</label>
http://tahe.developpez.com
192/325
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
3.7.6.2
lignes 13-31 : implmentent le formulaire. Celui-ci n'est pas visible lorsque le message d'attente est affich ( nghide="waiting.visible"). On retiendra que les quatre saisies sont mmorises dans (attributs ng-model) [waiting.time (ligne
16), server.url (ligne 20), server.login (ligne 24), server.password (ligne 28)] ;
lignes 34-39 : affichent la liste des mdecins. Cette liste n'est pas toujours visible (ng-show="medecins.show").
ligne 35 : une alternative la syntaxe <div ... translate="{{medecins.title}}" translate-values="{{medecins.model}}">
dj rencontre ;
ligne 36 : une liste non ordonne ;
ligne 37 : la liste des mdecins sera trouve dans le modle [medecins.data]. La directive Angular [ng-repeat] permet de
parcourir une liste. La syntaxe ng-repeat="medecin in medecins.data" demande ce que la balise <li> soit rpte pour chaque
lment de la liste [medecins.data]. L'lment courant de la liste est appele [medecin] ;
ligne 37 : pour chaque <li>, on crit le titre, le prnom et le nom du mdecin courant dsign par la variable [medecin] ;
lignes 42-47 : affichent la liste des erreurs. Cette liste n'est pas toujours visible (ng-show="errors.show"). Cet affichage suit le
mme modle que l'affichage de la liste des mdecins. De faon gnrale, 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]
Le contrleur C et le modle M
http://tahe.developpez.com
193/325
couche [services]
Routeur
Utilisateur
config
utils
Donnes
rseau
dao
3.7.6.3
lignes 6-9 : le module [rdvmedecins] dclare une dpendance sur le module [base64] fourni par la bibliothque [angularbase64] qui est l'une des dpendances du projet. Ce module sert coder en Base64, la chane [login:password] envoye au
service web pour s'authentifier ;
lignes 12-13 : la fonction d'initialisation qui contient nos messages internationaliss. De nouveaux messages apparaissent.
Nous ne les prsenterons plus ;
lignes 69-70 : le service [config] qui paramtre notre application. De nouvelles cls de message y sont ajoutes. Nous ne les
prsenterons plus ;
lignes 318-319 : le service [utils] qui contient des mthodes utilitaires. De nouvelles y sont rajoutes. Nous les
prsenterons ;
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 contrleur C de la vue V que nous venons de prsenter. Nous allons le prsenter maintenant car c'est lui
le chef d'orchestre qui ragit aux demandes de l'utilisateur ;
Le contrleur C
http://tahe.developpez.com
194/325
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
// annulation attente
53.
function cancel() {
54.
// on termine la tche
55.
task.reject();
56.
// on met jour l'UI
57.
$scope.waiting.visible = false;
58.
$scope.medecins.show = false;
59.
$scope.errors.show = false;
60.
}
61.
62.
}
63. ])
64. ;
http://tahe.developpez.com
195/325
ligne 9 : [$scope.medecins] va rassembler les informations ncessaires l'affichage de la liste des mdecins :
1.
<!-- la liste des mdecins -->
2.
<div class="alert alert-success" ng-show="medecins.show">
3.
{{medecins.title|translate:medecins.model}}
4.
<ul>
5.
<li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}}
{{medecin.nom}}</li>
6.
</ul>
7. </div>
L'attribut [medecins.title] sera le titre du bandeau. Il est dfini dans le service [config]. L'attribut [medecins.show] va
contrler l'affichage ou non du bandeau (attribut ng-show="medecins.show"). L'attribut [medecins.model] est un
dictionnaire vide et le restera. Il sert simplement illustrer l'utilisation de la variante de traduction utilise ligne 3. Non
dfini encore, l'attribut [medecins.data] qui contiendra la liste des mdecins (ligne 5).
ligne 10 : [$scope.errors] va rassembler les informations ncessaires l'affichage de la liste des erreurs :
1.
<!-- la liste d'erreurs -->
2.
<div class="alert alert-danger" ng-show="errors.show">
3.
{{errors.title|translate:errors.model}}
4.
<ul>
5.
<li ng-repeat="message in errors.messages">{{message|translate}}</li>
6.
</ul>
7. </div>
L'attribut [errors.title] sera le titre du bandeau. Il est dfini dans le service [config]. L'attribut [errors.show] va contrler
l'affichage ou non du bandeau (attribut ng-show="errors .show"). L'attribut [errors.model] est un dictionnaire vide
et le restera. Il sert simplement illustrer l'utilisation de la variante de traduction utilise ligne 3. Non dfini encore,
l'attribut [errors.messages] qui contiendra la liste des messages d'erreur afficher (ligne 5).
ligne 16 : la tche asynchrone. Le contrleur va lancer successivement deux tches asynchrones. Les rfrences sur ces
tches successives seront places dans la variable [task]. Cela permettra de les annuler (ligne 55) ;
ligne 19 : la mthode excute lorsque l'utilisateur clique sur le bouton [Liste des mdecins] :
<button class="btn btn-primary" ng-click="execute()">Liste des mdecins</button>
lignes 21-23 : l'interface visuelle est mise jour : le message d'attente est affich, tout le reste est cach ;
ligne 25 : on cre la tche asynchrone de l'attente. On recevra un signal (tche ralise) au bout du temps saisi par
l'utilisateur dans le formulaire ;
ligne 26 : on rcupre la promesse de la tche asynchrone. C'est avec elle que le programme qui lance la tche travaille. Il
faut cependant avoir la rfrence de la tche elle-mme afin de pouvoir l'annuler (ligne 55) ;
lignes 28-32 : on dfinit le travail faire lorsque l'attente sera termine ;
ligne 30 : on utilise la mthode [dao.getData] pour lancer une nouvelle tche asynchrone. On lui passe les informations
dont elle a besoin :
l'URL racine du service web [$scope.server.url], par exemple [http://localhost:8080];
le login [$scope.server.login] pour s'identifier, par exemple [admin];
le mot de passe [$scope.server.password] pour s'identifier, par exemple [admin];
l'URL qui rend le service demand [config.urlSvrMedecins], ici [/getAllMedecins]. Au total l'URL complte sera
[http://localhost:8080/getAllMedecins] ;
La mthode [dao.getData] rend un rsultat qui a deux formes possibles :
{err: 0, data: [med1, med2, ...]} o [medi] est un objet reprsentant un mdecin (titre, prenom, nom),
{err: n, messages: [msg1, msg2, ...]} o [msgi] est un message d'erreur et n est diffrent de 0 ;
http://tahe.developpez.com
196/325
lignes 34-50 : de l'explication prcdente, il dcoule que ces lignes se seront excute que lorsque la tche [dao.getData]
sera termine. Le paramtre [result] pass la fonction de la ligne 34 est construit par la mthode [dao.getData] et transmis
au code appelant par l'opration [task.resolve(result)] o [result] est de la forme suivante :
{err: 0, data: [med1, med2, ...]} o [medi] est un objet reprsentant un mdecin (titre, prenom, nom),
{err: n, messages: [msg1, msg2, ...]} o [msgi] est un message d'erreur et n est diffrent de 0 ;
ligne 37 : on regarde le code d'erreur [result.err] ;
lignes 38-42 : s'il n'y a pas d'erreur (result.err==0), alors on rcupre la liste des mdecins et on l'affiche ;
lignes 44-47 : si au contraire il y a erreur (result.err !=0), alors on rcupre la liste des messages d'erreur et on l'affiche ;
lignes 53-56 : le message d'attente avec son bouton d'annulation est prsent tant que les deux oprations asynchrones ne
sont pas termines. Voyons ce qui se passe selon le moment de l'annulation :
il faut tout d'abord comprendre que les lignes 19-50 sont excutes d'une traite. Une seule tche asynchrone a alors
t lance, celle de la ligne 25,
aprs cette premire excution, 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 tche de la ligne 25 ne soit termine, la mthode de la ligne 53 est
alors excute et la tche est annule avec chec (ligne 55) ;
lignes 56-59 : l'interface est mise jour : on raffiche le formulaire et tout le reste est cach,
il a alors retour la vue V et le navigateur va traiter l'vnement suivant. Puisqu'il y a eu fin de tche, la promesse de
cette tche est obtenue, ce qui cre un vnement. Il est alors trait ;
les lignes 28-32 sont ensuite excutes. Il n'y a pas de fonction dfinie pour le cas d'chec, donc aucun code n'est
excut. On obtient une nouvelle promesse, celle toujours rendue par [promise.then] et toujours obtenue,
l'venement ayant t trait, il y a retour la vue V et le navigateur va traiter l'vnement suivant. Puisque la [promise]
de la ligne 28 a t traite, celle de la ligne 34 va tre rsolue, ce qui va provoquer un nouvel vnement. Il est alors
trait ;
les lignes 34-49 vont alors tre excutes leur tour, car la promesse utilise ligne 34 a t obtenue. De nouveau, parce
qu'il n'y a pas de fonction dfinie pour le cas d'chec, aucun code n'est excut,
on arrive ainsi la ligne 50. Il n'y a plus d'attente de tche et la nouvelle vue V est affiche ;
supposons maintenant que l'annulation intervient pendant que la seconde tche asynchrone [dao.getData] est en cours
d'excution. Le raisonnement prcdent peut tre tenu de nouveau. La fin de la tche va provoquer l'excution des
lignes 34-50 avec une fin de tche avec chec. On va dcouvrir bientt que la mthode [dao.getData] ralise un appel
HTTP asynchrone vers le service web. Cet appel ne sera pas annul mais son rsultat 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 vnements du navigateur.
Les vnements sont provoqus par l'utilisateur (un clic) ou par des oprations systme telles que la fin d'une opration asynchrone.
L'tat de repos du navigateur est l'affichage de la vue V. Il est tir de ce repos par un vnement qui se produit et qu'il traite alors.
Ds que l'vnement a t trait, il revient son tat de repos. La vue V est alors mise jour si l'vnement trait a modifi son
modle M. Le navigateur est tir de son tat de repos par l'vnement suivant.
Tout se passe dans un unique thread. Deux vnements ne sont jamais traits simultanment . Leur excution est
squentielle. Le navigateur ne passe l'vnement suivant que lorsque le prcdent lui laisse la main, en gnral parce qu'il a t
trait totalement.
Il nous reste un point expliquer. Pour afficher les messages d'erreur, nous crivons :
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true,
model: {}};
La liste des messages est fournie par la mthode [utils.getErrors] dfinie dans le service [utils]. Cette mthode est la suivante :
1. // analyse des erreurs dans la rponse du serveur JSON
2.
function getErrors(data) {
3.
// data {err:n, messages:[]}, err!=0
4.
// erreurs
5.
var errors = [];
6.
// code d'erreur
7.
var err = data.err;
8.
switch (err) {
9.
case 2 :
10.
// not authorized
11.
errors.push('not_authorized');
12.
break;
13.
case 3 :
14.
// forbidden
15.
errors.push('forbidden');
16.
break;
http://tahe.developpez.com
197/325
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
3.7.6.4
case 4 :
// erreur locale
errors.push('not_http_error');
break;
case 6 :
// document non trouv
errors.push('not_found');
break;
default :
// autres cas
errors = data.messages;
break;
}
// si pas de msg, on en met un
if (! errors || errors.length == 0) {
errors=['error_unknown'];
}
// on rend la liste des erreurs
return errors;
}
lignes 2-3 : le paramtre [data] reu 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 internationaliss. Pour cette raison, ce ne sont
pas les messages eux-mmes qu'on met dans le tableau, mais leurs cls d'internationalisation sauf la ligne 27. Dans ce cas,
on utilise l'attribut [messages] du paramtre [data]. Ces messages sont de vrais messages et non des cls de message. La vue
V va cependant les traiter comme des cls de message qui ne seront alors pas trouves. Dans ce cas, le module [translate]
affiche la cl de message qu'il n'a pas trouve, donc ici un vrai message. C'est le rsultat souhait ;
lignes 32-34 : traitent le cas ou [data.messages] ligne 27 vaut null. Cela arrive avec le service web crit. Il aurait fallu viter
ce cas.
Le service [dao]
Application web / navigateur
couche [prsentation]
couche [services]
Routeur
Utilisateur
config
utils
Donnes
rseau
dao
Le service [dao] assure les changes HTTP avec le service web / JSON. Son code est le suivant :
1. angular.module("rdvmedecins")
2.
.factory('dao', ['$http', '$q', 'config', '$base64', 'utils',
3.
function ($http, $q, config, $base64, utils) {
4.
5.
// logs
6.
utils.debug("[dao] init");
7.
8.
// ----------------------------------mthodes prives
9.
// obtenir des donnes auprs du service web
10.
function getData(serverUrl, username, password, urlAction, info) {
http://tahe.developpez.com
198/325
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
// opration asynchrone
var task = $q.defer();
// url requte HTTP
var url = serverUrl + urlAction;
// authentification basique
var basic = "Basic " + $base64.encode(username + ":" + password);
// la rponse
var rponse;
// les requtes http doivent tre toutes authentifies
var headers = $http.defaults.headers.common;
headers.Authorization = basic;
// on fait la requte HTTP
var promise;
if (info) {
promise = $http.post(url, info, {timeout: config.timeout});
} else {
promise = $http.get(url, {timeout: config.timeout});
}
promise.then(success, failure);
// on retourne la tche elle-mme afin qu'elle puisse tre annule
return task;
// success
function success(response) {
// response.data={status:0, data:[med1, med2, ...]} ou {status:x, data:[msg1,
msg2, ...]
utils.debug("[dao] getData[" + urlAction + "] success rponse", response);
// rponse
var payLoad = response.data;
rponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages:
payLoad.data};
// on rend la rponse
task.resolve(rponse);
}
http://tahe.developpez.com
// failure
function failure(response) {
utils.debug("[dao] getData[" + urlAction + "] error rponse", response);
// on analyse le status
var status = response.status;
var error;
switch (status) {
case 401 :
// unauthorized
error = 2;
break;
case 403:
// forbidden
error = 3;
break;
case 404:
// not found
error = 6;
break;
case 0:
// erreur locale
error = 4;
break;
default:
// autre chose
error = 5;
}
// on rend la rponse
task.resolve({err: error, messages: [response.statusText]});
}
199/325
76.
77.
78.
79.
80. }]);
lignes 77-79 : le service n'a qu'un unique champ : la mthode [getData] qui permet d'obtenir des informations auprs du
service web / JSON ;
ligne 2 : apparat une dpendance [$http] que nous n'avions pas encore rencontre. C'est un service prdfini 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 excut ;
ligne 10 : la mthode [getData] admet cinq paramtres :
[serverUrl] : l'URL racine du service web (http://localhost:8080) ;
[urlAction] : l'URL du service particulier demand (/getAllMedecins) ;
[username] : le login de l'utilisateur ;
[password] : son mot de passe ;
[info] : objet rassemblant des informations complmentaires lorsque l'URL du service particulier demand est
demand via une opration POST. Dans le cas de l'URL (/getAllMedecins), ce paramtre n'a pas t pass. Il est donc
[undefined] ;
ligne 12 : on cre une tche asynchrone ;
ligne 14 : l'URL complte du service demand (http://localhost:8080/getAllMedecins);
ligne 16 : l'authentification se fait en envoyant l'entte HTTP suivant :
Authorization:Basic code
ligne 23 : les mthodes du service [$http] renvoient des promesses. Elle seront mmorises dans la variable [promise] ;
ligne 27 : parce qu'ici, le paramtre [info] a la valeur [undefined], c'est la ligne 27 qui est excute. L'URL
(http://localhost:8080/getAllMedecins) est demande avec un GET. Pour ne pas attendre trop longtemps, on fixe un dlai
d'attente maximum (timeout) pour obtenir la rponse du serveur. Par dfaut, ce dlai est d'une seconde ;
ligne 29 : on dfinit les deux mthodes excuter lorsque la promesse est obtenue :
[success] : dfinie ligne 34, est la mthode excuter lorsque la promesse est obtenue sur un succs de la tche ;
[failure] : dfinie ligne 45, est la mthode excuter lorsque la promesse est obtenue sur un chec de la tche ;
les deux mthodes (on devrait dire fonctions) sont dfinies l'intrieur de la fonction [getData]. C'est possible en
Javascript. Les variables dfines dans [getData] sont connues dans les deux fonctions internes [success, failure] ;
ligne 31 : on retourne la tche cre ligne 12. Il faut se rappeler ici le code appelant :
1.
2.
3.
promise = promise.then(function () {
// on demande la liste des mdecins;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password,
config.urlSvrMedecins);
4.
return task.promise;
5. });
http://tahe.developpez.com
200/325
Le code est un texte de trois chiffres qui indique si l'appel a russi ou non. Grossirement, on peut dire que les codes 2xx
et 3xx sont des codes de russite, les autres tant des codes d'chec. Le texte est un court texte d'explication. Voici deux
rponses possibles, l'un en cas de russite, l'autre en cas d'chec :
HTTP/1.1 200 OK
HTTP/1.1 404 Not Found
ligne 36 : on affiche sur la console la rponse du serveur. Dans l'erreur [404 Not Found], on obtient quelque chose
comme :
[dao] getData[/getAllMedecins] error rponse : {"data":"...","status":404,"config":
{...},"statusText":"Not Found"}
Dans cette rponse, nous n'utiliserons que les champs [data], [status] et [statusText].
ligne 38 : on rcupre le champ [data] de la rponse. Il aura l'une des formes suivantes :
{status: 0, data: [med1, med2, ...]} o [medi] est un objet reprsentant un mdecin (titre, prenom, nom),
{status: n, data: [msg1, msg2, ...]} o [msgi] est un message d'erreur et n est diffrent de 0 ;
ligne 39 : on construit la rponse {0,data} ou {n,messages}. La premire rponse contient les mdecins dans le champ
[data]. La seconde signale une erreur qui s'est produite ct serveur. Celui a gr celle-ci, gnr 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 compltement. C'est pour cela que les deux cas sont traits dans la mme fonction [success] ;
ligne 41 : la tche est termine [task.resolve] et on rend l'une des deux rponses :
{err: 0, data: [med1, med2, ...]} o [medi] est un objet reprsentant un mdecin (titre, prenom, nom),
{err: n, messages: [msg1, msg2, ...]} o [msgi] est un message d'erreur et n est diffrent de 0 ;
Il faut relier ce code la faon dont cette rponse est rcupre dans le code appelant du contrleur :
1.
2.
3.
4.
http://tahe.developpez.com
201/325
5.
6.
...
}
ligne 45 : la fonction [failure] lorsque la tche 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 appele ;
ligne 46 : on affiche la rponse sur la console ;
ligne 48 : on se rappelle que la rponse du serveur a la forme :
{"data":"...","status":404,"config":{...},"statusText":"Not Found"}
Maintenant qu'on a une vue la fois globale et dtaille de l'application, nous pouvons commencer les tests.
3.7.6.5
Tests de l'application - 1
http://tahe.developpez.com
202/325
ligne 1 : Angular signale une erreur sur laquelle nous allons revenir ;
ligne 2 : le log de la mthode [dao.getData]. On y trouve des choses intressantes :
[status] vaut 0, indiquant par l, qu'il n'y a pas eu d'appel HTTP. En consquence [statusText] est vide,
[url] vaut [http://localhost:8080/getAllMedecins] ce qui est correct ;
l'entte 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. Commenons par revenir sur l'architecture gnrale de l'application client /
serveur :
http://tahe.developpez.com
203/325
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
(Cross-Origin Resource Sharing). Le serveur [2] donne son accord en envoyant des enttes HTTP prcis. C'est parce qu'ici, notre
serveur [2] ne les a pas envoys que le navigateur a refus de faire l'appel HTTP demand par l'application.
Entrons maintenant dans les dtails. Examinons les changes rseau qui ont eu lieu lors de l'appel HTTP. Pour cela, dans le
navigateur Chrome, nous faisons [F12] pour obtenir les outils du dveloppeur et nous slectionnons l'onglet [Network] pour voir les
changes rseau :
http://tahe.developpez.com
204/325
On peut voir dans [1], que le navigateur a envoy une requte HTTP [OPTIONS] sur l'URL demande. [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 rponse en [2]. Pour indiquer qu'il
accepte des requtes de clients qui ne sont pas dans son domaine, il doit renvoyer un entte particulier appel [Access-ControlAllow-Origin]. Et c'est parce qu'il ne l'a pas renvoy qu'Angular n'a pas excut l'appel HTTP demand et a renvoy l'erreur :
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. No 'Access-Control-Allow-Origin'
header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed
access.
Nous devons donc modifier notre serveur pour qu'il envoie l'entte HTTP attendu.
3.7.6.6
http://tahe.developpez.com
205/325
Nous revenons sous Eclipse. Afin de conserver l'acquis, nous dupliquons la version actuelle du serveur web / JSON [rdvmedecinswebapi-v2] dans [rdvmedecins-webapi-v3] [1] :
2
1
Nous faisons une premire modification dans [ApplicationModel] qui est l'un des lments de configuration du service web :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
package rdvmedecins.web.models;
ligne 17 : nous crons un boolen qui indique si on accepte ou non les clients trangers au domaine du serveur ;
lignes 21-23 : la mthode d'accs cette information ;
...
@Component
public class ApplicationModel implements IMetier {
// la couche [mtier]
@Autowired
private IMetier mtier;
// donnes provenant de la couche [mtier]
private List<Medecin> mdecins;
private List<Client> clients;
private List<String> messages;
// donnes de configuration
private boolean CORSneeded = true;
...
public boolean isCORSneeded() {
return CORSneeded;
}
}
http://tahe.developpez.com
206/325
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
import javax.servlet.http.HttpServletResponse;
lignes 28-31 : dfinissent un contrleur pour l'URL [/getAllMedecins] lorsqu'elle est demande avec la commande HTTP
[OPTIONS] ;
ligne 29 : la mthode [getAllMedecins] admet pour paramtre l'objet [HttpServletResponse] qui va tre envoy au client
qui a fait la demande. Cet objet est inject par Spring ;
ligne 30 : on dlgue le traitement de la demande la mthode prive 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'entte HTTP :
import
import
import
import
org.springframework.beans.factory.annotation.Autowired;
org.springframework.stereotype.Controller;
org.springframework.web.bind.annotation.RequestMapping;
org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// envoi des options au client
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// on fixe le header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
}
}
Access-Control-Allow-Origin: *
qui signifie que le serveur accepte les clients de tout domaine (*).
Nous sommes dsormais prts pour de nouveaux tests. Nous lanons la nouvelle version du service web et nous dcouvrons que le
problme 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 mthode [getAllMedecins] de la ligne 29 n'est jamais appele.
Aprs quelques recherches, on dcouvre que Spring MVC traite lui-mme les commandes HTTP [OPTIONS] avec un traitement
par dfaut. Aussi c'est toujours Spring qui rpond et jamais la mthode [getAllMedecins] de la ligne 29. Ce comportement par
dfaut de Spring MVC peut tre chang. Nous introduisons une nouvelle classe de configuration, pour configurer le nouveau
comportement :
http://tahe.developpez.com
207/325
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
package rdvmedecins.web.config;
ligne 8 : la classe est une classe de configuration Spring. Elle dclare des beans qui seront placs dans le contexte de
Spring ;
ligne 12 : le bean [dispatcherServlet] sert dfinir la servlet qui gre les demandes des clients. Elle est de type
[DispatcherServlet]. Cette servlet est normalement cre par dfaut. Si on la cre nous-mmes, on peut alors la
configurer ;
ligne 14 : on cre 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 configure ;
import
import
import
import
org.springframework.boot.autoconfigure.EnableAutoConfiguration;
org.springframework.context.annotation.Bean;
org.springframework.web.servlet.DispatcherServlet;
org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
package rdvmedecins.web.config;
3.7.6.7
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
Tests de l'application - 2
Nous lanons la nouvelle version du service web / JSON et essayons d'obtenir la liste des mdecins avec notre client Angular. Nous
examinons les changes rseau dans l'onglet [Network] :
http://tahe.developpez.com
208/325
en [1], on peut constater que l'entte HTTP [Access-Control-Allow-Origin: *] est dsormais prsent dans la rponse du
serveur. Et pourtant a ne marche toujours pas. Nous examinons en [2], les logs de la console. On y trouve le log suivant :
On voit que le navigateur attend un nouvel entte HTTP [Access-Control-Allow-Headers] qui lui dirait qu'on a le droit de
lui envoyer l'entte 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 accompagne d'un
entte d'authentification, il demande si le serveur accepte celui-ci.
Nous modifions notre serveur web / JSON pour envoyer cet entte. La classe [RdvMedecinsCorsController] volue comme
suit :
1.
2.
3.
4.
5.
6.
7.
8. }
Nous relanons le serveur et redemandons la liste des mdecins avec le client Angular :
http://tahe.developpez.com
209/325
Cette fois, c'est bon. Les logs console montrent la rponse reue par la mthode [dao.getData] :
1. [dao] getData[/getAllMedecins] success rponse : {"data":{"status":0,"data":
[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},
{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},
{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},
{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]},"status":200,"config
":{"method":"GET","transformRequest":[null],"transformResponse":
[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":
{"Accept":"application/json, text/plain, */*","Authorization":"Basic
YWRtaW46YWRtaW4="}},"statusText":"OK"}
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] ;
http://tahe.developpez.com
210/325
On se connecte sous l'identit [user / user] qui n'a pas accs l'application (seul [admin] y a accs) :
Cette fois-ci, l'erreur n'est plus [Erreur d'authentification] mais [Accs refus].
3.7.7
Nous reprenons l'application prcdente pour cette fois prsenter la liste des clients dans une liste droulante de type [Bootstrap
select]) (cf paragraphe 3.6.6, page 157).
3.7.7.1
La vue V
http://tahe.developpez.com
211/325
Pour obtenir la vue V, nous dupliquons le code [app-16.html] dans [app-17.html] et le modifions de la faon suivante :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
ng-show="errors.show">
</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
http://tahe.developpez.com
212/325
3.7.7.2
Le contrleur C et le modle M
Le code Javascript du fichier [rdvmedecins-05] est obtenu par recopie du fichier [rdvmedecins-04] :
Quasiment rien ne change, sauf dans le contrleur qui est dsormais adapt pour fournir la liste des clients :
1. angular.module("rdvmedecins")
2.
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
3.
function ($scope, utils, config, dao, $translate) {
4.
// ------------------- initialisation modle
5.
// modle
6.
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time:
undefined};
7.
$scope.waitingTimeText = config.waitingTimeText;
8.
$scope.server = {url: undefined, login: undefined, password: undefined};
9.
$scope.clients = {title: config.listClients, show: false, model: {}};
10.
$scope.errors = {show: false, model: {}};
11.
$scope.urlServerLabel = config.urlServerLabel;
12.
$scope.loginLabel = config.loginLabel;
13.
$scope.passwordLabel = config.passwordLabel;
14.
15.
// tche asynchrone
16.
var task;
17.
18.
// excution action
19.
$scope.execute = function () {
20.
// on met jour l'UI
21.
$scope.waiting.visible = true;
22.
$scope.clients.show = false;
23.
$scope.errors.show = false;
24.
// attente simule
25.
task = utils.waitForSomeTime($scope.waiting.time);
26.
var promise = task.promise;
27.
// attente
28.
promise = promise.then(function () {
29.
// on demande la liste des clients;
30.
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password,
config.urlSvrClients);
31.
return task.promise;
32.
});
33.
// on analyse le rsultat de l'appel prcdent
34.
promise.then(function (result) {
35.
// result={err: 0, data: [client1, client2, ...]}
http://tahe.developpez.com
213/325
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
trs peu de choses changent dans le contrleur. Il fournissait une liste de mdecins. Il fournit dsormais une liste de
clients ;
ligne 9 : [$scope.clients] sera le modle du bandeau des clients dans la vue V ;
ligne 30 : c'est l'URL [/getAllClients] qui est dsormais utilise ;
lignes 35-36 : les deux formes de rponse rendue par la mthode [dao.getData]. On a maintenant des clients au lieu de
mdecins ;
ligne 44 : une instruction assez rare dans un code Angular. On manipule directement le DOM (Document Object Model).
Ici on veut appliquer la mthode [selectpicker] (fait partie de [bootstrap-select.min.js]) aux lments du DOM qui ont la
classe [selectpicker] [$('.selectpicker')]. Il n'y en a qu'un, la liste droulante :
<select data-style="btn-primary" class="selectpicker" select-enable="">
....
</select>
Au paragraphe 3.6.6, page 157, il a t montr que cela stylisait la liste droulante de la faon suivante :
Comme il a t fait pour les mdecins, nous sommes amens modifier le service web galement.
3.7.7.3
http://tahe.developpez.com
214/325
package rdvmedecins.web.controllers;
lignes 29-32 : la mthode [getAllClients] va grer la demande HTTP [OPTIONS] que va lui envoyer le navigateur ;
3.7.7.4
...
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// envoi des options au client
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// on fixe le header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// on autorise le header [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
}
// liste des mdecins
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
Tests de l'application 1
Nous sommes dsormais prts pour un test. Nous lanons le serveur web puis entrons des valeurs valides dans le formulaire
Angular. Nous obtenons la rponse suivante :
http://tahe.developpez.com
215/325
Ce message d'erreur est affich lorsqu'Angular n'a pu faire la requte HTTP demande. Il faut en chercher alors les causes dans les
logs de la console. On y trouve le message suivant :
XMLHttpRequest cannot load http://localhost:8080/getAllClients. No 'Access-Control-Allow-Origin' header
is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.
Un problme qu'on croyait rsolu. On va alors voir les changes rseau qui se sont produits :
On voit que l'opration [getAllClients] avec la mthode HTTP [OPTIONS] s'est bien passe mais que l'opration [getAllClients]
avec la mthode HTTP [GET] a t annule. La rponse la demande [OPTIONS] a t la suivante :
Les enttes HTTP du CORS sont bien l. Examinons maintenant les changes HTTP lors du GET :
http://tahe.developpez.com
216/325
C'est le log que fait systmatiquement la mthode [dao.getData] la rception de la rponse sa demande HTTP. On peut
remarquer deux choses :
[status=0] : cela veut dire que c'est Angular qui a annul la requte HTTP ;
3.7.7.5
Les mthodes [GET] et [POST] sont traites dans la classe [RdvMedecinsController]. Nous devons la modifier pour que ces
mthodes envoient les enttes CORS. Nous le faisons de la faon 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 = RequestMethod.GET)
14.
public Reponse getAllClients(HttpServletResponse response) {
15.
// enttes CORS
16.
rdvMedecinsCorsController.getAllClients(response);
http://tahe.developpez.com
217/325
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
}
28. ...
3.7.7.6
// tat de l'application
if (messages != null) {
return new Reponse(-1, messages);
}
// liste des clients
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
ligne 8 : nous voulons rutiliser le code que nous avons plac dans le contrleur [RdvMedecinsCorsController]. Aussi
injectons-nous celui-ci ici ;
ligne 14 : la mthode qui traite la demande [GET /getAllClients]. Nous faisons deux modifications :
ligne 14 : nous injectons l'objet [HttpServletResponse] dans les paramtres de la mthode,
ligne 16 : nous utilisons les mthodes de la classe [RdvMedecinsCorsController] pour mettre dans cet objet les enttes
CORS ;
Tests de l'application 2
Nous lanons la nouvelle version du service web et redemandons la liste des clients. Nous obtenons la rponse suivante :
2
Dans les logs console, la mthode [dao.getData] a affich la rponse qu'elle a reue :
[dao] getData[/getAllClients] success rponse : {"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":"http://localhost:8080/getAllClients","headers":
{"Accept":"application/json, text/plain, */*","Authorization":"Basic
YWRtaW46YWRtaW4="}},"statusText":"OK"}
http://tahe.developpez.com
218/325
Donc la mthode a bien reu la liste des clients. Une fois le code vrifi, on en vient suspecter l'instruction suivante qu'on ne
matrise pas trs bien :
1. // on style la liste droulante
2. $('.selectpicker').selectpicker();
On a donc localis le problme. C'est l'application de la mthode [selectpicker] la liste droulante qui pose problme. Lorsqu'on
regarde le code source de la page errone, on a la chose suivante :
http://tahe.developpez.com
219/325
on dcouvre qu'en [1], la liste droulante est bien prsente avec ses lments mais qu'elle n'est pas affiche
[style='display:none'] ;
en [2], on voit le bouton [bootstrap select] affich. Les lments de la liste droulante devraient apparatre dans la liste <ul
role='menu'>. Ils n'y sont pas et on a donc une liste vide. Il semble que lorsque la mthode [selectpicker] a t applique
la liste droulante, son contenu tait vide ce moment l ;
par le suivant :
1.
2.
3.
4. });
Le style [bootstrap-select] est appliqu au travers d'une fonction [$timeout]. Nous avons dj rencontr cette fonction qui permet
d'excuter une fonction pass un certain dlai. Ici, l'absence de dlai vaut un dlai nul. Les lignes prcdentes mettent un vnement
dans la liste d'attente des vnements du navigateur. Lorsque le traitement de l'vnement en cours (clic sur le bouton [Liste des
clients]) va tre termin, la vue V va tre affiche. Puis aussitt aprs, le navigateur va consulter sa liste d'vnements. A cause de
son dlai nul, l'vnement [$timeout] va tre en tte de liste et trait. Le style [bootstrap-select] est alors appliqu une liste
droulante remplie. Voyons le rsultat :
http://tahe.developpez.com
220/325
Le bouton [bootstrap-select] qui prcdemment tait vide contient dsormais la liste des clients.
3.7.7.7
http://tahe.developpez.com
221/325
$('.selectpicker').selectpicker();
On manipule un objet du DOM. Nombre de dveloppeurs Angular sont allergiques la manipulation du DOM dans le code d'un
contrleur. 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 crer de nouveaux lments ou attributs HTML. Voyons un premier exemple :
Nous crons le fichier JS [selectEnable] suivant :
1. angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
2.
return {
3.
link: function (scope, element, attrs) {
4.
$timeout(function () {
5.
var selectpicker = $('.selectpicker');
6.
selectpicker.selectpicker();
7.
});
8.
}
9.
};
10. }]);
La directive appartient au module [rvmedecins]. C'est une fonction qui accepte deux paramtres :
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 prdfini [$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 paramtres :
scope : le modle de la vue dans laquelle est utilise la directive ;
element : l'lment de la vue, objet de la directive ;
attrs : les attributs de cet lment ;
Prenons un exemple. La directive [selectEnable] pourrait tre utilise 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 applique n'importe quel lment 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].
La directive [selectEnable] pourrait tre galement utilise de la faon suivante :
<select-enable attr1='val1' attr2='val2' ...>...</select-enable>
Ici la directive [doSomething] est applique sous la forme d'une balise HTML <do-something>.
Revenons l'criture
<div select-enable="data"></div>
http://tahe.developpez.com
222/325
13.
link: function (scope, element, attrs) {
14.
$timeout(function () {
15.
$('.selectpicker').selectpicker();
16.
});
17.
}
18. };
19. }]);
lignes 14-16 : on retrouve le code que nous avions plac auparavant dans le contrleur. Celui-ci est excut lors de la
rencontre de la directive [select-enable] (sous forme d'lment ou d'attribut) lors de l'affichage de la vue V.
Pour mettre en oeuvre cette directive, nous copions le fichier [app-17.html] dans [app-17B.html] et le modifions de la faon
suivante :
1.
<select data-style="btn-primary" class="selectpicker" select-enable="">
2.
<option ng-repeat="client in clients.data" value="{{client.id}}">
3.
{{client.titre}} {{client.prenom}} {{client.nom}}
4.
</option>
5. </select>
ligne 1 : on applique la directive [selectEnable] l'lment HTML [select]. Comme il n'y a pas d'informations passer la
directive, nous crivons simplement [select-enable=""] ;
Nous modifions galement le contrleur en dupliquant le fichier JS [rdvmedecins-05.js] dans [rdvmedecins-05B.js] et nous
rfrenons le nouveau fichier JS dans le fichier [app-17B.html] et le fichier [selectEnable.js] 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 gr mais Angular ne signalera aucune
erreur.
1. <script type="text/javascript" src="rdvmedecins-05B.js"></script>
2. <script type="text/javascript" src="selectEnable.js"></script>
3.7.7.8
Tests de l'application 3
http://tahe.developpez.com
223/325
ne s'est pas droule au bon moment. On peut essayer de rsoudre le problme de diverses faons. Au bout de nombreux tests
infructueux, on se rend compte que l'opration ci-dessus, ne doit se drouler qu'une fois et uniquement lorsque la liste
droulante a t remplie. Pour obtenir ce rsultat, on rcrit la balise <select> de la faon suivante :
1.
2.
3.
4.
5.
Ligne 1, la balise <select> n'est gnre que si [clients.data] existe. Ce n'est pas le cas lors de l'affichage initial de la vue V. La balise
<select> ne sera donc pas gnre et la directive [selectEnable] pas value. Lorsque l'utilisateur va cliquer sur le bouton [Liste des
clients], [clients.data] aura une nouvelle valeur dans le modle M. Parce que le modle M a chang, la balise <select> va tre
http://tahe.developpez.com
224/325
rvalue et ici gnre. La directive [selectEnable] va donc tre value galement. Lorsqu'elle est value, les lignes 2-4 de la balise
<select> n'ont pas encore t values. On a donc une liste de clients vide. Si on crit la directive [selectEnable] de la faon
suivante :
1. angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function
($timeout, utils) {
2.
return {
3.
link: function (scope, element, attrs) {
4.
utils.debug("directive selectEnable");
5.
$('.selectpicker').selectpicker();
6.
}
7.
}
8. }]);
la ligne 5 va tre excute avec une liste vide et on aura alors une liste droulante vide l'affichage. Il faut alors crire :
1. angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function
($timeout, utils) {
2.
return {
3.
link: function (scope, element, attrs) {
4.
utils.debug("directive selectEnable");
5.
$timeout(function () {
6.
$('.selectpicker').selectpicker();
7.
})
8.
}
9.
}
10. }]);
pour avoir le rsultat attendu. A cause du [$timeout] de la ligne 5, la ligne 6 ne sera excute qu'aprs valuation complte de la vue
V, donc un moment ou la balise <select> aura tous ses lments.
3.7.8
Nous prsentons maintenant une application qui affiche l'agenda d'un mdecin.
3.7.8.1
La vue V de l'application
2
3
http://tahe.developpez.com
225/325
3.7.8.2
Le formulaire
Nous dupliquons le fichier [app-17.html] dans [app-18.html] puis nous modifions le code de la faon suivante :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- le message d'attente -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la demande -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="row" style="margin-bottom: 20px">
<div class="col-md-3">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" class="selectpicker">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
<div class="col-md-3">
<h2 translate="{{calendar.title}}"></h2>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="calendar.jour" min-date="calendar.minDate" show-weeks="true"
class="well well-sm"></datepicker>
</div>
</div>
</div>
<button class="btn btn-primary" ng-click="execute()">{{agenda.title|translate}}</button>
</div>
<!-- la liste d'erreurs -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- l'agenda -->
<div id="agenda" ng-show="agenda.show">
...
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
http://tahe.developpez.com
226/325
3.7.8.3
Le contrleur C
Seuls le service [utils] et le contrleur [rdvMedecinsCtrl] vont tre impacts par les modifications.
Le contrleur [rdvMedecinsCtrl] devient le suivant :
1. // contrleur
2. angular.module("rdvmedecins")
3.
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout',
'$filter', '$locale',
4.
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
5.
// ------------------- initialisation modle
6.
// modle
7.
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
8.
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
9.
$scope.errors = {show: false, model: {}};
10.
$scope.medecins = {
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: config.listMedecins};
18.
$scope.agenda = {title: config.getAgendaTitle, data: undefined, show: false};
19.
$scope.calendar = {title: config.getCalendarTitle, minDate: new Date(), jour: new Date()};
20.
// on style la liste droulante
21.
$timeout(function () {
22.
$('.selectpicker').selectpicker();
23.
});
24.
// locale franaise pour le calendrier
25.
angular.copy(config.locales['fr'], $locale);
26. ...
http://tahe.developpez.com
227/325
27.
}
28. ])
29. ;
ligne 4 : on rcupre l'attribut [value] du mdecin slectionn. On utilise ici de nouveau la mthode [selectpicker] qui
provient du fichier [bootstrap-select.min.js]. Il faut se souvenir de la forme des options de la liste droulante :
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
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 mthode [execute] sera termine, le bandeau d'attente sera affich et tout le reste cach ;
http://tahe.developpez.com
228/325
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36. }
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// mise en forme de l'affichage des horaires
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
// on cre un evt pour styler la table aprs l'affichage de la vue
$timeout(function () {
$("#creneaux").footable();
});
} else {
// il y a eu des erreurs pour obtenir l'agenda
$scope.errors = {
title: config.getAgendaErrors,
messages: utils.getErrors(result),
show: true
};
Le paramtre [result.data] 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 crneaux 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 crneau ;
http://tahe.developpez.com
229/325
3.7.8.4
ligne 27 : pour rendre la table 'responsive', il faut lui appliquer la mthode [footable]. On retrouve ici la mme difficult
que celle rencontre pour le composant [bootstrap-select]. Si on crit simplement la ligne 17, on constate que la table n'est
pas 'responsive'. On rsoud ce problme de la mme faon avec la fonction [$timeout] (ligne 26) ;
lignes 31-34 : le cas o l'appel HTTP a chou. On affiche alors les messages d'erreur ;
Affichage de l'agenda
Nous revenons maintenant au code de l'agenda dans le fichier [app-18.html]. C'est le suivant :
1. <!-- l'agenda -->
2.
<div id="agenda" ng-show="agenda.show">
3.
<!-- cas du mdecin sans crneaux de consultation -->
4.
<h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
5.
translate="agenda_medecinsanscreneaux"></h4>
6.
<!-- agenda du mdecin -->
7.
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
8.
<div class="tab-pane active col-md-6">
9.
<table creneaux-table id="creneaux" class="table">
10.
<thead>
11.
<tr>
12.
<th data-toggle="true">
13.
<span translate="agenda_creneauhoraire"></span>
14.
</th>
15.
<th>
16.
<span translate="agenda_client">Client</span>
17.
</th>
18.
<th data-hide="phone">
19.
<span translate="agenda_action">Action</span>
20.
</th>
21.
</tr>
22.
</thead>
23.
<tbody>
24.
<tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
25.
<td>
26.
<span
27.
ng-class="! creneauMedecin.rv ? 'status-metro status-active' : 'status-metro statussuspended'">
28.
{{creneauMedecin.creneau.text}}
29.
</span>
30.
</td>
31.
<td>
32.
<span>{{creneauMedecin.rv.client.titre}} {{creneauMedecin.rv.client.prenom}}
{{creneauMedecin.rv.client.nom}}</span>
33.
</td>
34.
<td>
http://tahe.developpez.com
230/325
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
lignes 4-5 : on se rappelle que [agenda.data] est l'agenda, que [agenda.data.creneauxMedecin] est un tableau d'objets de
type [creneauMedecin]. Chaque lment de ce dernier type a un attribut [creneauMedecin.creneau] qui est un crneau
horaire. Chaque crneau horaire a deux lments qui nous intressent :
[creneauMedecin.creneau.rv] qui est l'ventuel RV (rv!=null) pris sur le crneau ;
[creneauMedecin.creneau.text] qui est le texte [dbut:fin] du crneau horaire ;
ligne 4 : affiche un message spcial si le mdecin n'a pas de crneaux horaires. C'est improbable mais il se trouve que notre
base de donnes est incomplte et ce cas existe. La gnration HTML ou non du message est contrle par la directive
[ng-if] ;
La directive [ng-if] est diffrente des directives [ng-show, ng-hide]. Ces dernires se contentent de cacher une zone
prsente dans le document. Si [ng-if='false'], alors la zone est enleve du document. On l'a utilise ici pour illustration ;
ligne 9 : l'attribut [id='creneaux'] est important. C'est lui qui est utilis dans l'instruction :
$("#creneaux").footable();
http://tahe.developpez.com
231/325
1
4
3
2
ligne 24 : on parcourt le tableau [agenda.data.creneauxMedecin] ;
lignes 26-29 : on crit le texte [3]. On utilise la directive [ng-class] qui va gnrer l'attribut [class] de l'lment. Ici, si on a
[creneauMedecin.rv==null], cela veut dire que le crneau est libre et on met un fond vert au texte. Sinon, on met un fond
rouge ;
ligne 32 : on crit le nom du client pour qui a t pris le RV [4]. Si [rv==null], ces informations n'existent pas mais Angular
gre correctement ce cas et ne dclare pas d'erreur ;
lignes 34-39 : affichent l'un des deux boutons [Rserver] ou [Supprimer]. C'est l'existence ou non d'un rendez-vous qui fait
que l'on choisit l'un ou l'autre des boutons ;
3.7.8.5
Comme pour les exemples prcdents, le serveur web doit tre modifi pour que l'URL [/getAgendaMedecinJour] envoie les enttes
CORS :
// agenda du mdecin
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method =
RequestMethod.OPTIONS)
3.
public void getAgendaMedecinJour(HttpServletResponse response) {
4.
sendOptions(response);
5. }
Cette mthode va envoyer les enttes CORS pour la requte HTTP [OPTIONS]. On doit faire la mme chose pour la requte
HTTP [GET] dans la classe [RdvMedecinsController] :
1. @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
2.
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour, HttpServletResponse response) {
3.
// enttes CORS
4.
rdvMedecinsCorsController.getAgendaMedecinJour(response);
5. ...
6. }
3.7.8.6
Utilisation de directives
Comme il a t fait prcdemment, nous allons dporter la manipulation du DOM dans des directives. Nous avons deux
manipulations de DOM :
http://tahe.developpez.com
232/325
1.
2.
3.
4. });
1.
2.
3.
4. });
Pour le 1er cas, nous allons utiliser la directive [selectEnable] dj prsente. Pour le second cas, nous crons la directive [footable]
dans le fichier JS [footable.js] suivant :
1. angular.module("rdvmedecins").directive('footable', ['$timeout', 'utils', function ($timeout,
utils) {
2.
return {
3.
link: function (scope, element, attrs) {
4.
utils.debug("directive footable");
5.
$timeout(function () {
6.
$("#creneaux").footable();
7.
})
8.
}
9.
}
10. }]);
1.
ligne 1 : on applique la directive [selectEnable] (via l'attribut [select-enable]) la balise <select> des mdecins ;
=0">
2.
3.
4.
5. <tr>
ligne 3 : on applique la directive [footable] (via l'attribut [footable]) la table HTML de l'agenda ;
1.
2.
3.
4.
Le fichier [rdvmedecins-06B.js] est identique au fichier [rdvmedecins-06.js] deux dtails prs. Les lignes manipulant le DOM
disparaissent :
1.
2.
3.
4. });
http://tahe.developpez.com
233/325
1.
2.
3.
4. });
Cei fait, l'excution de l'application [app-18B.html] donne les mmes rsultats que celle de [app-18.html].
3.7.9
Nous prsentons maintenant une application qui permet de crer et d'annuler des rservations.
3.7.9.1
La vue V de l'application
en [1], on pourra rserver. La rservation qui sera faite le sera pour un client alatoire ;
en [2], on pourra supprimer les rservations que nous aurons faites ;
Nous dupliquons le fichier [app-18.html] dans [app-19.html] puis nous modifions le code de la faon suivante :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- le message d'attente -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la liste d'erreurs -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- l'agenda -->
<div id="agenda" ng-show="agenda.show">
..
<!-- agenda du mdecin -->
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table" footable="">
...
http://tahe.developpez.com
234/325
22.
<tbody>
23.
<tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
24. ...
25.
<td>
26.
<a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro
status-active" ng-click="reserver(creneauMedecin.creneau.id)">
27.
</a>
28.
<a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro
status-suspended" ng-click="supprimer(creneauMedecin.rv.id)">
29.
</a>
30.
</td>
31.
</tr>
32.
</tbody>
33.
</table>
34.
</div>
35.
</div>
36.
</div>
37. </div>
38. ....
39. <script type="text/javascript" src="rdvmedecins-07.js"></script>
40. <script type="text/javascript" src="footable.js"></script>
3.7.9.2
Le contrleur C
Le code JS de [rdvmedecins-07.js] est d'abord obtenu par recopie du fichier [rdvmedecins-06.js]. Puis il est modifi. On a toujours les
grands blocs de code habituels. Les modifications se font essentiellement dans le contrleur :
3.7.9.3
Initialisation du contrleur C
http://tahe.developpez.com
235/325
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
// modle
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
$scope.errors = {show: false, model: {}};
$scope.medecins = {
data: [
{id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
{id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
{id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
{id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
],
title: config.listMedecins
};
var mdecin = $scope.medecins.data[0];
var clients = [
{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: "Maurice"},
{id: 4, version: 1, titre: "Melle", nom: "BISTROU", prenom: "Brigitte"}
];
// locale franaise pour la date
angular.copy(config.locales['fr'], $locale);
var today = new Date();
var formattedDay = $filter('date')(today, 'yyyy-MM-dd');
var fullDay = $filter('date')(today, 'fullDate');
$scope.agenda = {title: config.agendaTitle, data: undefined, show: false, model: {titre:
mdecin.titre, prenom: mdecin.prenom, nom: mdecin.nom, jour: fullDay}};
// ---------------------------------------------------------------- agenda initial
// la tche asynchrone globale
var task;
// on demande l'agenda
getAgenda();
....
// ------------------------------------------------------------------ rservation
$scope.reserver = function (creneauId) {
};
// ------------------------------------------------------------ suppression RV
$scope.supprimer = function (idRv) {
...
...
};
// obtention de l'agenda
function getAgenda() {
}
// annulation attente
function cancel() {
...
} ]);
ligne 6 : configuration du message d'attente. Par dfaut, on attendra 3 secondes avant de faire un appel HTTP ;
ligne 7 : les informations ncessaires aux appels HTTP ;
ligne 8 : configuration du message d'erreurs ;
lignes 9-17 : les mdecins en dur ;
ligne 18 : un mdecin particulier. C'est pour ses crneaux qu'on fera des rservations ;
lignes 19-24 : les clients en dur ;
ligne 26 : on veut manipuler des dates franaises ;
ligne 27 : les rendez-vous seront pris la date d'aujourd'hui ;
ligne 28 : le service web de rservation attend des dates au format 'aaaa-mm-jj' ;
ligne 29 : la date d'aujourd'hui sous la forme [jeudi 26 juin 2014] ;
http://tahe.developpez.com
236/325
ligne 30 : configuration de l'agenda. L'attribut [model] transporte les paramtres du message internationalis qui va tre
affich :
agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
ligne 35 : la variable globale [task] reprsente un moment donn la tche asynchrone en cours d'excution ;
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.
3.7.9.4
Obtention de l'agenda
// obtention de l'agenda
function getAgenda() {
// le chemin de l'URL de service
var path = config.urlSvrAgenda + "/" + mdecin.id + "/" + formattedDay;
// on demande l'agenda
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password,
path);
// msg d'attente
$scope.waiting.visible = true;
// on analyse le rsultat de l'appel au service [dao]
task.promise.then(function (result) {
// fin d'attente
$scope.waiting.visible = false;
// erreur ?
if (result.err == 0) {
// on prpare le modle de l'agenda
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// mise en forme de l'affichage des horaires
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
} else {
// il y a eu des erreurs pour obtenir l'agenda
$scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result),
show: true};
25.
}
26.
});
27. }
http://tahe.developpez.com
237/325
ligne 4 : on utilise le mdecin cr lors de l'initialisation du contrleur ainsi que le jour format qui a t construit ;
Ce code a t isol dans une fonction car il est galement utilis par les fonctions [reserver] et [supprimer].
3.7.9.5
ligne 1 : on rappelle que le paramtre de la fonction [reserver] est le n du crneau (attribut id) ;
ligne 4 : un client est choisi de faon alatoire dans la liste des clients dfinie 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 excutes qu' la fin des 3 secondes ;
http://tahe.developpez.com
238/325
ligne 12 : l'URL du service de rservation [/ajouterRv]. Cette URL est particulire vis vis de celles qu'on a rencontres
jusqu' maintenant. Elle est dfinie comme suit dans le service web :
1.
ligne 1 : l'URL n'a pas de paramtres et elle est demande avec un POST ;
ligne 2 : les paramtres posts le sont sous la forme d'un objet JSON. Celui-ci sera dsrialis dans le paramtre
[post] (@RequestBody) ;
3.7.9.6
ligne 14 : on cre la valeur poster sous la forme d'un objet JS. Angular le srialisera en JSON lorsqu'il sera post ;
ligne 16 : l'appel HTTP est fait. La valeur poster est le dernier paramtre de la fonction [dao.getData]. Lorsque ce
paramtre est prsent, fonction [dao.getData] fait un POST au lieu d'un GET (revoir le code page 198, paragraphe
3.7.6.4) ;
ligne 18 : on retourne la promesse de l'appel HTTP ;
lignes 23-29 : ne sont excutes que lorsque l'appel HTTP a rendu sa rponse ;
ligne 23 : le paramtre [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 rservation s'est bien passe, on raffiche le nouvel agenda ;
Modification serveur
http://tahe.developpez.com
239/325
L'ajout est fait lignes 10-13. Les enttes des lignes 2-8 seront envoys pour l'URL [/ajouterRv] (ligne 10) et la mthode HTTP
[OPTIONS] (ligne 10).
La classe [RdvMedecinsController] est elle modifie de la faon suivante :
1.
2.
3.
4.
5.
Pour la mthode [POST] (ligne 1) et l'URL [/ajouterRv] (ligne 1), la mthode que nous venons d'ajouter dans
[RdvMedecinsCorsController] est appele (ligne 4), renvoyant donc les mmes enttes HTTP que pour la mthode HTTP
[OPTIONS].
3.7.9.7
Tests
http://tahe.developpez.com
240/325
Comme toujours dans ces cas l, il faut regarder les logs de la console :
1. [dao] getData[/ajouterRv] error rponse : {"data":"","status":0,"config":
{"method":"POST","transformRequest":[null],"transformResponse":
[null],"timeout":1000,"url":"http://localhost:8080/ajouterRv","data":{"jour":"2014-0630","idCreneau":1,"idClient":4},"headers":{"Accept":"application/json, text/plain,
*/*","Authorization":"Basic YWRtaW46YWRtaW4=","Content-Type":"application/json;charset=utf8"}},"statusText":""}
La mthode [dao.getData] a chou avec [status=0], ce qui signifie que c'est Angular qui a annul la requte. On a la cause de
l'erreur dans les logs :
XMLHttpRequest cannot load http://localhost:8080/ajouterRv. Request header field Content-Type is not
allowed by Access-Control-Allow-Headers.
1
2
La nouveaut est donc que sur une opration POST, le client Angular demande davantage d'autorisations au serveur. Il faut donc
modifier celui-ci pour qu'il les lui donne :
http://tahe.developpez.com
241/325
Dans la classe [RdvMedecinsCorsController], nous modifions la mthode prive qui gnre les enttes HTTP envoys pour les
commandes OPTIONS, GET et POST :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
ligne 7 : on a ajout une autorisation pour les enttes HTTP [accept, content-type] ;
ligne 9 : on a ajout une autorisation pour la mthode POST ;
3.7.9.8
http://tahe.developpez.com
242/325
12.
13.
ligne 1 : il faut se rappeler que le paramtre de la fonction est le n du rendez-vous supprimer. On a l un code trs
similaire celui de la rservation. Nous ne commentons que les diffrences ;
ligne 9 : l'URL du service est ici [/supprimerRV] et l galement elle est accde via un POST :
1.
Le paramtre post est l encore transmis sous une forme JSON. A la page 103, nous avons montr la nature du POST
ralis la main :
1
2
http://tahe.developpez.com
243/325
3.7.9.9
Modification serveur
L'ajout est fait lignes 13-16. Les enttes des lignes 2-10 seront envoys pour l'URL [/supprimerRv] (ligne 13) et la mthode HTTP
[OPTIONS] (ligne 13).
La classe [RdvMedecinsController] est elle, modifie de la faon suivante :
1.
2.
3.
4.
5.
Pour la mthode [POST] (ligne 1) et l'URL [/supprimerRv] (ligne 1), la mthode que nous venons d'ajouter dans
[RdvMedecinsCorsController] est appele (ligne 4), renvoyant donc les mmes enttes HTTP que pour la mthode HTTP
[OPTIONS].
3.7.10
Nous prsentons maintenant la mme application que prcdemment mais au lieu de rserver pour un client alatoire, celui-ci sera
slectionn dans une liste droulante.
3.7.10.1
La vue V de l'application
http://tahe.developpez.com
244/325
http://tahe.developpez.com
245/325
3.7.10.2
Le constructeur C
ligne 8 : l'objet [$scope.clients] configure la liste droulante des clients dans la vue V ;
lignes 14-16 : de faon 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 utilise ici ne fonctionne que parce que la fonction
[getClients] rend une promesse (promise) ;
http://tahe.developpez.com
246/325
C'est un code que nous avons dj rencontr et comment. L'lment important noter est ligne 31 :
ligne 27 : on rend la promesse de la ligne 10, --d la dernire promesse obtenue dans le code. Cette promesse ne sera
obtenue que lorsque l'appel HTTP aura rendu sa rponse ;
La mthode [reserver] volue lgrement :
1.
2.
3.
4.
5.
6.
ligne 4 : on ne rserve plus pour un client alatoire mais pour le client slectionn dans la liste des clients.
3.7.11
3.7.11.1
La vue V
3.7.11.2
http://tahe.developpez.com
247/325
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45. <script type="text/javascript" src="selectEnable2.js"></script>
3.7.11.3
La directive [selectEnable2]
10. }]);
ligne 4 : on fait afficher la valeur du paramtres [attrs] afin de faire comprendre le fonctionnement du code. On va
dcouvrir que attrs['id']='selectpickerClients' pour la liste des clients ;
ligne 6 : pour localiser dans le DOM un lment 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'])] ;
http://tahe.developpez.com
248/325
La directive [selectEnable2] utilise donc l'information transporte par l'un des attributs de l'lement HTML auquel elle est
applique.
3.7.11.4
Le contrleur C
// contrleur
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- initialisation modle
// le msg d'attente
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
// les informations de connexion
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
// les erreurs
$scope.errors = {show: false, model: {}};
// les mdecins
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
// les clients
$scope.clients = {title: config.listClients, show: false, model: {}};
...
...
3.7.11.5
}
} ]);
Les tests
3.7.12
Nous reprenons le mme exemple que prcdemment mais nous voulons allger le code HTML en utilisant une directive. En effet,
nous avons actuellement le code HTML suivant :
1. <!-- la liste des clients -->
http://tahe.developpez.com
249/325
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
Les lignes 14-26 sont identiques aux lignes 1-13. Elles s'appliquent des mdecins au lieu des clients. Nous voudrions pouvoir crire
la chose suivante :
1.
<!-- la liste des clients -->
2.
<list model="clients" ng-if="clients.show"></list>
3.
<!-- la liste des mdecins -->
4. <list model="medecins" ng-if="medecins.show"></list>
Ce code fait intervenir une nouvelle directive [list] que nous allons crer maintenant.
3.7.12.1
La directive [list]
La directive [list] est place dans le fichier JS [list.js]. Son code est le suivant :
1. angular.module("rdvmedecins")
2.
.directive("list", ['utils', '$timeout', function (utils, $timeout) {
3.
// instance de la directive retourne
4.
return {
5.
// lment HTML
6.
restrict: "E",
7.
// url du fragment
8.
templateUrl: "list.html",
9.
// scope unique chaque instance de la directive
10.
scope: true,
11.
// fonction lien avec le document
12.
link: function (scope, element, attrs) {
13.
utils.debug("directive list attrs", attrs);
14.
scope.model = scope[attrs['model']];
15.
utils.debug("directive list model", scope.model);
16.
$timeout(function () {
17.
$('#' + scope.model.id).selectpicker();
18.
})
19.
}
20.
}
21. }]);
http://tahe.developpez.com
250/325
ligne 6 : l'attribut [restrict] fixe les faons d'utiliser la directive. [restrict: "E"] signifie que la directive [list] est utilisable
comme lment HTML <list ...>...</list>. [restrict: "A"] signifie que la directive [list] est utilisable comme attribut, par
exemple <div ... list='...'>. [restrict: "AE"] signifie que la directive [list] est utilisable comme attribut et comme lment ;
ligne 8 : l'attribut [templateUrl] indique le nom du fragment HTML utiliser la rencontre de la balise. Ce fragment sera le
corps de la balise ;
ligne 10 : l'attribut [scope] fixe la porte du modle de la directive. [scope: true] signifie que deux lments de type <list>
auront chacun leur modle. Par dfaut, (scope non initialis), ils partagent leurs modles ;
ligne 12 : la fonction [link] que nous avons utilise dj plusieurs fois ;
Pour comprendre le code ci-dessus, il faut se rappeler l'utilisation qui va tre faite de la directive :
1.
<!-- la liste des clients -->
2.
<list model="clients" ng-if="clients.show"></list>
3.
<!-- la liste des mdecins -->
4. <list model="medecins" ng-if="medecins.show"></list>
La directive [list] est utilise comme lment HTML <list>. Cet lment a deux attributs :
[model] : qui va avoir pour valeur, l'lment du modle M de la vue V dans laquelle se trouve la directive [list]. Cet lment
va alimenter le modle de la directive ;
[ng-if] : qui va faire en sorte que le code HTML de la directive ne soit pas gnr s'il n'y a rien visualiser ;
Revenons au code de la fonction [link] de la directive :
1. link: function (scope, element, attrs) {
2.
utils.debug("directive list attrs", attrs);
3.
scope.model = scope[attrs['model']];
4.
utils.debug("directive list model", scope.model);
5.
$timeout(function () {
6.
$('#' + scope.model.id).selectpicker();
7.
})
8.
}
Maintenant, regardons le code HTML gnr par la directive. A cause de l'attribut [templateUrl: "list.html"] de la directive, il faut le
chercher dans le fichier [list.html] :
1. <!-- une liste de clients ou de mdecins -->
2. <div class="alert alert-info" ng-show="model.show">
3.
<div class="row">
4.
<div class="col-md-4">
5.
<h2 translate="{{model.title}}"></h2>
6.
<select data-style="btn-primary" id="{{model.id}}" ng-if="model.data">
7.
<option ng-repeat="element in model.data" value="{{element.id}}">
8.
{{element.titre}} {{element.prenom}} {{element.nom}}
9.
</option>
10.
</select>
11.
</div>
12. </div>
13. </div>
http://tahe.developpez.com
251/325
la premire chose qu'il faut se rappeler pour lire ce code est que la directive a cr un objet [scope.model] 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 [model.show] pour montrer / cacher la vue gnre par la directive ;
ligne 5 : utilisation de [model.title] pour mettre un titre ;
ligne 6 : utilisation de [model.id] pour mettre un id la balise <select>. Cet id est utilis par le code JS de la directive ;
ligne 6 : utilisation de [model.data] pour gnrer le <select> uniquement s'il y a des donnes afficher ;
lignes 7-9 : utilisation de [model.data] pour gnrer les lments de la liste droulante ;
3.7.12.2
Le code HTML
<div class="container">
<h1>Rdvmedecins - v1</h1>
3.7.12.3
Le contrleur C
lignes 7 et 9, nous ajoutons l'attribut [id] aux modles des mdecins et des clients ;
3.7.12.4
Les tests
Les tests donnent les mmes rsultats que dans l'exemple prcdent.
3.7.13
Nous restons dans l'tude des directives et nous gardons l'exemple de la liste droulante. On veut tudier ici le comportement de la
directive [list] lorsque le contenu de la liste droulante change.
http://tahe.developpez.com
252/325
3.7.13.1
Les vues V
en [2], on demande une seconde fois la liste des clients. Cette seconde liste est alors cumule la premire [3]. C'est la mise
jour du composant [Bootstrap select] qu'on veut tudier dans cet exemple.
3.7.13.2
La page HTML
La page HTML [app-23.html] est obtenue par recopie de [app-22.html] puis modifie de la faon suivante :
http://tahe.developpez.com
253/325
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- le message d'attente -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la liste d'erreurs -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- le bouton -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- directives -->
<script type="text/javascript" src="list2.js"></script>
3.7.13.3
La directive [list2]
La seule diffrence avec la directive [list] est ligne 16 : avec la mthode [selectpicker('refresh')], on demande au composant [Bootstrapselect] de se rafrachir. L'ide derrire cela est qu' chaque fois que l'utilisateur va demander une nouvelle liste de clients, on va
rafrachir la liste droulante. Ca ne va pas marcher mais c'est l'ide de base.
http://tahe.developpez.com
254/325
3.7.13.4
Le contrleur C
Le contrleur est dans le fichier [rdvmedecins-11.js] obtenu par recopie du fichier [rdvmedecins-10.js] :
1.
// les clients
2.
$scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
3. ...
4.
// liste des clients
5.
$scope.getClients = function getClients() {
6.
// on met jour l'UI
7.
$scope.waiting.visible = true;
8.
$scope.errors.show = false;
9.
// on demande la liste des clients;
10.
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password,
config.urlSvrClients);
11.
var promise = task.promise;
12.
// on analyse le rsultat de l'appel prcdent
13.
promise = promise.then(function (result) {
14.
// result={err: 0, data: [client1, client2, ...]}
15.
// result={err: n, messages: [msg1, msg2, ...]}
16.
if (result.err == 0) {
17.
// on met les donnes acquises dans un nouveau modle pour forcer la vue se
rafrachir
18.
$scope.clients = {title: $scope.clients.title, data:
$scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
19.
// on met jour l'UI
20.
$scope.clients.show = true;
21.
$scope.waiting.visible = false;
22.
} else {
23.
// il y a eu des erreurs pour obtenir la liste des clients
24.
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result),
show: true, model: {}};
25.
// on met jour l'UI
26.
$scope.waiting.visible = false;
27.
}
28.
});
29. }
ligne 1 : afin de permettre la concatnation de tableaux dans [clients.data], cet objet est initialis avec un tableau vide ;
ligne 18 : on concatne la nouvelle liste de clients avec celles dj prsentes dans le tableau [clients.data] ;
Maintenant on crit :
1. // on met les donnes acquises dans un nouveau modle pour forcer la vue se rafrachir
2. $scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data),
show: $scope.clients.show, id: $scope.clients.id};
Pour comprendre ce code, il faut se rappeler comment le modle M est utilis dans la vue V dans le cas de la directive [list2] :
1.
<!-- la liste des clients -->
2. <list2 model="clients" ng-if="clients.show"></list2>
Le modle utilis par la directive [list2] est [clients]. Elle ne sera rvalue dans la vue V, que si [clients] change dans le modle M de
la vue. La premire ide qui vient pour la modification est d'crire :
$scope.clients.data=$scope.clients.data.concat(result.data) ;
pour tenir compte du fait que la nouvelle liste de clients doit tre ajoute aux prcdentes. Ce faisant, on modifie [clients.data] mais
pas [clients]. Je ne connais pas les arcanes de Javascript mais il ne serait pas tonnant que [clients] soit un pointeur, ainsi que
http://tahe.developpez.com
255/325
[clients.data]. Le pointeur [clients] ne change pas lorsqu'on change le pointeur [clients.data]. La directive [list2] n'est alors pas
rvalue. C'est effectivement ce qu'on constate lorsqu'on dbogue l'application (F12 dans Chrome).
En crivant :
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show:
$scope.clients.show, id: $scope.clients.id};
On s'assure que [$scope.clients] reoit bien une nouvelle valeur. Le pointeur [$scope.clients] pointe sur un nouvel objet. La directive
[list2] devrait alors tre rvalue. Mais pourtant, on n'a pas le rsultat cherch. Examinons les copies d'cran lorsqu'on demande
deux fois la liste des clients :
http://tahe.developpez.com
256/325
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 droulante ;
entre les lignes 1 et 2, elle n'est pas active alors que la vue a t affiche une premire fois. C'est d son attribut [ ngif="clients.show"] dans la vue V :
<list2 model="clients" ng-if="clients.show"></list2>
ligne 3 : aprs l'obtention de la premire liste de mdecins, [clients.show] passe true et la directive est active ;
aprs 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 ;
http://tahe.developpez.com
257/325
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24. }]);
ligne 14 : la fonction [scope.$watch] permet d'observer une valeur du modle. Sa syntaxe est [scope.$watch('var'), f] o
[var] est l'identifiant d'une variable du modle et f la fonction excuter 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 paramtre de la fonction [scope.$watch] est la fonction excuter lorsque la variable observe change
de valeur. Le paramtre [newValue] est la nouvelle valeur de la variable, donc pour nous la nouvelle valeur de la variable
[clients] du modle ;
ligne 17 : cette nouvelle valeur est affecte au champ [model] du modle de la directive ;
Ci-dessus, on voit qu'aprs avoir obtenu la seconde liste de clients, la directive [list2] est bien excute de nouveau, ce que confirme
le rsultat [2].
3.7.14
http://tahe.developpez.com
258/325
Nous dcidons de mettre les codes HTML de ces deux messages dans des directives.
3.7.14.1
<div class="container">
<h1>Rdvmedecins - v1</h1>
3.7.14.2
La directive [waiting]
http://tahe.developpez.com
259/325
16.
17.
18.
19.
20.
scope.model = newValue;
});
}
}]);
Dans le code JS de l'application, le modle [$scope.waiting] de ce code HTML sera dfini de la faon suivante :
// le msg d'attente
$scope.waiting = {title: {text: config.msgWaiting, values: {}}, show: false, cancel: cancel, time:
3000};
3.7.14.3
La directive [errors]
20. }]);
Dans le code JS de l'application, le modle [$scope.errors] de ce code HTML sera dfini de la faon suivante :
// il y a eu des erreurs pour obtenir la liste des clients
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages:
utils.getErrors(result), show: true, model: {}};
http://tahe.developpez.com
260/325
3.7.15
Exemple 15 : navigation
Jusqu' maintenant, nous avons utilis des applications page unique. Nous abordons dans cet exemple, les applications plusieurs
pages et la navigation entre celles-ci.
3.7.15.1
8
6
en [6], la vue n 3 ;
en [7], on passe la page 1 ;
en [8], on est revenu la vue n 1 ;
3.7.15.2
Organisation du code
http://tahe.developpez.com
261/325
3.7.15.3
Les vues du dossier [views] seront affiches dans le conteneur suivant [app-25.html] :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container" ng-controller="mainCtrl">
<!-- la barre de navigation -->
<ng-include src="'views/navbar.html'"></ng-include>
http://tahe.developpez.com
src="modules/rdvmedecins-13.js"></script>
src="controllers/mainController.js"></script>
src="controllers/page1Controller.js"></script>
src="controllers/page2Controller.js"></script>
src="controllers/page3Controller.js"></script>
262/325
3.7.15.4
Le module de l'application
ligne 1 : on dfinit le module [rdvmedecins]. Il a une dpendance sur le module [ngRoute] fourni par la bibliothque
[angular-route.min.js]. C'est ce module qui permet le routage dfini aux lignes 6-24 ;
ligne 4 : dfinit la fonction [config] du module [rdvmedecins]. On rappelle que cette fonction est excute 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 : dfinissent 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/app-21.html#/page1]. On voit que c'est toujours l'URL du
conteneur [/rdvmedecins-angular-v1/app-21.html] qui est utilise mais avec une information supplmentaire derrire un
caractre #. C'est cette information supplmentaire que le routage Angular gre ;
ligne 8 : indique le fragment HTML insrer dans la directive [ng-view] du conteneur :
ligne 9 : indique le nom du contrleur de ce fragment ;
lignes 11-15 : dfinissent la vue afficher lorsque l'utilisateur demande l'URL [/page2] ;
lignes 16-20 : dfinissent la vue afficher lorsque l'utilisateur demande l'URL [/page3] ;
lignes 21-24 : dfinissent le routage exercer lorsque l'URL demande n'est pas l'une des trois prcdentes (otherwise,
ligne 21) ;
ligne 23 : redirection vers l'URL [/page1], donc vers la vue dfinie aux lignes 6-10 ;
3.7.15.5
http://tahe.developpez.com
263/325
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24. }]);
ligne 3 : le contrleur [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) ;
page
conteneur mainCtrl
main
11
page1
page1Ctrl
page1
page2
page2Ctrl
page2
page3
page3Ctrl
page3
Les lignes 7-11 ont une consquence trs particulire : elles dfinissent le [$scope] du contrleur [mainCtrl] et dans celui-ci, elles
crent quatre variables [main, page1, page2, page3]. Ces quatre variables vont tre utilises comme modles respectifs du conteneur
et des trois vues qu'il va contenir tour tour.
3.7.15.6
La barre de navigation
http://tahe.developpez.com
264/325
La barre de navigation est dfinie ligne 3. Cela signifie qu'elle ne connat que le modle [main]. Son code est le suivant :
1. <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
2.
<div class="container">
3.
<div class="navbar-header">
4.
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbarcollapse">
5.
<span class="sr-only">Toggle navigation</span>
6.
<span class="icon-bar"></span>
7.
<span class="icon-bar"></span>
8.
<span class="icon-bar"></span>
9.
</button>
10.
<a class="navbar-brand" href="#">RdvMedecins</a>
11.
</div>
12.
<div class="collapse navbar-collapse">
13.
<ul class="nav navbar-nav">
14.
<li class="active">
15.
<a href="">
16.
<span ng-click="main.showPage1()">Page 1</span>
17.
</a>
18.
</li>
19.
<li class="active">
20.
<a href="">
21.
<span ng-click="main.showPage2()">Page 2</span>
22.
</a>
23.
</li>
24.
<li class="active">
25.
<a href="">
26.
<span ng-click="main.showPage3()">Page 3</span>
27.
</a>
28.
</li>
29.
</ul>
30.
</div>
31. </div>
32. </div>
aux lignes 16, 21, 26, ce sont des mthodes du modle [main] qui sont utilises ;
ligne 16 : un clic sur le lien [Page1] va lancer l'excution de la mthode [$scope.main.showPage1]. Celle-ci est dfinie
dans le contrleur [mainCtrl] de la faon suivante :
1.
2.
3.
4.
5.
6.
7.
8. };
// modle global
var main = $scope.main = {};
main.text = "[Modle global]";
// mthodes exposes la vue
main.showPage1 = function () {
$location.path("/page1");
ligne 6 : du code qui prcde, on voit que la mthode [main.showPage1] est en ralit la mthode
[$scope.main.showPage1]. C'est donc bien celle-ci qui va s'excuter ;
ligne 7 : on change l'URL de l'application qui devient [/page1]. Revenons au routage qui a t dfini dans le module
principal :
1.
$routeProvider.when("/page1",
2.
{
3.
templateUrl: "views/page1.html",
4.
controller: 'page1Ctrl'
5. });
on voit que le fragment [views/page1.html] va tre insr dans le conteneur et que son contrleur est [page1Ctrl].
3.7.15.7
http://tahe.developpez.com
265/325
1. <h1>Page 1</h1>
2. <div class="alert alert-info">
3.
<ul>
4.
<li>Modle global : {{main.text}}</li>
5.
<li>Modle local : {{page1.text}}</li>
6.
</ul>
7. </div>
On se rappelle que dans la vue insre dans le conteneur, le modle [main] est visible. C'est ce qu'on veut vrifier ligne 4. Par
ailleurs, le contrleur [page1Ctrl] du fragment [views/page1.html] dfinit un modle [page1]. C'est lui qui est utilis ligne 5.
Le code du contrleur [page1Ctrl] est le suivant :
1. angular.module("rdvmedecins")
2.
.controller('page1Ctrl', ['$scope',
3.
function ($scope) {
4.
5.
// modle de la page 1
6.
var page1=$scope.page1;
7.
page1.text="[Modle local dans page 1]";
8. }]);
ligne 2 : le [$scope] inject ici n'est pas vide. Puisque le contrleur [page1Ctrl] contrle une zone insre dans un
conteneur contrl par [mainCtrl], le [$scope] de la ligne 2 contient les lments du [$scope] dfini par le contrleur
[mainCtrl]. Il est important de le comprendre. Le [$scope] dfini par le contrleur [mainCtrl] contient les lments suivants
[main, page1, page2, page3]. Cela signifie qu'on a accs aux modles de toutes les vues. Ce n'est pas forcment
dsirable mais c'est le cas ici. Dans la version finale du client Angular, nous utiliserons cette particularit pour stocker dans
le modle [main] les informations qui doivent tre partages entre vues. On aura l, un concept analogue au concept de
'session' ct serveur ;
ligne 6 : on rcupre dans le [$scope] le modle [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 mme modle que la vue [/page1] (voir les copies d'cran page 261).
3.7.15.8
Contrle de la navigation
Nous souhaitons maintenant contrler la navigation de la faon suivante [page1 --> page2 --> page3 --> page1]. Ainsi si l'utilisateur
est sur la page 1 [/page1] et qu'il tape dans son navigateur l'URL [/page3] alors cette navigation ne doit pas tre accepte et on doit
rester sur la page 1.
Pour obtenir ce rsultat, nous modifions les contrleurs des pages de la faon suivante :
1. angular.module("rdvmedecins")
2.
.controller('page1Ctrl', ['$scope', '$location',
3.
function ($scope, $location) {
http://tahe.developpez.com
266/325
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
// navigation autorise ?
var main = $scope.main;
if (main.lastUrl && main.lastUrl != '/page3') {
// on revient la dernire URL
$location.path(main.lastUrl);
return;
}
// on mmorise l'URL de la page
main.lastUrl = '/page1';
// modle de la page
var page1 = $scope.page1;
page1.text = "[Modle local dans page 1]";
}]);
ligne 12 : lorsqu'une page sera affiche, on mmorisera son URL dans le modle [main.lastUrl]. Nous utilisons ici le
concept dont nous avons parl prcdemment : utiliser le modle [main] pour stocker des informations partages par
toutes les vues. Ici, c'est la dernire URL consulte ;
le code des lignes 4-12 est dupliqu et adapt aux trois vues. Ici on est dans la vue [/page1] ;
ligne 5 : on rcupre le modle [main] ;
ligne 6 : si le modle [main.lastUrl] existe et s'il est diffrent de [/page3] alors la navigation est interdite (la dernire URL
visite existe et n'est pas /page3) ;
ligne 8 : on revient alors sur la dernire URL visite ;
Faisons un essai :
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
prsenter celui-ci, nous commenterons davantage les fonctionnalits de l'application que ses dtails d'implmentation. Pour ces
derniers, nous nous contenterons de faire rfrence l'exemple illustrant le cas d'utilisation alors tudi.
3.8
3.8.1
http://tahe.developpez.com
267/325
1
2
3.8.2
http://tahe.developpez.com
268/325
3.8.3
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
<!-- META -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Angular client for RdvMedecins">
<meta name="author" content="Serge Tah">
<!-- le CSS -->
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/>
<link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
<link href="assets/css/footable.core.min.css" rel="stylesheet"/>
</head>
<!-- contrleur [appCtrl], modle [app] -->
<body ng-controller="appCtrl">
<div class="container">
...
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script src="bower_components/footable/js/footable.js" type="text/javascript"></script>
<!-- angular js -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstraptpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
<!-- modules -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- services -->
<script type="text/javascript" src="services/config.js"></script>
<script type="text/javascript" src="services/dao.js"></script>
<script type="text/javascript" src="services/utils.js"></script>
<!-- directives -->
<script type="text/javascript" src="directives/waiting.js"></script>
<script type="text/javascript" src="directives/errors.js"></script>
<script type="text/javascript" src="directives/footable.js"></script>
<script type="text/javascript" src="directives/debug.js"></script>
<script type="text/javascript" src="directives/list.js"></script>
<!-- controllers -->
<script type="text/javascript" src="controllers/appController.js"></script>
<script type="text/javascript" src="controllers/loginController.js"></script>
<script type="text/javascript" src="controllers/homeController.js"></script>
<script type="text/javascript" src="controllers/agendaController.js"></script>
<script type="text/javascript" src="controllers/resaController.js"></script>
</body>
</html>
http://tahe.developpez.com
269/325
Quelque soit la vue affiche, elle aura toujours les lments 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
http://tahe.developpez.com
270/325
Tous les lments prcdents sont contrls par une directive [ng-show / ng-hide] qui fait que s'ils sont bien prsents, ils ne sont pas
forcment visibles.
3.8.4
La ligne 4 reoit les diffrentes vues de l'application. Celles-ci sont dfinies dans le module [main.js] :
http://tahe.developpez.com
271/325
Le rle de la configuration des diffrentes routes a t expliqu au paragraphe 3.7.15.4, page 263.
La vue [login.html] est vide, --d qu'elle ne rajoute aucun lment ceux dj prsents dans la page matre.
La vue [home.html] rajoute l'lment suivant la page matre :
http://tahe.developpez.com
272/325
3.8.5
Fonctionnalits de l'application
Les vues du client Angular ont dj t prsentes au paragraphe 1.3.3, page 11. Pour faciliter la lecture de ce nouveau chapitre,
nous les redonnons ici. La premire vue est la suivante :
http://tahe.developpez.com
273/325
9
10
11
12
13
14
en [6], la page d'entre de l'application. Il s'agit d'une application de prise de rendez-vous pour des mdecins ;
en [7], une case cocher qui permet d'tre ou non en mode [debug]. Ce dernier se caractrise par la prsence du cadre [8]
qui affiche le modle de la vue courante ;
en [9], une dure d'attente artificielle en millisecondes. Elle vaut 0 par dfaut (pas d'attente). Si N est la valeur de ce temps
d'attente, toute action de l'utilisateur sera excute aprs 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 prcd, c'est [http://localhost:8080];
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 rle (ADMIN) et user/user avec un rle (USER). Seul le rle ADMIN a le droit d'utiliser
l'application. Le rle USER n'est l que pour montrer ce que rpond 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 franais par dfaut et l'anglais.
en [1], on se connecte ;
http://tahe.developpez.com
274/325
une fois connect, on peut choisir le mdecin avec lequel on veut un rendez-vous [2] et le jour de celui-ci [3] ;
on demande en [4] voir l'agenda du mdecin choisi pour le jour choisi ;
http://tahe.developpez.com
275/325
http://tahe.developpez.com
276/325
Une fois le rendez-vous valid, on est ramen automatiquement l'agenda o le nouveau rendez-vous est dsormais inscrit. Ce
rendez-vous pourra tre ultrieurement supprim [7].
Les principales fonctionnalits ont t dcrites. Elles sont simples. Celles qui n'ont pas t dcrites sont des fonctions de navigation
pour revenir une vue prcdente. Terminons par la gestion de la langue :
http://tahe.developpez.com
277/325
3.8.6
Le module [main.js]
http://tahe.developpez.com
278/325
3.8.7
Ligne 1, tout le corps (body) de la page matre est contrl par le contrleur [appCtrl]. De par sa position, cela en fait un contrleur
gnral et principal de l'application. Comme il a t expliqu au paragraphe 3.7.15, page 261, le modle construit par ce contrleur
est hrit par toutes les vues qui viendront s'insrer dans la page matre.
Son code est le suivant :
1. angular.module("rdvmedecins")
2.
.controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
3.
function ($scope, config, utils, $location, $locale) {
4.
5.
// debug
6.
utils.debug("[app] init");
7.
8.
// ----------------------------------------initialisation page
9.
// les modles des # pages
10.
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
11.
$scope.login = {};
12.
$scope.home = {};
13.
$scope.agenda = {};
14.
$scope.resa = {};
15.
// modle de la page courante
16.
var app = $scope.app;
17.
...
18.
19.
// ---------------------------------- mthodes
20.
21.
// annulation tche courante
22.
app.cancel = function () {
23. ...
24.
};
25.
26.
// dconnexion
27.
app.deconnecter = function () {
28.
...
29.
};
30.
31.
// ce code doit rester l car il rfrence la fonction [cancel] qui prcde
32.
app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show:
true};
33.
}])
34. ;
Les lignes 10-14 dfinissent les cinq modles qui sont utiliss dans l'application :
Modle
Vue
Contrleur
$scope.app
app.html
appCtrl
$scope.login
login.html
loginCtrl
$scope.home
home.html
homeCtrl
$scope.resa
resa.html
resaCtrl
Ce qu'il est important de comprendre est que l'objet [$scope] tant le modle du contrleur de la page matre, est hrit par toutes
les vues et contrleurs. Ainsi le contrleur [loginCtrl] a accs aux lments [$scope.app, $scope.login, $scope.home, $scope.resa,
http://tahe.developpez.com
279/325
$scope.agenda]. Dit autrement un contrleur a accs aux modles des autres contrleurs. L'application tudie vite
soigneusement d'utiliser cette possibilit. Ainsi, par exemple, le contrleur [loginCtrl] travaille avec deux modles seulement :
le sien [$scope.login] ;
Dans [C2] :
var value=$scope.app.info ;
Dans les deux cas, $scope est hrit du contrleur [appCtrl] et est donc identique (c'est un pointeur) dans [C1] et [C2]. L'objet
[$scope.app] qui sert de mmoire partage entre les contrleurs sera souvent appel session dans les commentaires, par mimtisme
avec la session utilise dans les applications web classiques qui dsigne la mmoire partage entre requtes HTTP successives.
Revenons au code du contrleur [appCtrl] :
1.
// les modles des # pages
2.
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
3.
$scope.login = {};
4.
$scope.home = {};
5.
$scope.agenda = {};
6.
$scope.resa = {};
7.
// modle de la page courante
8.
var app = $scope.app;
9.
// [app.debug] et [utils.verbose] doivent toujours tre synchroniss
10.
app.debug = utils.verbose;
11.
app.debug.on = config.debug;
12.
// pas de titre de page pour l'instant
13.
app.titre = {show: false};
14.
// pas de barres de navigation
15.
app.navbarrun = {show: false};
16.
app.navbarstart = {show: false};
17.
// pas d'erreurs
18.
app.errors = {show: false};
19.
// locale par dfaut
20.
angular.copy(config.locales['fr'], $locale);
21.
// la vue courante
22.
app.view = {url: undefined, model: {}, done: false};
23.
// la tche courante
24. app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask),
isFinished: false};
ligne 8 : [$scope.app] sera le modle de la page matre. Ce sera aussi la mmoire partage entre les diffrents contrleurs.
Plutt que d'crire partout [$scope.app.champ=value], le pointeur [$scope.app] est affect la variable [app] et on crira
alors [app.champ=value]. Il faut simplement se souvenir que [app] est le modle expos la page matre ;
ligne 11 : [app.debug.on] est un boolen qui contrle le mode debug de l'application. Par dfaut il est true. Sa valeur est lie
la case cocher [debug] des barres de navigation ;
ligne 15 : [app.navbarrun.show] contrle l'affichage de la barre de navigation suivante :
http://tahe.developpez.com
280/325
ligne 22 : [app.view] contiendra des informations sur la vue courante, celle qui est actuellement affiche par la balise [ngview] de la page matre. Nous y noterons les information suivantes :
[url] : l'URL de la vue courante, par exemple [/agenda] ;
[model] : le modle de la vue courante, par exemple [$scope.agenda] ;
[done] : vrai indique que la vue courante a termin son travail et qu'on est en train de passer une autre vue ;
Ces informations servent au contrle de la navigation.
ligne 24 : lance une tche asynchrone, une attente simule. La tche asynchrone est rfrence par deux pointeurs
[app.view.model.task.action] et [app.task] ;
ligne 2 : la fonction [app.cancel] sert annuler la tche courante pour laquelle un message d'attente est actuellement
affich. Toutes les vues offrent ce message et donc l'annulation de la tche se fera ici ;
ligne 7 : la fonction [app.deconnecter] ramne l'utilisateur la page d'authentification. Toutes les vues, sauf la vue
[/login] offrent cette possibilit ;
3.8.8
// dconnexion
app.deconnecter = function () {
// on revient la page de login
$location.path(config.urlLogin);
Dans notre application, un moment donn, une seule tche asynchrone sera en cours d'excution. Il est possible d'en avoir
plusieurs. Par exemple, au dmarrage de l'application, celle-ci demande au service web la liste des mdecins et puis celle des clients
avec deux requtes HTTP successives. On pourrait faire la mme chose avec deux requtes HTTP simultanes. Angular offre les
outils pour cette gestion. Ici, nous n'avons pas fait ce choix.
La tche en cours d'excution est annule avec le code suivant dans le contrleur [appCtrl] :
1.
2.
3.
4.
5.
6.
7.
8.
http://tahe.developpez.com
281/325
9.
10. };
...
ligne 5 : la tche est cherche dans [app.view.model.task]. Aussi, tous les contrleurs feront en sorte que leurs tches
asynchrones soient rfrences par cet objet ;
ligne 6 : pour indiquer que la tche est finie ;
ligne 7 : pour terminer la tche avec un chec. Cette notation est diffrente de celle utilise dans les exemples Angular
tudis :
dans les exemples, l'objet [task] tait un objet [$q.defer()] qu'on pouvait terminer ;
dans la version finale, l'objet [task] est un objet avec les champs [action, isFinished] o [action] est l'objet [$q.defer()]
qu'on peut terminer et [isFinished] un boolen qui indique que l'action est termine ;
Examinons le cycle de vie de l'objet [task] sur un exemple. Au dmarrage, aprs le contrleur [appCtrl], c'est le contrleur
[loginCtrl] qui prend la main pour afficher la vue [views/login.html]. Son code d'initilisation est le suivant :
1.
// on rcupre le modle parent
2.
var login = $scope.login;
3.
var app = $scope.app;
4.
// vue courante
5. app.view = {url: config.urlLogin, model: login, done: false};
Ligne 5, on a [model=login]. Ceci signifie que lorsqu'on modifie l'objet [login], on modifie l'objet [app.view.model] donc
[$scope.app.view.model]. Lorsque dans le contrleur [loginCtrl], on veut faire une attente simule, on crit :
// attente simule
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
En ajoutant le champ [task] l'objet [login], c'est donc l'objet [$scope.app.view.model] qu'il a t ajout. Si l'utilisateur annule
l'attente, le code dans [appCtrl.cancel] :
1.
2.
3.
4.
5.
6.
3.8.9
Contrle de la navigation
Navigation autorise
/login
quelconque
oui
/home
/login
/home
oui
/agenda
oui
/home
/resa
oui
/agenda
oui
/agenda
/resa
oui
/agenda
/resa
http://tahe.developpez.com
282/325
Pour [resaCtrl] :
Pour [loginCtrl] :
il n'y a ici aucun contrle de navigation puisque la rgle 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 ;
http://tahe.developpez.com
283/325
/home
voici un exemple de code qui fait passer de l'URL [/home] l'URL [/agenda] :
Ci-dessus, on est dans la mthode [afficherAgenda] du contrleur [homeCtrl]. L'utilisateur a demand l'agenda d'un mdecin.
http://tahe.developpez.com
284/325
3.8.10
Les services
Les services [config, utils, dao] sont ceux dj dcrits lors de la prsentation d'Angular :
en [1] : on voit que le code fait environ 250 lignes. L'essentiel de ce code est l'externalisation des cls des messages
internatiomaliss [2]. On vite de mettre ces cls en dur dans le code ;
Service [utils]
http://tahe.developpez.com
285/325
ligne 8 : nous n'avions pas encore rencontr la variable [verbose]. Elle contrle la fonction [debug] de la faon suivante :
lignes 22-25 : la fonction [utils.debug] ne fait rien si [verbose.on] est valu false. Cette variable est lie une variable du
contrleur [appCtrl] :
ligne 21 : [app.debug] prend la valeur du pointeur [utils.verbose]. Donc toute modification faite sur [app.debug] sera faite
galement sur [utils.verbose] ;
ligne 22 : la valeur initiale de [app.debug.on] est prise dans le fichier de configuration. Par dfaut, 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 :
http://tahe.developpez.com
286/325
ligne 45 : une case cocher (type=checkbox) permet de changer la valeur de [app.debug.on] (attribut ng-model) ;
Service [dao]
3.8.11
Les directives
Les directives [errors, footable, list, waiting] sont celles dj dcrites lors de la prsentation d'Angular :
http://tahe.developpez.com
287/325
ligne 2 : la directive [debug] affiche son modle au format JSON dans un bandeau Bootstrap (ligne 1) ;
la directive [debug] est utilise ligne 35. Elle affiche donc la forme JSON du modle [$scope.app] lorsqu'on est en mode
debug (attribut ng-show). Cela donne des choses comme celle-ci :
http://tahe.developpez.com
288/325
Cela ncessite une bonne connaissance du code pour tre interprt mais lorsque celle-ci est acquise, l'information ci-dessus devient
utile pour le dbogage. On a surlign ici les lments du modle [$scope.app] affich. On rappelle que [$scope.app] est la mmoire
partage par les contrleurs ;
http://tahe.developpez.com
289/325
3.8.12
Le contrleur [loginCtrl]
Le contrleur [loginCtrl] est associ la vue [views/login.html] qui associe la page matre produit la page suivante :
http://tahe.developpez.com
290/325
Ce code d'initialisation se retrouvera dans chaque contrleur. Pour le contrleur C1 d'une vue V1 ayant le modle M1 on aura le
code d'initialisation suivant :
1. var app=$scope.app;
2. var M1=$scope.M1;
3. app.view={url: config.urlV1, model:M1, done:false};
ligne 18 : on se rappelle peut-tre que [appCtrl] a lanc une attente simule rfrence par l'objet [app.task.action]. On
utilise la [promise] de cette tche pour attendre sa fin ;
ligne 39 : la mthode [login.setLang] gre le changement de langues ;
ligne 47 : la mthode [login.authenticate] gre l'authentification de l'utilisateur ;
http://tahe.developpez.com
291/325
ligne 87 : le boolen [task.isFinished] est positionn true dans les cas suivants :
l'utilisateur a annul l'attente ;
la demande des mdecins s'est termine avec une erreur ;
lignes 91-98 : le cas o on a eu les clients ;
ligne 93 : [app.clients] est le modle de la directive [list] qui va afficher les clients dans une liste droulante ;
lignes 97-98 : on se prpare changer de vue (ligne 98) mais auparavant on indique que le contrleur a termin son travail
(ligne 97). On rappelle que [$scope.app.view.done] est utilis pour le contrle de navigation ;
Le point important noter ici est que les mdecins et les clients ont t mis en cache sur le navigateur. Ils ne seront dsormais plus
demands au service web.
http://tahe.developpez.com
292/325
3.8.13
Le contrleur [homeCtrl]
Le contrleur [homeCtrl] est associ la vue [views/home.html] qui associe la page matre produit la page suivante :
http://tahe.developpez.com
293/325
lignes 12-20 : c'est le contrle de navigation. Tous les contrleurs l'ont sauf [loginCtrl] car la page [/login.html] est
accessible sans conditions ;
lignes 25-28 : on retrouve l des lignes analogues celles rencontres dans le contrleur [loginCtrl]. [home] est ainsi le
modle de la vue associe au contrleur ;
ligne 33 : un attribut que nous n'avions pas encore rencontr. C'est le modle du bandeau de titre de la vue :
http://tahe.developpez.com
294/325
3.8.14
Le contrleur [agendaCtrl]
Le contrleur [agendaCtrl] est associ la vue [views/agenda.html] qui associe la page matre produit la page suivante :
http://tahe.developpez.com
295/325
http://tahe.developpez.com
296/325
3.8.15
Le contrleur [resaCtrl]
Le contrleur [resaCtrl] est associ la vue [views/resa.html] qui associe la page matre produit la page suivante :
http://tahe.developpez.com
297/325
http://tahe.developpez.com
298/325
3.8.16
http://tahe.developpez.com
299/325
4 Exploitation de l'application
Nous souhaitons maintenant exploiter l'application en-dehors des IDE STS (pour le serveur) et Webstorm (pour le client).
4.1
Nous avons vu au paragraphe 2.11.9, page 71, comment crer une archive war pour Tomcat. Nous rptons l'opration ici. Tout
d'abord, pour prserver l'existant, nous dupliquons le projet Eclipse [rdvmedecins-webapi-v3] dans [rdvmedecins-webapi-v4].
ligne 5 : il faut indiquer qu'on va gnrer une archive war (Web ARchive) ;
lignes 23-27 : il faut ajouter une dpendance sur l'artifact [spring-boot-starter-tomcat]. Cet artifact amne toutes les classes
de Tomcat dans les dpendances du projet ;
ligne 26 : cet artifact est [provided], --d que les archives correspondantes ne seront pas places dans le war gnr. En
effet, ces archives seront trouves sur le serveur Tomcat sur lequel s'excutera l'application ;
http://tahe.developpez.com
300/325
Il faut par ailleurs configurer l'application web. En l'absence de fichier [web.xml], cela se fait avec une classe hritant de
[SpringBootServletInitializer] :
package rdvmedecins.web.config;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(AppConfig.class);
}
}
Ceci fait, il peut tre ncessaire de mettre jour le projet Maven (j'ai du le faire) : [clic droit sur projet / Maven / Update project] ou
[Alt-F5].
Pour excuter le projet, on peut procder ainsi :
en [1], on excute le projet sur l'un des serveurs enregistrs dans l'IDE Eclipse ;
en [2], on choisit [tc Server Developer] qui est prsent par dfaut. C'est une variante de Tomcat ;
http://tahe.developpez.com
301/325
C'est normal. Rappelons que le service web n'a pas URL [/] dans ses mthodes. Lorsqu'on essaie l'URL [/getAllMedecins], on a la
rponse suivante :
En [1], on met l'URL du nouveau service web [http://localhost:8080/rdvmedecins-webapi-v4]. On obtient le rsultat suivant :
http://tahe.developpez.com
302/325
Pour excuter l'application en-dehors de l'IDE STS, il existe diverses solutions. En voici une.
Tlchargez une version de Tomcat [http://tomcat.apache.org/download-80.cgi] (juillet 2014) :
2
On choisit en [1] une version zippe qu'on dzippe en [2]. On revient dans STS :
http://tahe.developpez.com
303/325
dans l'onglet [Servers], on clique droit sur l'application [rdvmedecins-webapi-v4] et on slectionne l'option [Browse
Deployment Location] ;
en [4] : on copie le dossier [rdvmedecins-webapi-v4] ;
En [1], on met l'URL du nouveau service web [http://localhost:8080/rdvmedecins-webapi-v4]. On obtient le rsultat suivant :
http://tahe.developpez.com
304/325
4.2
Maintenant que le service web a t dploy sur Tomcat, nous allons maintenant dployer le client Angular sur un serveur lui aussi.
Ce peut trs bien tre le serveur qui hberge dj 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 :
http://tahe.developpez.com
305/325
Le fichier [index.html] a t modifi pour prendre en compte les changements de chemin des ressources utilises :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
...
<!-- le CSS -->
...
<link href="lib/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="lib/bootstrap-select.min.css" rel="stylesheet"/>
</head>
<!-- contrleur [appCtrl], modle [app] -->
<body ng-controller="appCtrl">
<div class="container">
...
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="lib/jquery.min.js"></script>
<script type="text/javascript" src="lib/bootstrap.min.js"></script>
<script type="text/javascript" src="lib/bootstrap-select.min.js"></script>
<script type="text/javascript" src="lib/footable.js"></script>
<!-- angular js -->
<script type="text/javascript" src="lib/angular.min.js"></script>
<script type="text/javascript" src="lib/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="lib/angular-route.min.js"></script>
<script type="text/javascript" src="lib/angular-translate.min.js"></script>
<script type="text/javascript" src="lib/angular-base64.min.js"></script>
<!-- modules -->
...
<!-- services -->
...
<!-- directives -->
...
<!-- controllers -->
....
</body>
</html>
Par ailleurs, le contrleur [loginCtrl] a t modifi pour pointer sur le bon serveur afin d'viter l'utilisateur de taper son URL :
1.
2.
3.
4.
// credentials
app.serverUrl = "http://localhost:8080/rdvmedecins-webapi-v4";
app.username = "admin";
app.password = "admin";
http://tahe.developpez.com
306/325
Puis connectons-nous au service web. Ca doit marcher. Ceci vrifi, arrtons le serveur Tomcat. Nous allons rutiliser le serveur
intgr de STS.
Dans STS, copions tout le contenu du dossier [rdvmedecins-angular-v3/app] dans le dossier [webapp] du projet [rdvmedecinswebapi-v4] (onglet Navigator) [1] :
2
1
Ceci fait, lanons [2], le serveur VMware de STS, puis demandons l'URL [http://localhost:8080/rdvmedecins-webapiv4/app/index.html] :
http://tahe.developpez.com
307/325
On a un problme de droits en [3]. Ce n'est pas tonnant car on a protg le service web. Il faut qu'on dclare que l'accs au fichier
[/app/index.html] est libre. Revenons dans Eclipse :
On se rappelle que les droits d'accs ont t dclars dans la classe [SecurityConfig]. Modifions celle-ci de la faon suivante :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17. }
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// le mot de passe est transmis par le header Authorization: Basic xxxx
http.httpBasic();
// la mthode HTTP OPTIONS doit tre autorise pour tous
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
// le dossier [app] est accessible tous
http.authorizeRequests() //
.antMatchers(HttpMethod.GET, "/app", "/app/**").permitAll();
// seul le rle ADMIN peut utiliser l'application
http.authorizeRequests() //
.antMatchers("/", "/**") // toutes les URL
.hasRole("ADMIN");
lignes 11-12 : on autorise tout le monde lire le dossier [app] et son contenu. On s'inspire, pour le faire, des lignes
prcdentes.
http://tahe.developpez.com
308/325
Maintenant, relanons le serveur Tomcat de STS puis demandons de nouveau l'URL [http://localhost:8080/rdvmedecins-webapiv4/app/index.html] :
4.3
On se souvient peu-tre que nous avons bataill dur pour grer les enttes CORS. Dans l'exemple prcdent :
La gnration ou non des enttes CORS est contrle par un boolen dfini dans la classe [ApplicationModel] :
// donnes de configuration
private boolean CORSneeded = true;
Nous passons le boolen ci-dessus false, nous relanons le service web et nous redemandons
[http://localhost:8080/rdvmedecins-webapi-v4/app/index.html]. On constate que l'application fonctionne.
4.4
l'URL
L'outil [Phonegap] [http://phonegap.com/] permet de produire un excutable pour mobile (Android, IoS, Windows 8, ...) partir
d'une application HTML / JS / CSS. Il y a diffrentes faons d'arriver ce but. Nous utilisons le plus simple : un outil prsent en
ligne sur le site de Phonegap [http://build.phonegap.com/apps].
http://tahe.developpez.com
309/325
en [3], on tlcharge l'application zippe [4] (le dossier [app] cr page 305 est zipp) ;
5
6
http://tahe.developpez.com
310/325
Lancez un mulateur [GenyMotion] pour une tablette Android (voir paragraphe 6.4, page 323) :
Ci-dessus, on lance un mulateur de tablette avec l'API 16 d'Android. Une fois l'mulateur lanc,
avec la souris, tirez le fichier [PGBuildApp-debug.apk] que vous avez tlcharg et dposez-le sur l'mulateur. Il va tre
alors install et excut ;
http://tahe.developpez.com
311/325
Il faut changer l'URL en [1]. Pour cela, dans une fentre de commande, tapez la commande [ipconfig] (ligne 1 ci-dessous) qui va
afficher les diffrentes adresses IP de votre machine :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
C:\Users\Serge Tah>ipconfig
Configuration IP de Windows
Carte rseau sans fil Connexion au rseau local* 15 :
Statut du mdia. . . . . . . . . . . . : Mdia dconnect
Suffixe DNS propre la connexion. . . :
Carte Ethernet Connexion au rseau local :
Suffixe DNS propre la connexion. . . :
Adresse IPv6 de liaison locale. . . . .:
Adresse IPv4. . . . . . . . . . . . . .:
Masque de sous-rseau. . . . . . . . . :
Passerelle par dfaut. . . . . . . . . :
ad.univ-angers.fr
fe80::698b:455a:925:6b13%4
172.19.81.34
255.255.0.0
172.19.0.254
Notez soit l'adresse IP Wifi (lignes 6-9), soit l'adresse IP sur le rseau local (lignes 11-17). Puis utilisez cette adresse IP dans l'URL
du serveur web :
http://tahe.developpez.com
312/325
Testez l'application sur l'mulateur. Elle doit fonctionner. Ct serveur, on peut ou non autoriser les enttes CORS dans la classe
[ApplicationModel] :
// donnes de configuration
private boolean CORSneeded = false;
Cela n'a pas d'importance pour l'application Android. Celle-ci ne s'excute pas dans un navigateur. Or l'exigence des enttes CORS
vient du navigateur et non pas du serveur.
4.5
On rpte l'opration prcdente avec un mulateur pour smartphone. On veut vrifier comment se comporte notre client sur des
petits crans :
http://tahe.developpez.com
313/325
en [4], on se connecte ;
en [5], la liste et le calendrier sont l'un sous l'autre au lieu d'tre l'un ct de l'autre ;
http://tahe.developpez.com
314/325
Au final, notre application s'adapte plutt bien au smartphone. Cela pourrait tre srement mieux mais cela reste utilisable.
http://tahe.developpez.com
315/325
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
prsenter 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'intgration continue.
De ce travail, je retiendrai deux points :
l'criture du service web Spring a t moyennement complique. Ds le dpart, je connaissais bien les concepts de Spring.
Je n'ai rencontr de difficults qu'avec la scurisation du service web puis plus tard avec la gestion des enttes HTTP
CORS, deux domaines que je ne connaissais pas ;
l'criture du client Angular a t beaucoup plus complexe pour diffrentes raisons :
j'avais une connaissance insuffisante du langage Javascript et de ses possibilits ;
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 simultane de plusieurs threads. Dans le
navigateur, il n'y a qu'un thread, et les tches asynchrones sont traites successivement et non pas en parallle. Plus
prcisment, des tches asynchrones peuvent s'excuter en parallle (requtes HTTP multiples par exemple) mais les
vnements qu'elles produisent lorsqu'elles sont termines, sont eux traits squentiellement. Il n'y a donc pas
d'exccution concurrente grer avec les nombreux problmes qui vont avec ;
http://tahe.developpez.com
316/325
Angular est un framework riche avec de nombreuses notions (MVC, directives, services, porte des modles, ...). Son
apprentissage est long ;
Angular n'impose pas de mthode de dveloppement. Ainsi pour arriver un mme rsultat, on peut utiliser
diffrentes architectures. C'est droutant. Je suis plus l'aise avec des frameworks ferms o tout le monde utilise les
mmes patrons de conception (design pattern). J'ai donc constamment cherch reproduire les modles de
conception que j'utilise cte serveur. Je suis satisfait du rsultat 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 ;
http://tahe.developpez.com
317/325
6 Annexes
Nous prsentons ici comment installer les outils utiliss dans ce document sur des machines windows 7.
6.1
Nous allons installer SpringSource Tool Suite [http://www.springsource.com/developer/sts], un Eclipse pr-quip avec de
nombreux plugins lis au framework Spring et galement avec une configuration Maven pr-installe.
2A
aller sur le site de SpringSource Tool Suite (STS) [1], pour tlcharger la version courante de STS [2A] [2B],
2B
3A
http://tahe.developpez.com
318/325
3B
5
le fichier tlcharg est un installateur qui cre l'arborescence de fichiers [3A] [3B]. En [4], on lance l'excutable,
en [5], la fentre de travail de l'IDE aprs avoir ferm la fentre de bienvenue. En [6], on fait afficher la fentre des
serveurs d'applications,
en [7], la fentre des serveurs. Un serveur est enregistr. C'est un serveur VMware compatible Tomcat.
L'utilisation de STS dans le cadre de l'application est explique au paragraphe 1.3.2, page 9.
6.2
Installation de [WampServer]
[WampServer] est un ensemble de logiciels pour dvelopper en PHP / MySQL / Apache sur une machine Windows. Nous
l'utiliserons uniquement pour le SGBD MySQL.
http://tahe.developpez.com
319/325
4
5
en [4], l'icne de [WampServer] s'installe dans la barre des tches en bas et droite de l'cran [4],
lorsqu'on clique dessus, le menu [5] s'affiche. Il permet de grer le serveur Apache et le SGBD MySQL. Pour grer celuici, on utiliser l'option [PhpPmyAdmin],
on obtient alors la fentre ci-dessous,
Nous donnerons peu de dtails sur l'utilisation de [PhpMyAdmin]. Nous montrons au paragraphe 1.3.1, page 8, comment l'utiliser
pour crer la base de donnes de l'application.
6.3
Installation de [Webstorm]
[WebStorm] (WS) est l'IDE de JetBrains pour dvelopper des applications HTML / CSS / JS. Je l'ai trouv parfait pour dvelopper
des applications Angular. Le site de tlchargement est [http://www.jetbrains.com/webstorm/download/]. C'est un IDE payant
mais une version d'valuation de 30 jours est tlchargeable. Il existe une version personnelle et une version tudiante peu
onreuses.
Son utilisation dans le cadre de l'application est dcrite au paragraphe 1.3.3, page 11. Pour installer des bibliothques JS au sein
d'une application, WS utilise un outil appel [bower]. Cet outil est un module de [node.js], un ensemble de bibliothques JS. Par
ailleurs, les bibliothques JS sont cherches sur un site Git, ncessitant un client Git sur le poste qui tlcharge.
http://tahe.developpez.com
320/325
6.3.1
Installation de [node.js]
Le site de tlchargement de [node.js] est [http://nodejs.org/]. Tlchargez l'installateur puis excutez-le. Il n'y a rien de plus faire
pour le moment.
6.3.2
L'installation de l'outil [bower] qui va permettre le tlchargement des bibliothques Javascript peut se faire de diffrentes faons.
Nous allons la faire partir de la console :
1. C:\Users\Serge Tah>npm install -g bower
2. C:\Users\Serge Tah\AppData\Roaming\npm\bower -> C:\Users\Serge
Tah\AppData\Roaming\npm\node_modules\bower\bin\bower
3. bower@1.3.7 C:\Users\Serge Tah\AppData\Roaming\npm\node_modules\bower
4. stringify-object@0.2.1
5. is-root@0.1.0
6. junk@0.3.0
7. ...
8. insight@0.3.1 (object-assign@0.1.2, async@0.2.10, lodash.debounce@2.4.1, req
9. uest@2.27.0, configstore@0.2.3, inquirer@0.4.1)
10. mout@0.9.1
11. inquirer@0.5.1 (readline2@0.1.0, mute-stream@0.0.4, through@2.3.4, async@0.8
12. .0, lodash@2.4.1, cli-color@0.3.2)
6.3.3
ligne 1 : la commande [node.js] qui installe le module [bower]. Pour que la commande marche, il faut que l'excutable
[npm] soit dans le PATH de la machine (voir paragraphe ci-aprs) ;
Installation de [Git]
Git est un systme de gestion de versions de logiciel. Il existe une version windows appele [msysgit] et disponible l'URL
[http://msysgit.github.io/]. Nous n'allons pas utiliser [msysgit] pour grer des versions de notre application mais simplement pour
tlcharger des bibliothques JS qui se trouvent sur des sites de type [https://github.com] qui ncessitent un protocole d'accs
spcial et qui est fourni par le client [msysgit]
L'assistant d'installation propose diffrentes tapes dont les suivantes :
Pour les autres tapes de l'installation, vous pouvez accepter les valeurs par dfaut proposes.
Une fois, l'installation de Git termine, vrifiez que l'excutable est dans le PATH de votre machine : [Panneau de configuration /
Systme et scurit / Systme / Paramtres systmes avancs] :
http://tahe.developpez.com
321/325
Vrifiez que :
6.3.4
Configuration de [Webstorm]
http://tahe.developpez.com
322/325
Ci-dessus, slectionnez l'option [1]. La liste des modules [node.js] dj installs apparat en [2]. Cette liste ne devrait contenir que la
ligne [3] du module [bower] si vous avez suivi le processus d'installation prcdent.
6.4
Les mulateurs fournis avec le SDK d'Android sont lents ce qui dcourage de les utiliser. L'entreprise [Genymotion] offre un
mulateur beaucoup plus performant. Celui-ci est disponible l'URL [https://cloud.genymotion.com/page/launchpad/download/]
(fvrier 2014).
Vous aurez vous enregistrer pour obtenir une version usage personnel. Tlchargez le produit [Genymotion] avec la machine
virtuelle VirtualBox ;
Installez puis lancez [Genymotion]. Tlchargez ensuite une image pour une tablette ou un tlphone :
http://tahe.developpez.com
323/325
3
4
2
6.5
une fois le tlchargement 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] (https://chrome.google.com/webstore) avec le navigateur Chrome ;
chercher l'application [Advanced Rest Client] :
http://tahe.developpez.com
324/325
pour l'obtenir, il vous faudra crer un compte Google. [Google Web Store] demande ensuite confirmation [1] :
en [2], l'extension ajoute est disponible dans l'option [Applications] [3]. Cette option est affiche sur chaque nouvel onglet
que vous crez (CTRL-T) dans le navigateur.
http://tahe.developpez.com
325/325