JAXB - Les adaptateurs
Table of Contents
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. Il est cependant possible d'utiliser un adaptateur pour permettre la traduction d'un document vers un type interface. Autre limitation dans l'autre sens, il n'est pas possible de traduire une classe non annotée en un document. Une première solution est d'utiliser la classe enveloppe JAXBElement. Une seconde solution est d'utiliser ici aussi un adpatateur.
Avec JAX-RS, on utilise des adaptateurs pour les traductions non seulement entre les objets et les documents, mais aussi entre les objets et les chemins ou requêtes composant les URI des ressources. Le problème est analogue au précédent et requiert de recourir aux adaptateurs.
Utilisation des adpatateurs
Dans les cas décrits ci-dessus, en l'absence d'adaptateur, il se produit une erreur.
Jersey (implémentation de référence)
- Pas d'adaptateur pour le contenu d'une requête
- Erreur 415 : type de média non supporté
- Erreur 500 : erreur interne
- Pas d'adaptateur pour un chemin ou une requête composant une URI
- Erreur 404 : pas de ressource
CXF (une autre implémentation de JAX-RS)
- Pas d'adaptateur pour le contenu d'une requête
- No message body writer/reader has been found for response/request class/type X
- Pas d'adaptateur pour la requête d'un chemin
- Parameter Class X has no constructor with single String parameter, static valueOf(String) or fromString(String) methods
Pour supprimer ces erreurs, il est nécessaire d'annoter X et de fournir un adaptateur.
- @XmlRootElement : annotation nécessaire en Jersey, mais plutôt superfétatoire
- @XmlJavaTypeAdapter(A.class), où A est un adaptateur d'une classe héritant de XmlAdapter (cf. paquet javax.xml.bind.annotation.adapters).
Distinguons les deux cas d'usage.
Liaison objet-document (data binding).
Hypothèses
Soient I une interface, A et B deux classes implémentant I telles que A n'est pas traduisible (marshallable) alors que B l'est. On suppose que l'adaptateur implémente deux méthodes :
- B marshall(I x),
- I unmarshall(B x),
où B implémente I et est traduisible.
Problème 1 : marshalling (envoi)
Un problème survient lorsqu'un A doit être traduit en un document.
Solution 1
On annote I par un adaptateur paramétré par I et B. La méthode B marshall(I x) est appelée, ce qui permet de transformer le A en un B traduisible.
Problème 2 : unmarshalling (réception)
A la réception d'un document à utiliser pour construire un I, aucune classe d'implémentation n'est connue par le data binder.
Solution 2
On annote I par un adaptateur paramétré par I et B. La classe B utilisée par l'adaptateur sert à la traduction inverse (unmarshalling), ce qui produit un B, puis la méthode I unmarshall(B x) est appelée pour le convertir en un I : comme dans notre cas B est un sous-type de I, la méthode correspond à une simple conversion implicite.
Liaison objet-chemin ou requête d'une URI
Hypothèses
Soient I une interface, A et B deux classes implémentant I telles que A ne peut pas être construit à partir d'une String alors que B peut l'être. On suppose que l'adapteur implémente deux méthodes :
- B marshall(I x),
- I unmarshall(B x),
où B implémente I et peut être construit à partir d'une String (B possède un constructeur B(String), ou une fonction B valueOf(String), ou une fonction B fromString(String)).
Remarque : si l'interface I possède une fonction I valueOf(String) ou I fromString(String), un adaptateur est inutile.
Problème 1 : traduction d'un objet en un chemin
Lors de la traduction d'un objet de type I en un chemin, c'est la méthode toString() qui est appelée. Il n'y a donc jamais de problème, qu'on utilise un A ou un B.
Solution 1
Rien à faire.
Problème 2 : traduction d'un chemin en un objet
Un problème survient lors de la traduction inverse, d'un chemin vers objet de type I : on ne sait pas construire un I à partir d'une String.
Solution 2
On annote I par un adaptateur paramétré par I et B. La classe B utilisée par l'adaptateur sert à la construction d'un objet B, en utilisant le constructeur ou les fonctions prenant une String en argument. La méthode I unmarshall(B x) est ensuite appelée pour produire un I (par conversion implicite dans notre cas).
Implémentation des adaptateurs
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 se simplifient souvent 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 } }