Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

Lire et écrire des fichiers XML Delphi TClientDataSet avec C#

[new:15/07/2010]Vous pouvez vous dire “quelle mouche le pique ?” A quoi bon en effet perdre son temps à bricoler des fichiers XML issus du TClientDataSet de Delphi puisque Delphi n’est plus qu’un souvenir (malgré quelques sursauts du côté d’Embarcadero à 3500 euros HT pour la version 2010, faut en vouloir !) et que le XML produit par le TClientDataSet est une curiosité ?

La chaleur de ce dimanche trop lourd peut-être ? Non, car des vieilleries en Delphi il en existe plein en circulation pour commencer (Delphi eut son heure de gloire il ne faut pas l’oublier, gloire méritée, même si la fin de l’histoire est un psychodrame), et que, comme je l’avais prédit il y a quelque années : “les delphistes seront (sont maintenant) les cobolistes des années 2010”. On y est en 2010, et effectivement tout comme les cobolistes des années 2000, une poignée de delphistes s’accroche à sa maigre connaissance en refusant obstinément d’évoluer (d’ailleurs vu les prix pratiqués par Embarcadero, la majorité des delphistes utilise toujours Delphi 5 ou 7, des produits qui ont dix ans voire plus…).

Utilité ?

Ce noyau dur d’irréductibles qui mourront en écrivant des begin/end produit encore des logiciels (peu il est vrai, les rares en poste font surtout de la maintenance de vieilles applis), qui parfois, sont utiles. On peut faire de mauvais choix pour ses outils mais fabriquer quelque chose d’utile, c’est presque paradoxal. Et il se peut que vous ayez à traiter de telles données et à vous taper tout le boulot, car un delphiste de 2010 refuse par essence toute espèce d’apprentissage et ce n’est pas lui qui vous fournira un fichier XML correctement formé…

Un exemple ? En dehors de nombreux softs de compta ou de gestion écrits avec Delphi, on trouve des choses comme Cumulus un logiciel de gestion de station météo. C’est écrit en Delphi, et je dirais même mieux “à la delphiste”, c’est à dire que c’est un peu le boxon. Les données sont éclatées en divers fichiers, certains sont des fichiers texte de type CSV, d’autres des fichiers INI (avec des champs qui sont parfois placés dans de mauvaises sections), et un fichier XML. Chouette ! Quand on l’ouvre : déception. Ce n’est pas une fichier XML bien formé, analysable facilement, c’est le fatras produit par le composant TClientDataSet. Je le reconnais au premier coup d’oeil… Le contraire serait dommage pour quelqu’un qui a écrit trois livres sur Delphi lus et agréés par Borland malgré tout…

Comme un tel soft est utile dans sa partie connexion à la station et recueil des données, il serait idiot de réinventer la poudre. Mais comme l’organisation des données sent l’amateurisme à plein nez, difficile d’en faire quelque chose directement. Dans un tel cas, qui est valable pour ce soft et tous les softs Delphi de ce type, il va falloir concevoir un DAL C# capable de lire et de réécrire tous ces fichiers (exactement de la même façon car le code Delphi est généralement assez peu “blindé”, donc ça pète si un octet n’est pas à sa place !).

Les fichiers XML issus du TClientDataSet utilisent ainsi une structure difficile à exploiter directement. En lecture quelques ruses permettent de trouver une parade, en écriture cela devient plus sportif, le moindre écart avec ce qui est attendu et le soft Delphi ne pourra pas relire le fichier.

Phase 1 : lire un XML TClientDataSet

La ruse est facile, .NET propose de longue date un composant assez proche, le DataSet. Seules “petites” différences : il produit un XML correctement formé, sait interpréter les schémas XSD et le “Set” de DataSet n’est pas usurpé puisque le DataSet est une mini base de données à lui tout seul capable de stocker plusieurs tables et leurs relations (le TClientDataSet ne travaille que sur une seule table). le “Set” (ensemble) se justifie parce que le TClientDataSet sait enregistrer un ensemble d’enregistrements (et non un ensemble de tables comme le Dataset .NET). Encore heureux, car un format qui ne sauvegarderait qu’un seul enregistrement et non un ensemble ne serait guère utile…

Dès lors, lire un fichier XML TClientDataSet peut s’effectuer directement de la façon suivante :

   1: sXMLFileName = openFileDialog1.FileName;
   2: aDS = new DataSet();
   3: aDS.ReadXml(sXMLFileName);
   4: dataGrid.DataSource = aDS;
   5: dataGrid.DataMember = "ROW";

On suppose ici un dialogue d’ouverture de fichier retournant le nom du fichier XML à ouvrir. On créé le DataSet, on lit le fichier en mémoire, puis on connecte le DataSet à une DataGrid. On utilise le DataMember “ROW” qui plonge à l’intérieur du fichier XML sur la collection d’enregistrements.

C’est beau, ça marche. Si c’est juste pour exploiter le fichier en lecture, c’est donc très simple.

Sauf… sauf qu’il existe un petit problème de codage. Par exemple le fichier issus de Cumulus contient parfois des codes XML remplaçant certains caractères mais laisse certains autres non traduits (les accentuées par exemple).

Le fichier est d’ailleurs refusé par un logiciel comme XmlSpy en raison de l’incohérence entre le codage et le contenu. Il faut dire que le codage n’est pas indiqué dans l’entête du fichier… Dans XmlSpy il suffit de dire que le mode par défaut est ANSI et on peut ouvrir le fichier.

C’est bien gentil, mais si c’est pour passer par XmlSpy, l’intérêt de notre ruse pour utiliser le fichier directement en C# n’a plus beaucoup d’intérêt…

Heureusement pour nous, le Framework .NET est riche et souple. Il faut ainsi lire le fichier XML non pas directement dans le DataSet mais depuis un flux à qui on indiquera le codage à utiliser:

   1: sXMLFileName = openFileDialog1.FileName;
   2: aDS = new DataSet();
   3: var rd = new StreamReader(sXMLFileName, Encoding.Default);
   4: aDS.ReadXml(rd);
   5: rd.Close();
   6: ...

Il faut ainsi passer par un StreamReader. Mais lorsqu’on regarde les options de l’énumération Encoding, on ne trouve pas de ANSI ! Pas de panique, c’est le mode Default qu’il faut utiliser et qui correspond à ANSI.

Et voilà pour la lecture !

Phase 2 : Sauvegarder le fichier sans rien casser.

 

Intermédiaire

La phase 2 implique une phase intermédiaire, la création d’un schéma XSD. Car si on tente de modifier le fichier tel qu’il est ouvert dans la Phase 1, les enregistrements qu’on pourra ajouter à la collection “ROW” vont se retrouver sous la racine du XML, avant ou après la vraie collection “ROW”, autant dire que le logiciel Delphi va planter illico à l’ouverture ! (Cumulus renvoie bien un message permettant de signaler l’erreur et de continuer, c’est un bel effort, mais le fichier n’est plus lu et toutes les données sont perdues).

Créer un schéma XSD à la main est assez enquiquinant, et pour un dimanche, un peu lourd qui plus est, c’est au dessus de mes forces.

Heureusement, XmlSpy sait inférer un schéma à partir d’un fichier XML. Comme tout le monde ne possède pas cet outil assez indispensable pourtant, voici le XSD à utiliser :

   1: <?xml version="1.0" encoding="UTF-8"?>
   2: <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
   3:     <xs:element name="ROWDATA">
   4:         <xs:complexType>
   5:             <xs:sequence>
   6:                 <xs:element ref="ROW" maxOccurs="unbounded"/>
   7:             </xs:sequence>
   8:         </xs:complexType>
   9:     </xs:element>
  10:     <xs:element name="ROW">
  11:         <xs:complexType>
  12:             <xs:attribute name="SnowLying" use="required">
  13:                 <xs:simpleType>
  14:                     <xs:restriction base="xs:string">
  15:                         <xs:enumeration value="FALSE"/>
  16:                     </xs:restriction>
  17:                 </xs:simpleType>
  18:             </xs:attribute>
  19:             <xs:attribute name="SnowFalling" use="required">
  20:                 <xs:simpleType>
  21:                     <xs:restriction base="xs:string">
  22:                         <xs:enumeration value="FALSE"/>
  23:                     </xs:restriction>
  24:                 </xs:simpleType>
  25:             </xs:attribute>
  26:             <xs:attribute name="SnowDepth" use="required">
  27:                 <xs:simpleType>
  28:                     <xs:restriction base="xs:byte">
  29:                         <xs:enumeration value="0"/>
  30:                     </xs:restriction>
  31:                 </xs:simpleType>
  32:             </xs:attribute>
  33:             <xs:attribute name="RowState" use="required">
  34:                 <xs:simpleType>
  35:                     <xs:restriction base="xs:byte">
  36:                         <xs:enumeration value="4"/>
  37:                     </xs:restriction>
  38:                 </xs:simpleType>
  39:             </xs:attribute>
  40:             <xs:attribute name="EntryDate" use="required">
  41:                 <xs:simpleType>
  42:                     <xs:restriction base="xs:int">
  43:                         <xs:enumeration value="20100620"/>
  44:                         <xs:enumeration value="20100710"/>
  45:                     </xs:restriction>
  46:                 </xs:simpleType>
  47:             </xs:attribute>
  48:             <xs:attribute name="Entry" use="required" type="xs:string"/>
  49:         </xs:complexType>
  50:     </xs:element>
  51:     <xs:element name="PARAMS">
  52:         <xs:complexType>
  53:             <xs:attribute name="CHANGE_LOG" use="required">
  54:                 <xs:simpleType>
  55:                     <xs:restriction base="xs:string">
  56:                         <xs:enumeration value="1 0 4 2 0 4"/>
  57:                     </xs:restriction>
  58:                 </xs:simpleType>
  59:             </xs:attribute>
  60:         </xs:complexType>
  61:     </xs:element>
  62:     <xs:element name="METADATA">
  63:         <xs:complexType>
  64:             <xs:sequence>
  65:                 <xs:element ref="FIELDS"/>
  66:                 <xs:element ref="PARAMS"/>
  67:             </xs:sequence>
  68:         </xs:complexType>
  69:     </xs:element>
  70:     <xs:element name="FIELDS">
  71:         <xs:complexType>
  72:             <xs:sequence>
  73:                 <xs:element ref="FIELD" maxOccurs="unbounded"/>
  74:             </xs:sequence>
  75:         </xs:complexType>
  76:     </xs:element>
  77:     <xs:element name="FIELD">
  78:         <xs:complexType>
  79:             <xs:attribute name="fieldtype" use="required">
  80:                 <xs:simpleType>
  81:                     <xs:restriction base="xs:string">
  82:                         <xs:enumeration value="boolean"/>
  83:                         <xs:enumeration value="date"/>
  84:                         <xs:enumeration value="i4"/>
  85:                         <xs:enumeration value="string"/>
  86:                     </xs:restriction>
  87:                 </xs:simpleType>
  88:             </xs:attribute>
  89:             <xs:attribute name="attrname" use="required">
  90:                 <xs:simpleType>
  91:                     <xs:restriction base="xs:string">
  92:                         <xs:enumeration value="Entry"/>
  93:                         <xs:enumeration value="EntryDate"/>
  94:                         <xs:enumeration value="SnowDepth"/>
  95:                         <xs:enumeration value="SnowFalling"/>
  96:                         <xs:enumeration value="SnowLying"/>
  97:                     </xs:restriction>
  98:                 </xs:simpleType>
  99:             </xs:attribute>
 100:             <xs:attribute name="WIDTH">
 101:                 <xs:simpleType>
 102:                     <xs:restriction base="xs:short">
 103:                         <xs:enumeration value="1024"/>
 104:                     </xs:restriction>
 105:                 </xs:simpleType>
 106:             </xs:attribute>
 107:         </xs:complexType>
 108:     </xs:element>
 109:     <xs:element name="DATAPACKET">
 110:         <xs:complexType>
 111:             <xs:sequence>
 112:                 <xs:element ref="METADATA"/>
 113:                 <xs:element ref="ROWDATA"/>
 114:             </xs:sequence>
 115:             <xs:attribute name="Version" use="required">
 116:                 <xs:simpleType>
 117:                     <xs:restriction base="xs:decimal">
 118:                         <xs:enumeration value="2.0"/>
 119:                     </xs:restriction>
 120:                 </xs:simpleType>
 121:             </xs:attribute>
 122:         </xs:complexType>
 123:     </xs:element>
 124: </xs:schema>

Ce XSD est “universel” il fonctionne pour tous les XML issus d’un TClientDataSet (enfin normalement, dites le moi si vous trouvez des exceptions).
[EDIT] Bien entendu quand je dis "universel" c'est la structure globale... le schéma de la table proprement-dit dépend ... de la table et change selon le cas. Ca me semblait évident mais en relisant le post je me suis aperçu que cela ne l'était pas forcément.[/EDIT]

Grâce à ce schéma nous allons pouvoir lire le fichier XML de façon plus précise, le DataSet s’y retrouvant mieux visiblement.

La lecture définitive devient donc :

   1: sXMLFileName = openFileDialog1.FileName;
   2: aDS = new DataSet();
   3: aDS.ReadXmlSchema(Path.ChangeExtension(sXMLFileName,".xsd"));
   4: var rd = new StreamReader(sXMLFileName, Encoding.Default);
   5: aDS.ReadXml(rd);
   6: rd.Close();

On suppose ici que le fichier XSD porte le même nom que le fichier XML à lire, avec l’extension “.xsd” au lieu de '”.xml”.

Mais à quoi ressemble un fichier XML du TClientDataSet ?

Il est vrai que tant qu’on se contentait de lire, et puisque nous avions trouvé une astuce, la question de savoir comment est réellement fait un tel fichier n’avait guère d’intérêt, sauf pour des archéologues pointilleux du genre à déterrer une dent qui a 3000 ans et l’analyser pour savoir que le type à qui elle appartenait avait manger des carottes dans l’année précédent sa mort. Très utile (sans rire, l’archéologie est essentielle, mais je trouve qu’elle vire parfois à la maniaquerie de psychopathe à tout vouloir déterrer, le moindre fragment de vase, de dent, surtout pour les périodes récentes. Savoir qu’un romain mangeait des carottes ou que les grecs se lavaient les pieds dans des pédiluves ronds, je vois mal l’intérêt, trouver Lucy ou les premiers dinosaures à plume en a un à l’inverse. Mais c’est un point de vue personnel).

Bref, il faut comprendre comment marche ces fichus fichiers XML.

D’abord il faut savoir qu’ils ne possèdent pas de changement de ligne. Une économie un peu mesquine comparée à l’avantage de pouvoir les lire facilement avec le bloc-notes, mais c’est comme ça. Donc il faut utiliser un outil capable de remettre tout ça en forme pour y voir quelque chose (XmlSpy, encore lui, sait le faire. Je n’ai pas d’action chez Altova je précise).

 

 

 

   1: <?xml version="1.0" standalone="yes"?>
   2: <DATAPACKET Version="2.0">
   3:     <METADATA>
   4:         <FIELDS>
   5:             <FIELD attrname="EntryDate" fieldtype="date"/>
   6:             <FIELD attrname="Entry" fieldtype="string" WIDTH="1024"/>
   7:             <FIELD attrname="SnowLying" fieldtype="boolean"/>
   8:             <FIELD attrname="SnowFalling" fieldtype="boolean"/>
   9:             <FIELD attrname="SnowDepth" fieldtype="i4"/>
  10:         </FIELDS>
  11:         <PARAMS CHANGE_LOG="1 0 4 2 0 4"/>
  12:     </METADATA>
  13:     <ROWDATA>
  14:         <ROW RowState="4" EntryDate="20100620" Entry="entrée 1 de test" SnowLying="FALSE" SnowFalling="FALSE" SnowDepth="0"/>
  15:         <ROW RowState="4" EntryDate="20100710" Entry="entrée 2 de test" SnowLying="FALSE" SnowFalling="FALSE" SnowDepth="0"/>
  16:     </ROWDATA>
  17: </DATAPACKET>

Le fichier contient un entête très succinct puis une racine DATAPACKET qui contient plusieurs sections. On trouve METADATA qui représente le schéma de la table, on trouve aussi ROWDATA une collection de ROW qui sont les vrais enregistrements, mais aussi PARAMS avec un attribut CHANGE_LOG suivi de plein de chiffres.

Normalement, si le programme Delphi est bien écrit (rareté) un appel à la validation des changements devrait être fait avant la sauvegarde (méthode MergeChangeLog du TClientDatSet). Si tel n’est pas le cas, comme ici, le fichier XML va conserver l’historique de tous les changements. C’est la section PARAMS avec son CHANGE_LOG qui va grossir inutilement avec le temps. C’est malin d’économiser des octets en ne mettant pas de changement de ligne et de perdre autant de place faute de comprendre comment marche le composant TClientDataSet ! C’est tout Delphi et les delphistes ça… A noter que si une clé primaire avait été définie (ce qui n’est pas le cas ici, encore un laxisme) le nom du champ serait indiqué dans cette section METADATA (PRIMARY KEY), idem pour le tri par défaut '”DEFAULT_ORDER”.

Notre but n’étant pas d’aller à la maniaquerie évoquée plus haut, tenons-nous en à ce qui est utile pour nous : reproduire cette structure.

Il va donc falloir reproduire le CHANGE_LOG dans ce cas puisqu’il est présent. Sa structure est simple mais elle ne se devine pas au premier coup d’œil, ce sont des triplets :

  • le premier chiffre indique le numéro de ligne
  • le second est le numéro de version de la précédente modification (s’il y en a)
  • le troisième vaut 4 pour un ajout, 8 pour une modification.

Nous n’irons pas chercher plus loin car lorsque nous sauvegarderons nous recréerons une structure de ce type en considérant que toutes les lignes sont des ajouts et qu’elles n’ont pas de version précédente. De fait la ligne 1 aura pour triplet 1 0 4, la ligne 2 : 2 0 4, etc.

Le DataSet nous joue des tours

Il est bien ce DataSet, on peut faire plein de choses, même lire des données aussi biscornues que celles d’un TClientDataSet Delphi ! Mais quand on sauvegarde, il ajoute son petit grain de sel, bien naturel pour un fichier XML bien formé : le nom du dataSet lui-même comme racine. On se retrouve ainsi avec un contenu entouré par la balise <NewDataSet>contenu</NewDataSet>. “NewDataSet” est le nom par défaut d’un DataSet. Ca peut donc être autre chose si vous avez nommé le DataSet.

Donc, pour sauvegarder les données il faudra d’une part recréer le CHANGE_LOG dans notre cas, et supprimer, dans tous les cas, la balise supplémentaire.

La Sauvegarde, enfin !

On y est ! Il est maintenant possible de reproduire la structure et les changements (ajouts, suppression de lignes, etc) tout en faisant en sorte que le logiciel Delphi puisse relire le fichier XML sans se douter que nous sommes passés par là ! (et c’est préférable si on ne veut pas planter le soft Delphi !).

   1: var lines = aDS.Tables["ROW"].Rows.Count;
   2: var sb = new StringBuilder();
   3: for (var i = 0; i < lines; i++) sb.Append((i + 1) + " 0 4 ");
   4: var logs = sb.ToString().Trim();
   5: aDS.Tables["PARAMS"].Rows[0][0] = logs;
   6: var sdn = aDS.DataSetName.ToUpper();
   7: var wd = new StreamWriter(sXMLFileName, false, Encoding.Default);
   8: wd.WriteLine(@"<?xml version=""1.0"" standalone=""yes""?>");
   9: wd.NewLine = Environment.NewLine;
  10: aDS.WriteXml(wd, XmlWriteMode.IgnoreSchema);
  11: wd.Close();
  12: var li = File.ReadAllLines(sXMLFileName);
  13: var ls = new List<string>(li.Count());
  14: foreach (var s in li)
  15: {
  16:     if (s.ToUpper().Contains("<" + sdn)) continue;
  17:     if (s.ToUpper().Contains("</" + sdn)) continue;
  18:     ls.Add(s);
  19: }
  20: File.WriteAllLines(sXMLFileName, ls);

Au départ on compte les lignes de la table “ROW” du DataSet. On s’en sert pour construire une chaîne constituée des fameux triplets du CHANGE_LOG (ligne 3). On stocke la chaine au bon endroit (ligne 5). En ligne 6 on prend note du nom du DataSet (comme cela on n’a pas se soucier de sa valeur, NewDataSet par défaut, mais sait-on jamais…).

Lignes 7 à 11 on utilise l’astuce de la lecture dans l’autre sens, avec un StreamWriter. On notera aussi qu’en ligne 8 on ajoute en début de fichier le marquage utilisé par le fichier Delphi (l’entête xml indiquant notamment le mode stand alone).

Le fichier est maintenant sauvegardé, mais avec une balise de trop (NewDataSet). Il faut une seconde passe (pas très économique j’en conviens surtout si le fichier est gros) qui est réalisée aux lignes 12 à 20. En fait on lit le fichier en mémoire en sautant les deux balises. Ensuite on écrase le fichier disque par cette nouvelle version. Brutal, un chouia goret comme méthode, mais le but est montrer ce qu’il faut mettre dans le fichier XML. A vous d’écrire ça de façon plus propre si nécessaire… A noter : la seconde passe fonctionne parce que le fichier XML produit par le DataSet a des sauts de ligne, s’il n’y en avait pas il faudrait utiliser une autre stratégie pour supprimer les balises gênantes.

Conclusion

Lire et écrire des fichiers XML issus d’un TClientDataSet Delphi n’est vraiment pas une tâche agréable. Mais cela fait partie du job d’un développeur de faire avec les données qu’on lui donne et qu’il n’a pas choisies.

Pas exaltant mais utile.

On ne peut pas se marrer tous les jours en parlant des dernières nouveautés de Microsoft, parfois ressurgissent des profondeurs des monstres d’un autre temps avec lesquels il faut bien composer !

J’espère en tout cas que si vous êtes confrontés à cette situation ce billet vous fera gagner du temps, surtout si vous n’y connaissiez rien en Delphi.

Pour des news techniques moins préhistoriques :

Stay Tuned !

blog comments powered by Disqus