JAXB Travaux Pratiques

Inria

Générez des classes Java à partir d'un schéma XML et réalisez la conversion à double-sens entre un document XML et un ensemble d'objets Java.

Prérequis

  • Programmation Java

Cours

  • Schémas
  • JAXB

JAXB

Java Architecture for XML Binding (JAXB) permet d'associer les documents XML à des objets Java et les schémas XML à des classes Java. JAXB prend en charge la désérialisation des documents XML en un graphe d'objets et l'opération inverse (sérialisation). JAXB offre un mécanisme d'annotations Java permettant d'indiquer comment faire la correspondance entre les propriétés des objets et leur représentation XML. JAXB dispose également de l'outil XJC (binding compiler) qui permet de générer un schéma XML à partir des classes Java annotées, ou au contraire générer les classes Java à partir d'un schéma XML.

Simple Zoo

On dispose du document XML :

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!-- Un petit Zoo avec quelques animaux -->
<zoo>
    <dauphin id="jhgtr13" photo="flipper.jpg" date-naissance="1997-04-01">
        <nom>Flipper</nom>
        <sexe>M</sexe>
        <taille unité="cm">215</taille>
        <poids unité="kg">105</poids>
    </dauphin>
    <dauphin id="lkjh45" photo="ecco.jpg" date-naissance="2003-10-23">
        <nom>Ecco</nom>
        <sexe>F</sexe>
        <taille unité="cm">202</taille>
        <poids unité="kg">98</poids>
    </dauphin>
    <dauphin id="kjlhy90" photo="oum.jpg" date-naissance="1996-12-25">
        <nom>Oum</nom>
        <sexe>F</sexe>
        <!-- c'est mon préféré -->
        <taille unité="cm">295</taille>
        <poids unité="kg" status="mesure approximative">190</poids>
    </dauphin>
    <requin id="plojk09" espèce="marteau" date-naissance="1998-06-05">
        <nom>Oussama</nom>
        <sexe>M</sexe>
        <taille unité="cm">455</taille>
        <poids unité="kg">540</poids>
    </requin>
    <requin id="vgyuh43" espèce="requin bleu" nom-savant="carcharias glaucus" date-naissance="2004-01-13">
        <nom>Saddam</nom>
        <sexe>M</sexe>
        <taille unité="cm">355</taille>
        <poids unité="kg" status="">425</poids>
    </requin>
</zoo>

et de son schema :

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">

    <xs:element name="animal" type="animal"/>

    <xs:complexType name="animal" abstract="true">
        <xs:sequence>
            <xs:element ref="nom"/>
            <xs:element ref="sexe"/>
            <xs:element ref="taille"/>
            <xs:element ref="poids"/>
            <xs:element minOccurs="0" ref="visite-médicale"/>
        </xs:sequence>
        <xs:attributeGroup ref="animal-attr"/>
    </xs:complexType>

    <xs:attributeGroup name="animal-attr">
        <xs:attribute name="id" use="required" type="xs:ID"/>
        <xs:attribute name="type"/>
        <xs:attribute name="espèce"/>
        <xs:attribute name="nom-savant"/>
        <xs:attribute name="photo" type="xs:anyURI"/>
        <xs:attribute name="date-naissance" use="required" type="xs:date"/>
    </xs:attributeGroup>

    <xs:element name="dauphin" substitutionGroup="animal">
        <xs:complexType>
            <xs:complexContent>
                <xs:extension base="animal"/>
            </xs:complexContent>
        </xs:complexType>
    </xs:element>

    <xs:element name="requin" substitutionGroup="animal">
        <xs:complexType>
            <xs:complexContent>
                <xs:extension base="animal"/>
            </xs:complexContent>
        </xs:complexType>
    </xs:element>

    <xs:element name="zoo">
        <xs:complexType>
            <xs:choice maxOccurs="unbounded">
                <xs:element ref="animal"/>
            </xs:choice>
        </xs:complexType>
    </xs:element>

    <xs:attributeGroup name="status-attr">
        <xs:attribute name="status"/>
    </xs:attributeGroup>

    <xs:element name="nom" type="xs:string"/>

    <xs:element name="sexe" type="sexe"/>

    <xs:element name="taille">
        <xs:complexType>
            <xs:simpleContent>
                <xs:extension base="mesure">
                    <xs:attributeGroup ref="status-attr"/>
                    <xs:attribute name="unité" use="required">
                        <xs:simpleType>
                            <xs:restriction base="xs:string">
                                <xs:enumeration value="μm"/>
                                <xs:enumeration value="mm"/>
                                <xs:enumeration value="cm"/>
                                <xs:enumeration value="dm"/>
                                <xs:enumeration value="m"/>
                                <xs:enumeration value="dam"/>
                            </xs:restriction>
                        </xs:simpleType>
                    </xs:attribute>
                </xs:extension>
            </xs:simpleContent>
        </xs:complexType>
    </xs:element>

    <xs:element name="poids">
        <xs:complexType>
            <xs:simpleContent>
                <xs:extension base="mesure">
                    <xs:attributeGroup ref="status-attr"/>
                    <xs:attribute name="unité" use="required">
                        <xs:simpleType>
                            <xs:restriction base="xs:string">
                                <xs:enumeration value="μg"/>
                                <xs:enumeration value="mg"/>
                                <xs:enumeration value="cg"/>
                                <xs:enumeration value="dg"/>
                                <xs:enumeration value="g"/>
                                <xs:enumeration value="dag"/>
                                <xs:enumeration value="hg"/>
                                <xs:enumeration value="kg"/>
                                <xs:enumeration value="t"/>
                            </xs:restriction>
                        </xs:simpleType>
                    </xs:attribute>
                </xs:extension>
            </xs:simpleContent>
        </xs:complexType>
    </xs:element>

    <xs:element name="visite-médicale" type="visite-médicale"/>

    <xs:simpleType name="visite-médicale">
        <xs:restriction base="xs:string">
            <xs:whiteSpace value="collapse"/>
            <xs:enumeration value="à faire"/>
        </xs:restriction>
    </xs:simpleType>

    <xs:simpleType name="sexe">
        <xs:restriction base="xs:string">
            <xs:whiteSpace value="collapse"/>
            <xs:enumeration value="M"/>
            <xs:enumeration value="F"/>
            <xs:enumeration value="?"/>
        </xs:restriction>
    </xs:simpleType>

    <xs:simpleType name="mesure">
        <xs:restriction base="xs:float">
            <xs:minInclusive value="0"/>
        </xs:restriction>
    </xs:simpleType>

</xs:schema>

Le document XML décrit les animaux d'un Zoo. On veut générer des classes Java automatiquement pour pouvoir écrire facilement une application.

Générez les classes Java correspondant au schéma avec XJC, en prenant soin de les mettre dans le paquet org.inria.ns.tp.jaxb.zoo.

$ xjc -d generate -p org.inria.ns.tp.jaxb.zoo -xmlschema tp/jaxb/zoo.xsd

Ecrivez un programme Java qui utilise ces classes pour lire le fichier zoo.xml, puis cherche les animaux dont la date de naissance est postérieure au 1er janvier 2000, ajoute pour ceux-là la nécessité de passer une visite médicale (voir dans les sources Java générées), puis enregistrez vos objets dans un fichier. Vérifiez que le nouveau fichier a produit la structure XML attendue, en particulier la présence de l'élément <visite-médicale>à faire</visite-médicale>.

  • pour convertir un document XML en objets Java, utilisez javax.xml.bind.Unmarshaller.
        // pour convertir le document XML en objets :
        JAXBContext jaxb = JAXBContext.newInstance(Zoo.class);
        Unmarshaller unmarshaller = jaxb.createUnmarshaller();
        Zoo zoo = (Zoo) unmarshaller.unmarshal(input);
  • pour obtenir une date en Java 8, utilisez java.time.LocalDate.
        // pour obtenir une date en Java 8 :
        LocalDate y2000 = LocalDate.parse("2000-01-01");
    
  • pour convertir un objet en document XML, utilisez javax.xml.bind.Marshaller.
        // pour convertir les objets en document XML :
        Marshaller marshaller = jaxb.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(zoo, output);
    for (JAXBElement<? extends Animal> animal : zoo.getAnimal()) {
        if ( animal.getValue().getDateNaissance().toGregorianCalendar()
                .toZonedDateTime().toLocalDate()
                .isAfter(date))
        ) {
            System.out.println(animal.getValue().getDateNaissance());
            animal.getValue().setVisiteMédicale(VisiteMédicale.A_FAIRE);
        }
    }

Le type XMLGregorianCalendar n'est pas naturel pour une classe, on lui préfère LocalDate. Remplacez le type de la date de naissance dans Animal :

    protected XMLGregorianCalendar dateNaissance;
par :
    protected LocalDate dateNaissance;
et ajoutez également sur cette propriété une annotation @XmlJavaTypeAdapter ; le rôle de cette annotation est d'indiquer comment convertir un XMLGregorianCalendar en LocalDate et inversement. Il suffit d'indiquer dans l'annotation la classe qui implémente XmlAdapter. Codez cet adaptateur en vous aidant de ces indications pour convertir les dates. Faites en sorte que votre programme fonctionne à nouveau.
Dans la classe Animal :
    @XmlAttribute(name = "date-naissance", required = true)
    @XmlSchemaType(name = "date")
    @XmlJavaTypeAdapter(DateAdapter.class)
    protected LocalDate dateNaissance;

    public LocalDate getDateNaissance() {
        return dateNaissance;
    }

    public void setDateNaissance(LocalDate value) {
        this.dateNaissance = value;
    }
(en gras, la classe à coder)
package org.inria.ns.tp.jaxb.zoo;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.GregorianCalendar;

import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

public class DateAdapter extends XmlAdapter<XMLGregorianCalendar, LocalDate>{

    @Override
    public LocalDate unmarshal(XMLGregorianCalendar xmlDate) throws Exception {
        Date utilDate = xmlDate.toGregorianCalendar().getTime();
        return LocalDateTime.ofInstant( utilDate.toInstant(), ZoneId.systemDefault() ).toLocalDate();
    }

    @Override
    public XMLGregorianCalendar marshal(LocalDate date) throws Exception {
        Date utilDate = Date.from( date.atStartOfDay( ZoneId.systemDefault() ).toInstant() );
        GregorianCalendar cal = new GregorianCalendar();
        cal.setTime(utilDate);
        return DatatypeFactory.newInstance().newXMLGregorianCalendar(cal);
    }

}
Dans l'application, la condition qui teste si la visite médicale est à faire est simplifiée par :
    if ( animal.getDateNaissance().isAfter(date)) {
        // ...
    }

Dans la classe Java Zoo, on ne veut pas voir le type JAXBElement. Remplacez partout JAXBElement<Animal> par Animal dans Zoo. Adaptez votre programme (celui qui marque les animaux pour la visite médicale) en conséquence et lancez-le. Que constatez-vous ? Pourquoi ?

JAXB ne sait pas obtenir directement le résultat souhaité, du fait qu'un animal peut-être n'importe laquelle des sous-classes d'un animal ; JAXB recourt au type JAXBElement juste pour porter l'information du nom de l'élément (dans un QName) en plus de l'objet lui-même.

Si on supprime partout le type JAXBElement, JAXB s'attend malgré tout à ce type et ne sait pas le traiter, à moins de lui dire explicitement comment : c'est le rôle d'un adaptateur.

Dans la classe Java Zoo, ajoutez un adaptateur pour convertir correctement un animal, et codez cet adaptateur. Faites en sorte que votre programme fonctionne à nouveau.

    @XmlElementRef(name = "animal", required = false)
    @XmlJavaTypeAdapter(AnimalAdapter.class)
    protected List<Animal> animal;
package org.inria.ns.tp.jaxb.zoo;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.namespace.QName;

public class AnimalAdapter extends XmlAdapter<JAXBElement<Animal>, Animal>{

    // le reste du code est trivial

}
    @Override
    public Animal unmarshal(JAXBElement<Animal> v) throws Exception {
        return v.getValue();
    }

    @SuppressWarnings("unchecked")
    @Override
    public JAXBElement<Animal> marshal(Animal v) throws Exception {
        QName name = new QName(v.getClass().getSimpleName().toLowerCase());
        return new JAXBElement<Animal>(name, (Class<Animal>) v.getClass(), v);
    }
Dans l'application, la boucle qui traite chaque animal est simplifiée par :
    for (Animal animal : zoo.getAnimal()) {
        if ( animal.getDateNaissance().isAfter(date)) {
            animal.setVisiteMédicale(VisiteMédicale.À_FAIRE);
        }
    }

Ecrivez un plugin XJC qui remplace le type JAXBElement par le type qu'il représente et génère le code de l'adaptateur. Inspirez-vous de ce plugin.

Si vous voulez lancer le plugin en ligne de commande au lieu d'utiliser Maven, vous pouvez procéder ainsi :
  1. créez quand même un pom minimaliste pour télécharger les composants nécessaires et construire le classpath :
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.2.11</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-xjc</artifactId>
            <version>2.2.11</version>
        </dependency>
  2. $ mvn dependency:resolve
  3. $ mvn dependency:build-classpath
  4. Copiez le classpath obtenu, ajoutez-y les classes de votre plugin
  5. Lancez la commande :
    java -Dcom.sun.tools.xjc.XJCFacade.nohack=true  -cp "TOUT LE CLASSPATH" 
        com.sun.tools.xjc.XJCFacade -d generate -p org.inria.ns.tp.jaxb.zoo
        -xmlschema src/resource/zoo.xsd -extension -XnomDeVotreExtension
NOTE : le paramètre -Dcom.sun.tools.xjc.XJCFacade.nohack=true permet à XJC d'accéder à la classe de votre plugin (explications ici).