INaLCO - M2 Ingénierie Multilingue

Techniques XML, cours n°4 

Jean-François Perrot

Deuxième approche du DOM (Document Object Model)
en Java


  1. L'exemple de la génération d'histogrammes.

  2. Question de conception
    1. Un problème conceptuel
    2. Prendre les objets au sérieux.
    3. Prendre la classe comme un objet.

  3. Détails de technique Java
    1. Lire un fichier ligne à ligne.
    2. Régler le codage des caractères

  4. Réalisations

L'exemple de la génération d'histogrammes.

Ce problème a déjà été traité en XSLT (notes de cours), au prix de quelques efforts d'imagination.
La solution XSLT suppose que le tableau des noms & notes est donné en XML,
alors que dans la pratique courante ce genre de document se trouve plus probablement sous la forme d'une feuille de calcul dans un tableur
comme Excel de Microsoft ou Calc de OpenOffice.

En DOM, d'une manière parfaitement systématique, nous procéderons en deux étapes :
  1. la conversion en format XML d'une donnée-source en texte (comme un tableur sait l'engendrer par une sauvegarde adéquate)
  2. le calcul du fichier représentant l'histogramme conformément aux attentes du logiciel de visualisation,
    par transformation du document XML.
    Cette seconde étape effectue exactement le même travail que la feuille de style XSLT rappelée ci-dessus.

Question de conception

  1. Un problème conceptuel

    Les opérations qui nous intéressent, transformations d'un format XML à un autre ou génération de XML à partir d'une source,
    sont de nature essentiellement fonctionnelle. L'approche par objets n'y joue aucun rôle.
    D'ailleurs, quand on les implémente en Perl ou en PHP, on écrit des fonctions, et non pas des classes.
    C'est le cadre rigide de Java qui nous impose d'écrire des classes, et donc de nous poser la question de la signification de ces classes.
    Or, elles n'en ont point, du moins a priori, et du coup une certaine dose d'arbitraire va entrer dans la conception.
    Voici deux voies d'approche.

  2. Prendre les objets au sérieux.

    Une classe est faite pour créer des instances. Il faut donc se préoccuper du rôle que ces instances vont jouer individuellement.
    Comme il s'agit de réaliser des transformations qui se définissent parfaitement sans aucune référence à un quelconque état d'un éventuel objet,
    ce rôle individuel est en général inexistant. C'est là le problème conceptuel évoqué ci-dessus.

    Avec un peu d'imagination, on arrive parfois à trouver une formulation faisant apparaître des objets plausibles.
    La manière dont la transformation Liste des Notes --> Histogramme est traitée ici en fournit un exemple.

    L'idée est de voir un objet, instance de la classe LN2Histo, comme le détenteur d'une information qu'il acquiert lors de sa création,
    et qu'il exploite sur demande pour fournir le document XML requis.
    Dans ces conditions, la classe LN2Histo va pouvoir être normalement constituée, avec un attribut privé (variable d'instance),
    un constructeur et une méthode :
    La mise en œuvre de la classe LN2Histo se fait alors de façon canonique,
    en créant une instance à partir d'un Document Noms-Notes, passé en paramètre au constructeur - appelons-le doc,
    puis en sollicitant cette instance pour obtenir le Document XML désiré, représentant l'histogramme- appelons-le res.
    Cette mise en œuvre s'écrit en deux lignes :
    LN2Histo mkh = new LN2Histo(doc); // création de l'instance
    Document res = mkh.makeXML(); // exploitation
    La façon dont on s'est procuré le Document doc (à partir d'un fichier, ou d'une transformation quelconque),
    et l'usage qui sera fait du Document res (envoyé dans un fichier ou bien utilisé directement), ne nous concernent pas ici.

  3. Prendre la classe comme un objet.

    Puisque Java interdit de définir des fonctions en dehors de classes,
    et que clairement certaines fonctions n'ont que faire de la distinction entre classes et instances,
    on les rélise comme des méthodes de classes (vocabulaire de Smalltalk, en Java static methods).
    C'est-à-dire qu'on considère la classe elle-même comme un objet, qui existe depuis le lancement du programme
    sans qu'il soit nécessaire de le créer.
    On trouve de nombreux exemples dans le système standard, depuis la classe java.lang.Math, porteuse d'une batterie de fonctions mathématiques,
    jusqu'aux fonctions de conversion Integer.parseInt et String.valueOf.

    D'un point de vue philosophique, cette attitude relève du réalisme au sens médiéval du terme,
    celui de la célèbre querelle des universaux (détails).
    D'un point de vue pratique, au-delà des différences de syntaxe, il faut bien être conscient du fait que
    les classes sont des objets très particuliers, et que procéder par méthodes de classes relève d'un style de programmation purement procédural
    et non plus "orienté objets".

    On en trouve ici un exemple de ce procédé avec la classe NNTxt2Xml
    qui construit un Document Noms-Notes (en format 1 au sens de l'exemple simple du cours 3)
    à partir d'un fichier-texte où les couples noms - notes apparaissent ligne par ligne, séparés par des blancs et des tabulations.
    Cette classe n'a aucun attribut, elle est là seulement pour porter la méthode de classe
    public static Document lireText(BufferedReader entree).

    Sa mise en œuvre se fait en une seule ligne : Document doc = NNTxt2Xml.lireText(entree);
    La provenance de l'objet BufferedReader entree, de même que l'usage qui sera fait de Document doc ne nous concernent pas ici.

Détails de technique Java

  1. Lire un fichier ligne à ligne.

    Cette opération de base (voyez Perl : @tab = <FICHIER>; ou PHP : $tab = file($nomFich);)
    se réalise en Java par une boucle itérant l'appel à la méthode readLine().
    Cette méthode appartient à la classe java.io.BufferedReader, si bien que l'opération se divise en trois phases :

    1. Obtenir une instance de BufferedReader à partir du nom du fichier : 
      le nom du fichier est une chaîne (String nomFich), on passe par une étape java.io.FileReader :
      BufferedReader entree = new BufferedReader(new FileReader(nomFich));

    2. Organiser la boucle de lecture.
      À la fin du fichier readLine() renvoie null, d'où l'emploi usuel d'une boucle do... while.
      String ligne = entree.readLine();
      do {
      /* traiter la ligne
      */
      ligne = entree.readLine();
      } while ( ligne != null );
    3. Écrire le traitement de la ligne : à titre d'exemple, celui de la méthode lireText de la classe NNTxt2Xml.
      1. lecture 
        à l'aide de split(exp. reg.), où l'expression régulière peut être donnée comme une chaîne de caractères
        (sur les expressions régulières en Java, voyez un résumé).
        String[] lex = ligne.split("[ \t]+");
        // Attention à ne pas oublier la tabulation !
        String nom = lex[0];
        String note = lex[1].trim();
      2. construction : la même que dans l'exemple DeuxVersUn.java vu précédemment.
        La variable listeNotes contient l'objet Document qui sera le résultat final de la construction.
        Element el = listeNotes.createElement("eleve");
        liste.appendChild(el);
        el.setAttribute("nom", nom);
        el.setAttribute("note", note);

  2. Régler le codage des caractères

    Cette opération est essentielle dans les deux sens, lecture et écriture.

    1. d'abord dans la réalisation de la sortie (écriture) d'un Document sur fichier :
      en Java, la construction par elle-même n'attribue aucun codage au Document construit
      [ à la différence de XML::DOM en Perl où on trouve une opération  createXMLDecl(version, charset) ]
      ce qui est conforme au principe général selon lequel les caractères sont représentés en mémoire de manière intrinsèque
      indépendamment de tout codage.
      C'est lors de l'écriture sur fichier que le choix du codage est fait, et dûment traduit dans l'en-tête XML qui est écrite.
      Ce choix est effectué automatiquement selon le codage attaché à l'objet flot (stream) sur lequel a lieu la sortie.
      Si aucune précision n'est donnée dans le programme, le codage est choisi "par défaut"
      en fonction de l'installation sur laquelle l'exécution a lieu, et le programme n'est donc pas portable.

      Cet objet "flot de caractères" devant ête enveloppé dans un javax.xml.transform.stream.StreamResult (cf. Bréviaire),
      il doit être instance d'une sous-classe de java.io.Writer, on choisit donc pour le réaliser la classe java.io.OutputStreamWriter.
      Les constructeurs de cette classe prennent un second argument qui spécifie le charset.

      • Pour écrire sur la sortie standard : Writer sortie = new OutputStreamWriter(System.out, "UTF-8");
      • Pour écrire dans un fichier nommé nomFich :
        Writer sortie = new OutputStreamWriter (new FileOutputStream (nomFich), "UTF-8");

      après quoi la sortie du Document doc s'écrit :
       DOMSource src = new DOMSource(doc);
      StreamResult res = new StreamResult(sortie);
      Transformer trans = TransformerFactory.newInstance().newTransformer();
      trans.transform(src, res);
      N.B. Si on veut un flot de sortie dans lequel on souhaite programmer soi-même les écritures, il est utile de choisir un PrintWriter
      plutôt qu'un OutputStreamWriter, pour pouvoir utiliser les méthodes habituelles print et println.
      On l'obtiendra par "PrintWriter ma_sortie = new PrintWriter(sortie, true);"
      le second argument "true" ayant pour effet que println vide le buffer (en anglais flush), c'est-à-dire provoque une impression immédiate
      sans attendre la fermeture du flot, comme on s'attend à le voir quand on fait des essais...


    2. dans la réalisation de l'entrée pour lire un fichier-texte.
      En effet l'analyse d'un fichier XML par le parseur (DocumentBuilder) tient automatiquement compte du codage annoncé dans l'en-tête XML
      avec UTF-8 comme valeur par défaut, si l'en-tête ne mentionne pas encoding.
      C'est donc seulement pour lire un fichier-texte, par exemple dans la classe NNTxt2Xml, qu'il faut se préoccuper du codage-source.
      C'est la classe java.io.InputStreamReader qui est compétente : ses constructeurs prennent un second argument qui spécifie le charset.
      À partir d'un nom de fichier nomFich, on construira ainsi le BufferedReader nécessaire à la lecture ligne à ligne :
      BufferedReader entree = 
      new BufferedReader (
      new InputStreamReader(new FileInputStream (nomFich), "UTF-8") );

Réalisations