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.
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.
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>
.
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);
java.time.LocalDate
.
// pour obtenir une date en Java 8 : LocalDate y2000 = LocalDate.parse("2000-01-01");
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.
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.
<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>
$ mvn dependency:resolve
$ mvn dependency:build-classpath
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
-Dcom.sun.tools.xjc.XJCFacade.nohack=true
permet à XJC d'accéder à la classe de votre plugin
(explications ici).