JAXB et le XML Data Binding

Jean-François Perrot


  1. Introduction
    1. XML Data Binding
    2. Références

  2. Le compilateur de schémas xjc
    1. Principe
    2. Premier exemple, avec le schéma en poupée russe Nom_note1-1.xsd
    3. Deuxième exemple, avec le schéma "à base de types" Voiture-3.xsd

  3. Mise en œuvre élémentaire
    1. Création d'une liste de noms-notes
    2. Lecture d'une liste de noms-notes
    3. Lecture d'une voiture

  4. Une expérience de modélisation en XMLS : un interprète MIL
    1. Principe
    2. L'affaire des groupes
    3. Conclusion

  1. Introduction


    API disponible dans le JDK 1.6,
    consacrée à la correspondance entre objets Java et données représentées en XML.
    Java API for XML Binding (tandis que JAXP se lit Java API for XML Processing)
    1. XML Data Binding

      D'une manière générale, l'adoption généralisée de la technique XML conduit à envisager XML comme un outil pour résoudre
      le problème classique de la persistance des objets (enregistrement sur disque grâce à une "sérialisation" [ Wikipédia]).

      On pose ainsi le problème du XML data binding [Wikipedia], qui n'est pas propre à Java :
      voyez PHP et son framework SDO (Service Data Objects) :  http://fr2.php.net/sdo.

      JAXB offre donc la version Java de ce problème général.

    2. Références

      Wikipedia : http://en.wikipedia.org/wiki/JAXB
      La JavaDoc :  http://java.sun.com/webservices/docs/1.6/api/javax/xml/bind/package-summary.html
      http://java.sun.com/webservices/technologies/index.jsp#Core_Web_Services 
      https://jaxb.dev.java.net/
      https://jaxb.dev.java.net/guide/

      Explications chez http://www.jmdoudoux.fr/java/dej/chap037.htm

      sur les DTDs : https://jaxb.dev.java.net/guide/Compiling_DTD.html
  2. Le compilateur de schémas xjc

    1. Principe

      Le compilateur xjc traduit un schéma XMLS en un package Java
      dont les classes correspondent aux différents types définis par le schéma.

      Ce package pourra être ensuite utilisé avec les outils de JAXB, afin de :

      • lire un fichier XML conforme à ce schéma,
        le transformer en objets Java instances des classes du package en question
        (opération de désérialisation, appelée unmarshalling dans le jargon de JAXB)

      • travailler sur ces objets, les mettre à jour, en supprimer certains, en créer d'autres...

      • renvoyer le système d'objets mis à jour dans le fichier (opérations de sérialisation, alias marshalling).

      Il se manifeste par une commande qui se trouve (sur MacOS 10.5) en
      /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Commands/xjc

      Par défaut le nom du package engendré est generated. Il peut être défini grâce à l'option "-p".
    2. Premier exemple, avec le schéma en poupée russe Nom_note1-1.xsd

      Compilation
      jfp% /System/Library....../xjc Nom_note1-1.xsd
      parsing a schema...
      compiling a schema...
      generated/Liste.java
      generated/ObjectFactory.java


      jfp% cd generated ; ls
      Liste.java        ObjectFactory.java
      jfp%

      Remarques sur les classes Liste et ObjectFactory
      La classe Liste traduit le type de l'élément-racine <liste>, défini "au vol" dans le schéma.
      C'est une classe-enveloppe, qui contient

      • une variable d'instance (il vaut mieux ne pas parler d'attribut dans un contexte XML !)
        • nommée eleve (au singulier !) ;
        • de type java.util.List, qui a pour valeur la collection des élèves
          plus précisément le type List étant générique, celui de notre variable est List<Liste.Eleve>
          sur Liste.Eleve voir plus loin ;
        • cette variable étant déclarée protected, elle est assortie d'une méthode d'accès getEleve().

      • une classe locale Eleve (donc désignée par Liste.Eleve)
        qui traduit le type de l'élément <eleve> défini "au vol" dans le schéma.

      La classe ObjectFactory, comme son nom l'indique, sert à créer les instances des classes définies dans le package.
      Dans ce cas particulièrement simple, on peut se passer d'elle et instancier directement les classes par new.
      So rôle est probablement plus important en présence de namespaces.
    3. Deuxième exemple, avec le schéma "à base de types" Voiture-3.xsd

      jfp% cd generated ; ls
       
      Automan.java   ObjectFactory.java   TypCar.java TypMot.java
       TypTrans.java  TypVoiture.java
       

      On constate que le compilateur xjc a produit une classe par complexType déclaré dans le schéma,
      et aussi pour le <xsd:simpleType name="automan"> (énumération),
      mais pas pour <xsd:simpleType name="tnbv"> (restriction).
      La restriction en question est passée  à la trappe et l'attribut correspondant est un banal entier !
      protected Integer nbVitesses;
      public void setNbVitesses(Integer value) {
              this.nbVitesses = value;
      }


  3. Mise en œuvre élémentaire

    1. Création d'une liste de noms-notes

      avec le package qui traduit le schéma Nom_note1-1.xsd et envoi dans un fichier XML conforme à ce schéma.

      Les opérations de création mettent à contribution la classe ObjectFactory.
      Ici, on peut sans inconvénient les simplifier en Liste ml = new Liste(); et en Liste.Eleve el = new Liste.Eleve();.

      fichier CreerListe.java

      package generated;

      import java.util.List;
      import java.io.FileOutputStream;

      import javax.xml.bind.JAXBContext;
      import javax.xml.bind.Marshaller;

      public class CreerListe {

          public static void main (String[] args) throws Exception{
         
              String fichOut = args.length == 1 ? args[0] : "";
         
          // (1) Création et remplissage de l'objet Java Liste
         
              ObjectFactory of = new ObjectFactory();
              Liste ml = of.createListe();
              List<Liste.Eleve> lal =  ml.getEleve(); // vide
             
              // String[] tabEl = {"Hélène", "André", "Noémie", "François"};
              String[] tabEl = {"H\u00E9l\u00E8ne", "Andr\u00E9", "No\u00E9mie", "Fran\u00E7ois"};
              int[] tabNot = {12, 15, 17, 9};
              for( int i = 0; i<tabEl.length; i++ ){
                  Liste.Eleve el = of.createListeEleve();
                  el.setNom (tabEl[i]);
                  el.setNote(tabNot[i]);
                  lal.add(el);
              }
             
          // (2) Sérialisation ("marshalling") de cet objet dans un fichier XML
         
              JAXBContext jc = JAXBContext.newInstance("generated");
              Marshaller mrs = jc.createMarshaller();
              mrs.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); // indenter
              if( fichOut.length() != 0 ){
                  mrs.marshal(ml, new FileOutputStream(fichOut));
              }else{
                  mrs.marshal(ml, System.out);
              }
             
          }//main
      }// class CreerListe


      Résultat :

      <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
      <liste>
          <eleve note="12" nom="Hélène"/>
          <eleve note="15" nom="André"/>
          <eleve note="17" nom="Noémie"/>
          <eleve note="9" nom="François"/>
      </liste>



    2. Lecture d'une liste de noms-notes

      avec le même package.
      Cette fois, plus besoin de ObjectFactory !

      fichier LireListe.java

      package generated;

      import java.util.List;
      import java.util.Iterator;
      import java.io.File;

      import javax.xml.bind.JAXBContext;
      import javax.xml.bind.Unmarshaller;

      public class LireListe {

          public static void main (String[] args) throws Exception{
         
              String fichIn = args[0] ;
         
              JAXBContext jc = JAXBContext.newInstance("generated");
              Unmarshaller unm = jc.createUnmarshaller();
             
              Liste ml = (Liste) unm.unmarshal(new File(fichIn));
              List<Liste.Eleve> lal =  ml.getEleve();
              Iterator<Liste.Eleve> it = lal.iterator();
              while( it.hasNext() ){
                  Liste.Eleve el = it.next();
                  System.out.println( el.getNom() + " a pour note : " + el.getNote() );
              }
             
          }//main
      }// class LireListe


      On note l'aisance  avec laquelle on a pu convertir le résultat de la désérialisation (unmarshalling) en une Liste.
      Cette facilité provient de ce que, dans le schéma, le type de l'élément <liste> lui est directement attaché
      (en "poupée russe") :
      <xsd:element name="liste">
          <xsd:complexType>....</xsd:complexType>
      </xsd:element>
      .

      On va voir dans l'exemple suivant qu'il en eût été est autrement si on avait écrit le type séparément, comme dans le schéma "à base de types" Nom_note1-2.xsd :
      <xsd:element name="liste" type="listEleves"/>
      <xsd:complexType name="listEleves">....</xsd:complexType>

    3. Lecture d'une voiture

      à partir d'un fichier conforme au schéma "à base de types" Voiture-3.xsd.

      fichier LireVoiture.java

      package generated;

      import javax.xml.bind.JAXBContext;
      import javax.xml.bind.Unmarshaller;
      import javax.xml.bind.JAXBElement;

      import java.io.File;

      public class LireVoiture {

          public static void main (String[] args) throws Exception{
         
              String fichIn = args[0];
             
              JAXBContext jc = JAXBContext.newInstance("generated");
              Unmarshaller unm = jc.createUnmarshaller();
             
              JAXBElement<TypVoiture> JmaVoiture =
                          (JAXBElement<TypVoiture>) unm.unmarshal(new File(fichIn));
              TypVoiture maVoiture = JmaVoiture.getValue();
             
              System.out.println (maVoiture.getMarque()+" "+maVoiture.getMod\u00E8le());
          }
      }//LireVoiture



      La déclaration séparée de l'élément-racine <Voiture> et de son type typVoiture a pour effet d'interdire la conversion
      TypVoiture maVoiture = (TypVoiture) unm.unmarshal(new File(fichIn));
      et d'obliger à passer par un objet inermédiaire de type JAXBElement<TypVoiture>.

  4. Une expérience de modélisation en XMLS : un interprète MIL

    Cette expérience montre que la manière d'écrire un schéma peut avoir de lourdes conséquences sur sa mise en œuvre via JAXB.
    MIL (MIni-Langage) et un modèle réduit de langage de programmation impératif utilisé dans des cours de compilation.
    On a proposé pour lui une syntaxe abstraite en XML, décrite par un schéma, et on a écrit un pretty-printeur en XSLT.
    On va voir que le schéma en question s'avère inadapté comme base pour un interprète normalement conçu.
    L'idée est de mettre en œuvre JAXB en vue d'un programme-application dont la structure est exactement connue à l'avance.
    1. Principe

      réaliser la séparation complète de l'analyse syntaxique et du traitement subséquent de l'arbre de syntaxe abstraite.
      Le pivot est la syntaxe abstraite, exprimée en XML et décrite par un schéma.
      Le parseur MIL est censé produire un fichier XML conforme à ce schéma.
      Ce schéma se traduit par JAXB en un système de classes qui constituent une spécification équivalente en Java.

      L'arbre de syntaxe abstraite s'obtient en tant qu'objet Java par unmarshalling du fichier XML.
      Le traitement (interprétation, pretty-printing, génération de code, etc) se programme en Java sur ce modèle de données, en style procédural classique.
      L'interprète s'écrit naturellement en Java de cette manière. On peut aussi l'écrire directement en DOM, mais ce sera beaucoup plus lourd
      Le pretty-printeur peut s'écrire aussi en XSLT...

      L'intérêt de la chose est évidemment d'avoir une définition "abstraite" en ce sens qu'elle offre deux matérialisations différentes :
      comme schéma XML et comme type Java.
      Peut-on parler de modèle ? En un certain sens, puisque justement on a quelque chose d'indépendant du langage !

      Encore faut-il que la traduction du schéma en Java aboutisse à une structure supportant l'interprète !
      Or l'observation montre que l'écriture en XMLS est sous-spécifiée. Elle offre des possibilités qui ne "passent pas" à travers le compilateur xjc.
      Notamment les groupes !
      Pour des raisons expliquées ci-après, la modélisation adoptée jusqu'ici pour la syntaxe abstraite de MIL emploie les groupes de manière essentielle.
    2. L'affaire des groupes

      Le schéma en question :

      L'idée de la modélisation à base de groupes :
      elle est inspirée par la pratique de la PPO axée sur l'héritage.
      on veut exploiter le fait qu'une constante, par exemple, est "évidemment" une exp. ar.,
      pour "sauter" le niveau ontologique ExpAr en écrivant pour une affectation
      <Affectation> <Var nom="q"/> <Cte val="0"/> </Affectation>
      au lieu de
      <Affectation> <Var nom="q"/> <ExpAr> <Cte val="0"/> </ExpAr> </Affectation>

      On modélise donc l'exp. ar. avec un groupe:

      <xsd:element name="Affectation">
          <xsd:complexType>
              <xsd:sequence>
                  <xsd:element ref="Var"/>
                  <xsd:group ref="Exp"/>
              </xsd:sequence>
          </xsd:complexType>
      </xsd:element>


      Mais le compilateur xjc ne traduit pas le groupe !

      Il engendre (on supprime les annotations pour y voir plus clair)

      public class Affectation {

          protected Var var;

          protected Cte cte;
          protected VarExp varExp;
          protected Bin bin;

      ....
      }

      alors que pour écrire l'interprète on a justement besoin d'un attribut ExpAr unique !

      C'est à dire qu'on veut avoir en Java :

      public class Affectation {

          protected Var var;

          protected ExpAr expAr;
      ....
      }

      Pour cela il faut écrire en XMLS :
      <xsd:element name="Affectation">
          <xsd:complexType>
              <xsd:sequence>
                  <xsd:element ref="Var"/>
                  <xsd:element ref="ExpAr"/>
              </xsd:sequence>
          </xsd:complexType>
      </xsd:element>

      ce qui va donner des instances comme
      <Affectation> <Var nom="q"/> <ExpAr> <Cte val="0"/> </ExpAr> </Affectation>
      ce que justement on voulait éviter.

      En fait on n'obtiendra pas exactement ça,
      car on aura besoin de définir un type pour ExpAr, nommé typExp
      (et non un élement nommé ExpAr avec un type anonyme), vu que plusieurs éléments différents porteront ce type
      (op_gauche, op_droit, terme_gauche, terme_droit).

      Dès lors, c'est le type et non l'élément qui sera traduit en classe !

      protected TypExp expAr;

      Même problème pour les instructions !
      On modélise "léger" :
      <xsd:element name="Sequence">
          <xsd:complexType>
              <xsd:sequence>
                  <xsd:group ref="Inst"
                     minOccurs="0" maxOccurs="unbounded" />
              </xsd:sequence>
          </xsd:complexType>
      </xsd:element>

      <xsd:group name="Inst">
          <xsd:choice>
              <xsd:element ref="Lecture"/>
              <xsd:element ref="Ecriture"/>
              <xsd:element ref="Affectation"/>
              <xsd:element ref="Boucle"/>
              <xsd:element ref="Conditionnelle"/>
              <xsd:element ref="Ligne"/>
              <xsd:element ref="Espace"/>
          </xsd:choice>
      </xsd:group>

      Ce qui donne en Java :
      public class Sequence {

          protected List<Object> inst;
      ....
      }


      Ce qui s'interprète ainsi :
         protected void exec (Sequence seq) {
            List<Object> instrs = seq.getInst();
            for( Object i : instrs ){
               if( i instanceof Affectation ){
                  exec( (Affectation) i );
               }else{
               if( i instanceof Lecture ){
                  exec( (Lecture) i );
               }else{
               if( i instanceof Ecriture ){
                  exec( (Ecriture) i );
               }else{
               if( i instanceof Boucle ){
                  exec( (Boucle) i );
               }else{
               if( i instanceof Conditionnelle ){
                  exec( (Conditionnelle) i );
               }else{
               if( i instanceof Espace ){
                  exec( (Espace) i );
               }else{
               if( i instanceof Ligne ){
                  exec( (Ligne) i );
               }else{
                   System.out.println("Instruction inconnue") ;
               }}}}}}}}
          }// exec (Sequence)


      Comme il n'y a pas d'instruction hors d'une séquence, on peut s'en contenter.
      En revanche, le problème se pose à nouveau pour les deux alternants des conditionnelles !

    3. Conclusion

      Contrairement au désir de légèreté, il faut nommer explicitement les éléments syntaxiques
      • qui jouent des rôles différents dans l'interprète
      • et qui doivent partager le même type
      Ainsi, les deux opérandes des expressions arithmétiques, les deux termes des comparaisons (tous de type ExpAr),
      et les deux alternants des conditionnelles (de type Sequence).
      Moyennant quoi ça marche !

      Le nouveau schéma :
      L'interprète