jeudi 16 mars 2023

[Java 11] Gestion de plusieurs formats de date en Java avec DateTimeFormatterBuilder

Introduction

Lorsqu’on travaille avec des dates en Java, il est souvent nécessaire de gérer plusieurs formats de date différents. Cela peut être dû au fait que les utilisateurs peuvent entrer des dates dans différents formats, ou que les données sont fournies par des sources externes dans des formats variés. Sans une gestion appropriée de ces formats, il peut être difficile de manipuler les dates correctement et cela peut entraîner des erreurs dans l’application.

Problème

La gestion de plusieurs formats de date en Java est un défi courant lors de la manipulation des dates. Les développeurs sont souvent confrontés à des problèmes lorsqu’ils doivent convertir des chaînes de caractères en dates, ou vice versa, en raison de la diversité des formats de date.

Solution

Heureusement, Java offre des fonctionnalités avancées pour gérer les dates, notamment la classe LocalDate qui permet de représenter une date sans tenir compte du fuseau horaire et de l’heure. Java 8 a également introduit une API Date/Time qui offre des fonctionnalités supplémentaires pour la manipulation des dates et heures, y compris la gestion de plusieurs formats de date différents.

Les développeurs peuvent utiliser des outils tels que DateTimeFormatter et DateTimeFormatterBuilder pour analyser et formater les dates dans différents formats. Ces outils permettent également de définir des modèles de formatage personnalisés et de gérer les erreurs liées à la conversion de dates.

Voici un exemple concret d’utilisation du DateTimeFormatter et du DateTimeFormatterBuilder pour gérer plusieurs formats de date :

package org.example;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.Locale;

public class DateParser {

    private static final String[] SUPPORTED_FORMATS = {
            "dd/MM/yyyy",
            "dd-MM-yyyy",
            "yyyy/MM/dd",
            "yyyy-MM-dd"
    };

    public static LocalDate parseDate(String dateString) throws DateTimeParseException {
        DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
        Arrays.stream(SUPPORTED_FORMATS).forEach(f ->
                builder.appendOptional(DateTimeFormatter.ofPattern(f))
        );

        DateTimeFormatter formatter = builder.toFormatter(Locale.ENGLISH);
        return LocalDate.parse(dateString, formatter);
    }
}

Et voici comment formater une date dans un format spécifique :

public static String formatDate(LocalDate date, String pattern) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
    return date.format(formatter);
}

Enfin, voici comment combiner ces deux méthodes pour convertir une date d’un format à un autre :

public static String formatDate(String dateString, String pattern) throws DateTimeParseException {
    LocalDate date = null;
    for (String format : SUPPORTED_FORMATS) {
        try {
            date = LocalDate.parse(dateString, DateTimeFormatter.ofPattern(format));
            break;
        } catch (DateTimeParseException e) {
            // Le format ne correspond pas, on essaye avec le suivant
        }
    }
    if (date == null) {
        throw new DateTimeParseException("Aucun format supporté pour la date fournie", dateString, -1);
    }

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
    return date.format(formatter);
}

Discussion

Ces exemples montrent comment Java peut être utilisé pour gérer efficacement plusieurs formats de date. En comprenant ces concepts et en utilisant les outils appropriés, les développeurs peuvent éviter les erreurs courantes liées à la manipulation des dates en Java.

Pour s’assurer que notre code fonctionne comme prévu, il est important d’écrire des tests unitaires. Voici quelques tests que vous pouvez utiliser pour tester le code ci-dessus :

package org.example;

import static org.junit.jupiter.api.Assertions.*;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.Collection;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

public class DateParserTest {

    private DateParser dateParser;

    @BeforeEach
    void setUp() {
        dateParser = new DateParser();
    }

    @ParameterizedTest(name = "{index} - Parsing {0} should return {1}")
    @MethodSource("dateStrings")
    void testParseDate(String dateString, LocalDate expectedDate) {
        assertEquals(expectedDate, dateParser.parseDate(dateString));
    }

    @ParameterizedTest(name = "{index} - Parsing invalid date string {0} should throw DateTimeParseException")
    @MethodSource("invalidDateStrings")
    void testParseDateThrowsDateTimeParseException(String dateString) {
        assertThrows(DateTimeParseException.class, () -> dateParser.parseDate(dateString));
    }

    static Collection<Object[]> dateStrings() {
        return Arrays.asList(new Object[][] {
                { "10/03/2022", LocalDate.of(2022, 3, 10) },
                { "10-03-2022", LocalDate.of(2022, 3, 10) },
                { "2022/03/10", LocalDate.of(2022, 3, 10) },
                { "2022-03-10", LocalDate.of(2022, 3, 10) },
                { "31/12/2023", LocalDate.of(2023, 12, 31) },
                { "31-12-2023", LocalDate.of(2023, 12, 31) },
                { "2023/12/31", LocalDate.of(2023, 12, 31) },
                { "2023-12-31", LocalDate.of(2023, 12, 31) }
        });
    }

    static Collection<String> invalidDateStrings() {
        return Arrays.asList(new String[] {
                "10/13/2022", // invalid month
                "02/29/2021", // not a leap year
                "2022/31/10", // invalid day
                "10-03-22", // 2-digit year
                "invalid", // not a date string
                "2022/03/10 10:00:00" // not a date string
        });
    }

}

Ces tests vérifient que la méthode parseDate peut analyser correctement une date dans différents formats et qu’elle lance une exception DateTimeParseException lorsqu’elle reçoit une chaîne de caractères qui ne correspond à aucun format de date supporté. Ces tests aident à s’assurer que notre code est robuste et qu’il se comporte comme prévu dans différentes situations. Ils sont un élément essentiel pour garantir la qualité du code et éviter les erreurs inattendues. En utilisant ces tests comme guide, les développeurs peuvent être plus confiants dans la robustesse de leur code et dans sa capacité à gérer correctement plusieurs formats de date.

mardi 7 mars 2023

[Spring Boot] Comment éviter de retourner la stack trace au client dans une application Spring?

 Lorsque vous développez une application Spring Boot, il est important de gérer correctement les erreurs et les exceptions pour éviter de renvoyer des informations sensibles à un client.

Voici quelques conseils et exemples de bonnes pratiques pour éviter de retourner la stack trace au client : A partir de java 8 et avec l'introduction des optionnelles je préfére les utiliser pour gérer les exceptions proporements Voiçi un exemple de code

java
@RequestMapping("/api/users/{id}")
public User getUserById(@PathVariable Long id) {
 log.info("Fetching user with id: {}", id);
User user = userRepository.findById(id) 
 .orElseThrow(() -> {
     log.error("User with id {} not found", id);
    return new UserNotFoundException("User not found with id: " + id);
 });
return user;
}

Sinon je vous laisse lire les autres manières de faire :

  1. Ne renvoyez jamais la stack trace directement au client

Mauvaise pratique :

java
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    return userService.getUserById(id);
}

Dans cet exemple, si une exception se produit lors de la recherche d'un utilisateur, la stack trace sera retournée au client.

Bonne pratique :

Dans l'exemple de mauvaise pratique, le renvoi de l'objet User directement peut renvoyer la stack trace au client en cas d'erreur, ce qui est peu convivial. En revanche, dans la bonne pratique, en renvoyant des réponses HTTP appropriées, vous offrez au client une réponse claire et structurée, améliorant ainsi l'expérience utilisateur.

java
@GetMapping("/users/{id}")
public
ResponseEntity<User> getUser(@PathVariable Long id) {
try {
    User user = userService.getUserById(id);
    return
ResponseEntity.ok(user);
 }
catch (UserNotFoundException e) {
    return
ResponseEntity.notFound().build();
 }
}

Dans cet exemple, nous retournons une réponse HTTP personnalisée en utilisant ResponseEntity. Si l'utilisateur n'est pas trouvé, nous retournons un code de statut 404 Not Found, plutôt que de renvoyer la stack trace au client.

  1. Loguez l'erreur pour faciliter le debugging

Mauvaise pratique :

java
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    return userService.getUserById(id);
}

Dans cet exemple, si une exception se produit lors de la recherche d'un utilisateur, aucune information sur l'erreur n'est enregistrée.

Bonne pratique :

La journalisation des erreurs est essentielle pour le débogage.

Dans l'exemple de mauvaise pratique, aucune information sur l'erreur n'est enregistrée, ce qui complique le processus de débogage. En revanche, la bonne pratique enregistre les erreurs dans les fichiers journaux, ce qui facilite l'identification et la résolution des problèmes.

java
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
try {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
 }
catch (UserNotFoundException e) {
 
logger.error("L'utilisateur avec l'ID {} n'a pas été trouvé", id);
return ResponseEntity.notFound().build();
 }
catch (Exception e) {
 logger.error("Une erreur s'est produite lors de la recherche de l'utilisateur avec l'ID {}", id, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
 }
}

Dans cet exemple, nous utilisons un logger pour enregistrer

3. Évitez d'utiliser les exceptions globales dans votre code. Cela peut renvoyer une stack trace complète au client, ce qui peut être exploité pour des attaques. Au lieu de cela, vous pouvez utiliser les exceptions personnalisées pour gérer les erreurs et renvoyer des messages d'erreur clairs aux clients.

Mauvaise pratique :

java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
public ResponseEntity<Object> handleException(Exception exception) {
return new ResponseEntity<>("Erreur interne du serveur", HttpStatus.INTERNAL_SERVER_ERROR);
 }
}

Bonne pratique :

Les exceptions personnalisées permettent de renvoyer des messages d'erreur clairs et explicites aux clients sans divulguer la stack trace complète. Dans l'exemple de bonne pratique, en utilisant @ExceptionHandler pour gérer l'exception personnalisée MyCustomException, vous assurez que le client reçoit un message d'erreur compréhensible, améliorant ainsi la convivialité de l'API.

java
@ExceptionHandler(MyCustomException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public String handleMyException(MyCustomException ex) {
return ex.getMessage();
}
  1. Utilisez les fichiers de propriétés de configuration pour masquer les messages d'erreur dans la sortie JSON. Vous pouvez utiliser le fichier application.properties pour activer la configuration suivante :

Mauvaise pratique :


spring.jackson.serialization.fail-on-empty-beans=false
Pourquoi c'est une mauvaise pratique ?
Si vous avez des objets vides qui sont sérialisés en JSON et que vous les renvoyez au client, assurez-vous que cela ne compromet pas la sécurité. Parfois, un objet vide peut contenir des informations sensibles, et le fait de renvoyer un JSON vide peut exposer ces informations.

Bonne pratique :

La configuration des propriétés Jackson pour masquer les erreurs dans la sortie JSON est essentielle pour la sécurité. En configurant ces propriétés correctement, vous empêchez les informations sensibles, y compris la stack trace, d'être renvoyées au client. Cela réduit le risque d'exposition d'informations confidentielles.


spring.jackson.default-property-inclusion=NON_NULL spring.jackson.serialization.fail-on-empty-beans=true spring.jackson.deserialization.fail-on-unknown-properties=true

en résumé voici un tableau comparatif des annotations et classes que vous pouvez utiliser pour gérer les exceptions dans votre application :

Annotation/ClasseAvantagesInconvénients
@ExceptionHandlerPermet de définir une méthode de gestion des exceptions pour un contrôleur spécifique.La méthode doit être appelée explicitement à partir du contrôleur pour être exécutée.
@ResponseStatusPermet de définir un code de réponse HTTP personnalisé pour une exception.Ne fournit pas de mécanisme de traitement de l'exception en soi.
@ControllerAdvicePermet de définir une classe qui fournit des méthodes de gestion des exceptions pour tous les contrôleurs de l'application.Peut être moins efficace que la gestion des exceptions au niveau du contrôleur en termes de performances, car toutes les méthodes de gestion des exceptions de la classe seront appelées pour chaque exception levée dans l'application.
ResponseStatusException.classPermet de créer une exception personnalisée qui renvoie un code de réponse HTTP spécifique au client.Ne fournit pas de mécanisme de traitement de l'exception en soi.

La configuration de ces propriétés dans votre fichier de configuration de l'application Spring Boot peut influencer la façon dont les erreurs sont gérées et renvoyées au client.

  • server.error.include-binding-errors: cette propriété contrôle l'inclusion des erreurs de liaison dans la réponse d'erreur envoyée au client. Si elle est définie sur "never", les erreurs de liaison ne seront pas incluses dans la réponse d'erreur.

  • server.error.include-exception: cette propriété contrôle l'inclusion de l'exception dans la réponse d'erreur envoyée au client. Si elle est définie sur "false", l'exception ne sera pas incluse dans la réponse d'erreur.

  • server.error.include-message: cette propriété contrôle l'inclusion du message d'erreur dans la réponse d'erreur envoyée au client. Si elle est définie sur "never", le message d'erreur ne sera pas inclus dans la réponse d'erreur.

  • server.error.include-stacktrace: cette propriété contrôle l'inclusion de la stack trace dans la réponse d'erreur envoyée au client. Si elle est définie sur "never", la stack trace ne sera pas incluse dans la réponse d'erreur.

Si ces propriétés sont correctement configurées, les erreurs ne devraient pas être renvoyées avec la stack trace au client, ce qui peut améliorer la sécurité