Skip to content
Snippets Groups Projects
Commit e9539807 authored by Yvan Peter's avatar Yvan Peter
Browse files

maj README + code

parent 8cdb5a88
No related branches found
No related tags found
No related merge requests found
...@@ -70,15 +70,15 @@ Un ingrédient comporte uniquement un identifiant et un nom. Sa ...@@ -70,15 +70,15 @@ Un ingrédient comporte uniquement un identifiant et un nom. Sa
représentation JSON (I2) prendra donc la forme suivante : représentation JSON (I2) prendra donc la forme suivante :
{ {
"id": "f38806a8-7c85-49ef-980c-149dcd81d306", "id": "f38806a8-7c85-49ef-980c-149dcd81d306",
"name": "mozzarella" "name": "mozzarella"
} }
Lors de la création, l'identifiant n'est pas connu car il sera fourni 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 par le JavaBean qui représente un ingrédient. Aussi on aura une
représentation JSON (I1) qui comporte uniquement le nom : représentation JSON (I1) qui comporte uniquement le nom :
{ "name": "mozzarella" } { "name": "mozzarella" }
### Architecture logicielle de la solution ### Architecture logicielle de la solution
...@@ -95,7 +95,7 @@ objets doivent respecter un ensemble de conventions : ...@@ -95,7 +95,7 @@ objets doivent respecter un ensemble de conventions :
- la classe est sérialisable - la classe est sérialisable
- elle fournit au moins un constructeur vide - elle fournit au moins un constructeur vide
- les attributs privés de la classe sont manipulables via des - les attributs privés de la classe sont manipulables via des
méthodes publiques **get**_Attribut_ et **set**_Attribut méthodes publiques **get**_Attribut_ et **set**_Attribut_
Les DTO et la classe `Ingredient`décrits dans la suite sont des Les DTO et la classe `Ingredient`décrits dans la suite sont des
JavaBeans. JavaBeans.
...@@ -143,26 +143,26 @@ montre la mise en place de l'environnement (`configure()`) et l'amorce ...@@ -143,26 +143,26 @@ montre la mise en place de l'environnement (`configure()`) et l'amorce
d'un premier test. d'un premier test.
~~~java ~~~java
public class IngredientResourceTest extends JerseyTest { public class IngredientResourceTest extends JerseyTest {
@Override @Override
protected Application configure() { protected Application configure() {
return new ApiV1(); return new ApiV1();
} }
@Test @Test
public void testGetEmptyList() { public void testGetEmptyList() {
Response response = target("/ingredients").request().get(); Response response = target("/ingredients").request().get();
// Vérification de la valeur de retour (200) // Vérification de la valeur de retour (200)
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
// Vérification de la valeur retournée (liste vide) // Vérification de la valeur retournée (liste vide)
List<IngredientDto> ingredients; List<IngredientDto> ingredients;
ingredients = response.readEntity(new GenericType<List<IngredientDto>>(){}); ingredients = response.readEntity(new GenericType<List<IngredientDto>>(){});
assertEquals(0, ingredients.size()); assertEquals(0, ingredients.size());
} }
~~~ ~~~
En héritant de JerseyTest, votre classe de test se comporte comme un En héritant de JerseyTest, votre classe de test se comporte comme un
...@@ -183,26 +183,13 @@ Pour commencer, on se contentera de l'implémentation minimale ...@@ -183,26 +183,13 @@ Pour commencer, on se contentera de l'implémentation minimale
suivante : suivante :
~~~java ~~~java
package fr.ulille.iut.pizzaland.dto; package fr.ulille.iut.pizzaland.dto;
public class IngredientDto {
public IngredientDto() {
}
}
~~~
A ce stade, vous pouvez lancer un premier test au moyen de la commande
`mvn test`. Évidemment, ce test va échouer.
~~~ public class IngredientDto {
$ mvn test
Results :
Failed tests: testGetEmptyList(fr.ulille.iut.pizzaland.IngredientResourceTest): expected:<200> but was:<404>
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0 public IngredientDto() {
}
}
~~~ ~~~
Pour réussir, ce premier test, nous allons mettre en place la Pour réussir, ce premier test, nous allons mettre en place la
...@@ -212,32 +199,33 @@ Une première implémentation de la ressource pourra avoir la forme ...@@ -212,32 +199,33 @@ Une première implémentation de la ressource pourra avoir la forme
suivante : suivante :
~~~java ~~~java
@Path("/ingredients") @Path("/ingredients")
public class IngredientResource { public class IngredientResource {
@Context @Context
public UriInfo uriInfo; public UriInfo uriInfo;
public IngredientResource() { public IngredientResource() {
} }
@GET @GET
public List<IngredientDto> getAll() { public List<IngredientDto> getAll() {
return new ArrayList<IngredientDto>(); return new ArrayList<IngredientDto>();
} }
} }
~~~ ~~~
Avec cette première implémentation, on va pouvoir tester notre Avec cette première implémentation, on va pouvoir tester notre
ressource : ressource :
~~~ ~~~
$ mvn test $ mvn test
Results : Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
~~~ ~~~
### Récupérer un ingrédient existant ### Récupérer un ingrédient existant
...@@ -246,19 +234,23 @@ ingrédient particulier à partir de son identifiant. ...@@ -246,19 +234,23 @@ ingrédient particulier à partir de son identifiant.
Pour cela voici un premier test qui permettra de vérifier cela : Pour cela voici un premier test qui permettra de vérifier cela :
~~~java ~~~java
import fr.ulille.iut.pizzaland.beans.Ingredient; import fr.ulille.iut.pizzaland.beans.Ingredient;
@Test @Test
public void testGetExistingIngredient() { public void testGetExistingIngredient() {
Ingredient ingredient = new Ingredient();
ingredient.setName("mozzarella"); Ingredient ingredient = new Ingredient();
ingredient.setName("Chorizo");
Response response = target("/ingredients/1").request().get(); dao.insert(ingredient);
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
Response response = target("/ingredients").path(ingredient.getId().toString()).request(MediaType.APPLICATION_JSON).get();
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
Ingredient result = Ingredient.fromDto(response.readEntity(IngredientDto.class)); Ingredient result = Ingredient.fromDto(response.readEntity(IngredientDto.class));
assertEquals(ingredient, result); assertEquals(ingredient, result);
} }
}
~~~ ~~~
Vous pourrez vérifier que le test échoue au moyen de la commande `mvn test` Vous pourrez vérifier que le test échoue au moyen de la commande `mvn test`
...@@ -267,27 +259,27 @@ Afin de réussir ce test, nous devons compléter la classe IngredientDto ...@@ -267,27 +259,27 @@ 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. avec les getter/setter correspondant aux propriétés de l'object JSON.
~~~java ~~~java
public class IngredientDto { public class IngredientDto {
private long id; private UUID id;
private String name; private String name;
public IngredientDto() {} public IngredientDto() {
}
public long getId() {
return id; public void setId(UUID id) {
} this.id = id;
}
public void setId(long id) {
this.id = id; public UUID getId() {
} return id;
}
public void setName(String name) {
this.name = name; public void setName(String name) {
} this.name = name;
}
public String getName() {
public String getName() {
return name; return name;
}
} }
~~~ ~~~
...@@ -299,10 +291,10 @@ Du côté de la ressource, on peut fournir une première implémentation : ...@@ -299,10 +291,10 @@ Du côté de la ressource, on peut fournir une première implémentation :
@GET @GET
@Path("{id}") @Path("{id}")
public IngredientDto getOneIngredient(@PathParam("id") long id) { public IngredientDto getOneIngredient(@PathParam("id") UUID id) {
Ingredient ingredient = new Ingredient(); Ingredient ingredient = new Ingredient();
ingredient.setId(1); ingredient.setId(id);
ingredient.setName("mozzarella"); ingredient.setName("Chorizo");
return Ingredient.toDto(ingredient); return Ingredient.toDto(ingredient);
} }
...@@ -313,84 +305,72 @@ JavaBean représente un ingrédient manipulé par la ressource. ...@@ -313,84 +305,72 @@ JavaBean représente un ingrédient manipulé par la ressource.
Voici une implémentation pour cette classe : Voici une implémentation pour cette classe :
~~~java ~~~java
package fr.ulille.iut.pizzaland.beans; package fr.ulille.iut.pizzaland.beans;
import fr.ulille.iut.pizzaland.dto.IngredientDto;
public class Ingredient {
private long id;
private String name;
public Ingredient() {
}
public Ingredient(long id, String name) { 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) {
this.id = id; this.id = id;
this.name = name; this.name = name;
} }
public void setId(long id) { public void setId(UUID id) {
this.id = id; this.id = id;
} }
public long getId() { public UUID getId() {
return id; return id;
} }
public String getName() { public String getName() {
return name; return name;
} }
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }
public static IngredientDto toDto(Ingredient i) { public static IngredientDto toDto(Ingredient i) {
IngredientDto dto = new IngredientDto(); IngredientDto dto = new IngredientDto();
dto.setId(i.getId()); dto.setId(i.getId());
dto.setName(i.getName()); dto.setName(i.getName());
return dto; return dto;
} }
public static Ingredient fromDto(IngredientDto dto) { public static Ingredient fromDto(IngredientDto dto) {
Ingredient ingredient = new Ingredient(); Ingredient ingredient = new Ingredient();
ingredient.setId(dto.getId()); ingredient.setId(dto.getId());
ingredient.setName(dto.getName()); ingredient.setName(dto.getName());
return ingredient; return ingredient;
} }
@Override @Override
public boolean equals(Object obj) { public String toString() {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Ingredient other = (Ingredient) obj;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return "Ingredient [id=" + id + ", name=" + name + "]"; return "Ingredient [id=" + id + ", name=" + name + "]";
} }
} }
~~~ ~~~
Le test devrait maintenant réussir : Le test devrait maintenant réussir :
~~~ ~~~
$ mvn test $ mvn test
~~~ ~~~
## Introduction de la persistence ## Introduction de la persistence
...@@ -402,44 +382,50 @@ base de données. ...@@ -402,44 +382,50 @@ base de données.
Pour cela nous allons devoir implémenter le DAO (Data Access Object) `IngredientDao` : Pour cela nous allons devoir implémenter le DAO (Data Access Object) `IngredientDao` :
~~~java ~~~java
package fr.ulille.iut.pizzaland.dao; package fr.ulille.iut.pizzaland.dao;
import java.util.List; import java.util.List;
import java.util.UUID;
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; import org.jdbi.v3.sqlobject.customizer.Bind;
import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.customizer.BindBean;
import org.jdbi.v3.sqlobject.statement.SqlUpdate; import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import fr.ulille.iut.pizzaland.beans.Ingredient; import fr.ulille.iut.pizzaland.beans.Ingredient;
public interface IngredientDao { public interface IngredientDao {
@SqlUpdate("CREATE TABLE IF NOT EXISTS ingredients (id INTEGER PRIMARY KEY, name VARCHAR UNIQUE NOT NULL)") @SqlUpdate("CREATE TABLE IF NOT EXISTS ingredients (id VARCHAR(128) PRIMARY KEY, name VARCHAR UNIQUE NOT NULL)")
void createTable(); void createTable();
@SqlUpdate("DROP TABLE IF EXISTS ingredients") @SqlUpdate("DROP TABLE IF EXISTS ingredients")
void dropTable(); void dropTable();
@SqlUpdate("INSERT INTO ingredients (name) VALUES (:name)") @SqlUpdate("INSERT INTO ingredients (id, name) VALUES (:id, :name)")
@GetGeneratedKeys void insert(@BindBean Ingredient ingredient);
long insert(String name);
@SqlQuery("SELECT * FROM ingredients") @SqlUpdate("DELETE FROM ingredients WHERE id = :id")
@RegisterBeanMapper(Ingredient.class) void remove(@Bind("id") UUID id);
List<Ingredient> getAll();
@SqlQuery("SELECT * FROM ingredients WHERE id = :id") @SqlQuery("SELECT * FROM ingredients WHERE name = :name")
@RegisterBeanMapper(Ingredient.class) @RegisterBeanMapper(Ingredient.class)
Ingredient findById(long id); 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);
}
~~~ ~~~
JDBI fonctionne par annotations : JDBI fonctionne par annotations :
- Les annotations `sqlUpdate` et `SqlQuery` correspondent à des - Les annotations `sqlUpdate` et `SqlQuery` correspondent à des
requêtes SQL en modification ou non. requêtes SQL en modification ou non.
- `@GetGeneratedKeys` permet de renvoyer la clé primaire générée par
la base de données.
- `@RegisterBeanMapper` permet d'associer une classe à un résultat - `@RegisterBeanMapper` permet d'associer une classe à un résultat
(les champs de la table sont associés aux propriétés du bean). (les champs de la table sont associés aux propriétés du bean).
...@@ -451,91 +437,89 @@ Nous allons utiliser le DAO pour insérer des données dans la table ...@@ -451,91 +437,89 @@ 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 afin de réaliser nos tests. Nous utiliserons une base de données de
tests qui est définie via la classe `BDDFactory`. tests qui est définie via la classe `BDDFactory`.
Les méthodes `setEnvUp` et `tearEnvDown` permettent de créer et Dans la classe `IngredientResourceTest`Les méthodes `setEnvUp` et `tearEnvDown` permettent de créer et
détruire la base de données entre chaque test. détruire la base de données entre chaque test.
~~~java ~~~java
import fr.ulille.iut.pizzaland.dao.IngredientDao; import fr.ulille.iut.pizzaland.dao.IngredientDao;
public class IngredientResourceTest extends JerseyTest { public class IngredientResourceTest extends JerseyTest {
private IngredientDao dao; private IngredientDao dao;
@Override @Override
protected Application configure() { protected Application configure() {
BDDFactory.setJdbiForTests(); BDDFactory.setJdbiForTests();
return new ApiV1(); return new ApiV1();
} }
@Before @Before
public void setEnvUp() { public void setEnvUp() {
dao = BDDFactory.buildDao(IngredientDao.class); dao = BDDFactory.buildDao(IngredientDao.class);
dao.createTable(); dao.createTable();
} }
@After @After
public void tearEnvDown() throws Exception { public void tearEnvDown() throws Exception {
dao.dropTable(); dao.dropTable();
} }
@Test @Test
public void testGetExistingIngredient() { public void testGetExistingIngredient() {
Ingredient ingredient = new Ingredient();
ingredient.setName("Chorizo");
dao.insert(ingredient);
Ingredient ingredient = new Ingredient(); Response response = target("/ingredients").path(ingredient.getId().toString()).request(MediaType.APPLICATION_JSON).get();
ingredient.setName("mozzarella");
long id = dao.insert(ingredient.getName()); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
ingredient.setId(id);
Response response = target("/ingredients/" + id).request().get(); Ingredient result = Ingredient.fromDto(response.readEntity(IngredientDto.class));
assertEquals(ingredient, result);
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); }
Ingredient result = Ingredient.fromDto(response.readEntity(IngredientDto.class));
assertEquals(ingredient, result);
}
~~~ ~~~
### La ressource avec la base de données ### La ressource avec la base de données
~~~java ~~~java
import fr.ulille.iut.pizzaland.BDDFactory; @Produces("application/json")
import fr.ulille.iut.pizzaland.dao.IngredientDao; @Path("/ingredients")
public class IngredientResource {
import java.util.stream.Collectors; private static final Logger LOGGER = Logger.getLogger(IngredientResource.class.getName());
import jakarta.ws.rs.WebApplicationException; private IngredientDao ingredients;
public class IngredientResource { @Context
private IngredientDao ingredients; public UriInfo uriInfo;
public IngredientResource() { public IngredientResource() {
ingredients = BDDFactory.buildDao(IngredientDao.class); ingredients = BDDFactory.buildDao(IngredientDao.class);
ingredients.createTable(); ingredients.createTable();
} }
@GET @GET
public List<IngredientDto> getAll() { public List<IngredientDto> getAll() {
LOGGER.info("IngredientResource:getAll"); LOGGER.info("IngredientResource:getAll");
List<IngredientDto> l = ingredients.getAll().stream().map(Ingredient::toDto).collect(Collectors.toList()); List<IngredientDto> l = ingredients.getAll().stream().map(Ingredient::toDto).collect(Collectors.toList());
LOGGER.info(l.toString());
return l; return l;
} }
@GET @GET
@Path("{id}") @Path("{id}")
public IngredientDto getOneIngredient(@PathParam("id") long id) { @Produces({ "application/json", "application/xml" })
public IngredientDto getOneIngredient(@PathParam("id") UUID id) {
LOGGER.info("getOneIngredient(" + id + ")"); LOGGER.info("getOneIngredient(" + id + ")");
try { try {
Ingredient ingredient = ingredients.findById(id); Ingredient ingredient = ingredients.findById(id);
return Ingredient.toDto(ingredient); LOGGER.info(ingredient.toString());
} return Ingredient.toDto(ingredient);
catch ( Exception e ) { } catch (Exception e) {
// Cette exception générera une réponse avec une erreur 404
throw new WebApplicationException(Response.Status.NOT_FOUND); throw new WebApplicationException(Response.Status.NOT_FOUND);
} }
} }
} }
~~~ ~~~
### Les tests fonctionnent avec la base de données ### Les tests fonctionnent avec la base de données
...@@ -543,11 +527,11 @@ Nous pouvons maintenant vérifier que la base fonctionne avec la base ...@@ -543,11 +527,11 @@ Nous pouvons maintenant vérifier que la base fonctionne avec la base
de données : de données :
~~~ ~~~
$ mvn test $ mvn test
Results : Results :
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
~~~ ~~~
## Complétons maintenant les différents tests ## Complétons maintenant les différents tests
...@@ -555,20 +539,20 @@ L'implémentation de la classe devrait fonctionner avec le test suivant ...@@ -555,20 +539,20 @@ L'implémentation de la classe devrait fonctionner avec le test suivant
: :
~~~java ~~~java
@Test @Test
public void testGetNotExistingIngredient() { public void testGetNotExistingIngredient() {
Response response = target("/ingredients/125").request().get(); Response response = target("/ingredients").path(UUID.randomUUID().toString()).request().get();
assertEquals(Response.Status.NOT_FOUND.getStatusCode(),response.getStatus()); assertEquals(Response.Status.NOT_FOUND.getStatusCode(),response.getStatus());
} }
~~~ ~~~
~~~ ~~~
$ mvn test $ mvn test
Results : Results :
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
~~~ ~~~
### Implementation de la méthode POST ### Implementation de la méthode POST
...@@ -577,40 +561,31 @@ ingrédients. Commençons par les différents tests : création, création ...@@ -577,40 +561,31 @@ 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. de deux ingrédients identiques et création d'ingrédient sans nom.
~~~java ~~~java
import fr.ulille.iut.pizzaland.dto.IngredientCreateDto; import fr.ulille.iut.pizzaland.dto.IngredientCreateDto;
@Test @Test
public void testCreateIngredient() { public void testCreateIngredient() {
IngredientCreateDto ingredientCreateDto = new IngredientCreateDto(); IngredientCreateDto ingredientCreateDto = new IngredientCreateDto();
ingredientCreateDto.setName("mozzarella"); ingredientCreateDto.setName("Chorizo");
Response response = target("/ingredients") Response response = target("/ingredients").request().post(Entity.json(ingredientCreateDto));
.request()
.post(Entity.json(ingredientCreateDto));
// On vérifie le code de status à 201
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
IngredientDto returnedEntity = response.readEntity(IngredientDto.class); IngredientDto returnedEntity = response.readEntity(IngredientDto.class);
// On vérifie que le champ d'entête Location correspond à assertEquals(target("/ingredients/" + returnedEntity.getId()).getUri(), response.getLocation());
// l'URI de la nouvelle entité
assertEquals(target("/ingredients/" +
returnedEntity.getId()).getUri(), response.getLocation());
// On vérifie que le nom correspond
assertEquals(returnedEntity.getName(), ingredientCreateDto.getName()); assertEquals(returnedEntity.getName(), ingredientCreateDto.getName());
} }
@Test @Test
public void testCreateSameIngredient() { public void testCreateSameIngredient() {
IngredientCreateDto ingredientCreateDto = new IngredientCreateDto(); Ingredient ingredient = new Ingredient();
ingredientCreateDto.setName("mozzarella"); ingredient.setName("Chorizo");
dao.insert(ingredientCreateDto.getName()); dao.insert(ingredient);
Response response = target("/ingredients") IngredientCreateDto ingredientCreateDto = Ingredient.toCreateDto(ingredient);
.request() Response response = target("/ingredients").request().post(Entity.json(ingredientCreateDto));
.post(Entity.json(ingredientCreateDto));
assertEquals(Response.Status.CONFLICT.getStatusCode(), response.getStatus()); assertEquals(Response.Status.CONFLICT.getStatusCode(), response.getStatus());
} }
...@@ -619,12 +594,11 @@ de deux ingrédients identiques et création d'ingrédient sans nom. ...@@ -619,12 +594,11 @@ de deux ingrédients identiques et création d'ingrédient sans nom.
public void testCreateIngredientWithoutName() { public void testCreateIngredientWithoutName() {
IngredientCreateDto ingredientCreateDto = new IngredientCreateDto(); IngredientCreateDto ingredientCreateDto = new IngredientCreateDto();
Response response = target("/ingredients") Response response = target("/ingredients").request().post(Entity.json(ingredientCreateDto));
.request()
.post(Entity.json(ingredientCreateDto));
assertEquals(Response.Status.NOT_ACCEPTABLE.getStatusCode(), response.getStatus()); assertEquals(Response.Status.NOT_ACCEPTABLE.getStatusCode(), response.getStatus());
} }
~~~ ~~~
Nous utiliserons un DTO spécifique `IngredientCreateDto` dans la Nous utiliserons un DTO spécifique `IngredientCreateDto` dans la
...@@ -637,67 +611,65 @@ Nous devons également fournir une implémentation de ...@@ -637,67 +611,65 @@ Nous devons également fournir une implémentation de
`IngredientCreateDto` pour pouvoir compiler notre code : `IngredientCreateDto` pour pouvoir compiler notre code :
~~~java ~~~java
package fr.ulille.iut.pizzaland.dto; package fr.ulille.iut.pizzaland.dto;
public class IngredientCreateDto { public class IngredientCreateDto {
private String name; private String name;
public IngredientCreateDto() {} public IngredientCreateDto() {}
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }
public String getName() { public String getName() {
return name; return name;
}
} }
}
~~~ ~~~
Nous pouvons maintenant compiler notre code de test et constater que Nous pouvons maintenant compiler notre code de test et constater que
ceux-ci échouent. ceux-ci échouent.
~~~ ~~~
$ mvn test $ mvn test
Results : Results :
Failed tests: testCreateSameIngredient(fr.ulille.iut.pizzaland.IngredientResourceTest): expected:<409> but was:<405> Failed tests: testCreateSameIngredient(fr.ulille.iut.pizzaland.IngredientResourceTest): expected:<409> but was:<405>
testCreateIngredientWithoutName(fr.ulille.iut.pizzaland.IngredientResourceTest): expected:<406> 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> testCreateIngredient(fr.ulille.iut.pizzaland.IngredientResourceTest): expected:<201> but was:<405>
Tests run: 6, Failures: 3, Errors: 0, Skipped: 0 Tests run: 6, Failures: 3, Errors: 0, Skipped: 0
~~~ ~~~
Nous pouvons maintenant implémenter notre méthode POST dans la Nous pouvons maintenant implémenter notre méthode POST dans la
ressource : ressource :
~~~java ~~~java
import jakarta.ws.rs.POST; import jakarta.ws.rs.POST;
import fr.ulille.iut.pizzaland.dto.IngredientCreateDto;
import fr.ulille.iut.pizzaland.dto.IngredientCreateDto;
@POST @POST
public Response createIngredient(IngredientCreateDto ingredientCreateDto) { public Response createIngredient(IngredientCreateDto ingredientCreateDto) {
Ingredient existing = ingredients.findByName(ingredientCreateDto.getName()); Ingredient existing = ingredients.findByName(ingredientCreateDto.getName());
if ( existing != null ) { if (existing != null) {
throw new WebApplicationException(Response.Status.CONFLICT); throw new WebApplicationException(Response.Status.CONFLICT);
} }
try { try {
Ingredient ingredient = Ingredient.fromIngredientCreateDto(ingredientCreateDto); Ingredient ingredient = Ingredient.fromIngredientCreateDto(ingredientCreateDto);
long id = ingredients.insert(ingredient.getName()); ingredients.insert(ingredient);
ingredient.setId(id);
IngredientDto ingredientDto = Ingredient.toDto(ingredient); IngredientDto ingredientDto = Ingredient.toDto(ingredient);
URI uri = uriInfo.getAbsolutePathBuilder().path("" + id).build(); URI uri = uriInfo.getAbsolutePathBuilder().path(ingredient.getId().toString()).build();
return Response.created(uri).entity(ingredientDto).build(); return Response.created(uri).entity(ingredientDto).build();
} } catch (Exception e) {
catch ( Exception e ) {
e.printStackTrace(); e.printStackTrace();
throw new WebApplicationException(Response.Status.NOT_ACCEPTABLE); throw new WebApplicationException(Response.Status.NOT_ACCEPTABLE);
} }
} }
~~~ ~~~
...@@ -707,14 +679,14 @@ fourni, nous devont ajouter une méthode `findbyName` à notre DAO ...@@ -707,14 +679,14 @@ fourni, nous devont ajouter une méthode `findbyName` à notre DAO
~~~java ~~~java
@SqlQuery("SELECT * FROM ingredients WHERE name = :name") @SqlQuery("SELECT * FROM ingredients WHERE name = :name")
@RegisterBeanMapper(Ingredient.class) @RegisterBeanMapper(Ingredient.class)
Ingredient findByName(String name); Ingredient findByName(@Bind("name") String name);
~~~ ~~~
Nous avons également besoin de rajouter les méthodes de conversion Nous avons également besoin de rajouter les méthodes de conversion
pour ce DTO à notre bean `Ingredient` : pour ce DTO à notre bean `Ingredient` :
~~~java ~~~java
import fr.ulille.iut.pizzaland.dto.IngredientCreateDto; import fr.ulille.iut.pizzaland.dto.IngredientCreateDto;
public static IngredientCreateDto toCreateDto(Ingredient ingredient) { public static IngredientCreateDto toCreateDto(Ingredient ingredient) {
IngredientCreateDto dto = new IngredientCreateDto(); IngredientCreateDto dto = new IngredientCreateDto();
...@@ -734,11 +706,11 @@ Nous avons également besoin de rajouter les méthodes de conversion ...@@ -734,11 +706,11 @@ Nous avons également besoin de rajouter les méthodes de conversion
Nous pouvons maintenant vérifier nos tests : Nous pouvons maintenant vérifier nos tests :
~~~ ~~~
$ mvn test $ mvn test
Results : Results :
Tests run: 6, Failures: 0, Errors: 0, Skipped: 0 Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
~~~ ~~~
Vous aurez peut-être un affichage d'exception liée au test de création Vous aurez peut-être un affichage d'exception liée au test de création
...@@ -748,39 +720,37 @@ exception qui a été traduite par un code d'erreur HTTP 406. ...@@ -748,39 +720,37 @@ exception qui a été traduite par un code d'erreur HTTP 406.
### Implémentation de la méthode DELETE ### Implémentation de la méthode DELETE
Les tests liés à la méthode DELETE sont les suivants : Les tests liés à la méthode DELETE sont les suivants :
~~~ ~~~java
@Test @Test
public void testDeleteExistingIngredient() { public void testDeleteExistingIngredient() {
Ingredient ingredient = new Ingredient(); Ingredient ingredient = new Ingredient();
ingredient.setName("mozzarella"); ingredient.setName("Chorizo");
long id = dao.insert(ingredient.getName()); dao.insert(ingredient);
ingredient.setId(id);
Response response = target("/ingredients/" + id).request().delete(); Response response = target("/ingredients/").path(ingredient.getId().toString()).request().delete();
assertEquals(Response.Status.ACCEPTED.getStatusCode(), response.getStatus()); assertEquals(Response.Status.ACCEPTED.getStatusCode(), response.getStatus());
Ingredient result = dao.findById(id); Ingredient result = dao.findById(ingredient.getId());
assertEquals(result, null); assertEquals(result, null);
} }
@Test @Test
public void testDeleteNotExistingIngredient() { public void testDeleteNotExistingIngredient() {
Response response = target("/ingredients/125").request().delete(); Response response = target("/ingredients").path(UUID.randomUUID().toString()).request().delete();
assertEquals(Response.Status.NOT_FOUND.getStatusCode(), assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
response.getStatus()); }
}
~~~ ~~~
Après avoir constaté que ces tests échouent, nous pouvons fournir une Après avoir constaté que ces tests échouent, nous pouvons fournir une
implémentation pour la méthode DELETE : implémentation pour la méthode DELETE :
~~~java ~~~java
import jakarta.ws.rs.DELETE; import jakarta.ws.rs.DELETE;
@DELETE @DELETE
@Path("{id}") @Path("{id}")
public Response deleteIngredient(@PathParam("id") long id) { public Response deleteIngredient(@PathParam("id") UUID id) {
if ( ingredients.findById(id) == null ) { if ( ingredients.findById(id) == null ) {
throw new WebApplicationException(Response.Status.NOT_FOUND); throw new WebApplicationException(Response.Status.NOT_FOUND);
} }
...@@ -795,8 +765,8 @@ Nous devons également implémenter la méthode remove dans ...@@ -795,8 +765,8 @@ Nous devons également implémenter la méthode remove dans
`IngredientDao` : `IngredientDao` :
~~~java ~~~java
@SqlUpdate("DELETE FROM ingredients WHERE id = :id") @SqlUpdate("DELETE FROM ingredients WHERE id = :id")
void remove(long id); void remove(@Bind("id") UUID id);
~~~ ~~~
Avec cette implémentation, nos tests réussissent. Avec cette implémentation, nos tests réussissent.
...@@ -806,22 +776,22 @@ Commençons par les tests correspondant à cette URI (GET ...@@ -806,22 +776,22 @@ Commençons par les tests correspondant à cette URI (GET
/ingredients/{id}/name) /ingredients/{id}/name)
~~~java ~~~java
@Test @Test
public void testGetIngredientName() { public void testGetIngredientName() {
Ingredient ingredient = new Ingredient(); Ingredient ingredient = new Ingredient();
ingredient.setName("mozzarella"); ingredient.setName("Chorizo");
long id = dao.insert(ingredient.getName()); dao.insert(ingredient);
Response response = target("ingredients/" + id + "/name").request().get(); Response response = target("ingredients").path(ingredient.getId().toString()).path("name").request().get();
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
assertEquals("mozzarella", response.readEntity(String.class)); assertEquals("Chorizo", response.readEntity(String.class));
} }
@Test @Test
public void testGetNotExistingIngredientName() { public void testGetNotExistingIngredientName() {
Response response = target("ingredients/125/name").request().get(); Response response = target("ingredients").path(UUID.randomUUID().toString()).path("name").request().get();
assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
} }
...@@ -830,15 +800,16 @@ Commençons par les tests correspondant à cette URI (GET ...@@ -830,15 +800,16 @@ Commençons par les tests correspondant à cette URI (GET
L'implémentation correspondant à ce test est simple : L'implémentation correspondant à ce test est simple :
~~~java ~~~java
@GET @GET
@Path("{id}/name") @Path("{id}/name")
public String getIngredientName(@PathParam("id") long id) { public String getIngredientName(@PathParam("id") UUID id) {
Ingredient ingredient = ingredients.findById(id); Ingredient ingredient = ingredients.findById(id);
if ( ingredient == null ) {
throw new WebApplicationException(Response.Status.NOT_FOUND); if (ingredient == null) {
} throw new WebApplicationException(Response.Status.NOT_FOUND);
}
return ingredient.getName();
return ingredient.getName();
} }
~~~ ~~~
...@@ -850,24 +821,19 @@ formulaire Web. Dans ce cas, le type de représentation sera ...@@ -850,24 +821,19 @@ formulaire Web. Dans ce cas, le type de représentation sera
On peut déjà préparer un test pour cette méthode de création : On peut déjà préparer un test pour cette méthode de création :
~~~java ~~~java
import jakarta.ws.rs.core.MediaType; @Test
import jakarta.ws.rs.core.Form;
import static org.junit.Assert.assertNotNull;
@Test
public void testCreateWithForm() { public void testCreateWithForm() {
Form form = new Form(); Form form = new Form();
form.param("name", "chorizo"); form.param("name", "chorizo");
Entity<Form> formEntity = Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE); Entity<Form> formEntity = Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE);
Response response = target("ingredients").request().post(formEntity); Response response = target("ingredients").request().post(formEntity);
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
String location = response.getHeaderString("Location"); String location = response.getHeaderString("Location");
long id = Integer.parseInt(location.substring(location.lastIndexOf('/') + 1)); String id = location.substring(location.lastIndexOf('/') + 1);
Ingredient result = dao.findById(id); Ingredient result = dao.findById(UUID.fromString(id));
assertNotNull(result); assertNotNull(result);
} }
~~~ ~~~
...@@ -875,27 +841,26 @@ On peut déjà préparer un test pour cette méthode de création : ...@@ -875,27 +841,26 @@ On peut déjà préparer un test pour cette méthode de création :
On peut maintenant fournir une implémentation pour cette méthode : On peut maintenant fournir une implémentation pour cette méthode :
~~~java ~~~java
@POST @POST
@Consumes("application/x-www-form-urlencoded") @Consumes("application/x-www-form-urlencoded")
public Response createIngredient(@FormParam("name") String name) { public Response createIngredient(@FormParam("name") String name) {
Ingredient existing = ingredients.findByName(name); Ingredient existing = ingredients.findByName(name);
if ( existing != null ) { if (existing != null) {
throw new WebApplicationException(Response.Status.CONFLICT); throw new WebApplicationException(Response.Status.CONFLICT);
} }
try { try {
Ingredient ingredient = new Ingredient(); Ingredient ingredient = new Ingredient();
ingredient.setName(name); ingredient.setName(name);
long id = ingredients.insert(ingredient.getName()); ingredients.insert(ingredient);
ingredient.setId(id);
IngredientDto ingredientDto = Ingredient.toDto(ingredient); IngredientDto ingredientDto = Ingredient.toDto(ingredient);
URI uri = uriInfo.getAbsolutePathBuilder().path("" + id).build(); URI uri = uriInfo.getAbsolutePathBuilder().path("" + ingredient.getId()).build();
return Response.created(uri).entity(ingredientDto).build(); return Response.created(uri).entity(ingredientDto).build();
} } catch (Exception e) {
catch ( Exception e ) {
e.printStackTrace(); e.printStackTrace();
throw new WebApplicationException(Response.Status.NOT_ACCEPTABLE); throw new WebApplicationException(Response.Status.NOT_ACCEPTABLE);
} }
...@@ -913,68 +878,72 @@ Quand cette variable aura la valeur `withdb`, nous allons remplir la ...@@ -913,68 +878,72 @@ Quand cette variable aura la valeur `withdb`, nous allons remplir la
base au démarrage avec le code suivant : base au démarrage avec le code suivant :
~~~java ~~~java
import fr.ulille.iut.pizzaland.beans.Ingredient; package fr.ulille.iut.pizzaland;
import fr.ulille.iut.pizzaland.dao.IngredientDao;
import java.io.FileReader; import org.glassfish.jersey.server.ResourceConfig;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import jakarta.json.bind.Jsonb; import fr.ulille.iut.pizzaland.beans.Ingredient;
import jakarta.json.bind.JsonbBuilder; 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;
@ApplicationPath("api/v1/") import java.io.FileReader;
public class ApiV1 extends ResourceConfig { import java.util.ArrayList;
import java.util.List;
public ApiV1() { import java.util.UUID;
packages("fr.ulille.iut.pizzaland"); 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");
String environment = System.getenv("PIZZAENV"); String environment = System.getenv("PIZZAENV");
if ( environment != null && environment.equals("withdb") ) { if ( environment != null && environment.equals("withdb") ) {
LOGGER.info("Loading with database"); LOGGER.info("Loading with database");
Jsonb jsonb = JsonbBuilder.create(); try {
try { FileReader reader = new FileReader( getClass().getClassLoader().getResource("ingredients.json").getFile() );
FileReader reader = new FileReader( getClass().getClassLoader().getResource("ingredients.json").getFile() ); List<Ingredient> ingredients = JsonbBuilder.create().fromJson(reader, new ArrayList<Ingredient>(){}.getClass().getGenericSuperclass());
List<Ingredient> ingredients = JsonbBuilder.create().fromJson(reader, new ArrayList<Ingredient>(){}.getClass().getGenericSuperclass());
IngredientDao ingredientDao = BDDFactory.buildDao(IngredientDao.class);
IngredientDao ingredientDao = BDDFactory.buildDao(IngredientDao.class); ingredientDao.dropTable();
ingredientDao.dropTable(); ingredientDao.createTable();
ingredientDao.createTable(); for ( Ingredient ingredient: ingredients) {
for ( Ingredient ingredient: ingredients) { ingredientDao.insert(ingredient);
ingredientDao.insert(ingredient.getName()); }
} catch ( Exception ex ) {
throw new IllegalStateException(ex);
} }
} catch ( Exception ex ) {
throw new IllegalStateException(ex);
}
} }
}
} }
}
~~~ ~~~
Dans un terminal, nous pouvons maintenant fixer la variable Dans un terminal, nous pouvons maintenant fixer la variable
d'environnemnet et démarrer notre serveur REST au moyen de la d'environnemnet et démarrer notre serveur REST au moyen de la
commande `mvn jetty:run` : commande `mvn exec:java` :
~~~ ~~~
$ export PIZZAENV=withdb $ export PIZZAENV=withdb
$ mvn exec:java $ mvn exec:java
~~~ ~~~
Dans un autre terminal, nous pouvons utiliser `curl` pour tester nos Dans un autre terminal, nous pouvons utiliser `curl` pour tester nos
différentes méthodes : différentes méthodes :
~~~ ~~~
$ curl -i localhost:8080/api/v1/ingredients $ curl -i localhost:8080/api/v1/ingredients
HTTP/1.1 200 OK HTTP/1.1 200 OK
Date: Sun, 09 Feb 2020 22:08:05 GMT Content-Type: application/json
Content-Type: application/json Content-Length: 760
Content-Length: 319
Server: Jetty(9.4.26.v20200117)
[{"id":1,"name":"mozzarella"},{"id":2,"name":"jambon"},{"id":3,"name":"champignons"},{"id":4,"name":"olives"},{"id":5,"name":"tomate"},{"id":6,"name":"merguez"},{"id":7,"name":"lardons"},{"id":8,"name":"fromage"},{"id":9,"name":"oeuf"},{"id":10,"name":"poivrons"},{"id":11,"name":"ananas"},{"id":12,"name":"reblochon"}] [{"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 # Implémentation de la ressource Pizza
......
...@@ -3,28 +3,37 @@ package fr.ulille.iut.pizzaland; ...@@ -3,28 +3,37 @@ package fr.ulille.iut.pizzaland;
import java.sql.DatabaseMetaData; import java.sql.DatabaseMetaData;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList;
import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.Jdbi; import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.sqlite3.SQLitePlugin; import org.jdbi.v3.sqlite3.SQLitePlugin;
import org.jdbi.v3.sqlobject.SqlObjectPlugin; import org.jdbi.v3.sqlobject.SqlObjectPlugin;
import fr.ulille.iut.pizzaland.dao.UUIDArgumentFactory;
public class BDDFactory { public class BDDFactory {
private static Jdbi jdbi = null; private static Jdbi jdbi = null;
private static String dbPath = "jdbc:sqlite:" + System.getProperty("java.io.tmpdir") private static String dbPath = "jdbc:sqlite:"
+ System.getProperty("file.separator") + System.getProperty("user.name") + "_"; + System.getProperty("java.io.tmpdir")
+ System.getProperty("file.separator")
+ System.getProperty("user.name")
+ "_";
public static Jdbi getJdbi() { public static Jdbi getJdbi() {
if (jdbi == null) { if ( jdbi == null ) {
jdbi = Jdbi.create(dbPath + "pizza.db").installPlugin(new SQLitePlugin()) jdbi = Jdbi.create(dbPath + "pizzas.db")
.installPlugin(new SqlObjectPlugin()); .installPlugin(new SQLitePlugin())
.installPlugin(new SqlObjectPlugin()).registerArgument(new UUIDArgumentFactory());
} }
return jdbi; return jdbi;
} }
public static void setJdbiForTests() { public static void setJdbiForTests() {
if (jdbi == null) { if ( jdbi == null ) {
jdbi = Jdbi.create(dbPath + "pizza_test.db").installPlugin(new SQLitePlugin()) jdbi = Jdbi.create(dbPath + "pizza_test.db")
.installPlugin(new SqlObjectPlugin()); .installPlugin(new SQLitePlugin())
.installPlugin(new SqlObjectPlugin());
} }
} }
...@@ -36,6 +45,17 @@ public class BDDFactory { ...@@ -36,6 +45,17 @@ public class BDDFactory {
return exist; return exist;
} }
public static void dropTables() throws SQLException {
Handle handle = getJdbi().open();
DatabaseMetaData dbm = handle.getConnection().getMetaData();
ResultSet tables = dbm.getTables(null, null, "", null);
ArrayList<String> tableNames = new ArrayList<String>();
while ( tables.next() ) {
tableNames.add(tables.getString("TABLE_NAME"));
}
tableNames.forEach(name -> handle.execute("drop table " + name));
}
public static <T> T buildDao(Class<T> daoClass) { public static <T> T buildDao(Class<T> daoClass) {
return getJdbi().onDemand(daoClass); return getJdbi().onDemand(daoClass);
} }
......
...@@ -8,7 +8,7 @@ import org.jdbi.v3.core.argument.Argument; ...@@ -8,7 +8,7 @@ import org.jdbi.v3.core.argument.Argument;
import org.jdbi.v3.core.config.ConfigRegistry; import org.jdbi.v3.core.config.ConfigRegistry;
public class UUIDArgumentFactory extends AbstractArgumentFactory<UUID> { public class UUIDArgumentFactory extends AbstractArgumentFactory<UUID> {
UUIDArgumentFactory() { public UUIDArgumentFactory() {
super(Types.VARCHAR); super(Types.VARCHAR);
} }
......
...@@ -27,6 +27,6 @@ public class IngredientResource { ...@@ -27,6 +27,6 @@ public class IngredientResource {
public List<IngredientDto> getAll() { public List<IngredientDto> getAll() {
LOGGER.info("IngredientResource:getAll"); LOGGER.info("IngredientResource:getAll");
return null; return new ArrayList<IngredientDto>();
} }
} }
...@@ -59,7 +59,7 @@ public class IngredientResourceTest extends JerseyTest { ...@@ -59,7 +59,7 @@ public class IngredientResourceTest extends JerseyTest {
// On vérifie la valeur retournée (liste vide) // On vérifie la valeur retournée (liste vide)
// L'entité (readEntity() correspond au corps de la réponse HTTP. // L'entité (readEntity() correspond au corps de la réponse HTTP.
// La classe jakarta.ws.rs.core.GenericType<T> permet de définir le type // La classe jakarta.ws.rs.core.GenericType<T> permet de définir le type
// de la réponse lue quand on a un type complexe (typiquement une liste). // de la réponse lue quand on a un type paramétré (typiquement une liste).
List<IngredientDto> ingredients; List<IngredientDto> ingredients;
ingredients = response.readEntity(new GenericType<List<IngredientDto>>() { ingredients = response.readEntity(new GenericType<List<IngredientDto>>() {
}); });
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment