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'autre parseur tel 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 fournit 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.

Image non disponible

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 :

Image non disponible

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 un référence su l'interface IXMLDOMNode qui est la représentation mémoire du nœud parsé par msxml3.dll.

Image non disponible

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 3 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 :

 
Sélectionnez
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.

 
Sélectionnez
initialization
  MSXML_DOM := TMSDOMImplementationFactory.Create;
  RegisterDOMVendor(MSXML_DOM);
 
Sélectionnez
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 autre la méthode CreateDocument, point d'entré du document XML.

 
Sélectionnez
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é 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é 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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 noeud Sablotron en une référence sur l'interface IDOMNode ?

On va écrire des fonctions utilitaires permettant de générer les noeuds 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.

 
Sélectionnez
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 :

 
Sélectionnez
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 noeud 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 une RegisterDOMVendor avec l'instance de classe dérivante de TDOMVendor est redéfinissant les méthodes Description et DOMImplementation.

 
Sélectionnez
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 :

 
Sélectionnez
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 2 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.

 
Sélectionnez
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 le parseur 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 importants 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