Newer
Older
# Tutoriel REST - premier développement d'une ressource
Pour ce premier TP REST, nous allons voir les principaux éléments du développement d'une ressource :
- POJO annoté
- Objets de transfert (Data Transfer Object - DTO)
- Gestion des représentations (JSON ou XML)
- Gestion du cache (ETag)
## Mise en place de l'environnement
Le développement sera basé sur [`Jersey`](https://eclipse-ee4j.github.io/jersey/) qui fournit une implémentation de référence de JAX-RS. Vous trouverez dans la [documentation](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest3x/index.html) l'utilisation des annotation standards ainsi que les aspects spécifiques de la plate-forme.
Le développement avec Jersey implique l'utilisation de `Maven` pour la gestion des phases de développement. Maven va télécharger les librairies nécessaires depuis un dépôt extérieur. Il faut donc le configurer pour passer par le proxy quand vous êtes en salle de TP.
Dans le répertoire `~/.m2/` (a créer si nécessaire), créez le fichier 'settings.xml' avec le contenu suivant :
~~~xml
<settings>
<proxies>
<proxy>
<id>ulille-proxy</id>
<active>true</active>
<protocol>http</protocol>
<host>cache.univ-lille.fr</host>
<port>3128</port>
</proxy>
<proxy>
<id>lille1-proxy-sec</id>
<active>true</active>
<protocol>https</protocol>
<host>cache.univ-lille.fr</host>
<port>3128</port>
</proxy>
</proxies>
</settings>
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
Le projet que vous avez récupéré contient le fichier de configuration `pom.xml`. Le projet est actuellement configuré pour utiliser java 11 :
~~~xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<inherited>true</inherited>
<configuration>
<release>11</release>
</configuration>
</plugin>
~~~
## Le code récupéré
L'arborescence de source contient les fichiers suivants :
~~~
src
├── main
│ └── java
│ └── fr
│ └── ulille
│ └── iut
│ └── tva
│ ├── DebugMapper.java
│ ├── dto
│ ├── Main.java
│ ├── ressource
│ │ └── TvaRessource.java
│ └── service
│ ├── CalculTva.java
│ └── TauxTva.java
└── test
└── java
└── fr
└── ulille
└── iut
└── tva
~~~
Pour ce TP, nous n'utiliserons pas l'arborescence de tests (ça sera pour la prochaine fois :-)). Il n'y aura pas de base de données à gérer afin de pouvoir nous concentrer sur la mise ne place d'une ressource REST.
Le service que nous allons exposer sous forme de ressource REST permet de faire des calculs de TVA. La définition des différents taux se trouve dans la classe [`TauxTva`](src/main/java/fr/ulille/iut/tva/service/TauxTva.java) et les différents calculs possibles dans la classe [`CalculTva`](src/main/java/fr/ulille/iut/tva/service/CalculTva.java).
Le serveur Web qui hébergera notre ressource est lancé directement dans la classe [`Main`](src/main/java/fr/ulille/iut/tva/Main.java). Cette classe met également en place l'environnement Jersey qui va rechercher dans les paquetages définis les ressources disponibles.
Le développement de la ressource se fera dans la classe [`TvaRessource`](src/main/java/fr/ulille/iut/tva/ressource/TvaRessource.java).
## Développement de la ressources
### Une première personnalisation
Pour l'instant, les ressources que nous allons développer seront disponibles à partir de l'URI suivante : `http://localhost:8080/myapp/`. Modifiez la classe `Main` de manière à ce que l'URI utilisée soit `http://localhost:8080/api/v1` correspondant à notre première version de l'API (Pour en savoir plus sur la façon de gérer les changements de version d'une API REST, vous pouvez consulter cet [article](https://medium.com/neoxia/rest-api-design-les-best-practices-conseill%C3%A9es-par-neoxia-1442e99d8671)).
Profiter en pour lire les commentaires dans le code de cette classe. Cela pourra vous servir plus tard...
### La première méthode
#### Implémentation
Nous allons éditer la classe [`TvaRessource`](src/main/java/fr/ulille/iut/tva/ressource/TvaRessource.java). Cette ressource devra être accessible via l'URI suivante : `http://localhost:8080/api/v1/tva`. Pour cela, vous devez annoter la classe avec [`@Path`](https://eclipse-ee4j.github.io/jaxrs-api/apidocs/3.0.0/jakarta/ws/rs/Path.html) :
~~~java
@Path("tva")
public class TvaRessource {
}
~~~
Une classe annotée avec `@Path` sera reconnue automatiquement comme une ressource REST par Jersey. Le chemin indiqué ici est relatif, il sera combiné avec l'URI définie dans la classe `Main`.
Nous allons pouvoir fournir dans cette classe une première méthode Java accessible via une requête HTTP GET. Nous utiliserons pour cela l'annotation [@GET](https://eclipse-ee4j.github.io/jaxrs-api/apidocs/3.0.0/jakarta/ws/rs/GET.html)
public double getValeurTauxParDefaut() {
return TauxTva.NORMAL.taux;
}
~~~
#### Test
Pour tester notre ressource, nous allons utiliser Maven pour compiler et lancer le serveur avec la commande `mvn compile exec:java` dans un terminal.
Dans un autre terminal, utilisez `curl` pour accéder à votre ressource et tester cette nouvelle méthode.
Vous devriez obtenir une réponse de ce type :
~~~
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 4
20.0
~~~
On peut remarquer que comme votre code a renvoyé un résultat sans lever d'erreur, Jersey a automatiquement :
- renvoyé le statut `200 OK`;
- choisi le type MIME le plus adapté pour le résultat renvoyé dans le corps de la réponse : `Content-Type: text/plain`;
- mis dans le corps de la réponse le résultat de votre méthode.
La requête HTTP peut varier notamment selon le chemin de l'URI ([@PathParam](https://eclipse-ee4j.github.io/jaxrs-api/apidocs/3.0.0/jakarta/ws/rs/PathParam.html)) ou les paramètres de requête ([@QueryParam](https://eclipse-ee4j.github.io/jaxrs-api/apidocs/3.0.0/jakarta/ws/rs/QueryParam.html))
La méthode ci-dessous renvoie la valeur du taux pour un niveau de TVA donné. Ce niveau de TVA (REDUIT, NORMAL, etc.) constitue une partie variable de l'URI qui sera notée entre `{}`. Le paramètre de la méthode est associé à cette partie du chemin grâce à l'annotation `@PathParam`.
~~~java
@GET
public double getValeurTaux(@PathParam("niveauTva") String niveau) {
}
~~~
Ajoutez cette méthode à votre classe puis testez la avec `curl`.
Ajoutez (et testez) à votre ressource une méthode `getMontantTotal` qui renverra le montant tva comprise pour un niveau de tva indiqué sur le chemin d'URI et une somme définie en paramètre de requête (par ex. `http://localhost:8080/api/v1/tva/reduit?somme=100`).
### Gérer les erreurs
Notre ressource renvoie correctement les informations demandées... tant que nos requêtes sont correctes. Essayez cette requête : `curl -i http://localhost:8080/api/v1/tva/valeur/inexistant`.
Le résultat n'est pas terrible. Pour gérer cela proprement, nous allons devoir renvoyer une erreur qui a du sens pour le client. Nous allons modifier la méthode `getValeurTaux` de manière à réagir à un niveau de TVA inconnu.
~~~java
public float getValeurTaux(@PathParam("niveauTva") String niveau) {
try {
return TauxTva.valueOf(niveau.toUpperCase()).taux;
}
catch ( Exception ex ) {
throw new NiveauTvaInexistantException();
}
}
~~~
Il nous faut ensuite définir la nouvelle exception utilisée de cette façon :
~~~java
package fr.ulille.iut.tva.ressource;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;
public class NiveauTvaInexistantException extends WebApplicationException {
private static final long serialVersionUID = 939875418210403804L;
public NiveauTvaInexistantException() {
super(Response.status(Response.Status.NOT_ACCEPTABLE).entity("Niveau de TVA inexistant").build());
}
~~~
On aurait pu se contenter de lever `NotAcceptableException` directement dans la méthode `getValeurTaux`, ce qui aurait renvoyé un code 406. La création de cette exception va nous permettre d'enrichir la réponse et notamment ici d'ajouter un message d'erreur dans le corps (_entity_) de la réponse.
Modifiez également la méthode `getMontantTotal` de manière à gérer l'erreur.
### Renvoyer des données plus complexes
Jusqu'à maintenant, nous avons transmis de simples valeurs numériques prises en charge directement par Jersey dans le corps de la réponse HTTP. Jersey peut faire la même chose avec des classes Java qui respectent les conventions [`JavaBean`](https://fr.wikipedia.org/wiki/JavaBeans), à minima :
- la classe possède un constructeur sans paramètres (constructeur par défaut);
- les propriétés de la classe sont associées à des accesseurs `get`/`set` pour chaque attribut (par. ex pour un attribut `name` --> `getName()`/`setName()`).
Pour un objet de ce type, Jersey va renvoyer dans le corps de la requête un objet (JSON par défaut). Chaque propriété de l'objet java sera représenté par une propriété de l'objet JSON.
Ces objets Java destinés à représenter les données échangées s'appellent des `Data Transfer Object` ou `DTO` (ils peuvent bien sûr également être transmis par le client lors d'une requête `POST` par exemple).
Nous allons ajouter une méthode permettant au client d'avoir la liste des niveaux de TVA avec le taux associé. Pour cela, nous allons d'abord définir un DTO pour représenter cela dans le paquetage `fr.ulille.iut.tva.dto`.
~~~java
package fr.ulille.iut.tva.dto;
private String label;
private double taux;
public InfoTauxDto(String label, double taux) {
this.label = label;
this.taux = taux;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public double getTaux() {
return taux;
}
public void setTaux(double taux) {
this.taux = taux;
}
}
~~~
Nous pouvons maintenant utiliser ce DTO pour renvoyer la liste des taux existants au client :
@Path("lestaux")
public List<InfoTauxDto> getInfoTaux() {
ArrayList<InfoTauxDto> result = new ArrayList<InfoTauxDto>();
for ( TauxTva t : TauxTva.values() ) {
result.add(new InfoTauxDto(t.name(), t.taux));
}
return result;
}
~~~
Un test de cette nouvelle méthode devrait renvoyer le résultat suivant :
~~~
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 135
[{"label":"NORMAL","taux":20.0},{"label":"INTERMEDIAIRE","taux":10.0},{"label":"REDUIT","taux":5.5},{"label":"PARTICULIER","taux":2.1}]
~~~
On peut observer que :
- pour chaque propriété de la classe `InfoTauxDto`, on a une propriété équivalente dans les objets JSON
- la liste renvoyée en Java a été convertie en tableau d'objets JSON
- Jersey a choisi le type MIME le plus adapté pour le résultat renvoyé dans le corps de la réponse : `Content-Type: application/json`
Vous pouvez maintenant ajouter une méthode `getDetail` qui répondra à une URI de ce type : `http://localhost:8080/api/v1/tva/details/{taux}?somme={valeur}` où `taux` correspond à un des niveaux de taux et `valeur` correspond à la somme utilisée pour le calcul.
Le résultat d'une requête sur cette URI ressemblera à cela :
~~~
$ curl -i http://localhost:8080/api/v1/tva/details/normal?somme=100
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 92
{"montantTotal":120.0,"montantTva":20.0,"somme":100.0,"tauxLabel":"NORMAL","tauxValue":20.0}
~~~
### Et les autres formats de données ?
Essayez de faire la requête suivante : `curl -i -H "Accept: application/xml" http://localhost:8080/api/v1/tva/lestaux`. Quel résultat obtenez vous ?
Du côté du serveur, vous devriez observer l'erreur suivante :
~~~
GRAVE: MessageBodyWriter not found for media type=application/xml, type=class java.util.ArrayList, genericType=java.util.List<fr.ulille.iut.tva.dto.InfoTauxDto>
~~~
Ce message indique que Jersey ne sait pas transformer la liste Java dans une représentation `application/xml`. `MessageBodyWriter` est la classe qui devrait être chargée de faire cette transformation (Elle le fait pour JSON).
Pour gérer XML, il faut d'abord s'assurer d'avoir les bonnes libraires dans le projet maven (c'est déjà le cas, ici) :
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
~~~xml
<dependencies>
...
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>3.0.0</version>
<scope>runtime</scope>
</dependency>
...
</dependencies>
~~~
Ces librairies vont nous permettre d'utiliser [JAXB (Java Architecture for XML Binding)](https://javaee.github.io/jaxb-v2/) qui offre les moyens de définir une transformation entre Java et XML. `JAXB` utilise notamment un ensemble d'annotations qui permettent de spécifier comment les attributs d'une classe seront transformés en XML (et inversement).
Ici, nous allons utiliser tous les comportements par défaut de JAXB et nous n'auront besoin que d'une annotation sur notre DTO :
~~~java
import jakarta.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class InfoTauxDto {
...
}
~~~
Nous pouvons maintenant ressayer notre requête précédente avec succès :
~~~xml
HTTP/1.1 200 OK
Content-Type: application/xml
Content-Length: 356
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><infoTauxDtoes><infoTauxDto><label>NORMAL</label><taux>20.0</taux></infoTauxDto><infoTauxDto><label>INTERMEDIAIRE</label><taux>10.0</taux></infoTauxDto><infoTauxDto><label>REDUIT</label><taux>5.5</taux></infoTauxDto><infoTauxDto><label>PARTICULIER</label><taux>2.1</taux></infoTauxDto></infoTauxDtoes>
~~~
Refaisons notre requête par défaut par acquis de conscience : `curl -i http://localhost:8080/api/v1/tva/lestaux`. Que se passe-t-il ?
Il semble que du coup, notre ressource réponde en priorité en XML, vérifiez qu'elle sait toujours répondre en JSON.
Pour régler ce problème, nous allons spécifier les représentations possibles avec un ordre de préférence (plutôt du JSON) au moyen de l'annotation [@Produces](https://eclipse-ee4j.github.io/jaxrs-api/apidocs/3.0.0/jakarta/ws/rs/Produces.html) :
~~~java
@GET
@Path("lestaux")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public List<InfoTauxDto> getInfoTaux() {
...
}
~~~
Faites en sorte que la méthode `getDetail` puisse également renvoyer du JSON (de préférence) ou de l'XML.