Parsing de fichier XML avec Castor
Dans un programme Java, le chargement de fichiers XML externe est quelque chose de très fréquent. On utilise alors souvent DOM ou SAX, et c'est en général assez fastidieux. En voici un exemple simple avec l'API DOM.
Heureusement, et comme souvent d'ailleurs, des gens ont pensé au problème d'automatiser et de simplifier tout ça. Voici un exemple avec la bibliothèque Castor.
Je pars du principe que la version de Java utilisée est au moins la 1.5 (nécessaire pour les traitements XML sans JAR additionnels).
Fonctionnement de Castor
Castor va générer pour vous les classes métiers correspondantes aux différents éléments de votre fichier XML. Le prérequis pour cette étape est d'avoir un schéma XSD correspondant à vos fichiers XML. C'est plutôt une bonne chose que d'en avoir un, car cela permet d'être certain de ce qui va rentrer dans votre programme, et d'avoir donc mois de cas d'erreur à traiter.
Castor propose pour la génération une tâche Ant, qui prendra donc en paramètres un fichier XSD, un ou deux fichiers de configuration si nécessaire, un répertoire de sortie pour les fichiers source Java générés.
Fichiers XML et XSD
Créer un nouveau projet (avec Eclipse par exemple), avec répertoire source (src) et destination (bin) séparés.
Dans le répertoire "./src", placer le fichier schéma suivant, nommé "shiporder.xsd" :
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!-- http://www.w3schools.com/Schema/schema_example.asp -->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:simpleType name="stringtype">
<xs:restriction base="xs:string"/>
</xs:simpleType>
<xs:simpleType name="inttype">
<xs:restriction base="xs:positiveInteger"/>
</xs:simpleType>
<xs:simpleType name="dectype">
<xs:restriction base="xs:decimal"/>
</xs:simpleType>
<xs:simpleType name="orderidtype">
<xs:restriction base="xs:string">
<xs:pattern value="[0-9]{6}"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="shipto">
<xs:sequence>
<xs:element name="name" type="stringtype"/>
<xs:element name="address" type="stringtype"/>
<xs:element name="city" type="stringtype"/>
<xs:element name="country" type="stringtype"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="item">
<xs:sequence>
<xs:element name="title" type="stringtype"/>
<xs:element name="note" type="stringtype" minOccurs="0"/>
<xs:element name="quantity" type="inttype"/>
<xs:element name="price" type="dectype"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="shiporder">
<xs:sequence>
<xs:element name="orderperson" type="stringtype"/>
<xs:element name="shipto" type="shipto"/>
<xs:element name="item" maxOccurs="unbounded" type="item"/>
</xs:sequence>
<xs:attribute name="orderid" type="orderidtype" use="required"/>
</xs:complexType>
<xs:element name="shiporder" type="shiporder"/>
</xs:schema>
Créer également dans le répertoire racine un fichier de données exemple "./shiporder.xml" :
<?xml version="1.0" encoding="ISO-8859-1"?> <shiporder orderid="889923" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="shiporder.xsd"> <orderperson>John Smith</orderperson> <shipto> <name>Ola Nordmann</name> <address>Langgt 23</address> <city>4000 Stavanger</city> <country>Norway</country> </shipto> <item> <title>Empire Burlesque</title> <note>Special Edition</note> <quantity>1</quantity> <price>10.90</price> </item> <item> <title>Hide your heart</title> <quantity>1</quantity> <price>9.90</price> </item> </shiporder>
Génération du code
Dans le répertoire "./lib", dans lequel nous allons mettre les JAR nécessaire à l'exécution projet :
- castor-1.2.jar
- castor-1.2-xml.jar
- castor-1.2-xml-schema.jar
- commons-io-1.1.jar
- commons-lang-2.4.jar
- commons-logging.jar
- jakarta-oro-2.0.8.jar
- log4j-1.2.8.jar
Et dans un répertoire "./lib/extra", placer les JAR nécessaire à la génération du code par Ant :
À la racine du projet, créer le fichier build ant "./build.xml" suivant :
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="build" name="ExempleCastor">
<property name="project.name" value="ExempleCastor" />
<property name="dist.dir" location="dist" />
<property environment="env" />
<property name="debuglevel" value="source,lines,vars" />
<property name="target" value="1.2" />
<property name="source" value="1.3" />
<path id="ExempleCastor.classpath">
<fileset dir="lib/">
<include name="*.jar" />
</fileset>
</path>
<path id="ExempleCastor.extra.classpath">
<fileset dir="lib/extra">
<include name="*.jar" />
</fileset>
<path refid="ExempleCastor.classpath" />
</path>
<target name="clean">
<delete dir="bin" />
<mkdir dir="bin" />
</target>
<!-- VERIFICATION DE LA JVM : pour que CASTOR fonctionne, il faut que la JVM soit au moins 1.5 -->
<target name="get-jvm">
<condition property="jvm.ok">
<or>
<equals arg1="${ant.java.version}" arg2="1.5" />
<equals arg1="${ant.java.version}" arg2="1.6" />
</or>
</condition>
</target>
<target name="check-jvm" depends="get-jvm" unless="jvm.ok">
<fail message="Wrong JVM - ${ant.java.version}" />
</target>
<target name="test-jvm" depends="check-jvm">
<echo message="JVM OK - ${ant.java.version}" />
</target>
<target name="generate-sources" depends="test-jvm" description="Generate Java source files from XSD.">
<taskdef name="castor-srcgen" classname="org.castor.anttask.CastorCodeGenTask" classpathref="ExempleCastor.extra.classpath" />
<delete dir="generated-source" />
<mkdir dir="generated-source" />
<castor-srcgen bindingfile="bindings.xml" casesensitive="on" verbose="yes" file="./src/shiporder.xsd" todir="generated-source" package="org.bouil.exemple.castor.schema.shiporder" types="j2" warnings="true" />
</target>
<target name="compile" depends="generate-sources">
<javac debug="true" destdir="bin" classpathref="ExempleCastor.classpath" source="${source}" target="${target}">
<src path="src" />
<src path="generated-source" />
</javac>
<copy todir="bin">
<fileset dir="src">
<exclude name="**/*.java" />
</fileset>
</copy>
</target>
<target name="jar" depends="compile">
<delete dir="dist" />
<mkdir dir="dist" />
<jar destfile="dist/${project.name}.jar" basedir="bin">
<manifest>
<attribute name="Main-Class" value="org.bouil.exemple.castor.Process" />
</manifest>
</jar>
</target>
<target name="build" depends="clean,generate-sources,compile,jar" />
</project>
Je ne détaille pas tout le script Ant, la tache nommée "generate-sources" est celle mettant en jeu Castor.
Le fichier, à la racine du projet, nommé "bindings.xml" est important, car sans celui ci, la génération des sources ne fonctionnerait pas.
En effet, j'ai volontairement dans le fichier XSD, nommé les éléments avec le même nom que les types de données. Ceci se voit par exemple ici :
<s:element name="shiporder" type="shiporder"/>
Castor va générer une classe pour le Bean représentant l'élément "shiporder", et une classe pour le type complexe "shiporder", qui ont le malheur de s'appeler pareil. La génération donnerait un message comme celui ci :
Warning: A class name generation conflict has occured between complexType '/complexType:shipto' and element '/complexType:shiporder/shipto'. Please use a Binding file to solve this problem.
Le fichier "bindings.xml" permet de résoudre ce problème :
<binding xmlns="http://www.castor.org/SourceGenerator/Binding"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.castor.org/SourceGenerator/Binding
C:\\Castor\\xsd\\binding.xsd"
defaultBinding="element">
<namingXML>
<complexTypeName>
<suffix>Type</suffix>
</complexTypeName>
</namingXML>
<!--
<elementBinding
name="/complexType:shiptotype">
<java-class name="ShipToType" />
</elementBinding>
-->
</binding>
La partie mise en commentaire permet, si nécessaire, de renommer un élément avec le nom choisi.
Lancer maintenant la tâche Ant avec la cible par défaut. Le répertoire "./generated-source" doit maintenant être rempli avec les sources générés par Castor.
Chargement du XML
Avec Eclipse, ajouter le répertoire "./generated-source" à la liste des répertoires contentant des sources Java.
La classe permettant de charger le fichier XML est assez simple. J'y ajouté également une validation du fichier XML par rapport au schéma XSD afin d'être certain que le fichier d'entrée est correct.
Le fichier XSD est volontairement chargé via le ClassLoader. Je considère en effet que ce fichier fait partie des sources, et n'est pas susceptible d'être modifié sans que les souces générés soit également modifiés.
Créer cette classe dans le fichier "./src/org/bouil/exemple/castor/Process.java" :
package org.bouil.exemple.castor;
import java.io.File;
import java.io.FileInputStream;
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.bouil.exemple.castor.schema.shiporder.Shiporder;
import org.exolab.castor.xml.Unmarshaller;
import org.xml.sax.InputSource;
public class Process {
public static void main(String[] args) throws Exception {
File fichierXML = new File("shiporder.xml");
validateXML(fichierXML);
unmarshal(fichierXML);
}
/**
* Valide le fichier XML par rapport au Schéma
* @param fichierXML
* @return
* @throws Exception
*/
public static boolean validateXML(File fichierXML) throws Exception {
try {
System.out.println("Validation XSD du fichier XML...");
long debut = System.currentTimeMillis();
SchemaFactory factory = SchemaFactory
.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schemaFile = factory.newSchema(new StreamSource(
Process.class.getClassLoader().getResourceAsStream(
"shiporder.xsd")));
Validator validator = schemaFile.newValidator();
validator
.validate(new StreamSource(new FileInputStream(fichierXML)));
long fin = System.currentTimeMillis();
long duree = fin - debut;
System.out.println(duree + " ms");
return true;
} catch (Exception e) {
throw e;
}
}
/**
* Charge le fichier XML dans le Bean généré par Castor
* @param fichierXML
* @throws Exception
*/
public static void unmarshal(File fichierXML) throws Exception {
System.out.println("Lecture du fichier XML en mémoire...");
long debut = System.currentTimeMillis();
InputSource inputSource = new InputSource(new FileInputStream(
fichierXML));
Shiporder shipOrder = (Shiporder) Unmarshaller.unmarshal(
Shiporder.class, inputSource);
long fin = System.currentTimeMillis();
long duree = fin - debut;
System.out.println(duree + " ms");
System.out.println("shipOrder " + shipOrder.getOrderid() + " chargée");
}
}
Exécution
L'exécution donne ceci :
Validation XSD du fichier XML... 79 ms Lecture du fichier XML en mémoire... 182 ms shipOrder 889923 chargée