Manipulation XML avec DOM4j
DOM4j est une bibliothèque Java largement utilisée pour lire, écrire et naviguer dans des documents XML. Elle repose sur le modèle DOM (Document Object Model), ce qui permet une manipulation intuitive de l'arbre XML.
Configuration et Méthodes Principales
Pour utiliser DOM4j, il faut intégrer la dépendance correspondante dans votre projet. Une fois configurée, l'API permet d'interagir avec les nœuds, les éléments, les attribtus et le texte.
- Nœuds :
Iterator<Node> nodeIterator()pour récupérer tous les nœuds enfants. - Éléments :
Element getRootElement()pour obtenir la racine,element("nom")pour un enfant spécifique, etelements()pour tous les enfants. - Attributs :
attributeValue("nom")pour la valeur,attribute("nom")pour l'objet, etattributes()pour la liste complète. - Texte :
getText()pour le texte courant,elementText("nom")pour le texte d'un enfant.
Modification et Sauvegarde
La création et la mise à jour de documents se font via des méthodes dédiées. Pour persister les modifications, on utilise un XMLWriter.
- Ajout :
DocumentHelper.createDocument(),addElement("nom"),addAttribute("nom", "valeur"). - Modification :
setValue()sur un attribut,setText()sur un élément. - Suppression :
detach()pour retirer un élément ou un attribut de son parenet.
Analyse Événementielle avec SAX
Contrairement à DOM, SAX (Simple API for XML) est un analyseur événementiel. Il lit le fichier séquentiellement et déclenche des callbacks, ce qui le rend très économe en mémoire.
API Fondamentales
L'analyse repose sur la classe SAXParser et un gestionnaire d'événements héritant de DefaultHandler.
SAXParserFactory fabrique = SAXParserFactory.newInstance();
SAXParser analyseur = fabrique.newSAXParser();
analyseur.parse(new File("ressources/annuaire.xml"), new GestionnaireEvenementsSax());
Le gestionnaire d'événements implémente les méthodes suivantes :
startDocument()/endDocument(): Début et fin du document.startElement(...)/endElement(...): Détection des balises ouvrantes et fermantes.characters(...): Extraction du contenu textuel entre les balises.
Comparaison : DOM vs SAX
| Caractéristique | DOM (ex: DOM4j) | SAX |
|---|---|---|
| Consommation mémoire | Charge tout le document en mémoire. Inadapté aux très gros fichiers. | Lit flux par flux. Idéal pour les fichiers volumineux. |
| Opérations | Lecture, écriture, modification et suppression. | Lecture seule. |
| Navigation | Accès aléatoire, navigation dans toutes les directions. | Navigation séquentielle stricte (de haut en bas). |
| Paradigme | Orienté objet (Arbre de nœuds). Plus intuitif. | Orienté événement. Plus complexe à implémenter. |
Navigation avec XPath
XPath est un langage de requête permettant de cibler précisément des nœuds dans un document XML. Avec DOM4j, il nécessite l'ajout de la dépendance jaxen.
Méthodes principales : selectNodes("expression") pour une liste de nœuds, et selectSingleNode("expression") pour un nœud unique.
/: Chemin absolu depuis la racine.//: Chemin relatif, recherche à n'imoprte quel niveau.*: Jokers pour tous les éléments.[]: Filtres conditionnels (ex://contact[@id='123']).@: Sélection d'attributs.text(): Sélection du contenu textuel.
Étude de Cas : Système de Gestion d'Annuaire
Pour illustrer ces concepts, nous allons concevoir une application de gestion de contacts stockée en XML. L'architecture sera découpée en plusieurs couches pour garantir la maintenabilité.
Modèle de Données
public class FicheContact {
private String identifiant;
private String nomComplet;
private String genre;
private int age;
private String telephone;
private String courriel;
private String pseudo;
public FicheContact(String identifiant, String nomComplet, String genre, int age,
String telephone, String courriel, String pseudo) {
this.identifiant = identifiant;
this.nomComplet = nomComplet;
this.genre = genre;
this.age = age;
this.telephone = telephone;
this.courriel = courriel;
this.pseudo = pseudo;
}
// Getters et Setters omis pour concision
@Override
public String toString() {
return String.format("[%s] %s (%s, %d ans) - %s", identifiant, nomComplet, genre, age, courriel);
}
}
Contrat de Service
import java.util.List;
public interface ServiceAnnuaire {
void ajouterFiche(FicheContact fiche) throws Exception;
void mettreAJourFiche(FicheContact fiche) throws Exception;
void supprimerFiche(String identifiant) throws Exception;
List<fichecontact> listerFiches();
}
</fichecontact>
Utilitaire XML
Pour éviter la duplication de code lors des opérations d'entrée/sortie, nous centralisons la logique de lecture et d'écriture.
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import java.io.FileOutputStream;
import java.io.OutputStream;
public class OutilXml {
private static final String CHEMIN_FICHIER = "data/annuaire.xml";
public static void sauvegarder(Document document) throws Exception {
try (OutputStream flux = new FileOutputStream(CHEMIN_FICHIER)) {
OutputFormat format = OutputFormat.createPrettyPrint();
format.setEncoding("UTF-8");
try (XMLWriter redacteur = new XMLWriter(flux, format)) {
redacteur.write(document);
}
}
}
public static Document charger() {
try {
return new SAXReader().read(CHEMIN_FICHIER);
} catch (DocumentException e) {
throw new RuntimeException("Impossible de charger le fichier XML", e);
}
}
}
Implémentation du Service
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class ServiceAnnuaireImpl implements ServiceAnnuaire {
@Override
public void ajouterFiche(FicheContact fiche) throws Exception {
File fichier = new File("data/annuaire.xml");
Document doc;
Element racine;
if (fichier.exists()) {
doc = OutilXml.charger();
racine = doc.getRootElement();
} else {
doc = DocumentHelper.createDocument();
racine = doc.addElement("annuaire");
}
Element noeudContact = racine.addElement("contact");
noeudContact.addAttribute("id", fiche.getIdentifiant());
noeudContact.addElement("nom").setText(fiche.getNomComplet());
noeudContact.addElement("genre").setText(fiche.getGenre());
noeudContact.addElement("age").setText(String.valueOf(fiche.getAge()));
noeudContact.addElement("telephone").setText(fiche.getTelephone());
noeudContact.addElement("courriel").setText(fiche.getCourriel());
noeudContact.addElement("pseudo").setText(fiche.getPseudo());
OutilXml.sauvegarder(doc);
}
@Override
public void mettreAJourFiche(FicheContact fiche) throws Exception {
Document doc = OutilXml.charger();
String xpath = "//contact[@id='" + fiche.getIdentifiant() + "']";
Element noeudContact = (Element) doc.selectSingleNode(xpath);
if (noeudContact != null) {
noeudContact.element("nom").setText(fiche.getNomComplet());
noeudContact.element("genre").setText(fiche.getGenre());
noeudContact.element("age").setText(String.valueOf(fiche.getAge()));
noeudContact.element("telephone").setText(fiche.getTelephone());
noeudContact.element("courriel").setText(fiche.getCourriel());
noeudContact.element("pseudo").setText(fiche.getPseudo());
OutilXml.sauvegarder(doc);
}
}
@Override
public void supprimerFiche(String identifiant) throws Exception {
Document doc = OutilXml.charger();
String xpath = "//contact[@id='" + identifiant + "']";
Element noeudContact = (Element) doc.selectSingleNode(xpath);
if (noeudContact != null) {
noeudContact.detach();
OutilXml.sauvegarder(doc);
}
}
@Override
public List<fichecontact> listerFiches() {
Document doc = OutilXml.charger();
List<fichecontact> annuaire = new ArrayList<>();
List<element> elements = doc.selectNodes("//contact");
for (Element elem : elements) {
FicheContact fiche = new FicheContact(
elem.attributeValue("id"),
elem.elementText("nom"),
elem.elementText("genre"),
Integer.parseInt(elem.elementText("age")),
elem.elementText("telephone"),
elem.elementText("courriel"),
elem.elementText("pseudo")
);
annuaire.add(fiche);
}
return annuaire;
}
}
</element></fichecontact></fichecontact>
Tests Unitaires
L'utilisation de JUnit permet de valider chaque opération de manière isolée avant l'intégration dans l'interface utilisateur.
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class TestServiceAnnuaire {
private ServiceAnnuaire service;
@BeforeEach
public void initialiser() {
service = new ServiceAnnuaireImpl();
}
@Test
public void testAjoutEtListage() throws Exception {
FicheContact nouvelleFiche = new FicheContact("C001", "Alice Dupont", "F", 28,
"0612345678", "alice@test.com", "alice95");
service.ajouterFiche(nouvelleFiche);
List<fichecontact> fiches = service.listerFiches();
assertFalse(fiches.isEmpty());
assertEquals("Alice Dupont", fiches.get(0).getNomComplet());
}
@Test
public void testMiseAJour() throws Exception {
FicheContact ficheModifiee = new FicheContact("C001", "Alice Martin", "F", 29,
"0687654321", "alice.martin@test.com", "alice_m");
service.mettreAJourFiche(ficheModifiee);
List<fichecontact> fiches = service.listerFiches();
assertEquals("Alice Martin", fiches.stream()
.filter(f -> f.getIdentifiant().equals("C001"))
.findFirst().get().getNomComplet());
}
@Test
public void testSuppression() throws Exception {
service.supprimerFiche("C001");
List<fichecontact> fiches = service.listerFiches();
assertTrue(fiches.stream().noneMatch(f -> f.getIdentifiant().equals("C001")));
}
}
</fichecontact></fichecontact></fichecontact>