Tutoriel sur le développement full stack d'une application Web avec Angular 7 et Spring Boot 2


précédentsommairesuivant

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.

Image non disponible

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.

Image non disponible

La configuration de ces ressources est effectuée dans le fichier application.properties qui se présente ainsi :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
############## 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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
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 = 3912193101593832821L;

    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
}
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
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 = 144293603488149743L;

    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) :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
2.
3.
4.
@Entity
@NamedQuery(name = "T.getAll", query = "select t from T t")                    
public class T {
}
 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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);
}
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
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 :

 
Sélectionnez
1.
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);
}
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
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 :

 
Sélectionnez
1.
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);
}
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
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).

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.

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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
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;
    }
}
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
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.

 
Sélectionnez
<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é :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
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.

Image non disponible

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.

Image non disponible

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
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.


précédentsommairesuivant

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

  

Copyright © 2019 Georges KEMAYO. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.