UP | HOME

TP3 - Partie 1 - Une bibliothèque - Mobilité des canaux

Table of Contents

1 Introduction - Liens comme ressources, ressources comme liens,

On s'intéresse aux hyperliens et à leur utilisation pour contrôler l'état d'une application, suivant le principe HATEOAS ("Hypermedia as the Engine of the Application State"). Un hyperlien correspond à un canal de communication. Le fait de pouvoir transmettre des hyperliens garantit alors la mobilité des canaux : elle permet de faire évoluer la topologie des connexions entre les agents et ainsi de produire un contrôle réparti du flot d'exécution d'une application.

Exemple typique : la découverte de services

  • Agents
    • Un serveur rendant service sur un canal
    • Un client cherchant un service
    • Un annuaire regroupant des canaux associés à des services
  • Protocole
    • Le serveur envoie un canal fournissant un service à l'annuaire, qui l'enregistre dans sa base.
    • Le client demande à l'annuaire un canal pour un service.
    • L'annuaire envoie au client un canal fournissant le service.
    • Le client utilise le canal pour obtenir le service.

On développe ci-dessous un exemple simple d'une bibliothèque pour illustrer l'intérêt de la mobilité des canaux. On commence par ajouter des livres à une bibliothèque; pour chaque livre ajouté, on reçoit un hyperlien pointant vers lui. Cet hyperlien peut ensuite être diffusé.

2 Bibliothèque : les règles chimiques

2.1 Serveur bibliothèque

Canaux (décrits par une méthode HTTP et un chemin, accompagnés de leurs arguments) :

  • POST@bibliotheque(livre, canalDeRetour) : ajout d'un livre à la bibliothèque
  • GET@bibliotheque/id(canalDeRetour) : accès à la sous-ressource livre d'identifiant id
  • GET@bibliotheque/id/titre(canalDeRetour) : titre du livre d'identifiant id
  • GET@bibliotheque(livre, canalDeRetour) : recherche du livre décrit par livre renvoyant un canal pointant vers le livre ou une erreur
  • GET@bibliotheque/catalogue(canalDeRetour) : catalogue de la bibliothèque défini par une liste de canaux pointant vers les livres

Etat du serveur :

  • Compteur(id) : compteur servant à identifier les livres
  • Catalogue(liste) : liste des canaux pointant vers les livres
  • Base(id, livre) : base de données contenant des couples (identifiant, livre)

On décrit un livre par un n-uplet (titre, …). On note :: le constructeur infixe de liste : (element :: liste).

2.2 Ajout d'un livre

POST@bibliotheque(livre, ar) & Compteur(id) & Catalogue(liste)
-> 
ar(bibliotheque/id) & Base(id, livre) & Catalogue((bibliotheque/id) :: liste) & Compteur(id+1)

2.3 Récupération d'un livre

// Cas d'un livre présent dans la base
GET@bibliotheque/id(ar) & Base(id, livre) -> ar(livre) & Base(id, livre)
// Cas d'un livre absent de la base
GET@bibliotheque/id(ar) & (Base(id, _) inactive) -> ar(ABSENT)

2.4 Récupération du titre d'un livre

// Cas d'un livre présent dans la base
GET@bibliotheque/id/titre(ar) & Base(id, (titreLivre, _)) 
  -> ar(titreLivre) & Base(id, (titreLivre, _))
// Cas d'un livre absent de la base
GET@bibliotheque/id/titre(ar) & (Base(id, _) inactive) -> ar(ABSENT)

2.5 Recherche d'un livre à partir d'une description

On suppose l'existence d'un prédicat desc décrit l pour vérifier qu'une description desc décrit le livre l. Pour simplifier, la recherche produit un libre vérifiant la description plutôt qu'une liste.

// Cas d'un livre présent dans la base
GET@bibliotheque(desc, ar) & Base(id, livre) & (desc décrit livre) 
  -> ar(bibliotheque/id) & Base(id, livre)
// Cas d'un livre absent de la base
GET@bibliotheque(desc, ar) & (Base(_, livre) & (desc décrit livre) inactive) 
  -> ar(ABSENT)

2.6 Catalogue

GET@bibliotheque/catalogue(ar) & Catalogue(liste) -> ar(liste) & Catalogue(liste)

3 Implémentation en Java

3.1 Le modèle Objet

Le modèle Objet, fourni, contient cinq interfaces principales, ainsi que leur implémentation :

  • Bibliothèque, décrivant les services de recherche de livre et de production de catalogue,
  • Archive, décrivant les sous-ressources livresques de la bibliothèque,
  • BibliothèqueArchive, héritière de Bibiliotheque et Archive, correspondant à la ressource principale,
  • Livre, décrivant non seulement le type de données représentant les livres de la bibliothèque mais aussi des sous-ressources des archives.

Le livre est simplement décrit par un titre : il pourrait posséder d'autres attributs propres à la bibliothèque comme une cote.

Pour représenter les hyperliens, deux classes génériques sont fournies :

  • Hyperlien<T> : hyperlien vers une ressource de type T,
  • Hyperliens<T> : liste d'hyperliens vers des ressources de type T.

Le type T ne sert qu'au typage. Ces classes sont des variantes de la classe abstraite Link, qui sert à représenter des hyperliens dans JAX-RS 2.0. En les faisant dépendre d'un type, elles fournissent une information utile, le type de la ressource vers laquelle l'hyperlien pointe. C'est pourquoi ces classes génériques sont préférées à celle de la norme.

3.2 Serveur

Créer un nouveau projet, de type Dynamic Web Projet.

  • Ajouter les dépendances suivantes dans le fichier pom.xml. La première permet d'importer Jersey, la seconde permet d'utiliser le format de données JSON. Par défaut, seul le format XML est géré. La troisième fournit un injecteur de dépendances. Enfin, la dernière permet d'utiliser le serveur HTTP Grizzly, à la fois léger et passant à l'échelle.

    <dependencies>
          <dependency>
                  <groupId>org.glassfish.jersey.containers</groupId>
                  <artifactId>jersey-container-servlet</artifactId>
                  <version>2.28</version>
          </dependency>
          <dependency>
                  <groupId>org.glassfish.jersey.media</groupId>
                  <artifactId>jersey-media-json-jackson</artifactId>
                  <version>2.28</version>
          </dependency>
          <dependency>
                  <groupId>org.glassfish.jersey.inject</groupId>
                  <artifactId>jersey-hk2</artifactId>
                  <version>2.28</version>
          </dependency>
          <dependency>
                  <groupId>org.glassfish.jersey.containers</groupId>
                  <artifactId>jersey-container-grizzly2-http</artifactId>
                  <version>2.28</version>
          </dependency>
    </dependencies>
    
  • Dans le fichier WebContent/WEB-INF/web.xml, ajouter après l'élément welcome-file-list la référence de la servlet à utiliser.

    <servlet>
       <servlet-name>Jersey</servlet-name>
       <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
    </servlet>
    <servlet-mapping>
       <servlet-name>Jersey</servlet-name>
       <url-pattern>/*</url-pattern>
    </servlet-mapping>
    

Importer l'archive codeFourni_ServeurBibliotheque.zip à partir de la racine.

Plusieurs paquets sont importés :

  • modele : le modèle objet,
  • infrastructure : du code lié à JAXB ou JAX-RS,
  • configuration : toutes les constantes utilisées pour configurer le service web et la configuration du service web,
  • serveur : code pour lancer le serveur Grizzly.

3.2.1 Annotations JAX-RS de la bibliothèque et des livres

Remarque préalable : pour hériter d'annotations JAX-RS, il est nécessaire de respecter certaines règles, décrites dans la spécification de JAX-RS (voir ci-dessous un extrait). En pratique, nous définissons les annotations des méthodes dans les interfaces pour les hériter dans les classes, qui n'en comportent aucune au niveau des méthodes.

Extrait de la spécification de JAX-RS, version 2, section 3.6, Annotation Inheritance

JAX-RS annotations may be used on the methods and method parameters of a super-class or an implemented interface. Such annotations are inherited by a corresponding sub-class or implementation class method provided that the method and its parameters do not have any JAX-RS annotations of their own. Annotations on a super-class take precedence over those on an implemented interface. The precedence over conflicting annotations defined in multiple implemented interfaces is implementation specific. Note that inheritance of class or interface annotations is not supported.

Annoter l'interface Bibliotheque de manière à vérifier les propriétés suivantes. On utilisera les constantes de la classe configuration.JAXRS.

Remarque : pour importer ces constantes, préférer une directive d'importation statique, qui donne un accès direct aux constantes.

import static configuration.JAXRS.*;
  • Chaque méthode est qualifiée par le type idoine de méthode HTTP. Cependant, la méthode chercher doit être qualifiée par PUT et non par GET, bien qu'elle soit pure et idempotente. En effet, une méthode PUT permet de transmettre des informations par le corps du message, contrairement à une méthode GET. Comme les informations qui nous intéressent constituent la description d'un livre, il est préférable de les transmettre par le corps du message plutôt que l'URL, via le chemin ou la requête.
  • Les chemins relatifs d'accès aux méthodes sont les suivants :

    • chercher : vide
    • repertorier : catalogue, valeur de la constante SOUSCHEMIN_CATALOGUE

    Ainsi, si l'URI de la ressource est http://localhost:8080/Projet/bibliotheque, une URI sera par exemple : http://localhost:8080/Projet/bibliotheque/catalogue.

    (Pour indiquer un chemin C, on utilise l'annotation @Path("C").)

  • Les données sont produites ou consommées au format application/xml, valeur de la constante TYPE_MEDIA.

    (Pour indiquer qu'un résultat est sérialisé suivant un certain format F, on utilise l'annotation @Produces(F) ou Consumes(F). On doit utiliser ici la constante TYPE_MEDIA comme argument.)

L'implémentation proposée, ImplemBibliotheque, implémente l'interface Bibliotheque mais aussi l'interface Archive, par l'intermédiaire de l'interface BibliothequeArchive.

Annoter la classe ImplemBibliotheque de manière à vérifier les propriétés suivantes.

  • Cette ressource est un singleton.
  • Le chemin relatif d'accès à la ressource est bibliotheque, valeur de la constante CHEMIN_BIBLIO.

3.2.2 Indirection vers des sous-ressources

A ce stade, il n'existe pas de relations entre la ressource principale, la bibliothèque, et ses sous-ressources, les livres. On aurait pu établir cette relation dans l'interface Bibliotheque, avec une méthode permettant d'obtenir un livre, étant donné son identifiant. Cependant, nous avons pris le parti de manipuler les livres de la biliothèque uniquement via leurs adresses, a priori quelconques puisque les livres peuvent être situés n'importe où. C'est pourquoi l'interface Bibliotheque ne renvoie que des hyperliens pointant vers des livres et ne propose pas de telle méthode permettant d'obtenir un livre. En revanche, l'interface Archive donne accès aux livres considérés comme des sous-ressources de la bibliothèque.

La ressource principale Archive contient donc des sous-ressources, les livres de type Livre. Une telle structure formée de ressources et de sous-ressources peut être définie statiquement, ce qui implique que toutes les ressources soient connues initialement. Si ce n'est pas le cas, elle doit être définie dynamiquement. Ici, l'archive est connue statiquement, mais ses sous-ressources livresques ne le sont pas, puisqu'elles sont ajoutées dynamiquement. Elles sont donc calculées à la demande, par une méthode, sousRessource, qui renvoie un livre étant donné un identifiant : c'est un accesseur à une sous-ressource (ou un sélecteur de sous-ressource). Il est à noter qu'une même méthode ne peut être à la fois la cible d'une requête (et donc annotée par une méthode Http) et l'accesseur à une sous-ressource. Il est nécessaire de déclarer deux méthodes : voir les méthodes sousRessource et getRepresentation, respectivement un accesseur à une sous-ressource et une méthode permettant d'obtenir une représentation.

Voir la section 3.4.1 de la spécification de JAX-RS, version 2.

  • Annoter les méthodes des interfaces Archive et Livre de manière à vérifier les propriétés suivantes (en utilisant le cas échéant les constantes de la classe configuration.JAXRS).
    • Les méthodes Archive.getRepresentation, Archive.ajouter et Livre.getTitre sont annotées par une méthode HTTP.
    • Le chemin relatif (complétant l'URI de base http://localhost:8080/Projet/bibliotheque) permettant d'obtenir le titre du livre d'identifiant id est id/titre, où titre est la valeur de la constante SOUSCHEMIN_TITRE. Quelle méthode est alors exécutée ?
    • Le chemin relatif (complétant l'URI de base http://localhost:8080/Projet/bibliotheque) permettant d'obtenir la représentation du livre d'identifiant id est id. Quelle méthode est alors exécutée ?
    • Le chemin relatif pour l'ajout d'un livre est vide.
    • Les données produites sont au format XML, indiqué par la valeur TYPE_MEDIA.

3.2.3 Annotations JAXB

Quelle est l'annotation qui prouve que les classes modele.ImplemLivre, infrastruture.jaxrs.HyperLien et infrastruture.jaxrs.HyperLiens sont traduisibles (marshallable) ?

Remarque : l'interface modele.Livre doit être annotée de la même manière.

Etudier la classe de configuration infrastructure.jaxb.FournisseurTraduction.

3.2.4 Conversion des informations de chemin

Les paramètres de chemin et les requêtes, des chaînes de caractères, doivent pouvoir être traduits en des identifiants ou des livres, et réciproquement. Définir dans les interfaces Livre et IdentifiantLivre une fonction fromString réalisant cette traduction.

Voir les explications consacrées au pilotage des traducteurs de données.

3.2.5 Filtrage des réponses

JAX-RS ne respecte pas les conventions habituelles concernant le statut HTTP des réponses, pour les requêtes POST renvoyant un objet, ni pour les requêtes GET ou PUT ne renvoyant rien (null ou une option Optional vide). C'est la raison pour laquelle nous développons des filtres.

Deux filtres, AdapterServeurReponsesGETNullEn404 et AdapterServeurReponsesPOSTCreated (du paquet infrastructure.jaxrs) sont fournis. Expliquer la transformation des messages que ces classes réalisent.

Annoter les méthodes de l'interface Archive lorsqu'elles doivent utiliser ces filtres.

Lorsqu'un client interroge une bibliothèque pour chercher un livre, il peut recevoir une réponse de statut 404, ce qui signifie que la requête a été infructueuse. Dans le code Java, nous avons choisi de traduire cette possibilité par le type de retour Optional<HyperLien<Livre>> pour la méthode chercher. Il est nécessaire de traduire les options côté serveur lors de l'envoi des réponses, côté client lors de la réception des réponses, par des filtres-intercepteurs. Commençons par le serveur.

  • Définir dans le paquet infrastructure.jaxrs.annotations une annotation ReponsesPUTOption comme les annotations ReponsesPOSTCreated ou ReponsesGETNull404.
  • Annoter la méthode chercher renvoyant une option dans Bibliotheque.
  • Dans le paquet infrastructure.jaxrs, définir la classe AdapterServeurReponsesPUTOptionEn404OuValeur implémentant ReaderInterceptor et ContainerResponseFilter, et l'annoter par @Provider et ReponsesPUTOption pour établir la liaison entre les méthodes exposées comme services et l'utilisant (la méthode chercher en l'occurrence) et ce filtre-intercepteur.
    • Lui donner pour priorité Priorities.HEADER_DECORATOR.
    • Implémenter les méthodes suivant la spécification générale suivante : si l'option est vide, transformer la réponse en 404, sinon, remplacer l'entité par la valeur de l'option. Détaillons.
      • Dans la méthode aroundReadFrom, réaliser la traduction (unmarshalling) du corps du message, pour récupérer la description du livre (de type Livre pour simplifier), en utilisant la méthode proceed. Mémoriser ce livre dans une propriété du contexte passé en argument. Cette mémorisation permet la communication entre l'intercepteur et le filtre.
      • Dans la méthode filter, si l'entité est de type Optional<?>, alors la convertir en Optional<?>. Si l'option n'est pas présente, convertir la réponse en 404 à l'aide de la méthode convertirEn404, sinon, modifier l'entité en la valeur de l'option.

        private static String DESCRIPTION_LIVRE = "descriptionLivre";
        private void convertirEn404(
          ContainerRequestContext requete, ContainerResponseContext reponse) {
          System.out.println("recherche : 404 NOT FOUND !");
          String contenu = requete.getUriInfo().getRequestUri().toString();
          // Utilisation de la propriété pour récupérer la description du livre recherché.
          contenu = contenu + " - " + requete.getProperty(DESCRIPTION_LIVRE);
          reponse.setEntity(contenu, null, MediaType.TEXT_PLAIN_TYPE);
          reponse.setStatus(Response.Status.NOT_FOUND.getStatusCode());
        }
        
  • Enregistrer ce filtre-intercepteur dans Service du paquet configuration. Si la classe est enregistrée, deux instances sont créées, l'une pour le filtre, l'autre pour l'intercepteur. Si une instance est enregistrée, elle joue le rôle de filtre et d'intercepteur simultanément. En l'absence d'état, ces deux situations sont équivalentes.

Voir la section "Communication entre les filtres et les intercepteurs" du cours JAX-RS - Filtres et intercepteurs.

3.2.6 Déploiement

Dans le fichier web.xml, à l'intérieur de l'élément servlet, référencer la définition du service dans la classe configuration.Service.

<init-param>
   <param-name>javax.ws.rs.Application</param-name>
   <param-value>configuration.Service</param-value>
</init-param>
<load-on-startup>1 </load-on-startup>

Etudier le code de cette classe. Cette configuration est propre à Jersey.

Lancer Tomcat. Tester avec un outil permettant de produire des requêtes http.

Alternative : lancer le serveur Grizzly (sur le port 8081), en exécutant l'application serveur.Lancement. L'adresse du serveur est fixée dans configuration.JAXRS et peut être modifiée.

3.3 Client

Créer un nouveau projet Java.

  • Le transformer en projet Maven.
  • Ajouter les dépendances suivantes dans le fichier pom.xml. Les deux premières donnent les bibliothèques côté client, tandis que les suivantes permettent à Jersey d'utiliser les formats XML et JSON pour échanger des données. La dernière fournit un injecteur de dépendances (hk2).

      <dependency>
         <groupId>org.glassfish.jersey.core</groupId>
         <artifactId>jersey-client</artifactId>
         <version>2.28</version>
      </dependency>
      <dependency>
         <groupId>org.glassfish.jersey.ext</groupId>
         <artifactId>jersey-proxy-client</artifactId>
         <version>2.28</version>
      </dependency>
      <dependency>
         <groupId>org.glassfish.jersey.media</groupId>
         <artifactId>jersey-media-jaxb</artifactId>
         <version>2.28</version>
      </dependency>
      <dependency>
         <groupId>org.glassfish.jersey.media</groupId>
         <artifactId>jersey-media-json-jackson</artifactId>
         <version>2.28</version>
      </dependency>
    <dependency>
      <groupId>org.glassfish.jersey.inject</groupId>
      <artifactId>jersey-hk2</artifactId>
      <version>2.28</version>
    </dependency>
    

Y importer à la racine l'archive codeFourni_ClientBibliotheque.zip.

Quatre paquets sont importés :

  • modele : le modèle objet,
  • client : les classes principales clientes,
  • infrastructure : du code lié à JAXB ou JAX-RS,
  • configuration : toutes les constantes utilisées pour configurer le service web.

3.3.1 Importation du modèle Objet

Annoter les interfaces Bibliotheque et Archive conformément aux annotations côté serveur. Noter une différence : il est inutile de définir une annotation côté client pour un filtre (ou un intercepteur). En effet, pour les filtres côté client, la norme JAX-RS ne propose pas de mécanismes utilisant une annotation des méthodes. On doit configurer les cibles de type WebTarget par les filtres utilisés. Vérifier les annotations de l'interface Livre.

Vérifier que les classes ImplemLivre et ImplemIdentifiantLivre implémentent la méthode toString. Pourquoi est-ce indispensable ? Est-ce utile que les interfaces associées fournissent une fonction valueOf ou fromString ?

Bien noter que les implémentations des ressources ne sont pas utiles : on y accède via des proxys (de type BibliothequeArchive). Pour les sous-ressources de type Livre, une implémentation est nécessaire : en effet, on utilise à la fois des proxys vers des livres et des représentations complètes de livres.

3.3.2 Filtrage des réponses

Les filtres et les intercepteurs appartiennent au paquet infrastructure.jaxrs.

Un premier filtre-intercepteur AdapterClientReponsesPOSTCreated est fourni. Expliquer la transformation des messages qu'il réalise.

Pour les réponses de type 404 non liéés à une option, aucun filtre n'est prévu : il sera donc nécessaire de traiter les exceptions de type WebApplicationException.

En revanche, des filtres ou intercepteurs sont prévus pour traiter les types options.

  • Un second filtre-intercepteur est fourni : la classe AdapterClientReponsesPUT404EnOption permet de transformer une réponse de statut 404 en une option vide si le type attendu est Optional. Etudier son code, et en particulier le rôle de la propriété ERREUR404 qui permet de relier le filtre à l'intercepteur.

Il reste à définir un intercepteur transformant une valeur en une option contenant cette valeur, lorsque le type attendu est Optional<?>.

  • Définir un intercepteur AdapterClientReponsesPUTEnOption de type ReaderInterceptor, avec la priorité Priorities.HEADER_DECORATOR + 2, ce qui implique que cet intercepteur intervient après le précédent.
    • Dans la méthode aroundReadFrom, si le type de la réponse vaut Optional.class, alors :
      • récupérer le type générique de la réponse,
      • le convertir en un ParameterizedType,
      • récupérer son seul type argument (qui dans notre cas, sera HyperLien<Livre>),
      • modifier le type de la réponse en utilisant la fonction Types.convertirTypeEnClasse du paquet infrastructure.langage,
      • enfin, renvoyer Optional.of(reponse.proceed()) ;
      • sinon, renvoyer reponse.proceed(), autrement dit la traduction du document en objet (via la fonction unmarshalling).

Pour pouvoir utiliser ces filtres-intercepteurs, les enregistrer dans la configuration définie dans la fonction configuration.JAXRS.client. Pour le filtre-intercepteur, noter la différence entre l'enregistrement d'une instance et celle d'une classe : dans le premier cas, un seul filtre-intercepteur est créé, dans le second, un filtre et un intercepteur sont créés, ce qui pourrait poser problème si un état devait être partagé entre eux.

3.3.3 Test

Configurer l'adresse dans la classe configuration.JAXRS (par les constantes SERVEUR et CHEMIN).

Déployer l'application serveur et lancer le serveur.

Premier test : lancer le client AppliCliente.

Second test : lancer le client ClientConcurrence plusieurs fois consécutivement de manière à étudier la concurrence. Qu'observe-t-on ?

Ajouter un verrou (de type java.util.concurrent.locks.Lock, objet de la classe java.util.concurrent.locks.ReentrantLock) dans la classe ImplemBibliotheque du serveur de manière à garantir que toutes les requêtes POST aboutissent effectivement à un ajout d'un livre dans la bibliothèque. Quel est l'avantage d'utiliser un tel verrou plutôt que le verrou associé à l'objet this (via la primitive synchronized) ?

Tester.

Créer une nouvelle classe cliente implémentant le scénario suivant. (On pourra s'inspirer de la classe AppliCliente.)

  • Ajouter à la bibliothèque dix livres aux titres deux à deux distincts.
  • Chercher un livre présent dans la bibliothèque.
  • Chercher un livre absent de la biblitohèque.
  • Définir un livre proxy à partir d'un hyperlien en utilisant la classe ClientRessource du paquet infrastructure.jaxrs.
  • Afficher son type.
  • Récupérer son titre.
  • Récupérer le livre associé.
  • Afficher son type.

Bien noter la manipulation des hyperliens et la relation entre les livres et les livres proxy, tous deux de type Livre.

A quoi sert la classe ClientRessource ?

Author: Hervé Grall
Version history: v1: 2015-11-04; v2: 2016-04-20[*text]; v3: 2016-11-15[update, *text]; v4: 2017-04-18; v5: 2017-11-28[update]; v6: 2018-04-17[+Option, +PUT, *text]; v7: 2019-04-11[*text].
Comments or questions: Send a mail.
The webpage content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.