Les contrôles d’intégrités avec Scub Foundation – Partie 2

Gestion dans la partie Noyau

Ce second article va concerner les deux projets core-interface et core-implementations. Nous allons partir d’un exemple simple qui est la création d’utilisateurs et voir comment mettre en place pas à pas, les couches modèles, DAO et service de l’application. Nous pourrons également voir comment tester le tout correctement.

Un utilisateur est représenté par les informations suivantes:

  • id: l’identifiant de l’utilisateur
  • userName: le pseudo de l’utilisateur
  • email: l’email de l’utilisateur
  • password: le mot de passe de l’utilisateur

Les règles métiers à mettre en place sont:

  • Les propriétés userName, email et password ne peuvent être “null” ou vides.
  • Le format de l’email doit être le bon.
  • Le mot de passe doit être validé pour s’assurer que l’administrateur n’a pas fait d’erreur dans la saisie.
  • Le mot de passe doit faire au moins 5 caractères.

Sommaire

Introduction
Contexte
Que sont les contrôles d’intégrités?
Un rapport complet
Revoir la façon d’écrire les règles métiers
Oval

Gestion dans la partie Noyau
Poser les contraintes dans les DTO
Utiliser les validateurs
La gestion des messages d’erreurs
Gérer les règles métiers qui ne sont pas dans les DTO
Les autres méthodes utiles des validateurs
Tester les contrôles d’intégrités avec JUnit

Gestion dans un client GWT
Utilisation du Callback prévu à cet effet
Utiliser le conteneur d’erreurs

Des contrôles supplémentaires sont nécessaires dans le service pour s’assurer qu’il réagit correctement. Une exception doit être levée si le DTO passé au service est null ou si l’objet utilisateur n’est pas trouvé en base lors d’une modification.

Création du DTO et de l’interface de service

Voici a quoi ressemblerait la classe UserDto en utilisant une version antérieure de Scub foundation. Nous allons voir ci dessous comment modifier les DTO pour utiliser Oval.

public class UserDto implements Serializable {
 
   private static final long serialVersionUID = 2089359963675880296L;
 
   private Long id;
 
   private String userName;
 
   private String email;
 
   private String password;
 
   private String confirmPassword;
 
....

L’interface de service quand à elle ne change pas:

public interface UserService {
   /**
    * Create or update user.
    * @param user the user to create or update.
    * @return the reated or updated user.
    */
   UserDto createOrUpdateUser(UserDto user);
….

Poser les contraintes dans les DTO

Toutes les règles métiers à mettre en place pourraient être implémentées grâce à des annotations fournies par Oval, mais nous ne le feront pas. Il y a deux règles principales à respecter lorsque nous allons poser les annotations dans les DTO:

  1. On ne peut poser dans les DTO, que les annotations correspondant à des règles métiers qui sont valables dans tous les cas. De ce fait nous ne pouvons pas poser d’annotation pour spécifier que l’id de l’utilisateur est requis puisque dans le cas d’une création, celui-ci est généré. Si nous avions deux services distinct pour gérer la création et la modification et un DTO pour chaque, cette contrainte aurait put être spécifiée dans le DTO correspondant au service de modification d’utilisateur.

  2. On ne peut utiliser des annotations pour les règles métiers qui concernent plusieurs propriétés du DTO. La règle métier qui spécifie que le mot de passe et sa confirmation doivent être égaux ne peut donc pas être mise en place grâce aux annotations. De la même façon, une règle qui ne s’active qu’en fonction de la valeur d’un autre champ ne peut être gérer grâce aux annotations. Exemple: Le type de numéro de téléphone (mobile, bureau…) est requis si le téléphone est saisi. En plus d’enfreindre la seconde règle, cet exemple va également à l’encontre de la première.

Nous nous limitons volontairement dans l’utilisation d’Oval dans nos DTO car ces derniers ne sont pas destinés à contenir toutes les règles métiers. Nous appelons les règles à mettre dans les DTO règles de contrôle. Si une règle respecte les deux conditions ci-dessus, alors c’est une règle de contrôle, sinon c’est une règle métier trop spécifique et elle doit dans ce cas être mise en place dans l’implémentation du service.

Voici a quoi ressemblera notre DTO en y ajoutant les annotations d’Oval:

public class UserDto extends AbstractDto {
 
   private static final long serialVersionUID = 2089359963675880296L;
 
   private Long id;
 
   @NotNull
   @NotBlank
   private String userName;
 
   @NotNull
   @Email
   private String email;
 
   @NotNull
   @MinLength(value = 5)
   private String password;
 
   @NotNull
   @MinLength(value = 5)
   private String confirmPassword;
 
  ....

Pour plus d’informations sur les annotations disponibles, je vous invite à étudier la documentation d’Oval à ce sujet.

Vous pourrez remarquer que le DTO n’implemente plus l’interface Serializable mais qu’il hérite par contre d’AbstractDto. Faire hériter votre DTO d’AbstractDto va l’autoriser à être validé par les validateurs fournis par Scub Foundation. Sans cet héritage, vous ne pourriez les utiliser.

Utiliser les validateurs

Il y a actuellement deux validateurs mis à disposition dans le socle à savoir BusinessValidator et IntegrityValidator et un troisième sera disponible dans la prochaine version du framework: TechnicalValidator. Le premier lève une BusinessException à la première erreur trouvée dans le DTO. Le fonctionnement reste donc le même que pour les implémentations de services utilisant les précédentes versions du socle: si une erreur métier est détectée, on lève une BusinessException. Le TechnicalValidator, qui verra le jour dans la prochaine version, lève des TechnicalException à la place des BusinessException. Pour finir, le validateur sur lequel nous allons nous attarder est l’IntegrityValidator qui lui lève une IntegrityControleException contenant un rapport détaillé de toutes les erreurs contenues dans le DTO fournis.

Pour se servir d’un validateur dans vos service, il suffit de créer un attribut de ce type nommé comme ci dessous et de créer le mutateur correspondant (setter) afin qu’ils puissent être injectés par Spring:

private TechnicalValidator technicalValidator;
private BusinessValidator businessValidator;
private IntegrityValidator integrityValidator;

Pour finir, afin de valider toutes les règles métiers que vous avez spécifiées grâce aux annotations dans le DTO, il suffit de demander au validateur de valider votre DTO:

@Override
public UserDto createOrUpdateUser(UserDto userDto) {
    integrityValidator.validateDto(userDto);
}

Il n’y a rien de plus à faire. Avec les quelques annotations et le validateur vous venez de vous éviter tout le code ci-dessous à écrire. Ce code correspond à un service développé avec une version antérieure à la 4.0 de Scub Foundation:

   @Override
   public UserDto createOrUpdateUser(UserDto userDto) {
       if(userDto == null) {
           throw new TechnicalException(messageSourceUtil.get(ErrorMessagesKeysUtils.ABSTRACT_DTO_NULL));
       }
       if(StringUtils.isBlank(userDto.getUserName())) {
           throw new BusinessException(messageSourceUtil.get(ErrorMessagesKeysUtils.USER_USER_NAME_REQUIRED));
       }
       if(StringUtils.isBlank(userDto.getEmail())) {
           throw new BusinessException(messageSourceUtil.get(ErrorMessagesKeysUtils.USER_EMAIL_REQUIRED));
       }
       if(!userDto.getEmail().matches("[_A-Za-z0-9-\\.@]{0,100}")) {
           throw new BusinessException(messageSourceUtil.get(ErrorMessagesKeysUtils.USER_EMAIL_FORMAT));
       }
       if(StringUtils.isBlank(userDto.getPassword())) {
           throw new BusinessException(messageSourceUtil.get(ErrorMessagesKeysUtils.USER_PASSWORD_REQUIRED));
       }
       if(userDto.getPassword().length() < 5) {
           throw new BusinessException(messageSourceUtil.get(ErrorMessagesKeysUtils.USER_PASSWORD_MIN_LENGTH));
       }
       if(userDto.getConfirmPassword().length() < 5) {
           throw new BusinessException(messageSourceUtil.get(ErrorMessagesKeysUtils.USER_CONFIRM_PASSWORD_REQUIRED));
       }
       if(userDto.getConfirmPassword().length() < 5) {
           throw new BusinessException(messageSourceUtil.get(ErrorMessagesKeysUtils.USER_CONFIRM_PASSWORD_MIN_LENGTH));
       }
}

Plutôt interressant non? Mais vous allez me dire qu’il y a des choses qui manque:

Nous n’avons pas spécifié la règle qui doit tester si le DTO n’est pas null. C’est le validateur qui se charge de cette étape. Afin qu’il puisse valider un DTO, il s’assure dans un premier temps de la non nullité de l’objet qui lui est fourni. Si null lui est donné, c’est une erreur de développement et il lève donc une TechnicalException avec le message “Validated object must not be null.”.

Avec d’anciennes versions de Scub Foundation, nous pouvions spécifier les messages d’erreurs dans les exceptions. C’est toujours le cas, nous allons voir comment faire car c’est un petit peu différent par rapport à précédement.

La gestion des messages d’erreurs

Oval intègre son propre mécanisme de messages d’erreurs et gère très bien les traductions. Les messages d’erreurs pourraient presque rester en l’état si il n’y avait pas le package et le nom de la classe à l’intérieur:

  • org.scub.foundation.example.integrity.controle.dto.UserDto.userName ne doit pas être null
  • org.scub.foundation.example.integrity.controle.dto.UserDto.confirmPassword ne doit pas avoir moins de 5 charactères
  • org.scub.foundation.example.integrity.controle.dto.UserDto.email n’est pas une adresse courriel valide
  • org.scub.foundation.example.integrity.controle.dto.UserDto.userName ne doit pas être blanc….

Ce sont des messages générés automatiquement par oval. Si les validateurs ne trouvent pas de messages appropriés, ils utiliseront ceux d’Oval. Afin de rechercher si il existe un message pour la contrainte dans le DTO qui n’est pas respectée,  les validateurs vont construire une clé en utilisant les informations à leur disposition et demander à l’objet “MessageSourceUtil” si il possède un message pour celle-ci. La clé est construite avec le nom du DTO suivit du nom de la propriété en erreur (séparé par un point) et terminée par le nom de la contrainte (toujours séparé par un point. Pour la contrainte de non nullité sur le pseudo, nous aurons donc comme clé UserDto.userName.NotNull. Si vous voulez définir un message spécifique pour cette contrainte, il vous suffit donc de rajouter une propriété dans le fichier properties de message que vous souhaitez:

messageError_fr_FR.properties:

UserDto.userName.NotNull=Le pseudo est requis.

Si la contrainte porte sur un sous DTO, il vous faut rajouter toute la hierarchie de propriétés pour descendre jusqu’à celle en erreur. Prenons l’exemple suivant: l’utilisateur doit appartenir à un groupe qui, dans le DTO, est représenté par un objet IdLabelDto nommé groupe. L’id du groupe est obligatoire afin de pouvoir retrouver l’enregistrement dans la base de données. La clé pour le message d’erreur de cette contrainte sera donc:

messageError_fr_FR.properties:

UserDto.groupe.id.NotNull=L’identifiant du groupe est requis.

Gérer les règles métiers qui ne sont pas dans les DTO

Il nous reste une règle métier à mettre en place puisque celle-ci n’est pas gérée grâce à une annotation dans le DTO, c’est celle qui valide que les deux mots de passe sont bien identiques. Il va donc falloir rajouter un message d’erreur au rapport en cas de non conformité de cette règle. Pour faire ceci, nous allons avoir besoin:

  1. du rapport afin d’ajouter l’erreur,
  2. de la chaine représentant l’attribut auquel sera rattachée l’erreur.

Afin de récupérer le rapport, au lieu de demander au validateur de valider le DTO, nous allons lui demander de nous remplir le rapport et de nous le fournir et de ne pas lever d’exception si celui ci est en erreur. Nous allons juste faire appel à la méthode getReport(…) plutôt qu’à la méthode validate(…):

@Override
public UserDto createOrUpdateUser(UserDto userDto) {
   final ReportDto report = integrityValidator.getReport(userDto);
}

Pour la chaine représentant l’attribut, nous allons créer une classe utilitaire fournissant toutes les chaines correspondant aux attributs de l’objet UserDto sur lesquels sont posées des annotations. Nous le faisons pour tous les attributs car nous allons en avoir besoin afin de tester le rapport dans les tests unitaires.

public final class UserDtoAttributesKeyUtils {
   /** Private constructor. */
   private UserDtoAttributesKeyUtils() {
   }
   /** The class name. */
   public static final String CLASS_NAME = "UserDto";
   /** The attribute userName. */
   public static final String USER_NAME = CLASS_NAME + ".userName";
   /** The attribute email. */
   public static final String EMAIL = CLASS_NAME + ".email";
   /** The attribute password. */
   public static final String PASSWORD = CLASS_NAME + ".password";
   /** The attribute password. */
   public static final String CONFIRM_PASSWORD = CLASS_NAME + ".confirmPassword";
}

Maintenant que nous avons toutes les informations nécessaires, nous allons pouvoir mettre en place la dernière règle métier:

Tester si les deux mots de passe sont égaux

  • Si ils sont différents:
    • Tester si un rapport d’attribut (sous rapport) existe pour l’attribut désiré
      • Si il n’existe pas:
        • Le créer
      • Finalement ajouter le message d’erreur au rapport.
  • Sinon rien faire.

 if (!StringUtils.equals(userDto.getPassword(), userDto.getConfirmPassword())) {
           if (!report.hasAttributeReport(UserDtoAttributesKeyUtils.CONFIRM_PASSWORD)) {
               report.addAttributeReport(UserDtoAttributesKeyUtils.CONFIRM_PASSWORD);
           }
           report.getAttributeReport(UserDtoAttributesKeyUtils.CONFIRM_PASSWORD).addError(messageSourceUtil.get(ErrorMessagesKeysUtils.USER_PASSWORDS_NOT_EQUALS));
       }

Pour finir, nous devons lever l’exception si jamais le rapport contient au moins une erreur puisque le validateur ne le fait plus. Mais, une fois encore, le validateur peut le faire pour vous:

integrityValidator.testReport(report);

Les autres méthodes utiles des validateurs

Les validateurs proposent tous des méthodes utilitaires pour faciliter les tests simples qui doivent lever des exceptions en cas d’echec. Ces méthodes sont similaires à celle proposées par le socle pour faire vos assertions dans les tests unitaires à savoir:

  • assertEquals(…)
  • assertNotSame(…)
  • assertNull(…)
  • assertNotNull(…)
  • assertTrue(…)
  • assertFalse(…)
  • fail(…)

Chacune de ces méthodes lève une exception en fonction du validateur que vous utilisez en cas d’echec du test. Pour les IntegrityValidator et les BusinessValidator ce sont des BusinessException et pour le TechnicalValidator  ce sont des TechnicalException qui seront levées. Voici comment l’utiliser:

if (userDto.getId() != null) { // if it's a modification
      user = userDao.getUserById(userDto.getId());
      integrityValidator.assertNotNull(ErrorMessagesKeysUtils.USER_NOT_FOUND, user);
}

Dans l’exemple ci-dessus, nous devrions lever une TechnicalException étant donné que l’objet n’a pas été trouvé en base car c’est une situation qui ne devrait pas arriver si l’application est bien développée. Le technicalValidator sera donc utile dans un cas comme celui-ci.

Tester les contrôles d’intégrités avec JUnit

Nous avons vu comment gérer nos règles métiers dans nos services, il va maintenant falloir vérifier qu’elles ont bien été écrites grâce aux tests unitaires. J’en profite pour vous rappeler qu’ils doivent être écrit avant l’implémentation des services. J’ai estimé que l’ordre dans lequel je présente les différrentes parties dans cet article était plus simple pour expliquer le fonctionnement.

Pour tester si une exception technique ou métier est bien levée quand elle le doit, on utilise un code similaire à celui-ci:

// test dto null
try {
    userService.createOrUpdateUser(null);
    fail(ERROR_MUST_FAIL);
} catch (TechnicalException t) {
     assertEquals(ERROR_WRONG_MESSAGE, getMessage(ErrorMessagesKeysUtils.ABSTRACT_DTO_NULL), t.getMessage());
}

Ce fonctionnement reste le même pour tester les TechnicalException et les BusinessException. Par contre, seul le principe est identique pour tester les contrôles d’intégrités en échec. Scub foundation met à disposition des méthodes utilitaires afin de pouvoir tester plus facilement le contenu des rapports:

  • assertErrorCountEquals(ReportDto report, int errorCount): vérifie que le nombre d’erreurs dans le rapport est bien égal à errorCount.
  • assertHasMessage(ReportDto report, String attribute, String message): vérifie que l’attribut à bien l’erreur spécifiée.

Avec ces deux méthodes, nous pouvons donc tester l’intégralité du rapport. Premièrement, on teste si le rapport comporte le bon nombre d’erreurs. Ensuite, on teste si chaque erreur est sur le bon champ car on peut très bien avoir le bon nombre d’erreurs, mais pas les bons messages ou encore avoir les bonnes erreurs mais pas sur les bons champs.

Voici un exemple d’utilisation:

// first integrity control: test all not null constraints
try {
    userService.createOrUpdateUser(user);
    fail(ERROR_MUST_FAIL);
} catch (IntegrityControlException e) {
     // In first, test the error count.
     assertErrorCountEquals(e.getReport(), 4);
     // After, test if the rapport has the given message for the given attribute.
     assertHasMessage(e.getReport(), UserDtoAttributesKeyUtils.USER_NAME, getMessage(ErrorMessagesKeysUtils.USER_USER_NAME_REQUIRED));
     assertHasMessage(e.getReport(), UserDtoAttributesKeyUtils.EMAIL, getMessage(ErrorMessagesKeysUtils.USER_EMAIL_REQUIRED));
     assertHasMessage(e.getReport(), UserDtoAttributesKeyUtils.PASSWORD, getMessage(ErrorMessagesKeysUtils.USER_PASSWORD_REQUIRED));
     assertHasMessage(e.getReport(), UserDtoAttributesKeyUtils.CONFIRM_PASSWORD, getMessage(ErrorMessagesKeysUtils.USER_PASSWORD_REQUIRED));
}

Vous pouvez donc, en un seule structure try/catch, valider quatre règles métier alors qu’il vous aurez fallu quatre structures try/catch si vous aviez levé une BusinessException par règle métier. Il faudra néammoins répéter cette structure  autant de fois que vous aurez de règles qui s’excluent mutuellement sur la même propriété. On ne peut pas tester en même temps le fait qu’une chaine de caractères ne soit pas null et qu’elle ne soit pas vide.

***

Dans le dernier article de cette trilogie sur les contrôles d’intégrités, nous aborderons comment gérer et afficher les erreurs dans un client GWT. Nous verrons avec quelle facilité les messages d’erreurs vont s’attacher aux champs du formulaire grâce à un callback spécifique et à un composant créé pour l’affichage des messages d’erreur dans les formulaires.

Vous pouvez télécharger les sources des projets core-implementation et core-interface des exemples utilisés dans cet article à l’adresse suivante.

<< Accéder à la première partie de la trilogie