Skip to content
Snippets Groups Projects
README.md 31.1 KiB
Newer Older
Yvan Peter's avatar
Yvan Peter committed
# Projet REST avec Jersey
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
## Récupération du projet initial
Yvan Peter's avatar
Yvan Peter committed
Pour récupérer le projet vous pouvez utiliser la commande `git clone
https://gitlab.univ-lille.fr/yvan.peter/m4102_tp3.git`

L'arborescence ci-dessous vous montre le contenu du projet qui vous
servira de point de départ. Maven est configuré grâce au fichier
`pom.xml` qui permet entre autre de spécifier les dépendances du
projet.
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
La classe `ApiV1` sera le point d'entrée de notre application REST qui
permet de configurer le chemin de l'URI (`@ApplicationPath`) ainsi que
les paquetages Java qui contiennent les ressources.
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
~~~
.
├── architecture.svg
├── pom.xml
├── README.md
└── src
    ├── main
    │   ├── java
    │   │   └── fr
    │   │       └── ulille
    │   │           └── iut
    │   │               └── pizzaland
    │   │                   ├── ApiV1.java
    │   │                   ├── BDDFactory.java
    │   │                   ├── beans
    │   │                   ├── dao
    │   │                   │   ├── UUIDArgumentFactory.java
    │   │                   │   └── UUIDArgument.java
    │   │                   ├── dto
    │   │                   │   └── IngredientDto.java
    │   │                   ├── Main.java
    │   │                   └── resources
    │   │                       ├── BDDClearRessource.java
    │   │                       └── IngredientResource.java
    │   └── resources
    │       ├── ingredients.json
    │       └── logging.properties
    └── test
        ├── java
        │   └── fr
        │       └── ulille
        │           └── iut
        │               └── pizzaland
        │                   └── IngredientResourceTest.java
        └── resources
            └── logging.properties
~~~
	
Yvan Peter's avatar
Yvan Peter committed
## Développement d'une ressource *ingredients*

### API et représentation des données

Yvan Peter's avatar
Yvan Peter committed
Nous pouvons tout d'abord réfléchir à l'API REST que nous allons offrir pour la ressource *ingredients*. Celle-ci devrait répondre aux URI suivantes :
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
| URI                      | Opération   | MIME                                                         | Requête         | Réponse                                                              |
| :----------------------- | :---------- | :---------------------------------------------               | :--             | :----------------------------------------------------                |
| /ingredients             | GET         | <-application/json<br><-application/xml                      |                 | liste des ingrédients (I2)                                           |
| /ingredients/{id}        | GET         | <-application/json<br><-application/xml                      |                 | un ingrédient (I2) ou 404                                            |
| /ingredients/{id}/name   | GET         | <-text/plain                                                 |                 | le nom de l'ingrédient ou 404                                        |
| /ingredients             | POST        | <-/->application/json<br>->application/x-www-form-urlencoded | Ingrédient (I1) | Nouvel ingrédient (I2)<br>409 si l'ingrédient existe déjà (même nom) |
| /ingredients/{id}        | DELETE      |                                                              |                 |                                                                      |
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed

Un ingrédient comporte uniquement un identifiant et un nom. Sa
Yvan Peter's avatar
Yvan Peter committed
représentation JSON (I2) prendra donc la forme suivante :
Yvan Peter's avatar
Yvan Peter committed
      "id": "f38806a8-7c85-49ef-980c-149dcd81d306",
      "name": "mozzarella"
    }

Yvan Peter's avatar
Yvan Peter committed
Lors de la création, l'identifiant n'est pas connu car il sera fourni
par le JavaBean qui représente un ingrédient. Aussi on aura une
Yvan Peter's avatar
Yvan Peter committed
représentation JSON (I1) qui comporte uniquement le nom :
Yvan Peter's avatar
Yvan Peter committed
    { "name": "mozzarella" }

### Architecture logicielle de la solution

La figure ci-dessous présente l'architecture globale qui devra être
mise en place pour notre développement :

![Architecture de la solution](architecture.svg "Architecture")

Yvan Peter's avatar
Yvan Peter committed
#### JavaBeans
JavaBean est un standard pour les objets Java permettant de les créer
et de les initialiser et de les manipuler facilement. Pour cela ces
objets doivent respecter un ensemble de conventions :

  - la classe est sérialisable
  - elle fournit au moins un constructeur vide
  - les attributs privés de la classe sont manipulables via des
Yvan Peter's avatar
Yvan Peter committed
    méthodes publiques **get**_Attribut_ et **set**_Attribut_
Yvan Peter's avatar
Yvan Peter committed

Les DTO et la classe `Ingredient`décrits dans la suite sont des
JavaBeans.

#### Data Transfer Object (DTO)
Les DTO correspondent à la représentation des données qui sera
transportée par HTTP. Ce sont des Javabeans qui possèdent les même
propriétés que la représentation (avec les getter/setter
correspondants).

Jersey utilisera les *setter* pour initialiser l'objet à partir
de la représentation JSON ou XML et les *getter* pour créer la
représentation correspondante.

#### Data Access Object (DAO)
Le DAO permet de faire le lien entre la représentation objet et le
Yvan Peter's avatar
Yvan Peter committed
contenu de la base de données.

Yvan Peter's avatar
Yvan Peter committed
Nous utiliserons la [librairie JDBI](http://jdbi.org/) qui permet
d'associer une interface à des requêtes SQL.
La classe `BDDFactory` qui vous est fournie permet un accès facilité
aux fonctionnalités de JDBI.

#### La représentation des données manipulées par la ressource
La classe `Ingredient` est un JavaBean qui représente ce qu'est un
ingrédient. Elle porte des méthodes pour passer de cette
représentation aux DTO.
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
Cela permet de découpler l'implémentation de la ressource qui traite
Yvan Peter's avatar
Yvan Peter committed
les requêtes HTTP et la donnée manipulée.
Yvan Peter's avatar
Yvan Peter committed

Cette classe pourrait
Yvan Peter's avatar
Yvan Peter committed
porter des comportements liés à cette donnée (par ex. calcul de TVA).

Yvan Peter's avatar
Yvan Peter committed
## Mise en œuvre

### Une première implémentation : récupérer les ingrédients existants
Nous allons réaliser un développement dirigé par les tests. Dans un
premier temps, nous allons commencer par un test qui récupère une
liste d'ingrédients vide qui sera matérialisée par un tableau JSON
vide `[]`.

Le code suivant qui se trouve dans la classe `IngredientResourceTest`
montre la mise en place de l'environnement (`configure()`) et l'amorce
d'un premier test.

~~~java
Yvan Peter's avatar
Yvan Peter committed
public class IngredientResourceTest extends JerseyTest {
      
  @Override
  protected Application configure() {
    return new ApiV1();
  }

  @Test
  public void testGetEmptyList() {
    Response response = target("/ingredients").request().get();

    // Vérification de la valeur de retour (200)
    assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
Yvan Peter's avatar
Yvan Peter committed
	// Vérification de la valeur retournée (liste vide)
	List<IngredientDto> ingredients;
    ingredients = response.readEntity(new GenericType<List<IngredientDto>>(){});
Yvan Peter's avatar
Yvan Peter committed
    assertEquals(0, ingredients.size());
  }
Yvan Peter's avatar
Yvan Peter committed

En héritant de JerseyTest, votre classe de test se comporte comme un
[`Client`
JAX-RS](https://docs.oracle.com/javaee/7/api/jakarta/ws/rs/client/Client.html). La
Yvan Peter's avatar
Yvan Peter committed
méthode `target()` notamment permet de préparer une requête sur une
URI particulière.

Yvan Peter's avatar
Yvan Peter committed
Vous pouvez compiler votre code ainsi que les tests au moyen
des commandes `mvn compile` et `mvn test-compile`. La compilation du
code et des tests se fera automatiquement si nécessaire quand vous
faites un `mvn test`.

Pour pouvoir compiler ce premier test, il faut au minimum fournir le
DTO `IngredientDto`.
Pour commencer,  on se contentera de l'implémentation minimale
suivante :

~~~java
Yvan Peter's avatar
Yvan Peter committed
package fr.ulille.iut.pizzaland.dto;
Yvan Peter's avatar
Yvan Peter committed
  public class IngredientDto {
Yvan Peter's avatar
Yvan Peter committed
  public IngredientDto() {
Yvan Peter's avatar
Yvan Peter committed
  }
}
Pour réussir, ce premier test, nous allons mettre en place la
Yvan Peter's avatar
Yvan Peter committed
ressource `IngredientResource`.

Une première implémentation de la ressource pourra avoir la forme
suivante :

~~~java
Yvan Peter's avatar
Yvan Peter committed
@Path("/ingredients")
public class IngredientResource {
Yvan Peter's avatar
Yvan Peter committed
  @Context
  public UriInfo uriInfo;
Yvan Peter's avatar
Yvan Peter committed
  public IngredientResource() {
  }
Yvan Peter's avatar
Yvan Peter committed
  @GET
  public List<IngredientDto> getAll() {
Yvan Peter's avatar
Yvan Peter committed
    return new ArrayList<IngredientDto>();
  }
Yvan Peter's avatar
Yvan Peter committed
Avec cette première implémentation, on va pouvoir tester notre
ressource : 

Yvan Peter's avatar
Yvan Peter committed
$ mvn test
Yvan Peter's avatar
Yvan Peter committed
Results :
Yvan Peter's avatar
Yvan Peter committed
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Yvan Peter's avatar
Yvan Peter committed

### Récupérer un ingrédient existant
Nous allons continuer en ajoutant la possibilité de récupérer un
ingrédient particulier à partir de son identifiant.
Pour cela voici un premier test qui permettra de vérifier cela :
Yvan Peter's avatar
Yvan Peter committed
import fr.ulille.iut.pizzaland.beans.Ingredient;
Yvan Peter's avatar
Yvan Peter committed
    @Test
    public void testGetExistingIngredient() {

        Ingredient ingredient = new Ingredient();
        ingredient.setName("Chorizo");
        dao.insert(ingredient);

        Response response = target("/ingredients").path(ingredient.getId().toString()).request(MediaType.APPLICATION_JSON).get();

        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
Yvan Peter's avatar
Yvan Peter committed
        Ingredient result = Ingredient.fromDto(response.readEntity(IngredientDto.class));
        assertEquals(ingredient, result);
Yvan Peter's avatar
Yvan Peter committed
    }
Yvan Peter's avatar
Yvan Peter committed
  }
Yvan Peter's avatar
Yvan Peter committed

Vous pourrez vérifier que le test échoue au moyen de la commande `mvn test`

Afin de réussir ce test, nous devons compléter la classe IngredientDto
avec les getter/setter correspondant aux propriétés de l'object JSON.

~~~java
Yvan Peter's avatar
Yvan Peter committed
public class IngredientDto {
    private UUID id;
    private String name;

    public IngredientDto() {
    }

    public void setId(UUID id) {
        this.id = id;
    }

    public UUID getId() {
        return id;
    }

    public void setName(String name) {
Yvan Peter's avatar
Yvan Peter committed
      this.name = name;
    }
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
    public String getName() {
      return name;
    }
Yvan Peter's avatar
Yvan Peter committed

Du côté de la ressource, on peut fournir une première implémentation :

~~~java
Yvan Peter's avatar
Yvan Peter committed
import jakarta.ws.rs.PathParam;
import fr.ulille.iut.pizzaland.beans.Ingredient;
Yvan Peter's avatar
Yvan Peter committed
    @GET
    @Path("{id}")
Yvan Peter's avatar
Yvan Peter committed
    public IngredientDto getOneIngredient(@PathParam("id") UUID id) {
Yvan Peter's avatar
Yvan Peter committed
      Ingredient ingredient = new Ingredient();
      ingredient.setId(id); // juste pour avoir le même id pour le test
      ingredient.setName("Chorizo");
Yvan Peter's avatar
Yvan Peter committed
	  
	  return Ingredient.toDto(ingredient);
    }
Yvan Peter's avatar
Yvan Peter committed

Pour cette méthode, nous avons introduit la classe `Ingredient`. Ce
JavaBean représente un ingrédient manipulé par la ressource.
Yvan Peter's avatar
Yvan Peter committed
Voici une implémentation pour cette classe :

~~~java
Yvan Peter's avatar
Yvan Peter committed
package fr.ulille.iut.pizzaland.beans;
Yvan Peter's avatar
Yvan Peter committed
import java.util.UUID;

import fr.ulille.iut.pizzaland.dto.IngredientDto;

public class Ingredient {
    private UUID id = UUID.randomUUID();
    private String name;

    public Ingredient() {
    }

    public Ingredient(String name) {
        this.name = name;
    }

    public Ingredient(UUID id, String name) {
Yvan Peter's avatar
Yvan Peter committed
        this.id = id;
        this.name = name;
Yvan Peter's avatar
Yvan Peter committed
    }

    public void setId(UUID id) {
Yvan Peter's avatar
Yvan Peter committed
        this.id = id;
Yvan Peter's avatar
Yvan Peter committed
    }
Yvan Peter's avatar
Yvan Peter committed
    public UUID getId() {
Yvan Peter's avatar
Yvan Peter committed
        return id;
Yvan Peter's avatar
Yvan Peter committed
    }
Yvan Peter's avatar
Yvan Peter committed
    public String getName() {
Yvan Peter's avatar
Yvan Peter committed
        return name;
Yvan Peter's avatar
Yvan Peter committed
    }
Yvan Peter's avatar
Yvan Peter committed
    public void setName(String name) {
Yvan Peter's avatar
Yvan Peter committed
        this.name = name;
Yvan Peter's avatar
Yvan Peter committed
    }
Yvan Peter's avatar
Yvan Peter committed
    public static IngredientDto toDto(Ingredient i) {
Yvan Peter's avatar
Yvan Peter committed
        IngredientDto dto = new IngredientDto();
        dto.setId(i.getId());
        dto.setName(i.getName());

Yvan Peter's avatar
Yvan Peter committed
    }
    
    public static Ingredient fromDto(IngredientDto dto) {
Yvan Peter's avatar
Yvan Peter committed
        Ingredient ingredient = new Ingredient();
        ingredient.setId(dto.getId());
        ingredient.setName(dto.getName());

        return ingredient;
Yvan Peter's avatar
Yvan Peter committed
    }

    @Override
    public String toString() {
        return "Ingredient [id=" + id + ", name=" + name + "]";
Yvan Peter's avatar
Yvan Peter committed

Le test devrait maintenant réussir :

Yvan Peter's avatar
Yvan Peter committed
$ mvn test
Yvan Peter's avatar
Yvan Peter committed
## Introduction de la persistence
Pour aller plus loin et mettre en place la création des ingrédients il
va falloir introduire la persistence. Pour cela, nous allons utiliser
la librairie JDBI qui permet d'associer un modèle objet aux tables de
base de données.

Yvan Peter's avatar
Yvan Peter committed
Pour cela nous allons devoir implémenter le DAO (Data Access Object) `IngredientDao` :
~~~java
Yvan Peter's avatar
Yvan Peter committed
package fr.ulille.iut.pizzaland.dao;
Yvan Peter's avatar
Yvan Peter committed
import java.util.List;
import java.util.UUID;
Yvan Peter's avatar
Yvan Peter committed
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
import org.jdbi.v3.sqlobject.customizer.Bind;
import org.jdbi.v3.sqlobject.customizer.BindBean;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
Yvan Peter's avatar
Yvan Peter committed
import fr.ulille.iut.pizzaland.beans.Ingredient;
Yvan Peter's avatar
Yvan Peter committed
public interface IngredientDao {
Yvan Peter's avatar
Yvan Peter committed
    @SqlUpdate("CREATE TABLE IF NOT EXISTS ingredients (id VARCHAR(128) PRIMARY KEY, name VARCHAR UNIQUE NOT NULL)")
    void createTable();
Yvan Peter's avatar
Yvan Peter committed
    @SqlUpdate("DROP TABLE IF EXISTS ingredients")
    void dropTable();
Yvan Peter's avatar
Yvan Peter committed
    @SqlUpdate("INSERT INTO ingredients (id, name) VALUES (:id, :name)")
    void insert(@BindBean Ingredient ingredient);
Yvan Peter's avatar
Yvan Peter committed
    @SqlUpdate("DELETE FROM ingredients WHERE id = :id")
    void remove(@Bind("id") UUID id);
Yvan Peter's avatar
Yvan Peter committed
    @SqlQuery("SELECT * FROM ingredients WHERE name = :name")
    @RegisterBeanMapper(Ingredient.class)
    Ingredient findByName(@Bind("name") String name);

    @SqlQuery("SELECT * FROM ingredients")
    @RegisterBeanMapper(Ingredient.class)
    List<Ingredient> getAll();

    @SqlQuery("SELECT * FROM ingredients WHERE id = :id")
    @RegisterBeanMapper(Ingredient.class)
    Ingredient findById(@Bind("id") UUID id);
}
Yvan Peter's avatar
Yvan Peter committed

JDBI fonctionne par annotations :
  - Les annotations `sqlUpdate` et `SqlQuery` correspondent à des
  requêtes SQL en modification ou non.
  - `@RegisterBeanMapper` permet d'associer une classe à un résultat
  (les champs de la table sont associés aux propriétés du bean).
Yvan Peter's avatar
Yvan Peter committed
  - `@Bind` permet d'associer un paramètre de méthode à un paramètre nommé dans la requête SQL.
Yvan Peter's avatar
Yvan Peter committed
  
Reprenons maintenant le code déjà écrit pour aller chercher les
ingrédients dans une base de données (nous utiliserons `Sqlite`).

### Les tests avec la base de données
Nous allons utiliser le DAO pour insérer des données dans la table
afin de réaliser nos tests. Nous utiliserons une base de données de
tests qui est définie via la classe `BDDFactory`.

Yvan Peter's avatar
Yvan Peter committed
Dans la classe `IngredientResourceTest`Les méthodes `setEnvUp` et `tearEnvDown` permettent de créer et
Yvan Peter's avatar
Yvan Peter committed
détruire la base de données entre chaque test.
Yvan Peter's avatar
Yvan Peter committed
import fr.ulille.iut.pizzaland.dao.IngredientDao;
Yvan Peter's avatar
Yvan Peter committed
public class IngredientResourceTest extends JerseyTest {
  private IngredientDao dao;
Yvan Peter's avatar
Yvan Peter committed
  @Override
  protected Application configure() {
     BDDFactory.setJdbiForTests();
Yvan Peter's avatar
Yvan Peter committed
     return new ApiV1();
  }
Yvan Peter's avatar
Yvan Peter committed
  @Before
  public void setEnvUp() {
    dao = BDDFactory.buildDao(IngredientDao.class);
    dao.createTable();
  }
Yvan Peter's avatar
Yvan Peter committed
  @After
  public void tearEnvDown() throws Exception {
     dao.dropTable();
  }
Yvan Peter's avatar
Yvan Peter committed
  @Test
  public void testGetExistingIngredient() {
    Ingredient ingredient = new Ingredient();
    ingredient.setName("Chorizo");
    dao.insert(ingredient);
Yvan Peter's avatar
Yvan Peter committed
    Response response = target("/ingredients").path(ingredient.getId().toString()).request(MediaType.APPLICATION_JSON).get();
Yvan Peter's avatar
Yvan Peter committed
    assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
Yvan Peter's avatar
Yvan Peter committed
    Ingredient result = Ingredient.fromDto(response.readEntity(IngredientDto.class));
    assertEquals(ingredient, result);
}
Yvan Peter's avatar
Yvan Peter committed

### La ressource avec la base de données

~~~java
Yvan Peter's avatar
Yvan Peter committed
@Produces("application/json")
@Path("/ingredients")
public class IngredientResource {
    private static final Logger LOGGER = Logger.getLogger(IngredientResource.class.getName());

    private IngredientDao ingredients;

    @Context
    public UriInfo uriInfo;

    public IngredientResource() {
Yvan Peter's avatar
Yvan Peter committed
        ingredients = BDDFactory.buildDao(IngredientDao.class);
        ingredients.createTable();
Yvan Peter's avatar
Yvan Peter committed
    }

    @GET
    public List<IngredientDto> getAll() {
Yvan Peter's avatar
Yvan Peter committed
        LOGGER.info("IngredientResource:getAll");

        List<IngredientDto> l = ingredients.getAll().stream().map(Ingredient::toDto).collect(Collectors.toList());
Yvan Peter's avatar
Yvan Peter committed
        LOGGER.info(l.toString());
Yvan Peter's avatar
Yvan Peter committed
        return l;
Yvan Peter's avatar
Yvan Peter committed
    }
Yvan Peter's avatar
Yvan Peter committed
    @GET
    @Path("{id}")
    @Produces({ "application/json", "application/xml" })
    public IngredientDto getOneIngredient(@PathParam("id") UUID id) {
Yvan Peter's avatar
Yvan Peter committed
        LOGGER.info("getOneIngredient(" + id + ")");
        try {
            Ingredient ingredient = ingredients.findById(id);
Yvan Peter's avatar
Yvan Peter committed
            LOGGER.info(ingredient.toString());
            return Ingredient.toDto(ingredient);
        } catch (Exception e) {
Yvan Peter's avatar
Yvan Peter committed
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }
Yvan Peter's avatar
Yvan Peter committed
    }
}
Yvan Peter's avatar
Yvan Peter committed

### Les tests fonctionnent avec la base de données
Nous pouvons maintenant vérifier que la base fonctionne avec la base
de données :

Yvan Peter's avatar
Yvan Peter committed
$ mvn test
Yvan Peter's avatar
Yvan Peter committed
Results :
Yvan Peter's avatar
Yvan Peter committed
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
Yvan Peter's avatar
Yvan Peter committed
## Complétons maintenant les différents tests
L'implémentation de la classe devrait fonctionner avec le test suivant
:

~~~java
Yvan Peter's avatar
Yvan Peter committed
  @Test
  public void testGetNotExistingIngredient() {
    Response response = target("/ingredients").path(UUID.randomUUID().toString()).request().get();
    assertEquals(Response.Status.NOT_FOUND.getStatusCode(),response.getStatus());
  }
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
$ mvn test
Yvan Peter's avatar
Yvan Peter committed
	
Yvan Peter's avatar
Yvan Peter committed
Results :
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
### Implementation de la méthode POST
Yvan Peter's avatar
Yvan Peter committed
Il va falloir implémenter la méthode POST pour la création des
ingrédients. Commençons par les différents tests : création, création
de deux ingrédients identiques et création d'ingrédient sans nom.

~~~java
Yvan Peter's avatar
Yvan Peter committed
import fr.ulille.iut.pizzaland.dto.IngredientCreateDto;
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
   @Test
Yvan Peter's avatar
Yvan Peter committed
    public void testCreateIngredient() {
        IngredientCreateDto ingredientCreateDto = new IngredientCreateDto();
Yvan Peter's avatar
Yvan Peter committed
        ingredientCreateDto.setName("Chorizo");
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
        Response response = target("/ingredients").request().post(Entity.json(ingredientCreateDto));
Yvan Peter's avatar
Yvan Peter committed

        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());

        IngredientDto returnedEntity = response.readEntity(IngredientDto.class);

Yvan Peter's avatar
Yvan Peter committed
        assertEquals(target("/ingredients/" + returnedEntity.getId()).getUri(), response.getLocation());
Yvan Peter's avatar
Yvan Peter committed
        assertEquals(returnedEntity.getName(), ingredientCreateDto.getName());
    }
Yvan Peter's avatar
Yvan Peter committed

    @Test
Yvan Peter's avatar
Yvan Peter committed
    public void testCreateSameIngredient() {
Yvan Peter's avatar
Yvan Peter committed
        Ingredient ingredient = new Ingredient();
        ingredient.setName("Chorizo");
        dao.insert(ingredient);
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
        IngredientCreateDto ingredientCreateDto = Ingredient.toCreateDto(ingredient);
        Response response = target("/ingredients").request().post(Entity.json(ingredientCreateDto));
Yvan Peter's avatar
Yvan Peter committed

        assertEquals(Response.Status.CONFLICT.getStatusCode(), response.getStatus());
    }

    @Test
    public void testCreateIngredientWithoutName() {
        IngredientCreateDto ingredientCreateDto = new IngredientCreateDto();

Yvan Peter's avatar
Yvan Peter committed
        Response response = target("/ingredients").request().post(Entity.json(ingredientCreateDto));
Yvan Peter's avatar
Yvan Peter committed

        assertEquals(Response.Status.NOT_ACCEPTABLE.getStatusCode(), response.getStatus());
    }
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed

Nous utiliserons un DTO spécifique `IngredientCreateDto` dans la
mesure où nous n'aurons que le nom de l'ingrédient pour la création.

La classe [`jakarta.ws.rs.client.Entity<T>`](https://docs.oracle.com/javaee/7/api/jakarta/ws/rs/client/Entity.html) permet de définir le corps de
Yvan Peter's avatar
Yvan Peter committed
la requête POST et le type de données associée (ici `application/json`).

Nous devons également fournir une implémentation de
`IngredientCreateDto` pour pouvoir compiler notre code :

~~~java
Yvan Peter's avatar
Yvan Peter committed
package fr.ulille.iut.pizzaland.dto;
Yvan Peter's avatar
Yvan Peter committed
	
Yvan Peter's avatar
Yvan Peter committed
public class IngredientCreateDto {
	private String name;
Yvan Peter's avatar
Yvan Peter committed
		
Yvan Peter's avatar
Yvan Peter committed
	public IngredientCreateDto() {}
Yvan Peter's avatar
Yvan Peter committed
		
Yvan Peter's avatar
Yvan Peter committed
	public void setName(String name) {
		this.name = name;
	}
Yvan Peter's avatar
Yvan Peter committed
 		
Yvan Peter's avatar
Yvan Peter committed
	public String getName() {
		return name;
Yvan Peter's avatar
Yvan Peter committed
	}
Yvan Peter's avatar
Yvan Peter committed
}
Yvan Peter's avatar
Yvan Peter committed

Nous pouvons maintenant compiler notre code de test et constater que
ceux-ci échouent.

Yvan Peter's avatar
Yvan Peter committed
$ mvn test
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
Results :
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
Failed tests:   testCreateSameIngredient(fr.ulille.iut.pizzaland.IngredientResourceTest): expected:<409> but was:<405>
	testCreateIngredientWithoutName(fr.ulille.iut.pizzaland.IngredientResourceTest): expected:<406> but was:<405>
	testCreateIngredient(fr.ulille.iut.pizzaland.IngredientResourceTest): expected:<201> but was:<405>
Yvan Peter's avatar
Yvan Peter committed
	
Yvan Peter's avatar
Yvan Peter committed
Tests run: 6, Failures: 3, Errors: 0, Skipped: 0
Yvan Peter's avatar
Yvan Peter committed

Nous pouvons maintenant implémenter notre méthode POST dans la
	ressource :
Yvan Peter's avatar
Yvan Peter committed
import jakarta.ws.rs.POST;
import fr.ulille.iut.pizzaland.dto.IngredientCreateDto;
Yvan Peter's avatar
Yvan Peter committed
	
Yvan Peter's avatar
Yvan Peter committed
    @POST
Yvan Peter's avatar
Yvan Peter committed
    public Response createIngredient(IngredientCreateDto ingredientCreateDto) {
        Ingredient existing = ingredients.findByName(ingredientCreateDto.getName());
Yvan Peter's avatar
Yvan Peter committed
        if (existing != null) {
Yvan Peter's avatar
Yvan Peter committed
            throw new WebApplicationException(Response.Status.CONFLICT);
        }
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
        try {
            Ingredient ingredient = Ingredient.fromIngredientCreateDto(ingredientCreateDto);
Yvan Peter's avatar
Yvan Peter committed
            ingredients.insert(ingredient);
Yvan Peter's avatar
Yvan Peter committed
            IngredientDto ingredientDto = Ingredient.toDto(ingredient);

Yvan Peter's avatar
Yvan Peter committed
            URI uri = uriInfo.getAbsolutePathBuilder().path(ingredient.getId().toString()).build();
Yvan Peter's avatar
Yvan Peter committed

            return Response.created(uri).entity(ingredientDto).build();
Yvan Peter's avatar
Yvan Peter committed
        } catch (Exception e) {
Yvan Peter's avatar
Yvan Peter committed
            e.printStackTrace();
            throw new WebApplicationException(Response.Status.NOT_ACCEPTABLE);
        }
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
    }
Yvan Peter's avatar
Yvan Peter committed

Comme nous vérifions qu'il n'y a pas déjà un ingrédient avec le nom
Yvan Peter's avatar
Yvan Peter committed
fourni, nous devont ajouter une méthode `findbyName` à notre DAO
Yvan Peter's avatar
Yvan Peter committed

~~~java
Yvan Peter's avatar
Yvan Peter committed
    @SqlQuery("SELECT * FROM ingredients WHERE name = :name")
Yvan Peter's avatar
Yvan Peter committed
    @RegisterBeanMapper(Ingredient.class)
Yvan Peter's avatar
Yvan Peter committed
    Ingredient findByName(@Bind("name") String name);
Yvan Peter's avatar
Yvan Peter committed

Nous avons également besoin de rajouter les méthodes de conversion
	pour ce DTO à notre bean `Ingredient` :
Yvan Peter's avatar
Yvan Peter committed
import fr.ulille.iut.pizzaland.dto.IngredientCreateDto;
Yvan Peter's avatar
Yvan Peter committed
		
Yvan Peter's avatar
Yvan Peter committed
  public static IngredientCreateDto toCreateDto(Ingredient ingredient) {
    IngredientCreateDto dto = new IngredientCreateDto();
    dto.setName(ingredient.getName());
Yvan Peter's avatar
Yvan Peter committed
        
Yvan Peter's avatar
Yvan Peter committed
    return dto;
  }
Yvan Peter's avatar
Yvan Peter committed
	
Yvan Peter's avatar
Yvan Peter committed
  public static Ingredient fromIngredientCreateDto(IngredientCreateDto dto) {
    Ingredient ingredient = new Ingredient();
    ingredient.setName(dto.getName());
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
    return ingredient;
  }
Yvan Peter's avatar
Yvan Peter committed

Nous pouvons maintenant vérifier nos tests :

Yvan Peter's avatar
Yvan Peter committed
$ mvn test
Yvan Peter's avatar
Yvan Peter committed
	
Yvan Peter's avatar
Yvan Peter committed
Results :
Yvan Peter's avatar
Yvan Peter committed
	
Yvan Peter's avatar
Yvan Peter committed
Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
Yvan Peter's avatar
Yvan Peter committed

Vous aurez peut-être un affichage d'exception liée au test de création
de doublon, toutefois le test est réussi puisqu'il a levé une
exception qui a été traduite par un code d'erreur HTTP 406.

Yvan Peter's avatar
Yvan Peter committed
### Implémentation de la méthode DELETE
Les tests liés à la méthode DELETE sont les suivants :

Yvan Peter's avatar
Yvan Peter committed
~~~java
   @Test
Yvan Peter's avatar
Yvan Peter committed
   public void testDeleteExistingIngredient() {
     Ingredient ingredient = new Ingredient();
     ingredient.setName("Chorizo");
     dao.insert(ingredient);
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
     Response response = target("/ingredients/").path(ingredient.getId().toString()).request().delete();
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
     assertEquals(Response.Status.ACCEPTED.getStatusCode(), response.getStatus());
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
     Ingredient result = dao.findById(ingredient.getId());
     assertEquals(result, null);
  }
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
  @Test
  public void testDeleteNotExistingIngredient() {
    Response response = target("/ingredients").path(UUID.randomUUID().toString()).request().delete();
    assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
  }
Yvan Peter's avatar
Yvan Peter committed
Après avoir constaté que ces tests échouent, nous pouvons fournir une
implémentation pour la méthode DELETE :

~~~java
Yvan Peter's avatar
Yvan Peter committed
import jakarta.ws.rs.DELETE;
Yvan Peter's avatar
Yvan Peter committed
	
Yvan Peter's avatar
Yvan Peter committed
  @DELETE
  @Path("{id}")
  public Response deleteIngredient(@PathParam("id") UUID id) {
    if ( ingredients.findById(id) == null ) {
      throw new WebApplicationException(Response.Status.NOT_FOUND);
    }
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
    ingredients.remove(id);
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
    return Response.status(Response.Status.ACCEPTED).build();
  }
Yvan Peter's avatar
Yvan Peter committed

Nous devons également implémenter la méthode remove dans
`IngredientDao` :

~~~java
Yvan Peter's avatar
Yvan Peter committed
   @SqlUpdate("DELETE FROM ingredients WHERE id = :id")
Yvan Peter's avatar
Yvan Peter committed
   void remove(@Bind("id") UUID id);
Yvan Peter's avatar
Yvan Peter committed

Avec cette implémentation, nos tests réussissent.

### Implémentation de la méthode GET pour récupérer le nom de l'ingrédient
Commençons par les tests correspondant à cette URI (GET
/ingredients/{id}/name)

~~~java
Yvan Peter's avatar
Yvan Peter committed
   @Test
Yvan Peter's avatar
Yvan Peter committed
   public void testGetIngredientName() {
     Ingredient ingredient = new Ingredient();
     ingredient.setName("Chorizo");
     dao.insert(ingredient);
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
     Response response = target("ingredients").path(ingredient.getId().toString()).path("name").request().get();
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
     assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
     assertEquals("Chorizo", response.readEntity(String.class));
  }
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
  @Test
  public void testGetNotExistingIngredientName() {
    Response response = target("ingredients").path(UUID.randomUUID().toString()).path("name").request().get();
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
    assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
  }
Yvan Peter's avatar
Yvan Peter committed

L'implémentation correspondant à ce test est simple :

~~~java
Yvan Peter's avatar
Yvan Peter committed
    @GET
Yvan Peter's avatar
Yvan Peter committed
    @Path("{id}/name")
Yvan Peter's avatar
Yvan Peter committed
    public String getIngredientName(@PathParam("id") UUID id) {
        Ingredient ingredient = ingredients.findById(id);

        if (ingredient == null) {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }

        return ingredient.getName();
Yvan Peter's avatar
Yvan Peter committed
    }
Yvan Peter's avatar
Yvan Peter committed

### Implémentation d'une méthode de création avec des données de formulaire
La création d'un ingrédient pourrait également se faire via un
formulaire Web. Dans ce cas, le type de représentation sera
`application/x-www-form-urlencoded`. 

On peut déjà préparer un test pour cette méthode de création :

~~~java
Yvan Peter's avatar
Yvan Peter committed
   @Test
Yvan Peter's avatar
Yvan Peter committed
    public void testCreateWithForm() {
        Form form = new Form();
        form.param("name", "chorizo");

        Entity<Form> formEntity = Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE);
        Response response = target("ingredients").request().post(formEntity);
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
        String location = response.getHeaderString("Location");
Yvan Peter's avatar
Yvan Peter committed
        String id = location.substring(location.lastIndexOf('/') + 1);
        Ingredient result = dao.findById(UUID.fromString(id));

Yvan Peter's avatar
Yvan Peter committed
        assertNotNull(result);
    }
Yvan Peter's avatar
Yvan Peter committed

On peut maintenant fournir une implémentation pour cette méthode :

~~~java
Yvan Peter's avatar
Yvan Peter committed
  @POST
  @Consumes("application/x-www-form-urlencoded")
  public Response createIngredient(@FormParam("name") String name) {
    Ingredient existing = ingredients.findByName(name);
    if (existing != null) {
      throw new WebApplicationException(Response.Status.CONFLICT);
    }
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
    try {
      Ingredient ingredient = new Ingredient();
      ingredient.setName(name);
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
      ingredients.insert(ingredient);
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
      IngredientDto ingredientDto = Ingredient.toDto(ingredient);
Yvan Peter's avatar
Yvan Peter committed
      URI uri = uriInfo.getAbsolutePathBuilder().path("" + ingredient.getId()).build();
Yvan Peter's avatar
Yvan Peter committed
      return Response.created(uri).entity(ingredientDto).build();
    } catch (Exception e) {
        e.printStackTrace();
        throw new WebApplicationException(Response.Status.NOT_ACCEPTABLE);
Yvan Peter's avatar
Yvan Peter committed
  }
Yvan Peter's avatar
Yvan Peter committed
# Créer une base de données de test
Nous avons maintenant implémenté et testé toutes les méthodes prévues
par notre API. Si nous voulons tester avec des clients, il serait bien
d'avoir quelques ingrédients dans la base de données. Pour cela, nous
allons donner la possibilité de créer des ingrédients au démarrage sur la base
d'une variable d'environnement : `PIZZAENV`.

Quand cette variable aura la valeur `withdb`, nous allons remplir la
base au démarrage avec le code suivant :

~~~java
Yvan Peter's avatar
Yvan Peter committed
package fr.ulille.iut.pizzaland;
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
import org.glassfish.jersey.server.ResourceConfig;
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
import fr.ulille.iut.pizzaland.beans.Ingredient;
import fr.ulille.iut.pizzaland.beans.Pizza;
import fr.ulille.iut.pizzaland.dao.IngredientDao;
import fr.ulille.iut.pizzaland.dao.PizzaDao;
import fr.ulille.iut.pizzaland.dto.PizzaCreateDto;
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Logger;

import jakarta.json.bind.JsonbBuilder;
import jakarta.ws.rs.ApplicationPath;

@ApplicationPath("api/v1/")
public class ApiV1 extends ResourceConfig {
    private static final Logger LOGGER = Logger.getLogger(ApiV1.class.getName());

    public ApiV1() {
        packages("fr.ulille.iut.pizzaland");
Yvan Peter's avatar
Yvan Peter committed
        String environment = System.getenv("PIZZAENV");
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
        if ( environment != null && environment.equals("withdb") ) {
            LOGGER.info("Loading with database");
            try {
                FileReader reader = new FileReader( getClass().getClassLoader().getResource("ingredients.json").getFile() );
                List<Ingredient> ingredients = JsonbBuilder.create().fromJson(reader, new ArrayList<Ingredient>(){}.getClass().getGenericSuperclass());

                IngredientDao ingredientDao = BDDFactory.buildDao(IngredientDao.class);
                ingredientDao.dropTable();
                ingredientDao.createTable();
                for ( Ingredient ingredient: ingredients) {
                        ingredientDao.insert(ingredient); 
                }
            } catch ( Exception ex ) {
                throw new IllegalStateException(ex);
Yvan Peter's avatar
Yvan Peter committed
            }
Yvan Peter's avatar
Yvan Peter committed
        } 
Yvan Peter's avatar
Yvan Peter committed
    }
Yvan Peter's avatar
Yvan Peter committed
}
Yvan Peter's avatar
Yvan Peter committed
Dans un terminal, nous pouvons maintenant fixer la variable
d'environnemnet et démarrer notre serveur REST au moyen de la
Yvan Peter's avatar
Yvan Peter committed
commande `mvn exec:java` :
Yvan Peter's avatar
Yvan Peter committed
$ export PIZZAENV=withdb
$ mvn exec:java
Yvan Peter's avatar
Yvan Peter committed
Dans un autre terminal, nous pouvons utiliser `curl` pour tester nos
différentes méthodes :

Yvan Peter's avatar
Yvan Peter committed
$ curl -i localhost:8080/api/v1/ingredients

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 760
Yvan Peter's avatar
Yvan Peter committed

Yvan Peter's avatar
Yvan Peter committed
[{"id":"f38806a8-7c85-49ef-980c-149dcd81d306","name":"mozzarella"},{"id":"d36903e1-0cc0-4bd6-a0ed-e0e9bf7b4037","name":"jambon"},{"id":"bc5b315f-442f-4ee4-96de-486d48f20c2f","name":"champignons"},{"id":"6a04320c-3a4f-4570-96d3-61faf3f898b0","name":"olives"},{"id":"c77deeee-d50d-49d5-9695-c98ec811f762","name":"tomate"},{"id":"c9375542-8142-43f6-b54d-0d63597cf614","name":"merguez"},{"id":"dee27dd6-f9b6-4d03-ac4b-216b5c9c8bd7","name":"lardons"},{"id":"657f8dd4-6bc1-4622-9af7-37d248846a23","name":"fromage"},{"id":"070d8077-a713-49a0-af37-3936b63d5ff2","name":"oeuf"},{"id":"5d9ca5c4-517f-40fd-aac3-5a823d680c1d","name":"poivrons"},{"id":"52f68024-24ec-46c0-8e77-c499dba1e27e","name":"ananas"},{"id":"dfdf6fae-f1b2-45fa-8c39-54e522c1933f","name":"reblochon"}]

# Implémentation de la ressource Pizza
Yvan Peter's avatar
Yvan Peter committed
Maintenant que vous avez une ressource `ingrédients` fonctionnelle, vous pouvez passer à l'implémentation de la ressource `Pizzas`. Pour cette ressource, vous devrez d'abord définir l'API dans le fichier `pizzas.md` (URI, méthodes, représentations). Ensuite, vous pourrez développer la ressource avec les tests associés.

## Note sur la base de données
Une pizza comprend des ingrédients. Pour développer cette ressource,
vous aurez donc besoin d'un table d'association au niveau de la base
de données. Cela pourra être géré au niveau du DAO grâce à
[JDBI](https://jdbi.org/#_default_methods). Cet extrait de code montre
comment faire :

~~~java
Yvan Peter's avatar
Yvan Peter committed
import org.jdbi.v3.sqlobject.transaction.Transaction;
Yvan Peter's avatar
Yvan Peter committed
  public interface PizzaDao {
Yvan Peter's avatar
Yvan Peter committed
    @SqlUpdate("CREATE TABLE IF NOT EXISTS Pizzas ....")
    void createPizzaTable();
Yvan Peter's avatar
Yvan Peter committed
    @SqlUpdate("CREATE TABLE IF NOT EXISTS PizzaIngredientsAssociation .....")
    void createAssociationTable();
Yvan Peter's avatar
Yvan Peter committed
    @Transaction
    default void createTableAndIngredientAssociation() {
      createAssociationTable();
      createPizzaTable();

Vous écrivez les différentes méthodes annotées avec `@SqlUpdate` ou
`@SqlQuery`. Vous utilisez ensuite ces méthodes au sein d'une méthode
ayant le mot clé `default`. C'est cette méthode que vous utiliserez
dans votre ressource.