I. Introduction▲
Kylix 2 et Delphi 6 introduisent un nouveau composant TXMLDocument permettant de manipuler facilement les documents au format XML. La particularité de ce composant est sa capacité à utiliser différents parseurs XML. Sous Windows avec Delphi 6 c'est le parseur de Microsoft (MSXML) qui est utilisé par défaut, mais il est possible de changer pour celui d'IBM, d'OpenXML (entièrement écrit en Delphi), etc. Avec Kylix 2 c'est ce dernier qui est utilisé par défaut. Malheureusement, contrairement à celui de Microsoft, OpenXML n'implémente pas de transformation XSL ni de XPath. Mais avec l'architecture du composant TXMLDocument, il est possible d'écrire une interface pour d'autres parseurs tels que GDOM/libXSL (Gnome) ou Sablotron fournissant un processeur XSL et une gestion de XPath.
L'objectif de cet article est de décrire l'architecture du composant TXMLDocument et de montrer comment implémenter une interface pour un parseur tiers.
Remarque : ceci est aussi valable pour Delphi 6, mais MSXML suffit largement pour les manipulations XML/XSL, alors qu'OpenXML fourni avec Kylix 2 n'implémente que le DOM.
II. Architecture de TXMLDocument▲
II-A. Modèle objet▲
Le composant TXMLDocument a la capacité de changer dynamiquement de parseur. Ceci est possible grâce à la définition dans l'architecture du composant d'une couche d'abstraction représentant les méthodes de manipulation du DOM. Cette couche d'abstraction (dans le fichier xmldom.pas) est entièrement faite à base d'interfaces (IInterface ou IUnknown). Pour rappel, les interfaces peuvent être comparées à des classes abstraites pures, c'est-à -dire que seules les méthodes sont déclarées et aucune implémentation n'est donnée. À partir de ces interfaces, on peut coder un comportement différent dans des classes les implémentant pour chacune des méthodes définies.
Chaque unité de parseur (msxmldom.pas, oxmldom.pas) implémente donc les interfaces du DOM et fait référence aux objets internes du parseur :
Pour MSXML, le TXMLNode a une référence sur un IDOMNode (interface du DOM) qui est implémenté par TMSDOMNode (msxmldom.pas). Cette classe garde une référence sur l'interface IXMLDOMNode qui est la représentation mémoire du nœud parsé par msxml3.dll.
Pour OpenXML, c'est la classe TOXDOMNode (oxmldom.pas) qui implémente le IDOMNode (interface du DOM). Cette classe a une référence du un TdomNode (XDom.pas) qui est la classe encapsulant le nœud pour le parseur OpenXML.
Remarque : en tout, on a trois objets en mémoire pour chaque nœud. Il faut donc garder à l'esprit que pour 1000 nœuds XML, on a 3000 objets en mémoire…
II-B. Choix du parseur▲
Le changement dynamique du parseur s'effectue via la propriété DOMVendor de type TDOMVendor. Cette classe abstraite est déclarée de la façon suivante :
TDOMVendor = class
public
function Description: string; virtual; abstract;
function DOMImplementation: IDOMImplementation; virtual; abstract;
end;
Dans la section initialization de chaque unité implémentant l'interfaçage pour chaque parseur, l'appel à la méthode RegisterDOMVendor permet d'enregistrer le nouveau parseur dans une liste interne globale (xmldom.pas) de tous les Vendors disponibles.
initialization
MSXML_DOM := TMSDOMImplementationFactory.Create;
RegisterDOMVendor(MSXML_DOM);
initialization
OpenXMLFactory := TOXDOMImplementationFactory.Create;
RegisterDOMVendor(OpenXMLFactory);
Dans les exemples précédents, TMSDOMImplementationFactory et TOXDOMImplementationFactory dérivent de TDOMVendor et redéfinissent les méthodes Description et DOMImplementation qui vont être appelées par le composant TXMLDocument. La méthode Description renvoie une chaîne de caractères indiquant le nom du parseur (MSXML, OpenXML, etc.). La méthode DOMImplementation renvoie une référence sur une classe implémentant l'interface IDOMImplementation fournissant entre autres la méthode CreateDocument, point d'entrée du document XML.
IDOMImplementation = interface
['{2BF4C0E0-096E-11D4-83DA-00C04F60B2DD}']
function hasFeature(const feature, version: DOMString): WordBool;
function createDocumentType(const qualifiedName, publicId, { DOM Level 2 }
systemId: DOMString): IDOMDocumentType; safecall;
function createDocument(const namespaceURI, qualifiedName: DOMString;{ DOM Level 2 }
doctype: IDOMDocumentType): IDOMDocument; safecall;
end;
III. Ajout d'un parseur tiers▲
III-A. Implémentation des interfaces▲
L'ajout d'un nouveau parseur au composant TXMLDocument passe par l'implémentation des interfaces décrites dans xmldom.pas. les plus importantes sont :
- IDOMImplementation (point d'entrée du document XML, création du document) ;
- IDOMNode (manipulation de base des nœuds) ;
- IDOMNodeEx (extensions : Text, XML, TransformNode (XSL)) ;
- IDOMNodeSelect (SelectNode, SelectNodes (Xpath))Â ;
- IDOMNodeList (manipulation des listes de nœuds) ;
- IDOMNamedNodeMap (manipulation des listes d'attributs)Â ;
- IDOMAttr (manipulation spécifique d'un attribut) ;
- IDOMElement (manipulation spécifique d'éléments) ;
- IDOMDocument (manipulation spécifique de documents).
On va donc créer une unité semblable à msxmldom.pas ou oxmldom.pas. Pour Sablotron, je l'ai appelée sablotxmldom.pas.
Il s'agit donc de créer des classes implémentant spécifiquement le comportement de l'appel aux méthodes du parseur sous-jacent. Par exemple si l'on crée la classe TSablotDOMImplementation implémentant IDOMImplementation :
TSablotDOMImplementation = class(TSablotDOMInterface, IDOMImplementation)
private
FSablotSituation: TSablotSituation;
protected
{ IDOMImplementation }
function hasFeature(const feature, version: DOMString): WordBool;
function createDocumentType(const qualifiedName, publicId,
systemId: DOMString): IDOMDocumentType; safecall;
function createDocument(const namespaceURI, qualifiedName: DOMString;
doctype: IDOMDocumentType): IDOMDocument; safecall;
public
constructor Create;
end;
on aura le code de createDocument :
function TSablotDOMImplementation.createDocument(const namespaceURI,
qualifiedName: DOMString; doctype: IDOMDocumentType): IDOMDocument;
var
Doc: TSDOM_Document;
begin
Sablot.SablotCreateDocument(FSablotSituation, @Doc);
Result := TSablotDOMDocument.Create(FSablotSituation, Doc);
end;
La donnée membre FSablotSituation est spécifique à l'implémentation de Sablotron. C'est une sorte de contexte permettant d'identifier sur quel document on est en train de travailler. Il est nécessaire pour tous les appels aux fonctions du parseur. La fonction SablotCreateDocument est une fonction de la bibliothèque Sablotron. Cette dernière est interfacée dans l'unité Sablot.pas où l'on retrouve les prototypes des fonctions publiés par le .so (ou la dll). TSablotDOMDocument est une classe qui implémente IDOMDocument.
Un problème se pose : comment adapter un objet TSDOM_Node qui est un nœud Sablotron en une référence sur l'interface IDOMNode ?
On va écrire des fonctions utilitaires permettant de générer les nœuds dans la représentation du DOM (Réference sur les interfaces du xmldom.pas). On a par exemple la fonction MakeNode qui à partir d'un node dans la représentation du parseur crée une référence sur IDOMNode. De même on fera un MakeNodeList pour adapter un TSDOM_NodeList en IDOMNodeList.
function MakeNode(SablotSituation: TSablotSituation; Node: TSDOM_Node): IDOMNode;
const
NodeClasses: array[ELEMENT_NODE..NOTATION_NODE] of TSablotDOMNodeClass =
(TSablotDOMElement, TSablotDOMAttr, TSablotDOMText, TSablotDOMCDataSection,
TSablotDOMEntityReference, TSablotDOMEntity, TSablotDOMProcessingInstruction,
TSablotDOMComment, TSablotDOMDocument, TSablotDOMDocumentType, TSablotDOMDocumentFragment,
TSablotDOMNotation);
var
NodeType: Integer;
begin
if Node <> nil then
begin
Sablot.SDOM_getNodeType(SablotSituation, Node, @NodeType);
Result := NodeClasses[NodeType].Create(SablotSituation, Node);
end
else
Result := nil;
end;
La fonction déclare un tableau de toutes les classes implémentant un descendant de IDOMNode. Après avoir récupéré le type du nœud passé en paramètre, on crée une instance grâce à la référence de classe correspondante.
On va pouvoir ensuite l'utiliser de la manière suivante :
function TSablotDOMDocument.createElement(const tagName: DOMString): IDOMElement;
var
Node: TSDOM_Node;
SName: string;
begin
SName := tagName;
Sablot.SDOM_createElement(FSablotSituation, SablotDocument, @Node, PChar(SName));
Result := MakeNode(FSablotSituation, Node) as IDOMElement;
end;
la fonction du parseur SDOM_CreateElement crée un nouveau nœud Sablotron (TSDOM_Node) puis on crée à partir de celui-ci un TSablotDOMElement implémentant l'interface IDOMElement du DOM.
III-B. Enregistrement et utilisation du nouveau parseur▲
Pour rendre disponible ce nouveau parseur au niveau de TXMLDocument, il suffit de rajouter dans la clause initialization un RegisterDOMVendor avec l'instance de classe dérivante de TDOMVendor est redéfinissant les méthodes Description et DOMImplementation.
initialization
Sablot_DOM := TSablotDOMImplementationFactory.Create;
RegisterDOMVendor(Sablot_DOM);
finalization
UnRegisterDOMVendor(Sablot_DOM);
Sablot_DOM.Free;
Dans l'exemple ci-dessus, TSablotDOMImplementationFactory dérive de TDOMVendor.
La méthode DOMImplementation doit renvoyer une référence sur IDOMImplemention. Il suffit donc d'instancier la classe l'implémentant :
function TSablotDOMImplementationFactory.DOMImplementation: IDOMImplementation;
begin
Result := TSablotDOMImplementation.Create;
end;
Maintenant nous sommes prêts à utiliser le nouveau parseur. Pour rendre disponible Sablotron dans la liste des DOMVendors, il suffit d'ajouter les deux fichiers Sablot.aps et salbotxmldom.pas dans un paquet (par exemple user.dpk) de conception et de l'installer.
On pourra aussi l'indiquer à l'exécution. Pour que l'enregistrement se fasse, il est impératif d'inclure notre unité dans la clause uses de l'unité utilisant le TXMLDocument.
var
XMLDocumentCompo: TXMLDocument;
XMLDocument: IXMLDocument;
begin
XMLDocumentCompo := TXMLDocument.Create(nil);
XMLDocumentCompo.DOMVendor := Sablot_DOM;
XMLDocumentCompo.XML.Text := '<document></document>';
XMLDocument := XMLDocumentCompo;
XMLDocument.Active := True;
end;
la variable Sablot_DOM est l'instance du TDOMVendor enregistrée dans l'initialization de l'unité. Une fois que l'on a fixé le DOMVendor, on utilisera l'interface IXMLDocument pour manipuler le document XML.
IV. Conclusion▲
Le composant TXMLDocument permet, en plus de manipuler facilement des documents XML, de changer de parseur permettant de gérer le XSL ou le XPath ce qui n'est actuellement pas possible avec les parseurs OpenXML ou IBM livrés avec Kylix 2. Autre grand avantage de Sablotron, sa portabilité : on va pouvoir utiliser notre interfaçage aussi bien sous Linux que sous Windows. En plus de l'interfaçage en Pascal des bibliothèques des parseurs, il est nécessaire d'implémenter de façon spécifique, pour chaque parseur, les comportements pour les interfaces du DOM définies dans Kylix 2. Ce travail reste assez fastidieux compte tenu du nombre important de méthodes.
Voici l'interface en Pascal ainsi que l'unité implémentation les interfaces du DOM pour utiliser le parseur Sablotron dans Kylix2. L'implémentation que j'en ai faite reste basique, mais suffisante pour la plupart des utilisations.
Sablotron: http://www.gingerall.com/charlie/ga/xml/p_sab.xml