I. Objectif▲
Il sera d'obtenir une requête SQL à partir de son nom et du groupe auquel elle appartient. Voyons pour commencer à quoi pourrait ressembler l'interface de notre QueryLoader :
2.
3.
4.
5.
6.
7.
public
interface
IQueryLoader {
Map<
String, String>
getQueries
(
String queriesName);
String getQuery
(
String groupName, String queryName);
}
Créons ensuite notre fichier XML qui nous servira d'exemple :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
<?xml version="1.0" encoding="UTF-8"?>
<queries>
<group
name
=
"group1"
>
<query
name
=
"query1"
>
SELECT field1 FROM table1
</query>
<query
name
=
"query2"
>
SELECT field2 FROM table2
</query>
</group>
<group
name
=
"group2"
>
<query
name
=
"query3"
>
SELECT field3 FROM table3
</query>
<query
name
=
"query4"
>
SELECT field4 FROM table4
</query>
</group>
</queries>
À partir de ce fichier, nous pouvons écrire assez facilement nos classes Java correspondantes. Tout d'abord notre requête :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
public
class
Query {
private
String name;
private
String query;
public
final
String getName
(
) {
return
name;
}
public
void
setName
(
String name) {
this
.name =
name;
}
public
final
String getQuery
(
) {
return
query;
}
public
final
void
setQuery
(
String query) {
this
.query =
query;
}
}
Puis notre groupe de requêtes :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
public
class
QueryGroup {
private
String name;
private
List<
Query>
queries =
new
LinkedList<
Query>(
);
public
String getName
(
) {
return
name;
}
public
final
void
setName
(
String name) {
this
.name =
name;
}
public
final
List<
Query>
getQueries
(
) {
return
queries;
}
public
final
void
setQueries
(
List<
Query>
queries) {
this
.queries =
queries;
}
}
Il nous faut aussi une classe pour contenir la liste des groupes de requêtes :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
public
class
Queries {
private
List<
QueryGroup>
queryGroups =
new
LinkedList<
QueryGroup>(
);
public
final
void
setQueryGroups
(
List<
QueryGroup>
queryGroups) {
this
.queryGroups =
queryGroups;
}
public
final
List<
QueryGroup>
getQueryGroups
(
) {
return
queryGroups;
}
}
Il nous reste à voir l'implémentation de notre interface IQueryLoader, nous le ferons très bientôt. Occupons-nous maintenant de JAXB.
II. Présentation de JAXB▲
JAXB est l'acronyme de Java Architecture for XML Binding. JAXB est donc capable de sérialiser des objets Java en un fichier XML, et de les désérialiser. Il existe également d'autres API pour manipuler le XML en Java, comme SAX (Simple API for XML) ou JAXP (Java API for XML Processing). SAX fait d'ailleurs partie de JAXP. Mais ces API n'ont pas la même finalité que JAXB, elles servent davantage à manipuler et parcourir l'arborescence XML qu'à réaliser la sérialisation d'objets en XML.
Si on prend l'exemple de SAX, on va parcourir l'arborescence XML avec un parser, instancié depuis une factory. Ce parser lit ses données à partir d'un InputStream, et appelle diverses méthodes d'un handler, que nous créons en héritant d'un DefaultHandler. Le début de l'analyse du document se fait ainsi par l'appel de la méthode startDocument(), l'arrivée sur une balise avec la méthode startElement(), et ainsi de suite pour chaque élément trouvé. Nous devons donc définir le comportement attendu en fonction des cas : est-ce une balise, est-ce celle attendue, ses attributs sont-ils corrects, etc. Tout ceci est plutôt lourd à mettre en place, d'autant plus que notre handler ne conserve aucune donnée en mémoire quand il parcourt le flux XML. C'est à notre implémentation de tout enregistrer.
Si on connait exactement le format des données attendues, ou mieux, si on dispose du schéma, JAXB est beaucoup plus simple à utiliser, puisque quelques classes Java annotées lui suffisent pour analyser le flux XML. Une API qui se rapprocherait de JAXB est Castor, qui fait la même chose : transformer un flux XML en POJO, et réciproquement. Mais plus ancienne, elle passe par l'analyse d'un fichier XML de mapping, parfois complexe à écrire.
Il existe d'autres solutions permettant la sérialisation, par exemple les classes XMLEncoder et XMLDecoder, Jakarta Commons Digester de la fondation Apache, ou encore l'API XStream. Ne connaissant pas ces solutions, je n'en parlerai pas, et vous laisse les découvrir à travers les tutoriels sur Developpez.com, dont vous trouverez les liens en fin d'article.
III. Utilisation de JAXB▲
Pour disposer de JAXB dans notre classpath, avec Maven il suffit d'ajouter les dépendances suivantes dans notre pom :
2.
3.
4.
5.
<dependency>
<groupId>
javax.xml.bind</groupId>
<artifactId>
jaxb-api</artifactId>
<version>
2.2.4</version>
</dependency>
Sinon, on peut le télécharger ici par exemple.
III-A. Annotations de nos classes▲
Nous avons dit que tout se faisait par annotations. Nous allons commencer par la classe correspondant à la racine de notre arborescence XML. Il s'agit ici de Queries, qui sera annotée avec javax.xml.bind.annotation.XmlRootElement :
2.
3.
4.
@XmlRootElement
public
class
Queries {
?
}
Cet élément correspond à la balise racine <queries/>, qui est unique par définition. C'est toujours la première balise rencontrée, il est inutile d'indiquer son nom. Il n'en va pas de même de la balise suivante, <group/>, dont nous devons indiquer le nom. L'annotation est javax.xml.bind.annotation.XmlElement, et se place sur le getter :
2.
3.
4.
@XmlElement
(
name =
"group"
)
public
final
List<
QueryGroup>
getQueryGroups
(
) {
return
queryGroups;
}
Passons maintenant au contenu de cette balise <queries/>, représenté par la classe QueryGroup. Cette balise contient un attribut, son nom. L'annotation est javax.xml.bind.annotation.XmlAttribute :
2.
3.
4.
@XmlAttribute
public
String getName
(
) {
return
name;
}
On retrouve l'annotation @XmlElement pour la balise <Query/> :
2.
3.
4.
@XmlElement
(
name =
"query"
)
public
final
List<
Query>
getQueries
(
) {
return
queries;
}
Finissons par la classe Query. Elle possède un attribut XML, son nom :
2.
3.
4.
@XmlAttribute
(
name =
"name"
)
public
final
String getName
(
) {
return
name;
}
Et aussi une valeur, la requête SQL, annotée avec javax.xml.bind.annotation.XmlValue :
2.
3.
4.
@XmlValue
public
final
String getQuery
(
) {
return
query;
}
Voilà pour ce qui est des annotations. Il n'y a rien de plus à faire, JAXB est maintenant capable très simplement de faire le lien entre nos POJO et notre fichier XML. La transformation d'objets Java en flux XML se nomme le marshalling, l'unmarshalling étant la transformation de XML en POJO. Et ceci se fait simplement avec une seule ligne de code :
Queries queries =
JAXB.unmarshal
(
xmlInputStream, Queries.class
);
Et c'est tout. Il ne nous reste plus qu'à tester que tout fonctionne.
III-B. Tests▲
On écrit une petite classe de test unitaire pour JUnit. Pour tester l'unmarshalling, on part du fichier, et on vérifie que toutes les requêtes sont bien présentes. Pour le test du marshalling, on commence par notre objet « unmarshallé », et qu'on « marshallera » dans une String. Et on « unmarshalle » à nouveau cette String dans un objet Queries, qu'il nous reste à comparer au premier. Voici le code du test :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
import
static
org.hamcrest.CoreMatchers.is;
import
static
org.junit.Assert.assertThat;
// Autres import
public
class
QueriesTest {
private
Queries queries;
@Before
public
void
before
(
) {
InputStream xmlStream =
Queries.class
.getResourceAsStream
(
"queriesTest.xml"
);
queries =
JAXB.unmarshal
(
xmlStream, Queries.class
);
}
@Test
public
void
unmarshallingTest
(
) throws
Exception {
List<
QueryGroup>
queryGroups =
queries.getQueryGroups
(
);
assertThat
(
queries.getQueryGroups
(
).size
(
), is
(
2
));
QueryGroup group =
queryGroups.get
(
0
);
assertThat
(
group.getName
(
), is
(
"group1"
));
assertThat
(
group.getQueries
(
).size
(
), is
(
2
));
List<
Query>
queryList =
group.getQueries
(
);
Query query =
queryList.get
(
0
);
assertThat
(
query.getName
(
), is
(
"query1"
));
assertThat
(
query.getQuery
(
).trim
(
), is
(
"SELECT field1 FROM table1"
));
query =
queryList.get
(
1
);
assertThat
(
query.getName
(
), is
(
"query2"
));
assertThat
(
query.getQuery
(
).trim
(
), is
(
"SELECT field2 FROM table2"
));
group =
queryGroups.get
(
1
);
assertThat
(
group.getName
(
), is
(
"group2"
));
assertThat
(
group.getQueries
(
).size
(
), is
(
2
));
queryList =
group.getQueries
(
);
query =
queryList.get
(
0
);
assertThat
(
query.getName
(
), is
(
"query3"
));
assertThat
(
query.getQuery
(
).trim
(
), is
(
"SELECT field3 FROM table3"
));
query =
queryList.get
(
1
);
assertThat
(
query.getName
(
), is
(
"query4"
));
assertThat
(
query.getQuery
(
).trim
(
), is
(
"SELECT field4 FROM table4"
));
}
@Test
public
void
marshallingTest
(
) throws
Exception {
StringWriter writer =
new
StringWriter
(
);
JAXB.marshal
(
queries, writer);
String xmlString =
writer.toString
(
);
System.out.println
(
xmlString);
Queries queries2 =
JAXB.unmarshal
(
new
StringReader
(
xmlString), Queries.class
);
checkQueries
(
queries2, queries);
}
private
void
checkQueries
(
Queries queries, Queries expected) {
List<
QueryGroup>
groups =
queries.getQueryGroups
(
);
List<
QueryGroup>
expectedGroups =
expected.getQueryGroups
(
);
checkGroups
(
groups, expectedGroups);
}
private
void
checkGroups
(
List<
QueryGroup>
groups, List<
QueryGroup>
expectedGroups) {
assertThat
(
groups.size
(
), is
(
expectedGroups.size
(
)));
Iterator<
QueryGroup>
iterator1 =
groups.iterator
(
);
Iterator<
QueryGroup>
iterator2 =
expectedGroups.iterator
(
);
while
(
iterator1.hasNext
(
)) {
checkGroup
(
iterator1.next
(
), iterator2.next
(
));
}
}
private
void
checkGroup
(
QueryGroup group, QueryGroup expectedGroup) {
assertThat
(
group.getName
(
), is
(
expectedGroup.getName
(
)));
List<
Query>
queriesList =
group.getQueries
(
);
List<
Query>
expectedQueriesList =
expectedGroup.getQueries
(
);
checkQueriesList
(
queriesList, expectedQueriesList);
}
private
void
checkQueriesList
(
List<
Query>
queriesList, List<
Query>
expectedQueriesList) {
assertThat
(
queriesList.size
(
), is
(
expectedQueriesList.size
(
)));
Iterator<
Query>
iterator1 =
queriesList.iterator
(
);
Iterator<
Query>
iterator2 =
expectedQueriesList.iterator
(
);
while
(
iterator1.hasNext
(
)) {
checkQuery
(
iterator1.next
(
), iterator2.next
(
));
}
}
private
void
checkQuery
(
Query query, Query expectedQuery) {
assertThat
(
query.getName
(
), is
(
expectedQuery.getName
(
)));
assertThat
(
query.getQuery
(
).trim
(
), is
(
expectedQuery.getQuery
(
).trim
(
)));
}
}
Nous avons imprimé notre String XML, ce qui nous permet de vérifier visuellement à quoi elle ressemble. Et maintenant que tout est correct, il nous reste à implémenter notre QueryLoader.
III-C. Implémentation du QueryLoader▲
Prenons de bonnes habitudes, et commençons par notre classe de test :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
import
static
org.hamcrest.CoreMatchers.is;
import
static
org.junit.Assert.assertThat;
// Autres imports
public
class
XmlQueryLoaderTest {
private
XmlQueryLoader queryLoader;
@Before
public
void
before
(
) {
InputStream xmlStream =
XmlQueryLoader.class
.getResourceAsStream
(
"queriesTest.xml"
);
queryLoader =
new
XmlQueryLoader
(
xmlStream);
}
@Test
public
void
getQueryTest
(
) throws
Exception {
assertThat
(
queryLoader.getQuery
(
"group1"
, "query1"
), is
(
"SELECT field1 FROM table1"
));
}
@Test
public
void
getQueriesTest
(
) throws
Exception {
Map<
String, String>
queries =
queryLoader.getQueries
(
"group2"
);
assertThat
(
queries.size
(
), is
(
2
));
assertThat
(
queries.get
(
"query3"
), is
(
"SELECT field3 FROM table3"
));
assertThat
(
queries.get
(
"query4"
), is
(
"SELECT field4 FROM table4"
));
}
}
Et enfin, l'implémentation à tester :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
public
class
XmlQueryLoader implements
IQueryLoader {
private
final
Map<
String, Map<
String, String>>
queriesGroup =
new
LinkedHashMap<
String, Map<
String, String>>(
);
public
XmlQueryLoader
(
InputStream xmlStream) {
Queries queries =
JAXB.unmarshal
(
xmlStream, Queries.class
);
for
(
QueryGroup group : queries.getQueryGroups
(
)) {
String groupName =
group.getName
(
);
Map<
String, String>
queryGroup =
new
LinkedHashMap<
String, String>(
);
queriesGroup.put
(
groupName, queryGroup);
for
(
Query query : group.getQueries
(
)) {
String queryName =
query.getName
(
);
String sqlQuery =
query.getQuery
(
);
queryGroup.put
(
queryName, sqlQuery.trim
(
));
}
}
}
@Override
public
Map<
String, String>
getQueries
(
String groupName) {
return
queriesGroup.get
(
groupName);
}
@Override
public
String getQuery
(
String groupName, String queryName) {
return
queriesGroup.get
(
groupName).get
(
queryName);
}
}
Nous avons utilisé une LinkedHashMap, car elle nous permet de parcourir les requêtes SQL dans leur ordre de déclaration si on veut les exécuter les unes à la suite des autres, par exemple pour enchainer la création de tables.
IV. Schéma XML▲
IV-A. Génération du schéma▲
JAXB offre la fonctionnalité de générer un schéma XML, ce qui nous permettra de valider le flux XML reçu. Pour ceci, les quelques lignes de code suivantes suffisent :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
final
File baseDir =
new
File
(
"."
);
class
MySchemaOutputResolver extends
SchemaOutputResolver {
@Override
public
Result createOutput
(
String namespaceUri, String suggestedFileName) throws
IOException {
return
new
StreamResult
(
new
File
(
baseDir, suggestedFileName));
}
}
JAXBContext context =
JAXBContext.newInstance
(
Queries.class
);
context.generateSchema
(
new
MySchemaOutputResolver
(
));
Nous obtenons un fichier schema1.xsd :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<
xs
:
schema
version
=
"1.0"
xmlns
:
xs
=
"http://www.w3.org/2001/XMLSchema"
>
<
xs
:
element
name
=
"queries"
type
=
"queries"
/>
<
xs
:
complexType
name
=
"queries"
>
<
xs
:
sequence>
<
xs
:
element
name
=
"group"
type
=
"queryGroup"
minOccurs
=
"0"
maxOccurs
=
"unbounded"
/>
</
xs
:
sequence>
</
xs
:
complexType>
<
xs
:
complexType
name
=
"queryGroup"
>
<
xs
:
sequence>
<
xs
:
element
name
=
"query"
type
=
"query"
minOccurs
=
"0"
maxOccurs
=
"unbounded"
/>
</
xs
:
sequence>
<
xs
:
attribute
name
=
"name"
type
=
"xs:string"
/>
</
xs
:
complexType>
<
xs
:
complexType
name
=
"query"
>
<
xs
:
simpleContent>
<
xs
:
extension
base
=
"xs:string"
>
<
xs
:
attribute
name
=
"name"
type
=
"xs:string"
/>
</
xs
:
extension>
</
xs
:
simpleContent>
</
xs
:
complexType>
</
xs
:
schema>
IV-B. Validation du flux▲
Nous pouvons utiliser le schéma pour valider le flux XML :
2.
3.
4.
5.
6.
SchemaFactory schemaFactory =
SchemaFactory.newInstance
(
"http://www.w3.org/2001/XMLSchema"
);
Schema schema =
schemaFactory.newSchema
(
new
File
(
baseDir, "schema1.xsd"
));
Unmarshaller unmarshaller =
context.createUnmarshaller
(
);
unmarshaller.setSchema
(
schema);
Queries unmarshall =
(
Queries) unmarshaller.unmarshal
(
xmlStream);
En cas de non-conformité du flux XML par rapport au schéma, la méthode unmarshall() lève une javax.xml.bind.UnmarshalException.
De la même manière que nous avons obtenu notre Unmarshaller, on obtient un Marshaller, auquel on peut préciser le schéma, et qui transformera nos objets en flux XML. Nemek me fait remarquer que la sérialisation (le marshalling) ne produit pas nécessairement un XML valide vis-à-vis du XSD, et qu'il est donc conseillé de préciser le schéma.
IV-C. Génération des classes▲
Le schéma ne sert pas qu'à la validation du flux XML. Nous pouvons aussi l'employer pour générer nos classes, à l'aide de l'utilitaire xjc du JDK :
xjc schema.xsd
Les classes générées sont par défaut dans le package generated. On retrouve bien nos trois classes Queries, QueryGroup et Query, ainsi qu'une classe ObjectFactory. Je vous laisse découvrir à quoi ressemblent ces classes, elles sont très proches des nôtres, avec essentiellement quelques commentaires Javadoc en plus.
Les deux options les plus utiles de xjc sont :
- -help : affiche l'aide et les différentes options disponibles ;
- -p : permet de préciser le package. Par exemple avec -p fr.atatorus.gen, les classes seront dans fr/atatorus/gen et non plus dans generated ;
- -d : précise le répertoire où seront les classes générées.
V. Conclusion▲
Voilà, j'espère que ce petit tutoriel vous a permis de découvrir JAXB, et d'apprécier sa facilité d'utilisation. Pour ma part, j'ai vraiment aimé sa simplicité. Ma première expérience avec le binding Java XML a commencé avec JAXP, où on devait tout faire soi-même. La découverte de Castor a été un soulagement, même si j'ai dû me débattre avec les fichiers de mapping. À côté, JAXB est un vrai bonheur !
Si vous voulez découvrir toutes les possibilités de JAXB, je vous renvoie à sa documentation.
VI. Remerciements▲
Je tiens à remercier Nemek, le_y@ms, Gueritarish et Keulkeul pour leur aide et leurs critiques apportées à la rédaction de ce tutoriel, ainsi que _Max_ et Claude Leloup pour leur relecture.
VII. Liens▲
Tutoriel sur la sérialisation XML en Java avec XMLEncoder et XMLDecoder.
Tutoriel sur Jakarta Commons Digester.
Tutoriel sur la sérialisation XML avec XStream.
Tutoriel d'origine sur mon blog.