Publié par

Il y a 8 années -

Temps de lecture 10 minutes

JAXB, le parsing XML — objet

Format privilégié pour les échanges inter-applications, XML est l’objet de nombreuses bibliothèques Java. Cependant, ces bibliothèques masquent toutes le data binding qu’elles effectuent ; la transformation d’un document XML en grappe d’objets. Nous voilà bien démunis dès lors qu’une application produit du XML comme une simple chaîne de caractères. L’utilisation d’API bas niveau (DOM, XPath) — attachées à la structure du document — se révélant fastidieuse, la majorité des implémentations JAX-RS (Jersey, CXF) ont retenu la même API de haut niveau — concentrée sur les données — : JAXB. Faisons de même.

Décrire un format d’échange

Lorsqu’une application produisant du XML n’a pas de mécanisme pour partager l’agencement de ses noeuds, il incombe à ses consommateurs de retenir une méthode pour l’exploiter au mieux. Cela peut être réalisé via une grappe d’objets équivalente à la sortie XML ; voyons comment à l’aide d’un dessert savoureux.

<recipe name="Compote de poires" type="dessert">
    <cooking duration="15">
    	<step optional="true">Réserver une gousse de vanille</step>
        <step>Éplucher et évider les poires</step>
        <step>Découper les poires en quartiers</step>
        <step>Verser les quartiers dans une casserole avec l'eau</step>
    </cooking>
    <menu>17-02-2011</menu>
    <menu>17-03-2011</menu>
</recipe>

JAXB identifie chaque noeud comme un élément doté d’attributs. Un élément est un type complexe doté d’une séquence d’éléments (toujours en premier) puis d’une liste d’attributs. Un élément peut porter une simple valeur textuelle lorsqu’il n’a pas de sous-noeuds. Un attribut ne dispose que d’une valeur textuelle. Chaque noeud XML est représenté par un élément du schéma puis, à l’aide de XJC, par un objet du même nom généré à partir de ce schéma (la génération XJC est abordée en annexe).

Voici la description du XML précédent à l’aide d’un XSD :

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <xsd:element name="recipe">
        <xsd:complexType>
            <xsd:sequence>
                <xsd:element name="menu" type="xsd:string" maxOccurs="unbounded" />
                <xsd:element name="cooking" type="cooking" />
            </xsd:sequence>
            <xsd:attribute name="name" type="xsd:string" />
            <xsd:attribute name="type" type="xsd:string" />
        </xsd:complexType>
    </xsd:element>

    <xsd:complexType name="cooking">
        <xsd:sequence>
            <xsd:element name="step" type="step" maxOccurs="unbounded" />
        </xsd:sequence>
        <xsd:attribute name="duration" type="xsd:int" />
    </xsd:complexType>
    
    <xsd:complexType name="step" mixed="true">
        <xsd:attribute name="optional" type="xsd:boolean" />
    </xsd:complexType>
</xsd:schema>

Les noeuds de ce schéma sont tous précédés de « xsd » car leur définition, sur la première ligne, nomme le XML Namespace (xmlns) ainsi. Les namespaces sont une manière de différentier les déclarations des imports d’en-tête les uns des autres, comme le ferrait un package en java.

Les variables sont typées selon la recommandation w3c. Lorsqu’une liste de types primitifs est nécessaire (noeud menu) sa déclaration n’occasionnera pas la création d’une classe, seulement d’un attribut typé. En revanche si cette liste dispose d’attributs (noeud step), il est nécessaire d’indiquer que son contenu est mixte : par défaut les éléments ont soit une valeur textuelle, soit une liste d’attributs et d’éléments, pas les deux.

Voici le code généré à partir du schéma précédent :

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "recipe")
public class Recipe {
    protected List<String> menu;
    protected Cooking cooking;
    @XmlAttribute
    protected String name;
    @XmlAttribute
    protected String type;
}

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "cooking")
public class Cooking {
    protected List<Step> step;
    @XmlAttribute
    protected Integer duration;
}

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "step")
public class Step {
    @XmlValue
    protected String content;
    @XmlAttribute
    protected Boolean optional;
}

Seule une classe est annotée @XmlRootElement. Elle est la seule (dans ce schéma) à pouvoir jouer le rôle de premier noeud. Une fois la grappe résultat générée, 3 lignes de code suffisent à réaliser le parsing XML :

public class JaxbTest {
    @Test
    public void should_parse_recipe() throws JAXBException {
        URL xmlUrl = Resources.getResource("recipe.xml");
        Recipe recipe = parse(xmlUrl, Recipe.class);
        assertEquals(Integer.valueOf(15), recipe.getCooking().getDuration());
    }

    private <T> T parse(URL url, Class<T> clazz) throws JAXBException {
        Unmarshaller unmarshaller = JAXBContext.newInstance(clazz).createUnmarshaller();
        return clazz.cast(unmarshaller.unmarshal(url));
    }
}

Contrairement aux bibliothèques de bas niveau, aucune conversion de type n’est nécessaire. La déclaration du type dans le schéma suffit à nous affranchir de cette responsabilité. En cas d’erreur de conversion (une chaine non convertible dans le type attendu) l’IllegalArgumentException correspondante est levée. En l’absence d’un noeud, les variables correspondantes sont null.

Raffiner le format d’échange

Une fois un data binding réussi, plusieurs opérations sont couramment nécessaires :

  1. limiter un champ à un ensemble fini de valeurs ;
  2. manipuler des dates plus simples que le XMLGregorianCalendar manipulé par défaut par JAXB ;
  3. nommer une classe différemment de l’élément qu’elle représente ;
  4. utiliser l’héritage entre éléments ;
  5. annoter manuellement des classes existantes.

Limiter un champ à un ensemble fini de valeurs

Limiter l’attribut « type » de recette à « entrée, plat, dessert » peut être fait de la façon suivante :

<xsd:element name="recipe">
    <xsd:attribute name="type" type="formule" />
</xsd:element>

<xsd:simpleType name="formule">
    <xsd:restriction base="xsd:string">
        <xsd:enumeration value="entree"></xsd:enumeration>
        <xsd:enumeration value="plat"></xsd:enumeration>
        <xsd:enumeration value="dessert"></xsd:enumeration>
    </xsd:restriction>
</xsd:simpleType>

Ce qui donne, une fois généré :

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "recipe")
public class Recipe {
    @XmlAttribute
    protected Formule type;
}

@XmlEnum
public enum Formule {
    @XmlEnumValue("entree")
    ENTREE("entree"),
    @XmlEnumValue("plat")
    PLAT("plat"),
    @XmlEnumValue("dessert")
    DESSERT("dessert");
    private final String value;
}

Lors du binding, s’il s’avérait que la valeur du champ ne corresponde pas à l’une de celles spécifiées ici, la valeur null serait retournée. JAXB fait le choix de positionner ses variables à null en cas de problème de valeur. Les problèmes de typage, eux, lèvent tous une exception.

Manipuler des dates simples

Pour manipuler des dates Java en lieu et place du XMLGregorianCalendar utilisé par défaut par JAXB, un convertisseur va être nécessaire. Attention, un nouveau namespace est nécessaire à sa déclaration. Il permet de redéfinir le typage de JAXB.

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
            xmlns:jxb="http://java.sun.com/xml/ns/jaxb" 
            jxb:version="2.0">

    <xsd:annotation><xsd:appinfo>
        <jxb:globalBindings>
            <jxb:javaType name="java.util.Date" xmlType="xsd:dateTime"
                          parseMethod="com.xebia.jaxb.JaxbDateConverter.parseDateTime" />
        </jxb:globalBindings>
    </xsd:appinfo></xsd:annotation>

    <xsd:element name="menu" type="xsd:dateTime" />
</xsd:schema>

Le convertisseur doit respecter la logique JAXB, si la valeur en entrée ne convient pas, aucune exception n’est levée et la valeur null est renvoyée.

public class JaxbDateConverter {

    public static Date parseDateTime(String s) {
        DateFormat formatter = new SimpleDateFormat("dd-MM-yyyy");
        try {
            return formatter.parse(s);
        } catch (ParseException e) {
            return null;
        }
    }
}

Lors de la génération, JAXB crée une classe adapter qu’il lie aux méthodes statiques du converter.

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "recipe")
public class Recipe {
    @XmlElement(type = String.class)
    @XmlJavaTypeAdapter(Adapter1.class)
    @XmlSchemaType(name = "dateTime")
    protected List<Date> menu;

Nommer une classe différemment de l’élément qu’elle représente

Pour nommer une classe différemment de l’élément qu’elle représente, il suffit d’ajouter les modifications suivantes à un schéma (attention à bien indiquer les deux namespaces) :

<xsd:element name="menu" type="xsd:dateTime" />
<xsd:annotation><xsd:appinfo>
    <jxb:class name="meal" />
</xsd:appinfo></xsd:annotation>

Utiliser l’héritage entre éléments

Utiliser l’héritage entre objet est aisé (les objets générés hériteront l’un de l’autre, bien entendu) :

<xsd:complexType name="menuxl">
    <xsd:complexContent>
        <xsd:extension base="menu" />
    </xsd:complexContent>
    <xsd:attribute name="cook" type="xsd:string" />
</xsd:complexType>

Annoter manuellement des classes existantes

Jusqu’ici nous avons fait reposer les objets d’échange sur une génération à l’aide d’un schéma. Annoter manuellement une classe en vue de lui binder du XML est également possible. Le schema correspondant peut même être généré à posteriori à partir des sources.

L’utilisation d’annotations manuelles permet d’avoir un controle plus fin sur la grappe d’objets, leur type et, pourquoi pas, d’utiliser des objets déjà utiles par ailleurs (ajouter un champ non concerné par le binding se fait à l’aide @XmlTransient).

Afin de respecter la logique JAXB, il est toutefois nécessaire de modifier les accesseurs des listes. Par convention, en l’absence de valeur, le code généré retourne des listes vides plutôt que null. JAXB, lors de la génération d’objets effectue cela via la modification des getters. Il est recommandé de faire de même.

public List<String> getMenu() {
    if (menu == null) {
        menu = new ArrayList<String>();
    }
    return menu;
}

Épilogue : un contrat d’échange côté serveur

Jusqu’ici nous avons considéré qu’aucun mécanisme décrivant l’agencement des noeuds XML n’était fourni du producteur aux consommateurs ; qu’il était résolu à postériori par ces derniers. Il est préférable, lorsque c’est possible, de les affranchir de cette contrainte en leur communiquant le schéma avec les données.

Pour ce faire, il est judicieux d’opter pour un mode de développement dirigé par le contrat. Générer les objets — côté serveur — de la couche d’échange au lieu de les annoter limite le couplage entre le schéma et son implémentation. Se restreindre aux possibilités de typage et d’agencement d’un schéma XSD garantit la compatibilité du format à tout type de consommateur (notamment autre que java). La documentation de référence Spring détaille ce propos en l’illustrant.

Annexe : outillage

Afin d’effectuer les manipulations présentées ici deux outils sont nécessaires, la dépendance maven de JAXB et le plugin de génération associé dont voici une version simple (exécutable via mvn jaxb2:xjc) :

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>jaxb2-maven-plugin</artifactId>
    <configuration>
    	<outputDirectory>${basedir}/src/main/java</outputDirectory>
    	<schemaDirectory>${basedir}/src/main/resources/xsd</schemaDirectory>
    	<packageName>com.xebia.jaxb.generated</packageName>
    	<schemaFiles>schema.xsd</schemaFiles>
    </configuration>
</plugin>

Pour prolonger l’aventure du data binding avec JAXB, une riche documentation est disponible en ligne.

Publié par

Publié par Yves Amsellem

Développeur depuis 5 ans — les 2 derniers chez Xebia — Yves tire de son expérience sur des sites à fort trafic une culture de la qualité, de l'effort commun et de l'innovation. Spécialisé du style d'architecture ReST, il intervient sur des projets web à forte composante JavaScript et NoSQL. Avec Benoît Guérout, il développe la librairie open source Jongo — Query in Java as in Mongo shell

Commentaire

7 réponses pour " JAXB, le parsing XML — objet "

  1. Publié par , Il y a 8 années

    Pour l’avoir utilisé sur de gros projets basés sur des échanges de flux Xml, je recommande chaudement cet outil. Et l’association avec un outil du genre XmlMerge devient magique…

  2. Publié par , Il y a 8 années

    A noter que certaines implémentations JAXB peuvent avoir quelques trucs sympa en plus.

    Comme @XmlPath pour MOXy, parfois bien utile si on veut (un)marshaller un objet qui n’est pas le reflet direct du xml produit/consommé.

  3. Publié par , Il y a 8 années

    A préciser aussi que Jaxb est devenu de plus en plus performant au fil des implémentations, si bien qu’il a dépassé Jibx qui tenait le haut du classement depuis longtemps.

  4. Publié par , Il y a 7 années

    Bonjour,

    Je m’imprègne actuellement de JAXB et là ce tuto m’est d’ue grande aide, merci encore. Par contre je dois vraiment mal m’y prendre mais dans la classe de test suivante :
    public class JaxbTest {
    @Test
    public void should_parse_recipe() throws JAXBException {
    URL xmlUrl = Resources.getResource(« recipe.xml »);

    }
    }

    Je ne parviens pas à trouver à quelle package la classe « Resources » appartient. J’utilise maven mais je ne sais pas non plus quelle dépendance je devrais renseigner. j’ai trouvé celle de google (guava) qui implémente une classe semblable mais une fois que je lance le test, il m signale des erreur au niveau de celle-ci. please, de l’aide :)

    Merci.

  5. Publié par , Il y a 6 années

    SAlut Bro,
    Comment ça vas ?

    Petite remarque concernant le plugin maven :

    je te conseille vivement pour des raisons de paramétrage et de maintenance du plugin maven d’utiliser le plugin suivant :

    http://java.net/projects/maven-jaxb2-plugin/pages/Home

    au lieu de celui org.codehaus.mojo

  6. Publié par , Il y a 4 années

    Bonjour,

    Est-ce que les objets associés à un objet sont inclus par JAXB dans la représentation XML de l’objet ? Est-ce paramétrable ? J’ai une entité avec une association 1-N et une association N-1. L’objet de l’association N-1 est inclus dans le XML mais pas la collection des objets de l’association 1-N. Je suis débutant en JAXB et je n’arrive pas à trouver de documentation sur ce problème.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.