Skip to content
Snippets Groups Projects
README.md 14.9 KiB
Newer Older
Yvan Peter's avatar
Yvan Peter committed
# 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.
Yvan Peter's avatar
Yvan Peter committed

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>
Yvan Peter's avatar
Yvan Peter committed

## Configuration de Maven
Yvan Peter's avatar
Yvan Peter committed
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).
Yvan Peter's avatar
Yvan Peter committed

## Développement de la ressources

### Une première personnalisation
Yvan Peter's avatar
Yvan Peter committed
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)).
Yvan Peter's avatar
Yvan Peter committed

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`.

Yvan Peter's avatar
Yvan Peter committed
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)
Yvan Peter's avatar
Yvan Peter committed

~~~java
@GET
@Path("tauxpardefaut")
public double getValeurTauxParDefaut() {
Yvan Peter's avatar
Yvan Peter committed
        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.

Yvan Peter's avatar
Yvan Peter committed
Dans un autre terminal, utilisez `curl` pour accéder à votre ressource et tester cette nouvelle méthode.
Yvan Peter's avatar
Yvan Peter committed

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.
Yvan Peter's avatar
Yvan Peter committed

### On joue avec les paramètres de requête
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))
Yvan Peter's avatar
Yvan Peter committed

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
Yvan Peter's avatar
Yvan Peter committed
@Path("valeur/{niveauTva}")
public double getValeurTaux(@PathParam("niveauTva") String niveau) {
Yvan Peter's avatar
Yvan Peter committed
  return TauxTva.valueOf(niveau.toUpperCase()).taux;
Yvan Peter's avatar
Yvan Peter committed
}
~~~

Ajoutez cette méthode à votre classe puis testez la avec `curl`.
Yvan Peter's avatar
Yvan Peter committed

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`).

Yvan Peter's avatar
Yvan Peter committed
### 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();
        }
    }

~~~

Yvan Peter's avatar
Yvan Peter committed
Il nous faut ensuite définir la nouvelle exception utilisée de cette façon :
Yvan Peter's avatar
Yvan Peter committed

~~~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.

Yvan Peter's avatar
Yvan Peter committed
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;

public class InfoTauxDto {
    private String label;
    private double taux;
    
    public InfoTauxDto() {}
    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;
    }
}
~~~

Yvan Peter's avatar
Yvan Peter committed
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));
Yvan Peter's avatar
Yvan Peter committed
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
Yvan Peter's avatar
Yvan Peter committed
  - 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}``taux` correspond à un des niveaux de taux et `valeur` correspond à la somme utilisée pour le calcul.
Yvan Peter's avatar
Yvan Peter committed

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).

Yvan Peter's avatar
Yvan Peter committed
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) :

~~~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.