Skip to content
Snippets Groups Projects
README.md 12 KiB
Newer Older
Yvan Peter's avatar
Yvan Peter committed
# Développement REST - tests et accès à la base de données
Yvan Peter's avatar
Yvan Peter committed

Pour ce TP, nous allons développer la ressource de gestion de tâches que vous avez utilisée pour le TP HTTP.

## Le code récupéré
L'arborescence de source contient les fichiers suivants :
~~~
src/
├── main
│   ├── java
│   │   └── fr
│   │       └── ulille
│   │           └── iut
│   │               └── todo
│   │                   ├── BDDFactory.java
│   │                   ├── dao
│   │                   │   ├── TacheDAO.java
│   │                   │   ├── UUIDArgumentFactory.java
│   │                   │   └── UUIDArgument.java
│   │                   ├── DebugMapper.java
│   │                   ├── dto
│   │                   │   └── CreationTacheDTO.java
│   │                   ├── Main.java
│   │                   ├── ressource
│   │                   │   ├── BDDClearRessource.java
│   │                   │   └── TodoRessource.java
│   │                   └── service
│   │                       ├── Tache.java
│   │                       └── TodoService.java
│   └── resources
│       └── logging.properties
└── test
    └── java
        └── fr
            └── ulille
                └── iut
                    └── todo
                        └── ressource
                            └── TodoRessourceTest.java
~~~

On peut constater un certain nombre de changements par rapport au TP précédent :

1. L'arborescence de test comprend maintenant une classe de tests pour notre ressource
2. Un paquetage `dao` a été ajouté qui contient le code lié à la gestion de la persistance
3. Une classe `BDDFactory` est fournie qui gère la connexion à la base de donnée
Yvan Peter's avatar
Yvan Peter committed
4. La configuration de l'affichage des logs est maintenant géré par un fichier de configuration externe `logging.properties`

## Test manuel et découverte du code
Yvan Peter's avatar
Yvan Peter committed
Avant de commencer à compléter le code fournit, nous allons tester un petit peu :

Dans un terminal, lancez le serveur avec la commande `mvn compile exec:java`.

Dans un autre terminal, essayez la commande suivante pour créer une tâche : `curl -i -H "Content-Type: application/json" -d '{"nom": "tâche 1", "description": "tester la création"}' http://localhost:8080/api/v1/taches`.

Vous devriez obtenir le résultat suivant :

~~~
HTTP/1.1 201 Created
Location: http://localhost:8080/api/v1/taches/4ba24fd7-5856-4678-8f9e-84802e3334dd
ETag: "1387196213"
Content-Type: application/json
Content-Length: 98

{"description":"tester la création","id":"4ba24fd7-5856-4678-8f9e-84802e3334dd","nom":"tâche 1"}
~~~

Une requête pour obtenir la liste des tâches vous confirmera que votre tâche est bien conservée de manière persistante :

~~~
$ curl -i http://localhost:8080/api/v1/taches

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 100

[{"description":"tester la création","id":"4ba24fd7-5856-4678-8f9e-84802e3334dd","nom":"tâche 1"}]
~~~
Yvan Peter's avatar
Yvan Peter committed
On peut également vérifier la présence de nos données dans la base de données ( ̀sqlite` ) :

~~~
$ sqlite3 /tmp/peter_todo.db 
SQLite version 3.33.0 2020-08-14 13:23:32
Enter ".help" for usage hints.
sqlite> select * from taches;
4ba24fd7-5856-4678-8f9e-84802e3334dd|tâche 1|tester la création
~~~

Yvan Peter's avatar
Yvan Peter committed
Les opérations de manipulation de la table `taches` sont définies dans la classe `TacheDAO` avec des annotations [JDBI](https://jdbi.org/#_sql_objects) :

~~~java
public interface TacheDAO {
    @SqlUpdate("create table if not exists taches (id varchar(128) primary key, nom varchar not null, description varchar)")
    void createTable();

    @SqlUpdate("drop table if exists taches")
    void dropTable();

    @SqlUpdate("insert into taches (id, nom, description) values (:id, :nom, :description)")
    int insert(@BindBean Tache tache);

    @SqlQuery("select * from taches")
    @RegisterBeanMapper(Tache.class)
    List<Tache> getAll();
}
~~~

Ce DAO est utilisé dans la classe `Tache` qui représente une tâche particulière. Les tâches sont manipulées via la classe `TodoService`.

Yvan Peter's avatar
Yvan Peter committed
Si vous regardez le code de la classe `Tache`, vous constaterez que nous avons choisi d'identifier les tâches au moyen d'un identifiant unique ([UUID](https://fr.wikipedia.org/wiki/Universally_unique_identifier)). Comme ce type n'est pas géré nativement par JDBI, nous avons dû utiliser un [mécanisme d'extension](https://jdbi.org/#_custom_arguments) permettant la conversion entre le type `java.util.UUID` et `String` (classes `UUIDArgument` et `UUIDArgumentFactory`).

# Développement de la ressource
Dans la mesure où nous allons maintenant avoir des tests, ce développement se fera en TDD.

## Définition de l'API pour notre gestion des tâches
Yvan Peter's avatar
Yvan Peter committed
Notre ressource sera accessible via le chemin `/api/v1`. Le tableau ci-dessous liste les URI et les méthodes supportées par notre ressource ainsi que les types MIME acceptés (->) et produits (<-) par la ressource. A la suite, vous trouverez une définition des représentations utilisées (au format JSON).
Yvan Peter's avatar
Yvan Peter committed

| URI                      | Méthode | MIME                                                                                       | Requête    | Réponse              |
|--------------------------|---------|--------------------------------------------------------------------------------------------|------------|----------------------|
Yvan Peter's avatar
Yvan Peter committed
| /taches                  | GET     | <-application/json<br><-application/xml                                        |            | liste des tâches (T2) |
| /taches                  | POST    | <-/->application/json<br>->application/x-www-form-urlencoded   | tâche (T1) |                      |
| /taches/{id}             | GET     | <-application/json<br><-application/xml                                  |            | la tâche (T2)         |
| /taches/{id}             | PUT     | <-/->application/json                                                  | tâche (T2)  | la nouvelle tâche (T2) |
Yvan Peter's avatar
Yvan Peter committed
| /taches/{id}             | DELETE  |                                                                                            |            | 204 No Content       |
| /taches/{id}/description | GET     | <-text/plain                                                                      |            | chaîne de caractères |

Le format de création d'une tâche (T1) correspond à ce type de représentation :
~~~json
{"description":"une description", "nom":"un nom de tâche"}
~~~

Le format de tâche complet (T2) correspond à ce type de représentation :

~~~json
{"description":"une description", "id":"4ba24fd7-5856-4678-8f9e-84802e3334dd", "nom":"un nom de tâche"}
~~~
Yvan Peter's avatar
Yvan Peter committed
Après avoir défini notre API, nous allons progressivement implémenter toutes les méthodes indiquées.

Yvan Peter's avatar
Yvan Peter committed
## Récupérer une tâche
Yvan Peter's avatar
Yvan Peter committed

### Les tests
Yvan Peter's avatar
Yvan Peter committed
Nous pouvons déjà lancer les tests pour voir ce qu'il se passe : `mvn test`.

~~~
Results :

Tests in error: 
  get_with_id_should_return_task(fr.ulille.iut.todo.ressource.TodoRessourceTest): HTTP 404 Not Found

Tests run: 9, Failures: 0, Errors: 1, Skipped: 0
~~~

Le test en erreur correspond à la fonctionnalité à développer :

~~~java
Yvan Peter's avatar
Yvan Peter committed
    @Test
Yvan Peter's avatar
Yvan Peter committed
    public void get_with_id_should_return_task() {
        Tache tache = new Tache();
        tache.setNom("test");
        tache.setDescription("description");
        dao.insert(tache);

        Tache returned = target("taches").path(tache.getId().toString()).request().get(Tache.class);

        assertEquals(tache, returned);
    }
~~~

Pour ce test, nous créons un objet `Tache` qui est ensuite directement inséré dans la base grâce au DAO. Nous utilisons ensuite l'API [Client](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest3x/client.html) pour faire notre requête `GET`. Pour simplifier le développement des tests, nous utilisons les [classes de test de Jersey](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest3x/test-framework.html).

Yvan Peter's avatar
Yvan Peter committed
Vous pouvez compléter notre classe de test en implémentant la méthode `get_with_wrong_id_should_return_404()`

Yvan Peter's avatar
Yvan Peter committed
### Le lien avec la base de données
Pour pouvoir récupérer une tâche dans la base de données en fonction de son identifiant, vous aller devoir ajouter une méthode dans la classe `TacheDAO` :

~~~java
Tache getById(String id);
~~~

Inspirez vous du code fournit et de la [documentation de JDBI](https://jdbi.org/#_sql_objects) pour mettre les bonnes annotations.

Une fois votre DAO amélioré, vous pouvez utiliser la nouvelle méthode pour compléter la classe `TodoService` :
Yvan Peter's avatar
Yvan Peter committed
~~~java
public Tache getTache(UUID id) {}
~~~

### Implémentation dans la ressource

Vous pouvez passer au développement de la méthode répondant au `GET` sur l'URI `/taches/{id}` dans la classe `TodoRessource`.
Yvan Peter's avatar
Yvan Peter committed

Relancez vos tests et vérifier qu'ils passent.

## Récupérer la description d'une tâche
Cette méthode devrait être plus facile à implémenter car elle ne change rien au niveau DAO.

Commencez par implémenter les tests liés à la récupération de la description :

~~~java
    @Test
    public void get_for_description_should_work_with_existing_task() {
    }

    @Test
    public void get_for_description_with_wrong_id_should_return_404() {
    }
~~~

Vous pouvez ensuite implémenter la méthode permettant de récupérer la description dans la ressource et vérifier que les tests passent.
Yvan Peter's avatar
Yvan Peter committed

## Créer une tâche à partir d'un formulaire
La création de tâche fonctionne avec un objet JSON en entrée. On souhaite également la rendre possible à partir d'un formulaire.

Yvan Peter's avatar
Yvan Peter committed
Commencez par implémenter la méthode de test `public void post_with_form_data_should_return_201_location_and_task()`. La classe [Form](https://eclipse-ee4j.github.io/jaxrs-api/apidocs/3.0.0/jakarta/ws/rs/core/Form.html) devrait vous être utile. Pensez à fixer le type Mime à `MediaType.APPLICATION_FORM_URLENCODED` dans votre requête.
Yvan Peter's avatar
Yvan Peter committed

Implémentez ensuite la méthode correspondante de la ressource :

~~~java
public Response createTacheFromForm(MultivaluedMap<String, String> formParams) {}
~~~

On pourrait identifier individuellement les différents paramètres de formulaire avec l'annotation `@FormParam`, ce qui nous donnerait la méthode `public Response createTacheFromForm(@FormParam("nom") String nom, @FormParam("description") String description)`. Ici, on va utiliser [MultivaluedMap](https://eclipse-ee4j.github.io/jaxrs-api/apidocs/3.0.0/jakarta/ws/rs/core/MultivaluedMap.html) qui sera initialisé par Jersey avec les différentes paires clé-valeur transportées dans la requête HTTP.

Yvan Peter's avatar
Yvan Peter committed
## Suppression d'une tâche
Implémentez maintenant la méthode de suppression d'une tâche. 

Vous devrez d'abord implémenter les tests suivants :

~~~java
    @Test
    public void delete_should_remove_task_and_return_204() {
    }

    @Test
    public void delete_with_wrong_id_should_return_404() {
    }
~~~

Pour implémenter la fonctionnalité, vous devez d'abord implémenter la méthode `int delete(String id);` du DAO, puis la méthode correspondante dans `TodoService`. Enfin, vous pourrez développer la méthode de la ressource qui répondra à la méthode HTTP DELETE. 
Yvan Peter's avatar
Yvan Peter committed

## Mise à jour d'une tâche
Yvan Peter's avatar
Yvan Peter committed
Nous allons maintenant implémenter la méthode PUT de notre ressource. Rappel, ma méthode PUT fonctionne par écrasement de l'ancienne représentation. Le client doit donc impérativement fournir une ressource complète (sinon, il recevra une erreur 400). Ici on va traiter la cas où la nouvelle représentation est fournie en JSON.
Yvan Peter's avatar
Yvan Peter committed

Voici les tests à implémenter :
~~~java
    @Test
    public void put_should_replace_existing_task_values_return_200_and_task() {
    }

    @Test
    public void partial_put_should_fail_with_400() {
    } 
~~~

Là encore, vous devez ajouter une méthode à votre DAO et à `TodoService` avant d'implémenter la méthode dans votre ressource. 
Yvan Peter's avatar
Yvan Peter committed

## Une ressource complète
Avec le code réalisé, vous avez une ressource (presque) complète qui gère les différentes méthodes HTTP avec différents types MIME.