Cette activité est une application Web JEE basée sur Spring MVC, Thymeleaf et Spring Data JPA qui permet de gérer les patients. L'application doit permettre les fonctionnalités suivantes :
- Afficher les patients
- Faire la pagination
- Chercher les patients
- Supprimer un patient
- Faire des améliorations supplémentaires
- Créer une page template
- Faire la validation des formulaires
- Faire la sécurité avec Spring Security
Tout d'abord, nous ajoutons des dépendances pour utiliser les fonctionnalités de Spring en modifiant le fichier pom.xml
comme suit :
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.webjars.npm/bootstrap-icons -->
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>bootstrap-icons</artifactId>
<version>1.11.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.webjars/bootstrap -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- https:
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
<version>3.1.0.M1</version>
</dependency>
</dependencies>
Ensuite,on doit configurer les propriétés suivantes dans le fichier de configuration
server.port=8884
#spring.datasource.url=jdbc:h2:mem:patients-db
#spring.h2.console.enabled=true
spring.datasource.url=jdbc:mysql://localhost:3306/appHospital-db?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
#on peut aussi specifier la format du dat dans properties au lieu des annotations
#spring.mvc.format.date=yyyy-MM-dd
Puis On crée notre application , on cree les entities avec des annotations JPA pour faire le mapping objet relationnel
:
package ma.enset.apphospital.entities;
import jakarta.persistence.*;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@Entity
//Builder
@Data @AllArgsConstructor @NoArgsConstructor @Builder
public class Patient {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotEmpty
@Size(min=4,max =30)
private String nom;
@Temporal(TemporalType.DATE)
@DateTimeFormat(pattern = "yyy-MM-dd")
private Date dateNaissance;
private boolean malade;
@DecimalMax("100")
private int score;
}
@Entity
: Indique que la classe est persistante est correspond à une table dans la base de donée-
@Data
: est utilisée pour générer automatiquement les méthodes toString(), equals(), hashCode(), ainsi que les méthodes getter et setter pour tous les champs de classe @AllArgsConstructor
: génère un constructeur prenant en compte tous les champs de la classe.@NoArgsConstructor
: génère un constructeur sans argument pour la classe.@Id
: Associer un champ de la table à la propriété en tant que clé primaire.@GeneratedValue(strategy = GenerationType.IDENTITY) :
Demander la génération automatique de la clé primaire au besoin@DateTimeFormat(pattern = "yyy-MM-dd")
: Pour spécifier le format de la date, il est possible de l'inclure directement dans les annotations ou de le définir dans le fichierapplication.properties
avec la cléspring.mvc.format.date=yyyy-MM-dd
.@Temporal(TemporalType.DATE)
: indique que seules les informations de date (année, mois et jour) doivent être stockées dans la base de données, sans tenir compte de l'heure.
PatientRepository
qui étend JpaRepository
. Cela signifie que PatientRepository hérite de toutes les méthodes définies dans JpaRepository, telles que save()
, findById()
, findAll()
, deleteById()
, etc., qui fournissent des fonctionnalités de base pour interagir avec les données des patients en utilisant Spring Data JPA
.
Cette interface peut également contenir des méthodes personnalisées qui utilisent soit les conventions de nommage
de Spring Data JPA, commefindByNomContains()
, soit l'annotation @Query
, permettant de définir des requêtes spécifiques à la base de données pour des opérations plus complexes.
package ma.enset.apphospital.repository;
import ma.enset.apphospital.entities.Patient;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface PatientRepository extends JpaRepository<Patient,Long> {
Page<Patient> findByNomContains(String keyword, Pageable pageable);//en respectnt les convention
/*@Query("select p from Patient p where p.nom like :x")//on utilise les annotations pour definir notre query
Page<Patient>chercher (@param("x")String keyword,Pageable pageable );*/
}
@Configuration
:Indique à Spring que cette classe est une configuration de l'application@EnableWebSecurity
: Cette annotation active la sécurité Web dans l'application. Elle permet de configurer la sécurité des requêtes HTTP@EnableMethodSecurity(prePostEnabled = true)
: Cette annotation active la sécurité basée sur les annotations pour les méthodes de l'application. Elle permet d'utiliser les annotations de sécurité telles que@PreAuthorize
et@PostAuthorize
pour sécuriser les méthodes.@PreAuthorize ,@PostAuthorize
: sont utilisées dans les contrôleurs pour restreindre l'accès aux méthodes en fonction des rôles des utilisateurs , On a remplacer ce code la- Pour que Spring puisse comparer les mots de passe, nous utilisons
{noop}
car cela signifie "no operation", indiquant à Spring de ne pas effectuer de hachage sur les mots de passe.
//http.authorizeRequests().requestMatchers("/user/**").hasRole("USER");
//http.authorizeRequests().requestMatchers("/admin/**").hasRole("ADMIN");
avec l'annotation @EnableMethodSecurity(prePostEnabled = true)
package ma.enset.apphospital.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private PasswordEncoder passwordEncoder;
//Configure authentication mechanism
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager(){
return new InMemoryUserDetailsManager(
User.withUsername("user1").password(passwordEncoder.encode("1234")).roles("USER").build(),
User.withUsername("user2").password(passwordEncoder.encode("1234")).roles("USER").build(),
User.withUsername("admin").password(passwordEncoder.encode("1234")).roles("USER","ADMIN").build()
);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/login").permitAll();
http.rememberMe();
http.authorizeRequests().requestMatchers("/webjars/**").permitAll();
//http.authorizeRequests().requestMatchers("/user/**").hasRole("USER");
//http.authorizeRequests().requestMatchers("/admin/**").hasRole("ADMIN");
// POUR AFFICHER UN ERREUR A UN UTILISATEUR QUE N'A PAS LE DROIT
http.exceptionHandling().accessDeniedPage("/notAuthorized");
http.authorizeRequests()
.anyRequest().authenticated();
return http.build();
}
}
@GetMapping
et @PostMapping
pour mapper les URL aux méthodes, ainsi que des annotations de sécurité comme @PreAuthorize
pour restreindre l'accès aux méthodes en fonction des rôles des utilisateurs , tous ces methodes retourne une vue qui sera affichée dans le navigateur du client.
Pour envoyer un attribut à une vue dans Spring MVC, on doit utiliser la méthode addAttribute()
de l'objet Model. Cette méthode prend deux arguments : le nom de l'attribut et sa valeur.
package ma.enset.apphospital.web;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import ma.enset.apphospital.entities.Patient;
import ma.enset.apphospital.repository.PatientRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.ArrayList;
import java.util.List;
@Controller
@AllArgsConstructor
public class PatientController {
// l'injection se fait via Autowired ou bien via constrecteur
private PatientRepository patientRepository;
//la creation d'une methode qui retourne une vue de type String
@GetMapping("/user/index")
public String index(Model model, @RequestParam(name = "page",defaultValue = "0") int page,
@RequestParam(name = "size",defaultValue = "7") int size,
@RequestParam(name = "keyword",defaultValue = "") String kw){
Page<Patient> patients=patientRepository.findByNomContains(kw,PageRequest.of(page,size));//pour afficher une liste de patients
model.addAttribute("patients",patients.getContent());
model.addAttribute("pages",new int[patients.getTotalPages()]);
model.addAttribute("currentPage",page);
model.addAttribute("keyword",kw);
return "patients";
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/admin/delete")
public String delete(Long id, @RequestParam(value = "keyword" , defaultValue = "") String keyword, @RequestParam (value = "page", defaultValue ="0") int page){
patientRepository.deleteById(id);
return "redirect:/user/index?page="+page+"&keyword="+keyword;
}
@GetMapping("/")
public String home(){
return "redirect:/user/index";
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/admin/formPatients")
public String formPatient(Model model){
model.addAttribute("patient",new Patient());
return "formPatients";
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping(path = "/admin/save")
public String save(Model model, @Valid Patient patient, BindingResult bindingResult,@RequestParam(defaultValue = "0") int page,@RequestParam(defaultValue = "") String keyword){
if (bindingResult.hasErrors())return "formPatients";
patientRepository.save(patient);
return "redirect:/user/index?page="+page+"&keyword="+keyword;
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/admin/editPatient")
public String editPatient(Model model,Long id,String keyword,int page ){
Patient patient=patientRepository.findById(id).orElse(null);
if(patient ==null) throw new RuntimeException("Patient introuvable !!");
model.addAttribute("patient",patient);
model.addAttribute("page",page);
model.addAttribute("keyword",keyword);
return "editPatient";
}
}
Thymeleaf
avec la fonctionnalité layout:fragment
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" type="text/css" href="/webjars/bootstrap/5.3.3/css/bootstrap.min.css">
<script src="/webjars/bootstrap/5.3.3/js/bootstrap.bundle.js"></script>
</head>
<body>
<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
<div class="container-fluid">
<div class="collapse navbar-collapse" id="mynavbar">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link active" th:href="@{/user/index}">Home</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">Patients</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" th:if="${#authorization.expression('hasRole(''ADMIN'')')}" th:href="@{/admin/formPatients}">Nouveau Patient</a></li>
<li><a class="dropdown-item" th:href="@{/user/index}">Chercher Patient </a></li>
</ul>
</li>
</ul>
<ul class="navbar-nav ">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" th:text="${#authentication.name}"></a>
<ul class="dropdown-menu">
<li>
<form method="post" th:action="@{/logout}">
<button class="dropdown-item" type="submit" >Logout</button>
</form>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<section layout:fragment="content1">
</section>
</body>
</html>