JAXB et le XML Data Binding
Jean-François Perrot
- Introduction
- XML Data Binding
- Références
- Le compilateur de schémas xjc
- Principe
- Premier exemple, avec le schéma en
poupée russe Nom_note1-1.xsd
- Deuxième exemple, avec le schéma "à
base de types" Voiture-3.xsd
- Mise en œuvre élémentaire
- Création d'une liste de noms-notes
- Lecture d'une liste de noms-notes
- Lecture d'une voiture
- Une expérience de modélisation en XMLS
: un interprète MIL
- Principe
- L'affaire des groupes
- Conclusion
-
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)
-
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.
-
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
-
-
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
".
-
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.
-
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;
}
-
-
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>
-
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>
-
à 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>
.
-
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.
-
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.
-
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 !
-
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
: