UP | HOME

JAX-RS - Pilotage des traducteurs de données

Table of Contents

Un framework objet pour des services doit typiquement recourir à deux sortes de traductions, qui sont des correspondances entre données :

Avec la norme JAX-RS, la première traduction intervient lorsqu'on utilise une annotation PathParam ou QueryParam, devant un paramètre d'un type objet, dans la déclaration d'une méthode associée à une requête. La norme JAX-RS précise les contraintes que le type objet doit alors vérifier. La seconde traduction intervient lorsqu'on consomme ou produit une valeur d'un type objet, dans une méthode associée à une requête. La norme JAX-RS permet de piloter des traducteurs fournis par les implémentations de la norme JAXB ou JSON-B, dédiée à la correspondance entre les objets et les documents XML ou JSON respectivement.

Correspondance entre objets et chaînes de caractères

La section 3.2 de la spécification JAX-RS (version 2.0) définit les contraintes que doit vérifier un type objet pour être traduit en chaînes de caractères.

  • Soit le type possède un constructeur à un seul paramètre de type String.
  • Soit le type possède une fonction (méthode déclarée static) appelée valueOf ou fromString, avec un seul paramètre de type String et renvoyant une instance de ce type. Si les deux méthodes sont présentes, alors la première doit être utilisée, sauf dans le cas d'un type énumération.

Si le type objet vérifie une de ces contraintes, voici comment s'effectue la correspondance.

  • objet vers chaîne de caractères - requête côté client : traduction en utilisant la méthode toString.
  • chaîne de caractères vers objet - requête côté serveur : traduction en utilisant le constructeur, la méthode valueOf ou celle fromString, appartenant au type déclaré de l'objet (interface ou classe).

Voici un exemple de deux méthodes appartenant à un service REST.

@POST
@Path("c/{id}")
public void post(@PathParam("id") IC x);

@POST
@Path("d/{id}")
public void post(@PathParam("id") D x);

Côté serveur, l'interface IC doit contenir une fonction fromString(String t) ou valueOf(String t), pour permettre la traduction, alors que la classe D doit contenir une telle fonction ou un constructeur D(String t).

public interface IC {
  public static IC fromString(String t){
    return new C(... t ...);
  }
  ...
}

Côté client, les classes d'implémentation C et D doivent définir une méthode toString.

public class C implements IC {
  ...
  @Override
  public String toString() {
    return ...;
  }
}
public class D implements ID {
  ...
  @Override
  public String toString() {
    return ...;
  }
}

Correspondance entre objets et documents

La norme JAX-RS (version 2.0 ou 2.1) se limite aux documents XML. Il existe cependant des extensions permettant de traiter d'autres formats comme JSON, tout en utilisant la même infrastructure. Cette infrastructure permet le pilotage du "data binder" JAXB mais aussi de celui JSON-B.

Les types racines

Tous les types (interfaces ou classes) susceptibles d'être en correspondance avec un document intégral (racine) doivent être annotées par XmlRootElement. Si c'est impossible, on doit recourir à une classe enveloppe, de type JAXBElement.

Toutes les classes ainsi annotées doivent être utilisées pour configurer le pilotage de JAXB. Précisément, on utilise un solveur de contextes permettant de récupérer le contexte JAXB, qui est le point d'entrée des fonctionnalités de la bibliothèque JAXB. Ce contexte doit être paramétré par les classes annotées par XmlRootElement ; on doit garantir qu'à toute interface annotée correspond une classe l'implémentant qui est annotée.

Fournisseur d'un contexte JAXB utilisé pour les traductions

@Provider
public class FournisseurTraduction implements ContextResolver<JAXBContext> {
  private JAXBContext context = null;
  public FournisseurTraduction(){}
  public JAXBContext getContext(Class<?> type) {
    if (context == null) {
      try {
        context = JAXBContext.newInstance(A.class, ...); // Classes annotées par XmlRootElement
      } catch (JAXBException e) {
      }
    }
    return context;
  }
}

Enregistrement du fournisseur dans la configuration du service

public class Service extends ResourceConfig {
  public Service(){
    ...
    this.register(FournisseurTraduction.class);
  }
}

Adaptateurs pour les interfaces

Par défaut, la norme JAXB n'autorise la traduction que vers une classe : il n'est pas possible de traduire vers une interface. La raison est simple : il n'est pas possible de construire un objet d'une interface sans connaître une classe d'implémentation. Si le problème est résolu pour un type racine, chaque interface annotée étant associée à une classe annotée dans le contexte JAXB, ce n'est pas le cas pour un type imbriqué qui doit être traduit. Lorsque ce type est une interface, un adaptateur JAXB doit être fourni.

Type racine IA possédant un type imbriqué IB à traduire

@XmlRootElement
public interface IA {
  ...
  IB getObj();
}

Lorsque le type IB est une interface, il est nécessaire de recourir à un adaptateur, reliant la classe d'implémentation B à l'interface IB.

@Provider
public class TraductionB extends
    javax.xml.bind.annotation.adapters.XmlAdapter<B, IB> {
 ...
}

Pour implémenter un adaptateur, on hérite de la classe générique XmlAdapter<T, NT>, où T est le type traduisible et NT celui non traduisible. On implémente deux méthodes, marshall et unmarshall, suivant le modèle suivant.

public class Adaptateur extends
  javax.xml.bind.annotation.adapters.XmlAdapter<T, NT> {

    @Override
    public T marshal(NT a) throws Exception {
      // Construction d'un objet traduisible b à partir de a 
      ...
      return b;
    }
    @Override
    public NT unmarshal(T b) throws Exception {
      // Construction d'un objet non traduisible a à partir de b 
      ...
      return a;
    }
}

Très souvent, le type T correspond à une classe d'implémentation de l'interface NT. Dans ce cas, les deux méthodes peuvent se simplifier ainsi.

public class Adaptateur extends
  javax.xml.bind.annotation.adapters.XmlAdapter<T, NT> {

    @Override
    public T marshal(NT a) throws Exception {
      T b = new T();
      b.setX(a.getX()); // pour chaque propriété X à traduire
      return b;
    }
    @Override
    public NT unmarshal(T b) throws Exception {
      return b; // conversion implicite
    }
}

Une fois l'adaptateur défini, il suffit d'annoter l'interface à traduire en indiquant l'adaptateur à utiliser.

@XmlJavaTypeAdapter(TraductionB.class) 
public interface IB {
  ...
}

Pilotage de la traduction

Voici comment la traduction s'opère. On suppose qu'on traduit un objet de la classe A implémentant l'interface IA en un document XML puis un tel document en un objet de type IA. On suppose aussi que l'objet contient un sous-objet de type IB imbriqué dans IA, qui doit aussi être traduit. Pour tout autre format que XML, le processus est analogue.

Configuration

  • IA et A annotés par XmlRootElement
  • A : classe implémentant IA et annotée pour la traduction
  • Définition d'un solveur de contextes ContextResolver<JAXBContext> permettant de récupérer un contexte JAXB reconnaissant A
  • Adaptateur pour l'interface IB imbriquée dans IA permettant la conversion entre IB et sa classe d'implémentation B
  • B : classe implémentant IB et annotée pour la traduction

Traduction d'un objet en un document

  • Entrée : objet de la classe A implémentant IA, contenant un sous-objet de type IB (une interface)
  • Récupération du contexte JAXB, qui doit reconnaître la classe A
  • Appel de la fonction de traduction JAXB marshall sur l'objet, traduisant l'objet en un document placé dans un flux de sortie
    • Pour la traduction du sous-objet de type IB, appel de la méthode marshall de l'adaptateur associé, permettant d'obtenir un objet de type B, puis poursuite de la traduction JAXB sur ce dernier objet de type B

Traduction d'un document en un objet

  • Entrée : document à traduire en un objet de type IA
  • Récupération du contexte JAXB, qui associe la classe A à l'interface IA
  • Appel de la fonction de traduction JAXB unmarshall sur le flux d'entrée contenant le document, traduisant le document en un objet de type A, le type étant identifié à partir du nom de la balise racine
    • Pour la traduction du sous-document en un objet de type IB, récupération de la classe B reliée à IB par l'adaptateur associé, puis poursuite de la traduction JAXB sur le flux d'entrée contenant le sous-document, traduisant le sous-document en un sous-objet de type B, et enfin appel de la méthode unmarshall de l'adaptateur associé, permettant d'obtenir un objet de type IB (par simple conversion)

Author: Hervé Grall
Version history: v1: 2015-10-30; v2: 2016-04-08[*text]; v3: 2016-10-25[update]; v4: 2017-04-18[major update]; v5: 2017-10-31[+JSON-B].
Comments or questions: Send a mail.
The webpage content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.