1. Présentation et objectifs

Le but est de continuer le développement de notre architecture "à la microservice".

Nous allons aujourd’hui implémenter des fonctionnalités de traduction dans notre micro-service pokemon-type!

En effet, nos données de Pokemon sont aujourd’hui en anglais uniquement, ce qui peut être décourageant pour nos futurs joueurs français !

trainer gui

Nous allons développer :

  1. La gestion des traductions dans notre api pokemon-type

  2. L’affichage du Pokedex traduit !

Nous allons également réécrire nos appels utilisant le RestTemplate pour utiliser les HTTP Interfaces.

Nous ne repartons pas uniquement de zéro pour ce TP. Nous nous appuyons sur les TP précédents

2. pokemon-type-api

2.1. Les données de traduction

Pour faciliter le travail, j’ai créé deux fichier JSON contenant les traductions des noms de Pokemon en français et anglais.

Ces fichiers sont disponible ici: translations-fr.json translations-en.json

Déposez ces fichiers dans votre répertoire src/main/resources.

2.2. Le BO

Créez un record Translation

fr.univ_lille.alom.pokemon_type_api.Translation
1
2
3
package fr.univ_lille.alom.pokemon_type_api;

public record Translation(int id, String name) {}

2.3. Le repository

2.3.1. L’interface

L’interface de ce repository de traduction est simple :

fr.univ_lille.alom.pokemon_type_api.TranslationRepository
1
2
3
4
5
6
7
package fr.univ_lille.alom.pokemon_type_api;

import java.util.Locale;

public interface TranslationRepository {
    String getPokemonName(int id, Locale locale);
}

2.3.2. Les tests unitaires

Implémentez les tests unitaires suivants :

fr.univ_lille.alom.pokemon_type_api.TranslationRepositoryImplTest.java
 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
package fr.univ_lille.alom.pokemon_type_api;

import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.Locale;

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

class TranslationRepositoryImplTest {

    private TranslationRepositoryImpl repository = new TranslationRepositoryImpl();

    @Test
    void getPokemonName_with1_inFrench_shouldReturnBulbizarre(){
        assertEquals("Bulbizarre", repository.getPokemonName(1, Locale.FRENCH));
        assertEquals("Bulbizarre", repository.getPokemonName(1, Locale.FRANCE));
    }

    @Test
    void getPokemonName_with1_inEnglish_shouldReturnBulbizarre(){
        assertEquals("Bulbasaur", repository.getPokemonName(1, Locale.ENGLISH));
        assertEquals("Bulbasaur", repository.getPokemonName(1, Locale.UK));
        assertEquals("Bulbasaur", repository.getPokemonName(1, Locale.US));
    }

    @Test
    void applicationContext_shouldLoadPokemonRepository(){
        var context = new AnnotationConfigApplicationContext("fr.univ_lille.alom.pokemon_type_api");
        var repoByName = context.getBean("translationRepositoryImpl");
        var repoByClass = context.getBean(TranslationRepository.class);

        assertEquals(repoByName, repoByClass);
        assertNotNull(repoByName);
        assertNotNull(repoByClass);
    }

}

2.3.3. L’implémentation

Développez l’implémentation du TranslationRepository.

fr.univ_lille.alom.pokemon_type_api.TranslationRepositoryImpl.java
 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
package fr.univ_lille.alom.pokemon_type_api;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Repository;

import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Map;

@Repository
public class TranslationRepositoryImpl implements TranslationRepository {

    record Key(Locale locale, int pokemonId){} (3)

    private Map<Key, Translation> translations;

    private ObjectMapper objectMapper;

    public TranslationRepositoryImpl() {
        try {
            // TODO (2)
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getPokemonName(int id, Locale locale) {
        // TODO (1)
    }
}
1 Implémentez la récupération du nom d’un Pokemon !
2 Alimentez la map des traductions en chargeant les fichiers, et en récupérant leur contenu
3 On utilise un record local à notre classe comme clé de Map !

La récupération d’un fichier dans le classpath peut se fair en Spring avec la classe ClassPathResource. Inspirez-vous du PokemonTypeRepository pour le reste.

2.4. Le service

Maintenant que nous avons un repository capable de gérer les traductions, nous devons les utiliser. Un bon endroit pour cela est la couche service.

Spring utilise la classe AcceptHeaderLocaleResolver dans la DispatcherServlet pour venir alimenter un objet LocaleContextHolder. Nous pouvons donc utiliser cet objet pour récupérer la langue demandée par la requête courante !

Ajoutez les tests unitaires suivant au PokemonTypeServiceImplTest:

PokemonTypeServiceImplTest.java
 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
@Test
void pokemonNames_shouldBeTranslated_usingLocaleResolver(){
    var pokemonTypeService = new PokemonTypeServiceImpl();

    var pokemonTypeRepository = mock(PokemonTypeRepository.class);
    pokemonTypeService.setPokemonTypeRepository(pokemonTypeRepository);
    when(pokemonTypeRepository.findPokemonTypeById(25)).thenReturn(new PokemonType());

    var translationRepository = mock(TranslationRepository.class);
    pokemonTypeService.setTranslationRepository(translationRepository);
    when(translationRepository.getPokemonName(25, Locale.FRENCH)).thenReturn("Pikachu-FRENCH");

    LocaleContextHolder.setLocale(Locale.FRENCH);

    var pikachu = pokemonTypeService.getPokemonType(25);

    assertEquals("Pikachu-FRENCH", pikachu.name());
    verify(translationRepository).getPokemonName(25, Locale.FRENCH);
}

@Test
void allPokemonNames_shouldBeTranslated_usingLocaleResolver(){
    var pokemonTypeService = new PokemonTypeServiceImpl();

    var pokemonTypeRepository = mock(PokemonTypeRepository.class);
    pokemonTypeService.setPokemonTypeRepository(pokemonTypeRepository);

    var pikachu = new PokemonType(25, null, null, null);
    var raichu = new PokemonType(26, null, null, null);
    when(pokemonTypeRepository.findAllPokemonType()).thenReturn(List.of(pikachu, raichu));

    // on simule le repository de traduction
    var translationRepository = mock(TranslationRepository.class);
    pokemonTypeService.setTranslationRepository(translationRepository);
    when(translationRepository.getPokemonName(25, Locale.FRENCH)).thenReturn("Pikachu-FRENCH");
    when(translationRepository.getPokemonName(26, Locale.FRENCH)).thenReturn("Raichu-FRENCH");

    LocaleContextHolder.setLocale(Locale.FRENCH);

    var pokemonTypes = pokemonTypeService.getAllPokemonTypes();

    assertEquals("Pikachu-FRENCH", pokemonTypes.get(0).name());
    assertEquals("Raichu-FRENCH", pokemonTypes.get(1).name());
    verify(translationRepository).getPokemonName(25, Locale.FRENCH);
    verify(translationRepository).getPokemonName(26, Locale.FRENCH);
}

Pour faire passer les tests unitaires, remplacez le nom du type de pokemon, après l’avoir récupéré du repository, par sa traduction.

Les records sont immutables, donc vous allez devoir trouver un moyen pour copier les données.

2.5. Le test d’intégration

Modifiez le PokemonTypeControllerIntegrationTest pour ajouter un test d’intégration :

PokemonTypeControllerIntegrationTest.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
void getPokemon_withId1_shouldReturnBulbasaur() {
    var bulbasaur = this.restTemplate.getForObject("http://localhost:" + port + "/pokemon-types/1", PokemonType.class);
    assertNotNull(bulbasaur);
    assertEquals(1, bulbasaur.id());
    assertEquals("Bulbasaur", bulbasaur.name()); (1)
}

@Test
void getPokemon_withId1AndFrenchAcceptLanguage_shouldReturnBulbizarre() {
    var headers = new HttpHeaders();
    headers.setAcceptLanguageAsLocales(List.of(Locale.FRENCH)); (2)

    var httpRequest = new HttpEntity<>(headers);

    var bulbizarreResponseEntity = this.restTemplate.exchange("http://localhost:" + port + "/pokemon-types/1", HttpMethod.GET, httpRequest, PokemonType.class);
    var bulbizarre = bulbizarreResponseEntity.getBody();

    assertNotNull(bulbizarre);
    assertEquals(1, bulbizarre.id());
    assertEquals("Bulbizarre", bulbizarre.name()); (3)
}
1 Cette requête sans paramètre particulier doit renvoyer la traduction par défaut (en anglais)
2 On construit une requête en y ajoutant un header "Accept-Language"
3 On doit bien récupérer le nom du type de Pokemon traduit !

2.6. Les tests avec Postman

Pour bien valider nos développements, nous pouvons également créer des tests avec Postman.

Vous pouvez aussi utiliser Bruno ou Insomnia pour cette partie ! Dans ce cas, le code des tests sera un peu différent.

Dans Postman, créez une Collection

postman create collection
postman create collection 2

Ajoutez-y quelques requêtes. Pour ce faire, créez une nouvelle requête, et enregistrez la dans votre collection.

postman create request

Utilisez l’onglet Tests pour y ajouter quelques tests. Cet onglet permet d’exécuter du code javascript, permettant par exemple de valider les codes de retour HTTP ou le JSON reçu.

Créez les requêtes suivantes, avec les tests associés :

2.6.1. GET http://localhost:8080/pokemon-types/1

pm.test("Bulbasaur", function () {
    var bulbasaur = pm.response.json();
    pm.expect(bulbasaur.id).to.eq(1);
    pm.expect(bulbasaur.name).to.eq("Bulbasaur");
});

2.6.2. GET http://localhost:8080/pokemon-types/1 - Accept-Language: fr

pm.test("Bulbasaur", function () {
    var bulbasaur = pm.response.json();
    pm.expect(bulbasaur.id).to.eq(1);
    pm.expect(bulbasaur.name).to.eq("Bulbizarre");
});

2.6.3. GET http://localhost:8080/pokemon-types

pm.test("all pokemon types", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData.length).to.eq(151);
});

pm.test("Bulbasaur", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData[0].name).to.eq("Bulbasaur");
});

pm.test("Ivysaur", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData[1].name).to.eq("Ivysaur");
});

2.6.4. GET http://localhost:8080/pokemon-types - Accept-Language: fr

pm.test("all pokemon types", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData.length).to.eq(151);
});

pm.test("bulbizarre", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData[0].name).to.eq("Bulbizarre");
});

pm.test("Herbizarre", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData[1].name).to.eq("Herbizarre");
});

2.6.5. Export de la collection

Exportez votre collection Postman, dans le répertoire src/test/resources/rest/ de votre API. Cela vous permettra de la réutiliser plus tard et de la partager avec les autres développeurs !

2.7. OpenApi et Swagger

Nous allons également exposer une interface de type Swagger afin de faciliter nos tests et nos développements.

Cette interface nous permettra également de donner aux consommateurs de notre API un moyen facile de voir les ressources disponibles et les tester !

Pour exposer un swagger, nous allons utiliser la librairie springdoc.

  1. Cette librairie analyse les Controlleurs Spring, pour générer de la documentation au format swagger.

Cette librairie ne fait pas partie de Spring. Spring propose la génération de documentation à travers leur module spring rest-docs

Ajoutez la dépendance suivante à votre pom.xml :

pom.xml
<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
  <version>2.6.0</version>
</dependency>

Votre IHM swagger sera disponible à l’url http://localhost:8080/swagger-ui.html, tandis que le JSON sera disponible à l’url http://localhost:8080/v2/api-docs.

Configurez springdoc pour n’afficher que vos propres controlleurs, et ignorer le Basic Error Controller.

Trouvez comment faire dans la doc de springdoc : https://springdoc.org/#springdoc-openapi-core-properties.

3. game-ui

3.1. Utilisation des HTTP Interfaces

Modifiez votre micro-service game-ui pour y intégrer la gestion de la locale!

Remplacez l’utilisation du RestTemplate par des HTTP Interfaces Spring.

Passez la Locale en paramètre lors de l’appel au micro-service Pokemon Type.

Vous pouvez récupérer la locale avec la méthode LocaleContextHolder.getLocale() de Spring directement dans le PokemonTypeServiceImpl du game-ui, et la transmettre en utilisant le header Accept-Language. . De cette manière, la langue utilisée lors des échanges sera celle du navigateur de l’utilisateur !

4. trainer-api

Implémentez sur l’API trainer :

  1. l’exposition d’un swagger / open API

  2. une collection Postman (ou Bruno) permettant de

    • récupérer la liste des dresseurs de Pokemon

    • récupérer un dresseur de Pokemon

    • créer un dresseur de Pokemon