V. Objectifs du back-end▲
Dans la section II-B de notre article, nous avions identifié quatre domaines fonctionnels pour notre application de gestion de la bibliothèque : Book, Customer, Loan et Category. Nous avons aussi fait le choix d'utiliser une architecture d'organisation du projet de type package by feauture. Pour rappel, cette architecture se distingue de l'architecture n-tiers (très répandue) par le fait qu'elle prescrit la mise de toutes classes Java d'un domaine fonctionnel au même niveau dans le package dédié, contrastant ainsi avec une organisation en couches. Au terme de notre développement, notre projet library devra ressembler à la figure ci-dessous et cela représente notre objectif.
Dans chacun de ces domaines coloriés en jaune sur la figure, nous devons créer quatre classes pertinentes :
- une classe entité (exemple, de la classe Book.java) correspondant à l'ORM (Object Relationnal Mapping) hibernate/Jpa ;
- une classe Dao (exemple, IBookDao.java) correspondant à la classe d'accès à la base de données et de traitement des requêtes sur les entités;
- une classe Service (exemple, BookServiceImpl.java) correspondant à la classe de traitement des règles métier ;
- une classe Rest Controller (exemple, BookRestController.java) correspondant à la classe d'exposition des services REST (ou web services) de notre application en direction des composants consommateurs tels que le front-end.
Si nous passons en revue, la liste des Users Stories de notre application édictée en section I-B, en dehors du besoin d'envoi de mail et des IHM, le reste se réduit à de simples opérations communément appelées CRUD (Create, Read, Update, Delete) pour les domaines Book, Customer, Category et Loan. Pour ce faire, nous ne présenterons que les concepts pertinents et non redondants de chacun de ces domaines. Une présentation vidéo complète de notre application sera faite à la fin de cet article. Nous vous donnerons aussi le lien d'accès aux codes sources.
VI. Configuration des ressources de l'application▲
Notre application Library a besoin de trois ressources pour fonctionner correctement et comme attendu :
- la base de données H2 et sa DataSource, pour permettre à l'application de s'y connecter ;
- une ressource Spring mail, afin de permettre à l'application de pouvoir envoyer des mails (cf. User Story 10) ;
- un fichier de données (categories.sql), correspondant aux différentes catégories de livres, à charger au démarrage de l'application. Il s'agit de données de référence qui ne sont pas susceptibles de changer. Dans notre application nous n'avons listé qu'un exemple non exhaustif.
La figure ci-dessous présente en jaune ces différentes ressources et le contenu du fichier de données categories.sql qu'il faut absolument configurer dans le package java/main/resources de l'application. Spring Boot sait détecter les ressources à placer dans ce package.
La configuration de ces ressources est effectuée dans le fichier application.properties qui se présente ainsi :
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.
############## DataSource Config #################
spring.datasource.name
=library-db
spring.datasource.username
=sa
spring.datasource.password
=
spring.datasource.url
= jdbc:h2:file:./src/main/resources/database/library-db
spring.datasource.driver-class-name
=org.h2.Driver
spring.datasource.sql-script-encoding
= UTF-8
spring.datasource.data
=classpath:data/categories.sql
############# Hibernate properties #################
spring.jpa.show-sql
=true
spring.jpa.hibernate.ddl-auto
=create-drop
spring.jpa.database-platform
=org.hibernate.dialect.H2Dialect
############# Enable H2 Database browser console #################
#http://localhost:port/library/h2-console/
spring.h2.console.enabled
=true
############# Email Config #################
spring.mail.default-encoding
=UTF-8
spring.mail.protocol
=smtp
spring.mail.host
=smtp.gmail.com
spring.mail.username
=noreply.library.test@gmail.com
spring.mail.password
=password1Test
spring.mail.port
= 587
spring.mail.properties.mail.smtp.auth
=true
spring.mail.properties.mail.smtp.starttls.enable
=true
spring.mail.test-connection
=false
#https://www.google.com/settings/security/lesssecureapps
Bien que vous puissiez ajouter vos propriétés personnelles dans l'application.properties, pour cette application, nous n'en avons pas eu besoin. Celles que nous avons utilisées sont les propriétés standards proposées par Spring Boot. Vous pouvez remarquer que les propriétés :
- spring.datasource.*, permettent de configurer l'accès à la base de données et sa localisation. Pour le cas d'espèce, nous avons configuré la base embarquée H2 qui persistera les données non en mémoire, mais dans un fichier /src/main/resources/database/library-db. La propriété spring.datasource.data permet à Spring Boot d'exécuter un script sql au démarrage de l'application. On pourra donc charger nos différentes catégories de livres. En affectant la valeur create-drop à la propriété spring.jpa.hibernate.ddl-auto, nous demandons à hibernate, à chaque fois que l'application démarre, de supprimer et de recréer le schéma de la base de données ;
- spring.jpa.*, permettent de configurer les paramètres Hibernate/JPA. Dans le cas d'espèce : toute requête sql exécutée sera tracée dans la console ; hibernate supprimera et recréera la base de données au démarrage de l'application. Enfin, le dialecte qu'il utilisera pour communiquer avec la base est celui d'H2 naturellement ;
- spring.h2.console.enable, active la possibilité de visualiser dans un navigateur les tables de notre base de données H2 (cf. lien en commentaire) ;
- spring.mail.*, permettent de configurer les ressources d'envoi de mails. Pour le cas d'espèce, nous avons choisi gmail.com comme le fournisseur du compte d'envoi de mails (on aurait pu choisir Yahoo, ou Hotmail, etc.). Nous détaillerons un peu plus cette partie dans la suite de cet article.
VII. Les entités hibernate/JPA▲
Dans la section II-A, nous vous avons présenté le modèle de données UML de notre application. De ce modèle de données, nous déduisons le modèle relationnel ci-dessous. Cela résulte de l'application des règles de passage UML :
- Category(code, label) ;
- Book(id, isbn, title, creation_date, total_examplaries, author) ;
- Customer(id, first_name, last_name, job, address, email, creation_date) ;
- Loan(book_id, customer_id, begin_date, end_date, status).
La propriété soulignée dans chaque relation ci-dessus correspond à sa clef primaire. La relation Loan se voit ainsi migrer les deux clefs primaires des relations Book et Customer correspondant par conséquent à sa clef primaire composée.
Cette modélisation permet ainsi de créer les classes entités hibernate/JPA suivantes :
- Classe entité Category :
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.
package
com.gkemayo.library.category;
import
javax.persistence.Column;
import
javax.persistence.Entity;
import
javax.persistence.Id;
import
javax.persistence.Table;
@Entity
@Table
(
name =
"CATEGORY"
)
public
class
Category {
public
Category
(
) {
}
public
Category
(
String code, String label) {
super
(
);
this
.code =
code;
this
.label =
label;
}
private
String code;
private
String label;
@Id
@Column
(
name =
"CODE"
)
public
String getCode
(
) {
return
code;
}
public
void
setCode
(
String code) {
this
.code =
code;
}
@Column
(
name =
"LABEL"
, nullable =
false
)
public
String getLabel
(
) {
return
label;
}
public
void
setLabel
(
String label) {
this
.label =
label;
}
// + les méthodes hashCode() et equals() que vous retrouverez dans les sources de cet article
}
- Classe entité Book :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
package
com.gkemayo.library.book;
import
java.time.LocalDate;
import
java.util.HashSet;
import
java.util.Set;
import
javax.persistence.CascadeType;
import
javax.persistence.Column;
import
javax.persistence.Entity;
import
javax.persistence.FetchType;
import
javax.persistence.GeneratedValue;
import
javax.persistence.GenerationType;
import
javax.persistence.Id;
import
javax.persistence.JoinColumn;
import
javax.persistence.ManyToOne;
import
javax.persistence.OneToMany;
import
javax.persistence.Table;
import
com.gkemayo.library.category.Category;
import
com.gkemayo.library.loan.Loan;
@Entity
@Table
(
name =
"BOOK"
)
public
class
Book {
private
Integer id;
private
String title;
private
String isbn;
private
LocalDate releaseDate;
private
LocalDate registerDate;
private
Integer totalExamplaries;
private
String author;
private
Category category;
Set<
Loan>
loans =
new
HashSet<
Loan>(
);
@Id
@GeneratedValue
(
strategy =
GenerationType.AUTO)
@Column
(
name =
"BOOK_ID"
)
public
Integer getId
(
) {
return
id;
}
public
void
setId
(
Integer id) {
this
.id =
id;
}
@Column
(
name =
"TITLE"
, nullable =
false
)
public
String getTitle
(
) {
return
title;
}
public
void
setTitle
(
String title) {
this
.title =
title;
}
@Column
(
name =
"ISBN"
, nullable =
false
, unique =
true
)
public
String getIsbn
(
) {
return
isbn;
}
public
void
setIsbn
(
String isbn) {
this
.isbn =
isbn;
}
@Column
(
name =
"RELEASE_DATE"
, nullable =
false
)
public
LocalDate getReleaseDate
(
) {
return
releaseDate;
}
public
void
setReleaseDate
(
LocalDate releaseDate) {
this
.releaseDate =
releaseDate;
}
@Column
(
name =
"REGISTER_DATE"
, nullable =
false
)
public
LocalDate getRegisterDate
(
) {
return
registerDate;
}
public
void
setRegisterDate
(
LocalDate registerDate) {
this
.registerDate =
registerDate;
}
@Column
(
name =
"TOTAL_EXAMPLARIES"
)
public
Integer getTotalExamplaries
(
) {
return
totalExamplaries;
}
public
void
setTotalExamplaries
(
Integer totalExamplaries) {
this
.totalExamplaries =
totalExamplaries;
}
@Column
(
name =
"AUTHOR"
)
public
String getAuthor
(
) {
return
author;
}
public
void
setAuthor
(
String author) {
this
.author =
author;
}
@ManyToOne
(
optional =
false
)
@JoinColumn
(
name =
"CAT_CODE"
, referencedColumnName =
"CODE"
)
public
Category getCategory
(
) {
return
category;
}
public
void
setCategory
(
Category category) {
this
.category =
category;
}
@OneToMany
(
fetch =
FetchType.LAZY, mappedBy =
"pk.book"
, cascade =
CascadeType.ALL)
public
Set<
Loan>
getLoans
(
) {
return
loans;
}
public
void
setLoans
(
Set<
Loan>
loans) {
this
.loans =
loans;
}
// + les méthodes hashCode() et equals() que vous retrouverez dans les sources de cet article
}
- Classe entité Customer :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
package
com.gkemayo.library.customer;
import
java.time.LocalDate;
import
java.util.HashSet;
import
java.util.Set;
import
javax.persistence.CascadeType;
import
javax.persistence.Column;
import
javax.persistence.Entity;
import
javax.persistence.FetchType;
import
javax.persistence.GeneratedValue;
import
javax.persistence.GenerationType;
import
javax.persistence.Id;
import
javax.persistence.OneToMany;
import
javax.persistence.Table;
import
com.gkemayo.library.loan.Loan;
@Entity
@Table
(
name =
"CUSTOMER"
)
public
class
Customer {
private
Integer id;
private
String firstName;
private
String lastName;
private
String job;
private
String address;
private
String email;
private
LocalDate creationDate;
Set<
Loan>
loans =
new
HashSet<
Loan>(
);
@Id
@GeneratedValue
(
strategy =
GenerationType.AUTO)
@Column
(
name =
"CUSTOMER_ID"
)
public
Integer getId
(
) {
return
id;
}
public
void
setId
(
Integer id) {
this
.id =
id;
}
@Column
(
name =
"FIRST_NAME"
, nullable =
false
)
public
String getFirstName
(
) {
return
firstName;
}
public
void
setFirstName
(
String firstName) {
this
.firstName =
firstName;
}
@Column
(
name =
"LAST_NAME"
, nullable =
false
)
public
String getLastName
(
) {
return
lastName;
}
public
void
setLastName
(
String lastName) {
this
.lastName =
lastName;
}
@Column
(
name =
"JOB"
)
public
String getJob
(
) {
return
job;
}
public
void
setJob
(
String job) {
this
.job =
job;
}
@Column
(
name =
"ADDRESS"
)
public
String getAddress
(
) {
return
address;
}
public
void
setAddress
(
String address) {
this
.address =
address;
}
@Column
(
name =
"EMAIL"
, nullable =
false
, unique =
true
)
public
String getEmail
(
) {
return
email;
}
public
void
setEmail
(
String email) {
this
.email =
email;
}
@Column
(
name =
"CREATION_DATE"
, nullable =
false
)
public
LocalDate getCreationDate
(
) {
return
creationDate;
}
public
void
setCreationDate
(
LocalDate creationDate) {
this
.creationDate =
creationDate;
}
@OneToMany
(
fetch =
FetchType.LAZY, mappedBy =
"pk.customer"
, cascade =
CascadeType.ALL)
public
Set<
Loan>
getLoans
(
) {
return
loans;
}
public
void
setLoans
(
Set<
Loan>
loans) {
this
.loans =
loans;
}
// + les méthodes hashCode() et equals() que vous retrouverez dans les sources de cet article
}
- Classe entité Loan :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
package
com.gkemayo.library.loan;
import
java.io.Serializable;
import
java.time.LocalDateTime;
import
javax.persistence.CascadeType;
import
javax.persistence.Column;
import
javax.persistence.Embeddable;
import
javax.persistence.FetchType;
import
javax.persistence.JoinColumn;
import
javax.persistence.ManyToOne;
import
com.gkemayo.library.book.Book;
import
com.gkemayo.library.customer.Customer;
@Embeddable
public
class
LoanId implements
Serializable {
/**
*
*/
private
static
final
long
serialVersionUID =
3912193101593832821
L;
private
Book book;
private
Customer customer;
private
LocalDateTime creationDateTime;
public
LoanId
(
) {
super
(
);
}
public
LoanId
(
Book book, Customer customer) {
super
(
);
this
.book =
book;
this
.customer =
customer;
this
.creationDateTime =
LocalDateTime.now
(
);
}
@ManyToOne
public
Book getBook
(
) {
return
book;
}
public
void
setBook
(
Book bbok) {
this
.book =
bbok;
}
@ManyToOne
public
Customer getCustomer
(
) {
return
customer;
}
public
void
setCustomer
(
Customer customer) {
this
.customer =
customer;
}
@Column
(
name =
"CREATION_DATE_TIME"
)
public
LocalDateTime getCreationDateTime
(
) {
return
creationDateTime;
}
public
void
setCreationDateTime
(
LocalDateTime creationDateTime) {
this
.creationDateTime =
creationDateTime;
}
// + les méthodes hashCode() et equals() que vous retrouverez dans les sources de cet article
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
package
com.gkemayo.library.loan;
import
java.io.Serializable;
import
java.time.LocalDate;
import
javax.persistence.AssociationOverride;
import
javax.persistence.AssociationOverrides;
import
javax.persistence.Column;
import
javax.persistence.EmbeddedId;
import
javax.persistence.Entity;
import
javax.persistence.EnumType;
import
javax.persistence.Enumerated;
import
javax.persistence.JoinColumn;
import
javax.persistence.Table;
@Entity
@Table
(
name =
"LOAN"
)
@AssociationOverrides
({
@AssociationOverride
(
name =
"pk.book"
, joinColumns =
@JoinColumn
(
name =
"BOOK_ID"
)),
@AssociationOverride
(
name =
"pk.customer"
, joinColumns =
@JoinColumn
(
name =
"CUSTOMER_ID"
))
}
)
public
class
Loan implements
Serializable {
/**
*
*/
private
static
final
long
serialVersionUID =
144293603488149743
L;
private
LoanId pk =
new
LoanId
(
);
private
LocalDate beginDate;
private
LocalDate endDate;
private
LoanStatus status;
@EmbeddedId
public
LoanId getPk
(
) {
return
pk;
}
public
void
setPk
(
LoanId pk) {
this
.pk =
pk;
}
@Column
(
name =
"BEGIN_DATE"
, nullable =
false
)
public
LocalDate getBeginDate
(
) {
return
beginDate;
}
public
void
setBeginDate
(
LocalDate beginDate) {
this
.beginDate =
beginDate;
}
@Column
(
name =
"END_DATE"
, nullable =
false
)
public
LocalDate getEndDate
(
) {
return
endDate;
}
public
void
setEndDate
(
LocalDate endDate) {
this
.endDate =
endDate;
}
@Enumerated
(
EnumType.STRING)
@Column
(
name =
"STATUS"
)
public
LoanStatus getStatus
(
) {
return
status;
}
public
void
setStatus
(
LoanStatus status) {
this
.status =
status;
}
// + les méthodes hashCode() et equals() que vous retrouverez dans les sources de cet article
}
Remarque : les différentes classes Java ci-dessus comportent plusieurs annotations :
- @Entity, qui permet à hibernate/JPA de les considérer comme des ORM (Object Relational Mapping) devant transporter des données entre l'application et la base de données ;
- @Table, qui permet de mapper cet ORM sur une table physique en base de données ;
- @Id, qui permet de consacrer un attribut de la classe comme étant sa clef primaire ; et @GeneratedValue pour la stratégie de génération des valeurs de cette clef primaire ;
- @Column, pour le mapping d'un attribut de classe à une colonne de table en base de données ;
- @AssociationOverrides, @Embeddable et @EmbeddedId, pour la gestion de clef primaire composée et de migration de clef étrangère ;
- @ManyToOne, @OneToMany et @JoinColumn, pour la gestion des associations n-1, 1-n entre deux entités.
VIII. Les Dao Spring Data JPA▲
Dans notre application, nous avons choisi pour nos classes DAO (Data Access Object) d'utiliser le framework Spring Data JPA pour la gestion d'accès aux données vers sa base H2.
VIII-A. Qu'est-ce que Spring Data JPA ?▲
Spring Data JPA est un framework qui a été construit pour faciliter le développement de la couche DAO chargée de la persistance et du requêtage des données dans une base de données relationnelle. C'est une sorte de surcouche ou alors une implémentation de la spécification JPA 2 (Java Persistence API). À cet effet, il propose des fonctionnalités standardisées pour la réalisation des opérations CRUD (Create, Read, Update, Delete) sur une base de données. En outre, il propose des fonctionnalités dédiées au tri, à la pagination, à la gestion transactionnelle, etc.
Il se distingue d'hibernate (qui est une autre implémentation de JPA) par le fait qu'il permet de désalourdir et de détacher le développeur des tâches de configuration liées à la gestion de la persistance des données, tant au niveau applicatif qu'au niveau plus fin des classes DAO. Cela permet ainsi au développeur de se focaliser sur la réalisation des règles métier plutôt que sur les tâches purement techniques. Ceux qui ont déjà eu à monter et utiliser hibernate ou JPA dans une application Java (surtout dans leur version xml) savent bien ce à quoi renvoient les configurations évoquées. Pour information, notez que JPA est une spécification, mais dispose également de sa propre implémentation du même nom. Nous ne nous étendrons pas plus dans cet article sur les problématiques de configuration hibernate/JPA.
Note : Spring Data JPA et hibernate sont tout à fait compatibles et peuvent cohabiter dans une même application.
- Dans un contexte où une application n'est pas Spring Bootée (c'est-à-dire, non générée par Spring Boot), pour utiliser Spring Data JPA, il suffit d'injecter la dépendance ci-dessous et d'utiliser l'annotation @EnableJpaRepositories sur la classe de configuration qui créera les beans Spring chargés de la persistance des données (DAO) :
2.
3.
4.
5.
<dependency>
<groupId>
org.springframework.data</groupId>
<artifactId>
spring-data-jpa</artifactId>
<version>
${version-souhaitée}</version>
</dependency>
- Dans un contexte Spring Boot où le framework Spring Data JPA est chargé via le starter spring-boot-starter-data-jpa et autoconfiguré via l'annotation @SpringBootApplication sur la classe de démarrage, nous n'avons rien à faire concernant une quelconque configuration.
Dans tous les cas, pour qu'une classe DAO de votre application soit prise en charge par le framework Spring Data JPA et considérée comme une couche d'accès aux données devant bénéficier de tous les services et facilités que propose ce dernier, elle devra respecter les conditions suivantes :
- être une interface Java ;
- porter l'annotation @Repository, pour permettre à Spring de l'injecter comme bean dans l'application;
- étendre l'une des interfaces suivantes : Repository, CrudRepository, JpaRepository ou PagingAndSortingRepository.
Note : il existe une hiérarchie ascendante entre les interfaces Repository, CrudRepository et JpaRepository qui chacune ajoute de nouvelles fonctionnalités au bénéfice du développeur. Mais ce n'est pas l'objet dans cet article de s'y attarder.
Le bout de code ci-dessous représente un exemple de classe Dao Spring Data JPA, nommée MyDao, dans laquelle T correspond à l'entité hibernate concernée par les requêtes dans cette classe et ID correspond au type de données de sa clef primaire :
2.
3.
4.
@Repository
public
interface
MyDao extends
JpaRepository<
T, ID>
{
}
Lorsqu'une interface étend JpaRepository, elle hérite de toutes les fonctionnalités CRUD (Create, Read, Update, Delete) que ce dernier fournit. On peut en citer quelques-unes : save(), saveAll(), delete(), deleteById(), deleteAll(), findById(), findAll(), exists(), existsById(), etc. C'est alors Spring Data JPA qui se chargera de créer pour nous une classe d'implémentation de notre DAO.
Nous l'avons dit, il existe un large panel de fonctionnalités que propose Spring Data JPA pour la gestion des données. Nous pensons que les plus immédiates que vous pourrez avoir besoin sont celles focalisées sur les opérations CRUD. Les fonctionnalités d'insertion (Create), de mise à jour (Update) et de suppression (Delete) ne posent en général pas de problème de variabilité dans leur utilisation. Ce qui n'est pas le cas pour la fonctionnalité de lecture (Read) de données en base où il existe plusieurs politiques. Nous nous arrêtons donc sur les trois politiques de lecture de données suivantes :
- l'utilisation de l'annotation @NamedQuery pour la construction des requêtes nommées. La requête nommée se marque sur l'entité concernée puis invoquée par la DAO à travers son nom. Exemple :
2.
3.
4.
@Entity
@NamedQuery
(
name =
"T.getAll"
, query =
"select t from T t"
)
public
class
T {
}
2.
3.
4.
@Repository
public
interface
MyDao extends
JpaRepository<
T, ID>
{
public
List<
T>
getAll
(
);
}
- l'utilisation de l'annotation @Query sur une méthode, directement dans la classe DAO. Exemple :
2.
3.
4.
5.
6.
@Repository
public
interface
MyDao extends
JpaRepository<
T, ID>
{
@Query
(
query =
"select t from T t"
)
public
List<
T>
getAll
(
);
}
- l'utilisation des méthodes prédéfinies qui doivent respecter un certain format afin de permettre à Spring Data JPA de générer la requête JPQL par déduction du nom de la méthode et de ses paramètres d'entrées. Exemple :
2.
3.
4.
5.
6.
7.
@Repository
public
interface
MyDao extends
JpaRepository<
T, ID>
{
public
List<
T>
findAll
(
);
public
List<
T>
findByXxxAndYyyIgnoreCase
(
String xXX, String yYYY);
}
En général, toute méthode dans une classe Spring Data JPA qui n'est marquée d'aucune annotation et qui commence par le préfixe find ou findBy est prise en charge par ce dernier. Dans cet exemple, il génèrera à l'invocation de chacune de ces méthodes les requêtes suivantes :
- findAll() : select t from T t ;
- findByXxxAndYyyIgnoreCase(String xXX, String yYYY) : select t from T t where lower(t.xXX) = lower(xXX) and lower(t.yYY) = lower(yYYY). La casse est ignorée lors de la comparaison sur les paramètres d'entrée.
S'il n'arrive pas générer la requête pour une méthode find, une exception de type InvalidJpaQueryMethodException est levée.
Pour terminer cette section, nous vous invitons à consulter le guide Spring Data JPA pour plus d'approfondissements.
VIII-B. Les classes Spring Data JPA de l'application▲
Pour notre application de gestion de la bibliothèque, Library, nous avons donc quatre classes Spring Data JPA, une pour chaque domaine :
- domaine Book -> IbookDao ;
- domaine Customer -> IcustomerDao ;
- domaine Category -> IcategoryDao ;
- domaine Loan -> ILoanDao.
Revenons aux users stories de notre application listées à la section I-B. Nous pouvons remarquer que les problématiques se réduisent à la mise en place des fonctions de création/recherche/mise-à-jour/suppression de livres/clients/prêts. Comme nous l'avons expliqué dans les sections ci-dessus, nos traitements de création/mise-à-jour/suppression peuvent directement être délégués aux fonctionnalités Spring Data JPA préfournies. Nos classes DAO se focaliseront donc sur les traitements de recherche complexes. Nous exposons ci-dessous deux exemples de classes DAO de l'application Library. Vous pourrez consulter le reste dans le code source téléchargeable à la fin de cet article.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
package
com.gkemayo.library.book;
import
java.util.List;
import
org.springframework.data.jpa.repository.JpaRepository;
import
org.springframework.data.jpa.repository.Query;
import
org.springframework.stereotype.Repository;
@Repository
public
interface
IBookDao extends
JpaRepository<
Book, Integer>
{
public
Book findByIsbnIgnoreCase
(
String isbn);
public
List<
Book>
findByTitleLikeIgnoreCase
(
String title);
@Query
(
"SELECT b "
+
"FROM Book b "
+
"INNER JOIN b.category cat "
+
"WHERE cat.code = :code"
)
public
List<
Book>
findByCategory
(
@Param
(
"code"
) String codeCategory);
}
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.
package
com.gkemayo.library.loan;
import
java.time.LocalDate;
import
java.util.List;
import
org.springframework.data.jpa.repository.JpaRepository;
import
org.springframework.data.jpa.repository.Query;
import
org.springframework.stereotype.Repository;
@Repository
public
interface
ILoanDao extends
JpaRepository<
Loan, Integer>
{
public
List<
Loan>
findByEndDateBefore
(
LocalDate maxEndDate);
@Query
(
"SELECT lo "
+
"FROM Loan lo "
+
"INNER JOIN lo.pk.customer c "
+
"WHERE UPPER(c.email) = UPPER(?1) "
+
" AND lo.status = ?2 "
)
public
List<
Loan>
getAllOpenLoansOfThisCustomer
(
String email, LoanStatus status);
@Query
(
"SELECT lo "
+
"FROM Loan lo "
+
"INNER JOIN lo.pk.book b "
+
"INNER JOIN lo.pk.customer c "
+
"WHERE b.id = ?1 "
+
" AND c.id = ?2 "
+
" AND lo.status = ?3 "
)
public
Loan getLoanByCriteria
(
Integer bookId, Integer customerId, LoanStatus status);
}
IX. Les classes de services▲
Les classes de services de notre application Library sont celles qui font directement appel aux DAO présentés précédemment, afin de récupérer les données, les traiter si nécessaire et les faire transiter vers les services de niveau supérieur qui en ont fait la demande (en l'occurrence les contrôleurs REST que nous présenterons dans la section suivante). Il s'agit donc d'une classe intermédiaire entre la classe DAO et la classe Contrôleur qu'il faut implémenter afin de respecter la hiérarchie des appels dans une application de type SOA (Service Oriented Architecture).
Comme dans la section précédente, nous vous présentons le contenu des classes chargées de la gestion du domaine des livres (Book) et des prêts (Loan). Ceux des domaines Catégorie (Category) et Client (Customer) sont totalement similaires et vous pourrez les consulter dans le code source téléchargeable à la fin de cet article.
- Domaine Book :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
package
com.gkemayo.library.book;
import
java.util.List;
public
interface
IBookService {
public
Book saveBook
(
Book book);
public
Book updateBook
(
Book book);
public
void
deleteBook
(
Integer bookId);
public
List<
Book>
findBooksByTitleOrPartTitle
(
String title);
public
Book findBookByIsbn
(
String isbn);
public
boolean
checkIfIdexists
(
Integer id);
public
List<
Book>
getBooksByCategory
(
String codeCategory);
}
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.
package
com.gkemayo.library.book;
import
java.util.List;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.stereotype.Service;
import
org.springframework.transaction.annotation.Transactional;
@Service
(
"bookService"
)
@Transactional
public
class
BookServiceImpl implements
IBookService {
@Autowired
private
IBookDao bookDao;
@Override
public
Book saveBook
(
Book book) {
return
bookDao.save
(
book);
}
@Override
public
Book updateBook
(
Book book) {
return
bookDao.save
(
book);
}
@Override
public
void
deleteBook
(
Integer bookId) {
bookDao.deleteById
(
bookId);
}
@Override
public
boolean
checkIfIdExists
(
Integer id) {
return
bookDao.existsById
(
id);
}
@Override
public
List<
Book>
findBooksByTitleOrPartTitle
(
String title) {
return
bookDao.findByTitleLikeIgnoreCase
((
new
StringBuilder
(
)).append
(
"%"
).append
(
title).append
(
"%"
).toString
(
));
}
@Override
public
Book findBookByIsbn
(
String isbn) {
return
bookDao.findByIsbnIgnoreCase
(
isbn);
}
@Override
public
List<
Book>
getBooksByCategory
(
String codeCategory) {
return
bookDao.findByCategory
(
codeCategory);
}
}
- Domaine Loan :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
package
com.gkemayo.library.loan;
import
java.time.LocalDate;
import
java.util.List;
public
interface
ILoanService {
public
List<
Loan>
findAllLoansByEndDateBefore
(
LocalDate maxEndDate);
public
List<
Loan>
getAllOpenLoansOfThisCustomer
(
String email, LoanStatus status);
public
Loan getOpenedLoan
(
SimpleLoanDTO simpleLoanDTO);
public
boolean
checkIfLoanExists
(
SimpleLoanDTO simpleLoanDTO);
public
Loan saveLoan
(
Loan loan);
public
void
closeLoan
(
Loan loan);
}
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.
package
com.gkemayo.library.loan;
import
java.time.LocalDate;
import
java.util.List;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.stereotype.Service;
import
org.springframework.transaction.annotation.Transactional;
@Service
(
"loanService"
)
@Transactional
public
class
LoanServiceImpl implements
ILoanService {
@Autowired
private
ILoanDao loanDao;
@Override
public
List<
Loan>
findAllLoansByEndDateBefore
(
LocalDate maxEndDate) {
return
loanDao.findByEndDateBefore
(
maxEndDate);
}
@Override
public
List<
Loan>
getAllOpenLoansOfThisCustomer
(
String email, LoanStatus status) {
return
loanDao.getAllOpenLoansOfThisCustomer
(
email, status);
}
@Override
public
Loan getOpenedLoan
(
SimpleLoanDTO simpleLoanDTO) {
return
loanDao.getLoanByCriteria
(
simpleLoanDTO.getBookId
(
), simpleLoanDTO.getCustomerId
(
), LoanStatus.OPEN);
}
@Override
public
boolean
checkIfLoanExists
(
SimpleLoanDTO simpleLoanDTO) {
Loan loan =
loanDao.getLoanByCriteria
(
simpleLoanDTO.getBookId
(
), simpleLoanDTO.getCustomerId
(
), LoanStatus.OPEN);
if
(
loan !=
null
) {
return
true
;
}
return
false
;
}
@Override
public
Loan saveLoan
(
Loan loan) {
return
loanDao.save
(
loan);
}
/**
* On fera de la suppression logique, car le statut de l'objet Loan est positionné à CLOSE.
*/
@Override
public
void
closeLoan
(
Loan loan) {
loanDao.save
(
loan);
}
}
Dans ces différentes classes, nous notons l'utilisation des annotations suivantes :
- @Service, qui permet à Spring de considérer la classe qui la porte comme un bean qu'il créera au démarrage de l'application ;
- @Transactional au niveau classe, qui ordonne à spring de traiter toutes les méthodes publiques de la classe en mode transactionnel ;
- @Autowired, qui permet à Spring d'invoquer et d'injecter un bean « supposé » existant dans le contexte de la classe appelante.
LoanStatus est une simple enum Java contenant les valeurs OPEN et CLOSE. Et SimpleLoanDTO est un POJO contenant les champs bookId, customerId, beginDate et endDate ainsi que leurs getter/setter.
X. Les contrôleurs REST▲
X-A. Quelques notions sur les API RESTful▲
Dans un système distribué où les applications ont besoin d'intercommuniquer pour s'échanger des données, il existe plusieurs moyens pour y parvenir. Parmi ceux-ci, nous avons les moyens de communication synchrone implémentés par des technologies telles que RMI (Remote Method Invocation), CORBA (Common Object Request Broker Architecture), SOAP (Simple Object Access Protocol) et REST (REspresentional State Transfer) et les moyens de communication asynchrone implémentés par des technologies de type JMS (Java Messaging Service) ou des architectures de type CQRS (Command and Query Responsibility Segregation) aussi appelées Event Sourcing.
Dans les applications web qui sont donc de type client/serveur, la technologie REST est celle qui est la plus utilisée aujourd'hui (en 2019) à raison de la simplicité de sa mécanique. En effet, REST s'appuie sur le protocole HTTP pour assurer la communication entre un client et un serveur. Il correspond donc à une API (Application Programming Interface) qui utilise et étend les méthodes HTTP pour standardiser les moyens de communication entre client et serveur. Les méthodes HTTP -- aussi appelées verbes HTTP -- les plus utilisées sont :
- GET, dédiée à la lecture d'une ressource exposée sur un serveur ;
- POST, permet la création d'un ou de plusieurs objets sur un serveur au travers d'une ressource dédiée ;
- PUT, permet la mise à jour d'un ou de plusieurs objets sur un serveur au travers d'une ressource dédiée ;
- DELETE, permet la suppression d'un ou de plusieurs objets sur un serveur au travers d'une ressource.
De façon prosaïque, nous pouvons voir une ressource comme tout élément construit sur le serveur et qui peut n'être accédée qu'à travers un unique chemin appelé URI (Uniform Resource Identifier) afin de récupérer, créer, mettre à jour ou supprimer des données. Dans les applications web Java, ces ressources sont généralement représentées par des méthodes publiques qualifiées de web services et implémentées au sein de classes appelées contrôleur REST. Tout contrôleur REST exposant des web services et respectant les principes de la spécification REST est qualifié d'API RESTful. Pour votre culture, il existe une échelle appelée Model de Maturité de Richardson qui spécifie un classement des niveaux d'une API dite RESTful.
Il existe plusieurs frameworks permettant d'implémenter des API RESTful dans nos applications. Les plus connus sont Jersey et Spring Webmvc qui proposent plusieurs annotations et fonctionnalités permettant de spécifier des ressources et leurs méthodes d'accès (GET, POST, PUT, DELETE, etc.) sur un serveur d'application.
Dans notre application Library, puisque nous utilisons Spring Boot et sommes donc en écosystème Spring, nous choisissons de réaliser nos contrôleurs REST via la dépendance Spring Webmvc qui a été automatiquement injectée par le starter spring-boot-starter-web.
X-B. Un petit détour sur Spring Mail▲
Notre application Library, conformément à la User story 10 aura besoin d'envoyer des mails aux clients de la bibliothèque. L'implémentation de ce service d'envoi de mail sera réalisée dans l'un des contrôleurs REST dont nous afficherons le code source dans la section suivante. En attendant, nous vous présentons rapidement ci-dessous les préalables nécessaires pour faire du mailing avec Java Spring.
1. Il faut ajouter à votre application dans un fichier properties qui sera chargé par Spring, les ressources nécessaires. Exemple de l'application.properties exposé à la section V que nous reprécisons ci-dessous :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
############## DataSource Config #################
spring.mail.default-encoding
=UTF-8
spring.mail.protocol
=smtp
spring.mail.host
=smtp.gmail.com
spring.mail.username
=noreply.library.test@gmail.com
spring.mail.password
=password1Test
spring.mail.port
= 587
spring.mail.properties.mail.smtp.auth
=true
spring.mail.properties.mail.smtp.starttls.enable
=true
spring.mail.test-connection
=false
#https://www.google.com/settings/security/lesssecureapps
Dans ce paramétrage, UTF-8 correspond à l'encodage du texte contenu dans les mails. Le protocole de mailing utilisé est SMTP (Simple Message Transfer Protocol) avec comme serveur celui gmail de Google sur le port 587. Notre application utilisera le compte noreply.library.test@gmail.com/passwordTest pour envoyer ses mails dans un contexte sécurisé (spring.mail.properties.mail.smtp.auth=true). Enfin, la connexion de l'application au serveur smtp utilisera le protocole TLS (spring.mail.properties.mail.smtp.starttls.enable=true). Pour information, pour permettre à une application/robot d'envoyer des mails via gmail, il faut se connecter une première fois manuellement dans le compte gmail concerné, puis aller à l'adresse suivante https://www.google.com/settings/security/lesssecureapps pour désactiver la sécurité manuelle.
2. Injection de la dépendance Maven pour Spring Mail :
- Si votre application n'est pas Spring Bootée, vous devez ajouter la dépendance suivante dans le pom.xml :
2.
3.
4.
5.
<dependency>
<groupId>
org.springframework</groupId>
<artifactId>
spring-context-support</artifactId>
<version>
${version-souhaitée}</version>
</dependency>
- Si votre application est générée via Spring Boot comme l'application Library, vous devez ajouter le starter suivant :
2.
3.
4.
5.
<dependency>
<groupId>
org.springframework.boot</groupId>
<artifactId>
spring-boot-starter-mail</artifactId>
//Spring Boot chargera lui même la version
</dependency>
3. Enfin, dans votre classe Java qui va gérer l'envoi de mail, il faut injecter, via l'annotation @Autowired, le bean JavaMailSender, fourni par Spring Mail. Il ne nous restera plus qu'à utiliser sa méthode send() pour envoyer effectivement le mail. Exemple :
2.
3.
4.
5.
6.
7.
8.
9.
10.
public
class
MailSender {
@Autowired
private
JavaMailSender javaMailSender;
public
void
sendMail
(
) throws
MailException {
//... créer ici l'objet message (de type SimpleMailMessage ou MimeMessage) à envoyer
javaMailSender.send
(
message);
}
}
X-C. Quelques contrôleurs Rest de l'application▲
Nous vous exposons ci-dessous, les classes CustomerRestController et LoanRestController. Vous pouvez consulter les contrôleurs BookRestController et CategoryRestController, similaires aux deux premiers, directement dans le code source téléchargable à la fin de cet article.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
package
com.gkemayo.library.customer;
import
java.time.LocalDate;
import
java.util.Date;
import
java.util.List;
import
java.util.stream.Collectors;
import
org.modelmapper.ModelMapper;
import
org.slf4j.Logger;
import
org.slf4j.LoggerFactory;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.data.domain.Page;
import
org.springframework.http.HttpStatus;
import
org.springframework.http.ResponseEntity;
import
org.springframework.mail.MailException;
import
org.springframework.mail.SimpleMailMessage;
import
org.springframework.mail.javamail.JavaMailSender;
import
org.springframework.util.CollectionUtils;
import
org.springframework.util.StringUtils;
import
org.springframework.web.bind.annotation.DeleteMapping;
import
org.springframework.web.bind.annotation.GetMapping;
import
org.springframework.web.bind.annotation.PathVariable;
import
org.springframework.web.bind.annotation.PostMapping;
import
org.springframework.web.bind.annotation.PutMapping;
import
org.springframework.web.bind.annotation.RequestBody;
import
org.springframework.web.bind.annotation.RequestMapping;
import
org.springframework.web.bind.annotation.RequestParam;
import
org.springframework.web.bind.annotation.RestController;
import
org.springframework.web.util.UriComponentsBuilder;
import
io.swagger.annotations.Api;
import
io.swagger.annotations.ApiOperation;
import
io.swagger.annotations.ApiResponse;
import
io.swagger.annotations.ApiResponses;
@RestController
@RequestMapping
(
"/rest/customer/api"
)
public
class
CustomerRestController {
public
static
final
Logger LOGGER =
LoggerFactory.getLogger
(
CustomerRestController.class
);
@Autowired
private
CustomerServiceImpl customerService;
@Autowired
private
JavaMailSender javaMailSender;
/**
* Ajoute un nouveau client dans la base de données H2. Si le client existe déjà, on retourne un code indiquant que la création n'a pas abouti.
*
@param
customerDTORequest
*
@return
*/
@PostMapping
(
"/addCustomer"
)
public
ResponseEntity<
CustomerDTO>
createNewCustomer
(
@RequestBody
CustomerDTO customerDTORequest) {
//, UriComponentsBuilder uriComponentBuilder
Customer existingCustomer =
customerService.findCustomerByEmail
(
customerDTORequest.getEmail
(
));
if
(
existingCustomer !=
null
) {
return
new
ResponseEntity<
CustomerDTO>(
HttpStatus.CONFLICT);
}
Customer customerRequest =
mapCustomerDTOToCustomer
(
customerDTORequest);
customerRequest.setCreationDate
(
LocalDate.now
(
));
Customer customerResponse =
customerService.saveCustomer
(
customerRequest);
if
(
customerResponse !=
null
) {
CustomerDTO customerDTO =
mapCustomerToCustomerDTO
(
customerResponse);
return
new
ResponseEntity<
CustomerDTO>(
customerDTO, HttpStatus.CREATED);
}
return
new
ResponseEntity<
CustomerDTO>(
HttpStatus.NOT_MODIFIED);
}
/**
* Met à jour les données d'un client dans la base de données H2. Si le client n'est pas retrouvé, on retourne un code indiquant que la mise à jour n'a pas abouti.
*
@param
customerDTORequest
*
@return
*/
@PutMapping
(
"/updateCustomer"
)
public
ResponseEntity<
CustomerDTO>
updateCustomer
(
@RequestBody
CustomerDTO customerDTORequest) {
//, UriComponentsBuilder uriComponentBuilder
if
(!
customerService.checkIfIdexists
(
customerDTORequest.getId
(
))) {
return
new
ResponseEntity<
CustomerDTO>(
HttpStatus.NOT_FOUND);
}
Customer customerRequest =
mapCustomerDTOToCustomer
(
customerDTORequest);
Customer customerResponse =
customerService.updateCustomer
(
customerRequest);
if
(
customerResponse !=
null
) {
CustomerDTO customerDTO =
mapCustomerToCustomerDTO
(
customerResponse);
return
new
ResponseEntity<
CustomerDTO>(
customerDTO, HttpStatus.OK);
}
return
new
ResponseEntity<
CustomerDTO>(
HttpStatus.NOT_MODIFIED);
}
/**
* Supprime un client dans la base de données H2. Si le client n'est pas retrouvé, on retourne le Statut HTTP NO_CONTENT.
*
@param
customerId
*
@return
*/
@DeleteMapping
(
"/deleteCustomer/{customerId}"
)
public
ResponseEntity<
String>
deleteCustomer
(
@PathVariable
Integer customerId) {
customerService.deleteCustomer
(
customerId);
return
new
ResponseEntity<
String>(
HttpStatus.NO_CONTENT);
}
/**
* Retourne le client ayant l'adresse email passée en paramètre.
*
@param
email
*
@return
*/
@GetMapping
(
"/searchByEmail"
)
public
ResponseEntity<
CustomerDTO>
searchCustomerByEmail
(
@RequestParam
(
"email"
) String email) {
//, UriComponentsBuilder uriComponentBuilder
Customer customer =
customerService.findCustomerByEmail
(
email);
if
(
customer !=
null
) {
CustomerDTO customerDTO =
mapCustomerToCustomerDTO
(
customer);
return
new
ResponseEntity<
CustomerDTO>(
customerDTO, HttpStatus.OK);
}
return
new
ResponseEntity<
CustomerDTO>(
HttpStatus.NO_CONTENT);
}
/**
* Retourne la liste des clients ayant le nom passé en paramètre.
*
@param
lastName
*
@return
*/
@GetMapping
(
"/searchByLastName"
)
public
ResponseEntity<
List<
CustomerDTO>>
searchBookByLastName
(
@RequestParam
(
"lastName"
) String lastName) {
//, UriComponentsBuilder uriComponentBuilder
List<
Customer>
customers =
customerService.findCustomerByLastName
(
lastName);
if
(
customers !=
null
&&
!
CollectionUtils.isEmpty
(
customers)) {
List<
CustomerDTO>
customerDTOs =
customers.stream
(
).map
(
customer ->
{
return
mapCustomerToCustomerDTO
(
customer);
}
).collect
(
Collectors.toList
(
));
return
new
ResponseEntity<
List<
CustomerDTO>>(
customerDTOs, HttpStatus.OK);
}
return
new
ResponseEntity<
List<
CustomerDTO>>(
HttpStatus.NO_CONTENT);
}
/**
* Envoie un mail à un client. L'objet MailDTO contient l'identifiant et l'email du client concerné, l'objet du mail et le contenu du message.
*
@param
loanMailDto
*
@param
uriComponentBuilder
*
@return
*/
@PutMapping
(
"/sendEmailToCustomer"
)
public
ResponseEntity<
Boolean>
sendMailToCustomer
(
@RequestBody
MailDTO loanMailDto, UriComponentsBuilder uriComponentBuilder) {
Customer customer =
customerService.findCustomerById
(
loanMailDto.getCustomerId
(
));
if
(
customer ==
null
) {
String errorMessage =
"The selected Customer for sending email is not found in the database"
;
LOGGER.info
(
errorMessage);
return
new
ResponseEntity<
Boolean>(
false
, HttpStatus.NOT_FOUND);
}
else
if
(
customer !=
null
&&
StringUtils.isEmpty
(
customer.getEmail
(
))) {
String errorMessage =
"No existing email for the selected Customer for sending email to"
;
LOGGER.info
(
errorMessage);
return
new
ResponseEntity<
Boolean>(
false
, HttpStatus.NOT_FOUND);
}
SimpleMailMessage mail =
new
SimpleMailMessage
(
);
mail.setFrom
(
loanMailDto.MAIL_FROM);
mail.setTo
(
customer.getEmail
(
));
mail.setSentDate
(
new
Date
(
));
mail.setSubject
(
loanMailDto.getEmailSubject
(
));
mail.setText
(
loanMailDto.getEmailContent
(
));
try
{
javaMailSender.send
(
mail);
}
catch
(
MailException e) {
return
new
ResponseEntity<
Boolean>(
false
, HttpStatus.FORBIDDEN);
}
return
new
ResponseEntity<
Boolean>(
true
, HttpStatus.OK);
}
/**
* Transforme une entity Customer en un POJO CustomerDTO
*
*
@param
customer
*
@return
*/
private
CustomerDTO mapCustomerToCustomerDTO
(
Customer customer) {
ModelMapper mapper =
new
ModelMapper
(
);
CustomerDTO customerDTO =
mapper.map
(
customer, CustomerDTO.class
);
return
customerDTO;
}
/**
* Transforme un POJO CustomerDTO en une entity Customer
*
*
@param
customerDTO
*
@return
*/
private
Customer mapCustomerDTOToCustomer
(
CustomerDTO customerDTO) {
ModelMapper mapper =
new
ModelMapper
(
);
Customer customer =
mapper.map
(
customerDTO, Customer.class
);
return
customer;
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
package
com.gkemayo.library.loan;
import
java.time.LocalDate;
import
java.time.LocalDateTime;
import
java.util.Collections;
import
java.util.List;
import
java.util.function.Function;
import
java.util.stream.Collectors;
import
org.slf4j.Logger;
import
org.slf4j.LoggerFactory;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.format.annotation.DateTimeFormat;
import
org.springframework.http.HttpStatus;
import
org.springframework.http.ResponseEntity;
import
org.springframework.util.CollectionUtils;
import
org.springframework.web.bind.annotation.GetMapping;
import
org.springframework.web.bind.annotation.PostMapping;
import
org.springframework.web.bind.annotation.RequestBody;
import
org.springframework.web.bind.annotation.RequestMapping;
import
org.springframework.web.bind.annotation.RequestParam;
import
org.springframework.web.bind.annotation.RestController;
import
org.springframework.web.util.UriComponentsBuilder;
import
com.gkemayo.library.book.Book;
import
com.gkemayo.library.customer.Customer;
import
io.swagger.annotations.Api;
import
io.swagger.annotations.ApiOperation;
import
io.swagger.annotations.ApiResponse;
import
io.swagger.annotations.ApiResponses;
@RestController
@RequestMapping
(
"/rest/loan/api"
)
public
class
LoanRestController {
public
static
final
Logger LOGGER =
LoggerFactory.getLogger
(
LoanRestController.class
);
@Autowired
private
LoanServiceImpl loanService;
/**
* Retourne l'historique des prêts en cours dans la bibliothèque jusqu'à une certaine date maximale.
*
@param
maxEndDateStr
*
@return
*/
@GetMapping
(
"/maxEndDate"
)
public
ResponseEntity<
List<
LoanDTO>>
searchAllBooksLoanBeforeThisDate
(
@RequestParam
(
"date"
) String maxEndDateStr) {
List<
Loan>
loans =
loanService.findAllLoansByEndDateBefore
(
LocalDate.parse
(
maxEndDateStr));
// on retire tous les élts null que peut contenir cette liste => pour éviter les NPE par la suite
loans.removeAll
(
Collections.singleton
(
null
));
List<
LoanDTO>
loanInfosDtos =
mapLoanDtosFromLoans
(
loans);
return
new
ResponseEntity<
List<
LoanDTO>>(
loanInfosDtos, HttpStatus.OK);
}
/**
* Retourne la liste des prêts en cours d'un client.
*
@param
email
*
@return
*/
@GetMapping
(
"/customerLoans"
)
public
ResponseEntity<
List<
LoanDTO>>
searchAllOpenedLoansOfThisCustomer
(
@RequestParam
(
"email"
) String email) {
List<
Loan>
loans =
loanService.getAllOpenLoansOfThisCustomer
(
email, LoanStatus.OPEN);
// on retire tous les élts null que peut contenir cette liste => pour éviter les NPE par la suite
loans.removeAll
(
Collections.singleton
(
null
));
List<
LoanDTO>
loanInfosDtos =
mapLoanDtosFromLoans
(
loans);
return
new
ResponseEntity<
List<
LoanDTO>>(
loanInfosDtos, HttpStatus.OK);
}
/**
* Ajoute un nouveau prêt dans la base de données H2.
*
@param
simpleLoanDTORequest
*
@param
uriComponentBuilder
*
@return
*/
@PostMapping
(
"/addLoan"
)
public
ResponseEntity<
Boolean>
createNewLoan
(
@RequestBody
SimpleLoanDTO simpleLoanDTORequest,
UriComponentsBuilder uriComponentBuilder) {
boolean
isLoanExists =
loanService.checkIfLoanExists
(
simpleLoanDTORequest);
if
(
isLoanExists) {
return
new
ResponseEntity<
Boolean>(
false
, HttpStatus.CONFLICT);
}
Loan LoanRequest =
mapSimpleLoanDTOToLoan
(
simpleLoanDTORequest);
Loan loan =
loanService.saveLoan
(
LoanRequest);
if
(
loan !=
null
) {
return
new
ResponseEntity<
Boolean>(
true
, HttpStatus.CREATED);
}
return
new
ResponseEntity<
Boolean>(
false
, HttpStatus.NOT_MODIFIED);
}
/**
* Clôture le prêt de livre d'un client.
*
@param
simpleLoanDTORequest
*
@param
uriComponentBuilder
*
@return
*/
@PostMapping
(
"/closeLoan"
)
public
ResponseEntity<
Boolean>
closeLoan
(
@RequestBody
SimpleLoanDTO simpleLoanDTORequest,
UriComponentsBuilder uriComponentBuilder) {
Loan existingLoan =
loanService.getOpenedLoan
(
simpleLoanDTORequest);
if
(
existingLoan ==
null
) {
return
new
ResponseEntity<
Boolean>(
false
, HttpStatus.NO_CONTENT);
}
existingLoan.setStatus
(
LoanStatus.CLOSE);
Loan loan =
loanService.saveLoan
(
existingLoan);
if
(
loan !=
null
) {
return
new
ResponseEntity<
Boolean>(
true
, HttpStatus.OK);
}
return
new
ResponseEntity<
Boolean>(
HttpStatus.NOT_MODIFIED);
}
/**
* Transforme une liste d'entités Lo Loan en liste LoanDTO.
*
*
@param
loans
*
@return
*/
private
List<
LoanDTO>
mapLoanDtosFromLoans
(
List<
Loan>
loans) {
Function<
Loan, LoanDTO>
mapperFunction =
(
loan) ->
{
// dans loanDTO on n'ajoute que les données nécessaires
LoanDTO loanDTO =
new
LoanDTO
(
);
loanDTO.getBookDTO
(
).setId
(
loan.getPk
(
).getBook
(
).getId
(
));
loanDTO.getBookDTO
(
).setIsbn
(
loan.getPk
(
).getBook
(
).getIsbn
(
));
loanDTO.getBookDTO
(
).setTitle
(
loan.getPk
(
).getBook
(
).getTitle
(
));
loanDTO.getCustomerDTO
(
).setId
(
loan.getPk
(
).getCustomer
(
).getId
(
));
loanDTO.getCustomerDTO
(
).setFirstName
(
loan.getPk
(
).getCustomer
(
).getFirstName
(
));
loanDTO.getCustomerDTO
(
).setLastName
(
loan.getPk
(
).getCustomer
(
).getLastName
(
));
loanDTO.getCustomerDTO
(
).setEmail
(
loan.getPk
(
).getCustomer
(
).getEmail
(
));
loanDTO.setLoanBeginDate
(
loan.getBeginDate
(
));
loanDTO.setLoanEndDate
(
loan.getEndDate
(
));
return
loanDTO;
}
;
if
(!
CollectionUtils.isEmpty
(
loans)) {
return
loans.stream
(
).map
(
mapperFunction).sorted
(
).collect
(
Collectors.toList
(
));
}
return
null
;
}
/**
* Transforme un SimpleLoanDTO en Loan avec les données minimalistes nécessaires
*
*
@param
loanDTORequest
*
@return
*/
private
Loan mapSimpleLoanDTOToLoan
(
SimpleLoanDTO simpleLoanDTO) {
Loan loan =
new
Loan
(
);
Book book =
new
Book
(
);
book.setId
(
simpleLoanDTO.getBookId
(
));
Customer customer =
new
Customer
(
);
customer.setId
(
simpleLoanDTO.getCustomerId
(
));
LoanId loanId =
new
LoanId
(
book, customer);
loan.setPk
(
loanId);
loan.setBeginDate
(
simpleLoanDTO.getBeginDate
(
));
loan.setEndDate
(
simpleLoanDTO.getEndDate
(
));
loan.setStatus
(
LoanStatus.OPEN);
return
loan;
}
}
Suite à la l'affichage de ces deux contrôleurs REST qui réalisent des opérations de création/modification/suppression/mise à jour d'un nouveau client/Prêts + l'envoi de mail à un client (CustomerRestController), vous remarquez qu'un bon nombre d'annotations Spring ont été utilisées. Elles sont possibles grâce au starter spring-boot-starter-web ajouté dans le pom.xl, qui à son tour injectera la dépendance Spring Webmvc correspondant à l'implémentation Spring d'API RESTful :
- @RestController : permet de marquer une classe comme étant une qui exposera des ressources appelées web services ;
- @RequestMapping : permet de spécifier l'URI d'un web service ou d'une classe représentant le Contrôleur REST ;
- @GetMapping : marque une ressource (et donc un web service) comme accessible par la méthode GET de HTTP. Spécifie aussi l'URI de la ressource ;
- @PostMapping : marque une ressource comme accessible par la méthode POST de HTTP. Spécifie aussi l'URI de la ressource ;
- @PutMapping : marque une ressource comme accessible par la méthode PUT de HTTP. Spécifie aussi l'URI de la ressource ;
- @DeleteMapping : marque une ressource comme accessible par la méthode DELETE de HTTP. Spécifie aussi l'URI de la ressource.
En appliquant les définitions données ci-dessus, nous observons que notre application Library expose :
- pour le contrôleur CustomerRestController, sept web services représentés par les méthodes publiques suivantes : createNewCustomer, updateCustomer, deleteCustomer, searchCustomers, searchCustomerByEmail, searchBookByLastName, sendMailToCustomer ;
- pour le contrôleur LoanRestController, quatre web services représentés par les méthodes suivantes : searchAllBooksLoanBeforeThisDate, searchAllOpenedLoansOfThisCustomer, createNewLoan, closeLoan.
Ces contrôleurs REST s'appuient sur les classes de services (exemple : CustomerService, LoanService) présentées plus haut pour faire appel aux classes DAO afin d'accéder à la base de données H2.
Dans les classes CustomerRestController et LoanRestController, nous avons utilisé d'autres annotations fournies par Spring qui concourent à la mise en place complète de web services. À savoir, @RequestBody, @RequestParam, @PathVariable qui sont les moyens de passage de paramètres du client vers le serveur. Nous avons aussi utilisé l'objet ResponseEntity qui joue l'effet inverse permettant ainsi au serveur d'encapsuler les données qu'il renverra au client. Nous n'entrerons pas plus dans les détails, il existe de nombreux articles sur Internet qui s'étendent de long en large sur ces notions.
Note : nous avons ajouté le framework ModelMapper pour gérer le transfert de données entre deux objets Java de même nature.
XI. Documenter et tester l'API REST avec Swagger▲
XI-A. Swagger et comment on le configure ?▲
Définition
Swagger est une méthode formelle de spécifications permettant de décrire et de produire une documentation au format JSON de l'API REST d'une application. En d'autres termes, il a pour objectif de regrouper au sein d'un même objet JSON, une description de chaque web service exposé dans votre application. Cette description peut porter sur : le type de méthode d'accès (GET, POST, etc.), l'URI du Web Service, les paramètres d'entrée et de sortie du web service, les codes HTTP de retour possibles, etc.
Swagger a été créé en 2011 et a vu au fil du temps sa spécification évoluer d'une version 1 à la version 3 actuelle. Il existe plusieurs frameworks qui implémentent swagger et qui proposent une interface web de l'objet JSON. Cette interface graphique est plus simple à la lecture et permet même de faire des tests réels de votre API REST.
Configuration
1- Dans l'application Library, nous utilisons le framework Springfox dont voici les dépendances à ajouter au pom.xml de l'application. Notez que Spring Boot ne propose aucun starter permettant d'inclure et d'utiliser directement swagger.
<dependency>
<groupId>
io.springfox</groupId>
<artifactId>
springfox-swagger2</artifactId>
<version>
2.9.2</version>
</dependency>
<dependency>
<groupId>
io.springfox</groupId>
<artifactId>
springfox-swagger-ui</artifactId>
<version>
2.9.2</version>
</dependency>
2- Ajouter l'annotation @EnableSwagger2 et configurer le bean Docket au niveau de la classe de démarrage, LibraryApplication de l'application. Voici l'exemple de ce que nous avons configuré :
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.
package
com.gkemayo.library;
import
org.springframework.boot.SpringApplication;
import
org.springframework.boot.autoconfigure.SpringBootApplication;
import
org.springframework.context.annotation.Bean;
import
springfox.documentation.builders.ApiInfoBuilder;
import
springfox.documentation.builders.PathSelectors;
import
springfox.documentation.builders.RequestHandlerSelectors;
import
springfox.documentation.service.ApiInfo;
import
springfox.documentation.service.Contact;
import
springfox.documentation.spi.DocumentationType;
import
springfox.documentation.spring.web.plugins.Docket;
import
springfox.documentation.swagger2.annotations.EnableSwagger2;
@SpringBootApplication
@EnableSwagger2
public
class
LibraryApplication {
public
static
void
main
(
String[] args) {
SpringApplication.run
(
LibraryApplication.class
, args);
}
@Bean
public
Docket api
(
) {
return
new
Docket
(
DocumentationType.SWAGGER_2)
.select
(
)
.apis
(
RequestHandlerSelectors.basePackage
(
"com.gkemayo.library"
))
.paths
(
PathSelectors.any
(
))
.build
(
)
.apiInfo
(
apiInfo
(
));
}
private
ApiInfo apiInfo
(
) {
return
new
ApiInfoBuilder
(
).title
(
"Library Spring Boot REST API Documentation"
)
.description
(
"REST APIs For Managing Books loans in a Library"
)
.contact
(
new
Contact
(
"Georges Kemayo"
, "https://gkemayo.developpez.com/"
, "noreply.library.test@gmail.com"
))
.version
(
"1.0"
)
.build
(
);
}
}
Docket est le bean qui permet de configurer les données du rendu graphique de la documentation JSON de notre application. Il propose un ensemble d'attributs permettant de configurer : les packages contenant l'API REST concerné par la documentation Swagger (ici, com.gkemayo.library), les URI spécifiques des API REST à documenter (ici, tous les URI seront documentés : PathSelectors.any()), et d'autres informations générales à afficher (titre d'entête de la page, contact du/des contributeurs, version de l'API REST documentée, etc.).
Pour l'application Library déployée dans un Tomcat sur le port 8082, nous utilisons le lien http://localhost:8082/library/swagger-ui.html#/ pour afficher la page web Swagger.
Cette figure présente ainsi l'API REST de l'application Library. Les différents web services exposés sont repartis suivant les contrôleurs REST dans lesquels ils ont été définis. L'on peut remarquer la simplicité et la lisibilité au niveau de la description de chaque web service. Cette documentation a par conséquent l'avantage de permettre aux consommateurs de cette API REST de capter rapidement ce que fait chaque web service. Si l'on clique sur l'une des lignes représentant un web service, par exemple /addBook, elle se déplie et affiche une fenêtre qui apporte plus de détails sur le web service (ses paramètres d'entrées, son objet retour, les potentiels codes retour HTTP qu'il peut renvoyer, etc.) et même son exécution à travers le bouton Try it out.
Enfin, si vous avez besoin de l'objet JSON correspond à l'API REST de l'application, il s'obtient sur l’URL relatif suivant : /v2/api-docs. Exemple de l'application Library : http://localhost:8082/library/v2/api-docs.
XI-B. Exemple du CustomerRestController▲
Dans la précédente section, nous avons vu que le bean de type Docket permettait de configurer, entre autres, l'affichage des informations générales sur la page web Swagger, à savoir le titre, le contact des contributeurs de l'API REST, la version actuelle de l'API, etc.). Mais nous avons observé sur les captures affichées dans cette même section que les différents contrôleurs REST et leurs web services étaient aussi documentés. Mais comment est-ce possible ? me demanderez-vous. Eh bien, sachez que cela ne se fait pas automatiquement. La documentation Swagger des contrôleurs REST et des web services qui y sont exposés n'est possible que par le concours de quelques annotations. Pour le cas de l'application Library, nous avons utilisé les annotations suivantes :
- @Api : utilisée sur une classe de type contrôleur REST pour décrire globalement ce qu'elle fait ;
- @ApiOperation : utilisée sur un web service pour préciser ce qu'il fait exactement et aussi son objet retour ;
- @ApiResponses et @ApiResponse : pour décrire les différents codes retour HTTP que peut renvoyer un web service ;
- @ApiModel et @ApiModelProperty : utilisées pour décrire respectivement une classe POJO portant des données échangées entre le client et le serveur et pour décrire chaque attribut du POJO.
Voici donc, l'exemple de la classe CustomerRestController avec les annotations Swagger :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
package
com.gkemayo.library.customer;
import
java.time.LocalDate;
import
java.util.Date;
import
java.util.List;
import
java.util.stream.Collectors;
import
org.modelmapper.ModelMapper;
import
org.slf4j.Logger;
import
org.slf4j.LoggerFactory;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.data.domain.Page;
import
org.springframework.http.HttpStatus;
import
org.springframework.http.ResponseEntity;
import
org.springframework.mail.MailException;
import
org.springframework.mail.SimpleMailMessage;
import
org.springframework.mail.javamail.JavaMailSender;
import
org.springframework.util.CollectionUtils;
import
org.springframework.util.StringUtils;
import
org.springframework.web.bind.annotation.DeleteMapping;
import
org.springframework.web.bind.annotation.GetMapping;
import
org.springframework.web.bind.annotation.PathVariable;
import
org.springframework.web.bind.annotation.PostMapping;
import
org.springframework.web.bind.annotation.PutMapping;
import
org.springframework.web.bind.annotation.RequestBody;
import
org.springframework.web.bind.annotation.RequestMapping;
import
org.springframework.web.bind.annotation.RequestParam;
import
org.springframework.web.bind.annotation.RestController;
import
org.springframework.web.util.UriComponentsBuilder;
import
io.swagger.annotations.Api;
import
io.swagger.annotations.ApiOperation;
import
io.swagger.annotations.ApiResponse;
import
io.swagger.annotations.ApiResponses;
@RestController
@RequestMapping
(
"/rest/customer/api"
)
@Api
(
value =
"Customer Rest Controller: contains all operations for managing customers"
)
public
class
CustomerRestController {
public
static
final
Logger LOGGER =
LoggerFactory.getLogger
(
CustomerRestController.class
);
@Autowired
private
CustomerServiceImpl customerService;
@Autowired
private
JavaMailSender javaMailSender;
/**
* Ajoute un nouveau client dans la base de données H2. Si le client existe déjà, on retourne un code indiquant que la création n'a pas abouti.
*
@param
customerDTORequest
*
@return
*/
@PostMapping
(
"/addCustomer"
)
@ApiOperation
(
value =
"Add a new Customer in the Library"
, response =
CustomerDTO.class
)
@ApiResponses
(
value =
{
@ApiResponse
(
code =
409
, message =
"Conflict: the customer already exist"
),
@ApiResponse
(
code =
201
, message =
"Created: the customer is successfully inserted"
),
@ApiResponse
(
code =
304
, message =
"Not Modified: the customer is unsuccessfully inserted"
) }
)
public
ResponseEntity<
CustomerDTO>
createNewCustomer
(
@RequestBody
CustomerDTO customerDTORequest) {
//, UriComponentsBuilder uriComponentBuilder
Customer existingCustomer =
customerService.findCustomerByEmail
(
customerDTORequest.getEmail
(
));
if
(
existingCustomer !=
null
) {
return
new
ResponseEntity<
CustomerDTO>(
HttpStatus.CONFLICT);
}
Customer customerRequest =
mapCustomerDTOToCustomer
(
customerDTORequest);
customerRequest.setCreationDate
(
LocalDate.now
(
));
Customer customerResponse =
customerService.saveCustomer
(
customerRequest);
if
(
customerResponse !=
null
) {
CustomerDTO customerDTO =
mapCustomerToCustomerDTO
(
customerResponse);
return
new
ResponseEntity<
CustomerDTO>(
customerDTO, HttpStatus.CREATED);
}
return
new
ResponseEntity<
CustomerDTO>(
HttpStatus.NOT_MODIFIED);
}
/**
* Met à jour les données d'un client dans la base de données H2. Si le client n'est pas retrouvé, on retourne un code indiquant que la mise à jour n'a pas abouti.
*
@param
customerDTORequest
*
@return
*/
@PutMapping
(
"/updateCustomer"
)
@ApiOperation
(
value =
"Update/Modify an existing customer in the Library"
, response =
CustomerDTO.class
)
@ApiResponses
(
value =
{
@ApiResponse
(
code =
404
, message =
"Not Found : the customer does not exist"
),
@ApiResponse
(
code =
200
, message =
"Ok: the customer is successfully updated"
),
@ApiResponse
(
code =
304
, message =
"Not Modified: the customer is unsuccessfully updated"
) }
)
public
ResponseEntity<
CustomerDTO>
updateCustomer
(
@RequestBody
CustomerDTO customerDTORequest) {
//, UriComponentsBuilder uriComponentBuilder
if
(!
customerService.checkIfIdexists
(
customerDTORequest.getId
(
))) {
return
new
ResponseEntity<
CustomerDTO>(
HttpStatus.NOT_FOUND);
}
Customer customerRequest =
mapCustomerDTOToCustomer
(
customerDTORequest);
Customer customerResponse =
customerService.updateCustomer
(
customerRequest);
if
(
customerResponse !=
null
) {
CustomerDTO customerDTO =
mapCustomerToCustomerDTO
(
customerResponse);
return
new
ResponseEntity<
CustomerDTO>(
customerDTO, HttpStatus.OK);
}
return
new
ResponseEntity<
CustomerDTO>(
HttpStatus.NOT_MODIFIED);
}
/**
* Supprime un client dans la base de données H2. Si le client n'est pas retrouvé, on retourne le Statut HTTP NO_CONTENT.
*
@param
customerId
*
@return
*/
@DeleteMapping
(
"/deleteCustomer/{customerId}"
)
@ApiOperation
(
value =
"Delete a customer in the Library, if the customer does not exist, nothing is done"
, response =
String.class
)
@ApiResponse
(
code =
204
, message =
"No Content: customer sucessfully deleted"
)
public
ResponseEntity<
String>
deleteCustomer
(
@PathVariable
Integer customerId) {
customerService.deleteCustomer
(
customerId);
return
new
ResponseEntity<
String>(
HttpStatus.NO_CONTENT);
}
@GetMapping
(
"/paginatedSearch"
)
@ApiOperation
(
value=
"List customers of the Library in a paginated way"
, response =
List.class
)
@ApiResponses
(
value =
{
@ApiResponse
(
code =
200
, message =
"Ok: successfully listed"
),
@ApiResponse
(
code =
204
, message =
"No Content: no result founded"
),
}
)
public
ResponseEntity<
List<
CustomerDTO>>
searchCustomers
(
@RequestParam
(
"beginPage"
) int
beginPage,
@RequestParam
(
"endPage"
) int
endPage) {
//, UriComponentsBuilder uriComponentBuilder
Page<
Customer>
customers =
customerService.getPaginatedCustomersList
(
beginPage, endPage);
if
(
customers !=
null
) {
List<
CustomerDTO>
customerDTOs =
customers.stream
(
).map
(
customer ->
{
return
mapCustomerToCustomerDTO
(
customer);
}
).collect
(
Collectors.toList
(
));
return
new
ResponseEntity<
List<
CustomerDTO>>(
customerDTOs, HttpStatus.OK);
}
return
new
ResponseEntity<
List<
CustomerDTO>>(
HttpStatus.NO_CONTENT);
}
/**
* Retourne le client ayant l'adresse email passée en paramètre.
*
@param
email
*
@return
*/
@GetMapping
(
"/searchByEmail"
)
@ApiOperation
(
value=
"Search a customer in the Library by its email"
, response =
CustomerDTO.class
)
@ApiResponses
(
value =
{
@ApiResponse
(
code =
200
, message =
"Ok: successfull research"
),
@ApiResponse
(
code =
204
, message =
"No Content: no result founded"
),
}
)
public
ResponseEntity<
CustomerDTO>
searchCustomerByEmail
(
@RequestParam
(
"email"
) String email) {
//, UriComponentsBuilder uriComponentBuilder
Customer customer =
customerService.findCustomerByEmail
(
email);
if
(
customer !=
null
) {
CustomerDTO customerDTO =
mapCustomerToCustomerDTO
(
customer);
return
new
ResponseEntity<
CustomerDTO>(
customerDTO, HttpStatus.OK);
}
return
new
ResponseEntity<
CustomerDTO>(
HttpStatus.NO_CONTENT);
}
/**
* Retourne la liste des clients ayant le nom passé en paramètre.
*
@param
lastName
*
@return
*/
@GetMapping
(
"/searchByLastName"
)
@ApiOperation
(
value=
"Search a customer in the Library by its Last name"
, response =
List.class
)
@ApiResponses
(
value =
{
@ApiResponse
(
code =
200
, message =
"Ok: successfull research"
),
@ApiResponse
(
code =
204
, message =
"No Content: no result founded"
),
}
)
public
ResponseEntity<
List<
CustomerDTO>>
searchBookByLastName
(
@RequestParam
(
"lastName"
) String lastName) {
//, UriComponentsBuilder uriComponentBuilder
List<
Customer>
customers =
customerService.findCustomerByLastName
(
lastName);
if
(
customers !=
null
&&
!
CollectionUtils.isEmpty
(
customers)) {
List<
CustomerDTO>
customerDTOs =
customers.stream
(
).map
(
customer ->
{
return
mapCustomerToCustomerDTO
(
customer);
}
).collect
(
Collectors.toList
(
));
return
new
ResponseEntity<
List<
CustomerDTO>>(
customerDTOs, HttpStatus.OK);
}
return
new
ResponseEntity<
List<
CustomerDTO>>(
HttpStatus.NO_CONTENT);
}
/**
* Envoie un mail à un client. L'objet MailDTO contient l'identifiant et l'email du client concerné, l'objet du mail et le contenu du message.
*
@param
loanMailDto
*
@param
uriComponentBuilder
*
@return
*/
@PutMapping
(
"/sendEmailToCustomer"
)
@ApiOperation
(
value=
"Send an email to customer of the Library"
, response =
String.class
)
@ApiResponses
(
value =
{
@ApiResponse
(
code =
200
, message =
"Ok: Email successfully sent"
),
@ApiResponse
(
code =
404
, message =
"Not Found: no customer found, or wrong email"
),
@ApiResponse
(
code =
403
, message =
"Forbidden: Email cannot be sent"
)
}
)
public
ResponseEntity<
Boolean>
sendMailToCustomer
(
@RequestBody
MailDTO loanMailDto, UriComponentsBuilder uriComponentBuilder) {
Customer customer =
customerService.findCustomerById
(
loanMailDto.getCustomerId
(
));
if
(
customer ==
null
) {
String errorMessage =
"The selected Customer for sending email is not found in the database"
;
LOGGER.info
(
errorMessage);
return
new
ResponseEntity<
Boolean>(
false
, HttpStatus.NOT_FOUND);
}
else
if
(
customer !=
null
&&
StringUtils.isEmpty
(
customer.getEmail
(
))) {
String errorMessage =
"No existing email for the selected Customer for sending email to"
;
LOGGER.info
(
errorMessage);
return
new
ResponseEntity<
Boolean>(
false
, HttpStatus.NOT_FOUND);
}
SimpleMailMessage mail =
new
SimpleMailMessage
(
);
mail.setFrom
(
loanMailDto.MAIL_FROM);
mail.setTo
(
customer.getEmail
(
));
mail.setSentDate
(
new
Date
(
));
mail.setSubject
(
loanMailDto.getEmailSubject
(
));
mail.setText
(
loanMailDto.getEmailContent
(
));
try
{
javaMailSender.send
(
mail);
}
catch
(
MailException e) {
return
new
ResponseEntity<
Boolean>(
false
, HttpStatus.FORBIDDEN);
}
return
new
ResponseEntity<
Boolean>(
true
, HttpStatus.OK);
}
/**
* Transforme un entity Customer en un POJO CustomerDTO
*
*
@param
customer
*
@return
*/
private
CustomerDTO mapCustomerToCustomerDTO
(
Customer customer) {
ModelMapper mapper =
new
ModelMapper
(
);
CustomerDTO customerDTO =
mapper.map
(
customer, CustomerDTO.class
);
return
customerDTO;
}
/**
* Transforme un POJO CustomerDTO en en entity Customer
*
*
@param
customerDTO
*
@return
*/
private
Customer mapCustomerDTOToCustomer
(
CustomerDTO customerDTO) {
ModelMapper mapper =
new
ModelMapper
(
);
Customer customer =
mapper.map
(
customerDTO, Customer.class
);
return
customer;
}
}
Dans la page suivante, nous vous présenterons l'implémentation du front-end de l'application Library. Ce front-end autrement appelé Client est une application Angular dans laquelle nous développerons des services qui consommeront les web services exposés dans Library.