/**
 * xml.js : utilitaires pour créer, charger, analyser, sérialiser, transformer
 *          et extraire des données à partir de documents XML.
 *
 * Tiré du livre JavaScript — La référence, 5e édition,
 * de David Flanagan. Copyright 2006 Éditions O'Reilly (ISBN : 2-84177-415-5)
 */

// Vérifier que le module n'a pas déjà été chargé.
var XML_SA;
if (XML_SA && (typeof XML_SA != "object" || XML_SA.NAME))
    throw new Error("L'espace de noms 'XML_SA' existe déjà");

// Créer notre espace de noms et quelques méta-informations.
XML_SA = {};
XML_SA.NAME = "XML";     // Nom de cet espace de noms.
XML_SA.VERSION = 1.0;    // Version de cet espace de noms.

/**
 * Créer un nouvel objet Document. Si aucun argument n’est donné, le document
 * est vide. Si une balise racine est précisée, le document ne contiendra
 * que cette balise. Si la balise racine possède un préfixe d’espace de
 * noms, le deuxième argument doit spécifier l’URL qui identifie cet espace.
 */
XML_SA.nouveauDocument = function(nomBaliseRacine, urlEspaceDeNoms) {
    if (!nomBaliseRacine) nomBaliseRacine = "";
    if (!urlEspaceDeNoms) urlEspaceDeNoms = "";
    
    if (document.implementation && document.implementation.createDocument) {
        // Procéder à la manière du standard W3C.
        return document.implementation.createDocument(urlEspaceDeNoms,
                                                      nomBaliseRacine, null);
    }
    else { // Procéder à la manière d’IE.
        // Créer un document vide en tant qu’objet ActiveX. Si l’élément 
        // racine n’existe pas, nous n’avons rien d’autre à faire.
        var doc = new ActiveXObject("MSXML2.DOMDocument");

        // Si la balise racine est indiquée, initialiser le document.
        if (nomBaliseRacine) {
            // Rechercher un préfixe d’espace de noms.
            var prefixe = "";
            var nomBalise = nomBaliseRacine;
            var p = nomBaliseRacine.indexOf(':');
            if (p != -1) {
                prefixe = nomBaliseRacine.substring(0, p);
                nomBalise = nomBaliseRacine.substring(p+1);
            }

            // Si nous avons un espace de noms, nous devons avoir un préfixe.
            // Sinon, nous oublions tout préfixe.
            if (urlEspaceDeNoms) {
                if (!prefixe) prefixe = "a0"; // Utilisé par Firefox.
            }
            else prefixe = "";

            // Créer l’élément racine (avec l’espace de noms facultatif)
            // sous forme d’une chaîne de caractères.
            var texte = "<" + (prefixe?(prefixe+":"):"") +  nomBalise +
                (urlEspaceDeNoms
                 ?(" xmlns:" + prefixe + '="' + urlEspaceDeNoms +'"')
                 :"") +
                "/>";
            // Faire interpréter cette chaîne par le document vide.
            doc.loadXML(texte);
        }
        return doc;
    }
};

/**
 * Télécharger de manière synchrone le document XML désigné par l’URL et 
 * le retourner comme un objet Document.
 */
XML_SA.charger = function(url) {

	if (document.load != null && document.load != undefined || isIE()) {
		// Ici avec Firefox, Opera, IE (possède load, mais test spécial).
    	var docXML = XML_SA.nouveauDocument();  
	    docXML.async = false;  // Nous voulons un téléchargement synchrone.
	   	docXML.load(url);      // Charger et analyser.
	    return docXML;         // Retourner le document.
	} else {
		// Ici avec Safari.
		var docXML = null;
	   	var requete = HTTP.nouvelleRequete();
    	requete.open("GET", url, false);
	    requete.send(null);
		if (requete.status == 0 || requete.status == 200) {
			docXML = requete.responseXML;
		}
		return docXML;
	}
};

/**
 * Télécharger de manière asynchrone et analyser le document XML désigné
 * par l’URL. Lorsque le document est prêt, le passer à la fonction de 
 * rappel indiquée. Cette fonction retourne immédiatement, sans valeur 
 * de retour.
 */
XML_SA.chargerAsynchrone = function(url, rappel) {
    var docXML = XML_SA.nouveauDocument();

    // Si nous avons créé le document XML avec createDocument, utiliser
    // onload pour déterminer la fin de son téléchargement.
    if (document.implementation && document.implementation.createDocument) {
        docXML.onload = function() { rappel(docXML); };
    }
    // Sinon, utiliser onreadystatechange, comme pour XMLHttpRequest.
    else {
        docXML.onreadystatechange = function() {
            if (docXML.readyState == 4) rappel(docXML);
        };
    }

    // Démarrer le téléchargement et l’interprétation.
    docXML.load(url);
};

/**
 * Analyser le document XML contenu dans la chaîne passée en argument et
 * retourner un objet Document qui le représente.
 */
XML_SA.analyser = function(texte) {
    if (typeof DOMParser != "undefined") {
        // Mozilla, Firefox et navigateurs connexes.
        return (new DOMParser()).parseFromString(texte, "application/xml");
    }
    else if (typeof ActiveXObject != "undefined") {
        // Internet Explorer.
        var doc = XML_SA.nouveauDocument();  // Créer un document vide.
        doc.loadXML(texte);               // Y placer le texte traité.
        return doc;                       // Le retourner.
    }
    else {
        // En dernier ressort, tenter de télécharger le document depuis
        // une URL data:. Cela semble fonctionner dans Safari. Merci
        // à Manos Batsis et à sa bibliothèque Sarissa
        // (sarissa.sourceforge.net) pour cette technique.
        var url = "data:text/xml;charset=utf-8," + encodeURIComponent(texte);
        var requete = new XMLHttpRequest();
        requete.open("GET", url, false);
        requete.send(null);
        return requete.responseXML;
    }
};

/**
 * Retourner un objet Document dans lequel est placé le contenu de la balise
 * <xml> ayant l’identifiant précisé. Si cette balise possède un attribut src,
 * le document XML est chargé depuis l’URL correspondante.
 *
 * Puisque les îlots de données sont, en général, consultés plus d’une fois,
 * cette fonction place en cache le document retourné.
 */
XML_SA.obtenirIlotDonnees = function(id) {
    var doc;

    // Commencer par consulter le cache.
    doc = XML_SA.obtenirIlotDonnees.cache[id];
    if (doc) return doc;
    
    // Rechercher l’élément désigné.
    doc = document.getElementById(id);

    // S’il possède un attribut "src", récupérer le document depuis
    // l’URL correspondante.
    var url = doc.getAttribute('src');
    if (url) {
        doc = XML_SA.charger(url);
    }
    // Sinon, le contenu de la balise <xml> correspond au document que 
    // nous voulons retourner. Dans Internet Explorer, doc est déjà l’objet
    // Document recherché. Dans les autres navigateurs, doc fait référence
    // à un élément HTML et nous devons copier son contenu dans un nouvel
    // objet Document.
    else if (!doc.documentElement) {// Si ce n’est pas déjà un document...

        // Trouver l’élément de document dans la balise <xml>. Il s’agit
        // du premier enfant de la balise <xml> qui est également un élément,
        // et non du texte, un commentaire ou une instruction de traitement.
        var elementDoc = doc.firstChild;
        while(elementDoc != null) {
            if (elementDoc.nodeType == 1 /*Node.ELEMENT_NODE*/) break;
            elementDoc = elementDoc.nextSibling;
        }
        
        // Créer un document vide.
        doc = XML_SA.nouveauDocument();
        
        // Si le nœud <xml> a du contenu, l’importer dans le nouveau document.
        if (elementDoc) doc.appendChild(doc.importNode(elementDoc, true));
    }

    // Placer le document dans le cache, puis le retourner.
    XML_SA.obtenirIlotDonnees.cache[id] = doc;
    return doc;
};

XML_SA.obtenirIlotDonnees.cache = {}; // Initialiser le cache.

/**
 * Cette classe XML_SA.Transformateur encapsule une feuille de style XSL. Si
 * le paramètre feuilleDeStyle est une URL, nous la chargeons. Sinon, nous
 * supposons qu’il contient un objet DOM Document approprié.
 */
XML_SA.Transformateur = function(feuilleDeStyle) {
    // Si nécessaire, charger la feuille de style.
    if (typeof feuilleDeStyle == "string")
        feuilleDeStyle = XML_SA.load(feuilleDeStyle);
    this.feuilleDeStyle = feuilleDeStyle;

    // Dans les navigateurs de type Mozilla, créer un objet XSLTProcessor
    // et lui passer la feuille de style.
    if (typeof XSLTProcessor != "undefined") {
        this.processor = new XSLTProcessor();
        this.processor.importStylesheet(this.feuilleDeStyle);
    }
};

/**
 * Voici la méthode transformer() de la classe XML_SA.Transformateur. Elle
 * transforme le nœud XML indiqué en utilisant la feuille de style
 * encapsulée. On suppose que le résultat de la transformation est du
 * HTML et on l’utilise pour remplacer le contenu de l’élément indiqué.
 */
XML_SA.Transformateur.prototype.transformer = function(noeud, element) {
    // Si l’élément est indiqué par son identifiant, le rechercher.
    if (typeof element == "string") element = document.getElementById(element);

    if (this.processor) {
        // Si nous avons créé un XSLTProcessor (c’est-à-dire, nous sommes
        // dans Mozilla), l’utiliser. Transformer le nœud en un objet
        // DOM DocumentFragment.
        var fragment = this.processor.transformToFragment(noeud, document);
        // Effacer le contenu existant de l’élément.
        element.innerHTML = "";
        // Insérer les nœuds transformés.
        element.appendChild(fragment);
    }
    else if ("transformNode" in noeud) {
        // Si le nœud possède une fonction transformNode() (dans IE),
        // l’utiliser. Notez qu’elle retourne une chaîne de caractères.
        element.innerHTML = noeud.transformNode(this.feuilleDeStyle);
    }
    else {
        // Sinon, nous ne pouvons rien faire.
        throw "Ce navigateur ne prend pas en charge XSLT.";
    }
};

/**
 * Fonction utile lorsqu’une feuille de style n’est utilisée 
 * qu’une seule fois.
 */
XML_SA.transformer = function(docXML, feuilleDeStyle, element) {
    var transformateur = new XML_SA.Transformateur(feuilleDeStyle);
    transformateur.transformer(docXML, element);
}

/**
 * La classe XML_SA.ExpressionXPath encapsule une requête XPath et sa
 * correspondance entre le préfixe d’espaces de noms et l’URL. Une fois
 * l’objet XML_SA.ExpressionXPath créé, il peut être évalué une ou plusieurs
 * fois (dans un ou plusieurs contextes) à l’aide des méthodes obtenirNoeud()
 * ou obtenirNoeuds().
 *
 * Ce constructeur prend en premier argument le texte de l’expression XPath.
 * 
 * Si l’expression inclut des espaces de noms XML, le second argument doit
 * être un objet JavaScript qui associe les préfixes d’espaces de noms
 * aux URL qui définissent ces espaces. Les propriétés de cet objet sont
 * les préfixes, leurs valeurs sont les URL.
 */
XML_SA.ExpressionXPath = function(texteXPath, espacesDeNoms) {
    this.texteXPath = texteXPath;        // Mémoriser l’expression.
    this.espacesDeNoms = espacesDeNoms;  // Et les associations d’espaces 
                                         // de noms.

    if (document.createExpression) {
        // Si le navigateur est conforme au W3C, utiliser l’API du W3C
        // pour compiler le texte de la requête XPath.
        this.exprXPath = 
            document.createExpression(texteXPath, 
                                      // Cette fonction reçoit en argument
                                      // un préfixe d’espaces de noms et
                                      // retourne l’URL.
                                      function(prefixe) {
                                          return espacesDeNoms[prefixe];
                                      });
    }
    else {
        // Sinon, nous supposons pour le moment que nous sommes dans IE
        // et transformons l’objet espacesDeNoms sous la forme textuelle
        // requise par Internet Explorer.
        this.chaineEspaceDeNoms = "";
        if (espacesDeNoms != null) {
            for(var prefixe in espacesDeNoms) {
                // Ajouter un espace si la chaîne n’est pas vide.
                if (this.chaineEspaceDeNoms) this.chaineEspaceDeNoms += ' ';
                // Puis, ajouter l’espace de noms.
                this.chaineEspaceDeNoms += 'xmlns:' + prefixe + '="' +
                    espacesDeNoms[prefixe] + '"';
            }
        }
    }
};

/**
 * Voici la méthode obtenirNoeuds() de XML_SA.ExpressionXPath. Elle évalue
 * l’expression XPath dans le contexte indiqué. L’argument contexte doit 
 * être un objet Document ou Element. La valeur de retour est un tableau 
 * ou un objet de type tableau qui contient les nœuds correspondant à
 * l’expression.
 */
XML_SA.ExpressionXPath.prototype.obtenirNoeuds = function(contexte) {
    if (this.exprXPath) {
        // Si nous sommes dans un navigateur conforme au W3C, nous avons
        // compilé l’expression dans le constructeur. Nous évaluons
        // maintenant cette version compilée dans le contexte indiqué.
        var resultat =
            this.exprXPath.evaluate(contexte, 
                                    // Type du résultat attendu.
                                    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
                                    null);

        // Copier le résultat dans un tableau.
        var a = new Array(resultat.snapshotLength);
        for(var i = 0; i < resultat.snapshotLength; i++) {
            a[i] = resultat.snapshotItem(i);
        }
        return a;
    }
    else {
        // Si le navigateur n’est pas conforme au W3C, tenter d’évaluer
        // l’expression en utilisant l’API d’Internet Explorer.
        try {
            // Nous avons besoin de l’objet Document pour préciser
            // l’espace de noms.
            var doc = contexte.ownerDocument;
            // Si le contexte ne contient pas ownerDocument, il s’agit 
            // du document.
            if (doc == null) doc = contexte;
            // Technique propre à IE pour spécifier la correspondance 
            // entre les préfixes et les URL.
            doc.setProperty("SelectionLanguage", "XPath");
            doc.setProperty("SelectionNamespaces", this.chaineEspaceDeNoms);

            // Dans IE, le contexte doit être un Element, non un Document.
            // S’il est un document, utiliser à la place documentElement.
            if (contexte == doc) contexte = doc.documentElement;
            // Utiliser la méthode selectNodes() d’IE pour
            // évaluer l’expression.
            return contexte.selectNodes(this.texteXPath);
        }
        catch(e) {
            // Si l’API d’IE ne fonctionne pas, arrêter tout.
            throw "Ce navigateur ne prend pas en charge XPath.";
        }
    }
}

/**
 * Voici la méthode obtenirNoeud() de XML_SA.ExpressionXPath. Elle évalue
 * l’expression XPath dans le contexte indiqué et retourne un seul nœud
 * correspondant (ou null si aucun nœud ne correspond). Si plusieurs nœuds
 * correspondent, elle retourne le premier nœud du document. L’implémentation
 * diffère de la méthode obtenirNoeuds() uniquement par le type de retour.
 */
XML_SA.ExpressionXPath.prototype.obtenirNoeud = function(contexte) {
    if (this.exprXPath) {
        var resultat =
            this.exprXPath.evaluate(contexte, 
                                    // Nous voulons uniquement la première
                                    // correspondance.
                                    XPathResult.FIRST_ORDERED_NODE_TYPE,
                                    null);
        return resultat.singleNodeValue;
    }
    else {
        try {
            var doc = contexte.ownerDocument;
            if (doc == null) doc = contexte;
            doc.setProperty("SelectionLanguage", "XPath");
            doc.setProperty("SelectionNamespaces", this.chaineEspaceDeNoms);
            if (contexte == doc) contexte = doc.documentElement;
            // Dans IE, appeler selectSingleNode() à la place
            // de selectNodes().
            return contexte.selectSingleNode(this.texteXPath);
        }
        catch(e) {
            throw "Ce navigateur ne prend pas en charge XPath.";
        }
    }
};

// Fonction utilitaire pour créer un objet XML_SA.ExpressionXPath et 
// appeler obtenirNoeuds().
XML_SA.obtenirNoeuds = function(contexte, exprXPath, espacesDeNoms) {
    return (new XML_SA.ExpressionXPath(exprXPath, 
                                    espacesDeNoms)).obtenirNoeuds(contexte);
};

// Fonction utilitaire pour créer un objet XML_SA.ExpressionXPath et 
// appeler obtenirNoeud().
XML_SA.obtenirNoeud  = function(contexte, exprXPath, espacesDeNoms) {
    return (new XML_SA.ExpressionXPath(exprXPath,
                                    espacesDeNoms)).obtenirNoeud(contexte);
};

/**
 * Sérialiser un Document ou un Element XML et le retourner sous forme
 * d’une chaîne de caractères.
 */
XML_SA.serialiser = function(noeud) {
    if (typeof XMLSerializer != "undefined")
        return (new XMLSerializer()).serializeToString(noeud);
    else if (noeud.xml) return noeud.xml;
    else 
        throw "XML_SA.serialiser n’est pas prise en charge ou ne peut sérialiser "
           + noeud;
};

/*
 * Développer tous les modèles se trouvant à ou sous l’élément e. Si un
 * modèle utilise des expressions XPath avec un espace de noms, passer la
 * correspondance entre un préfixe et une URL en second argument, comme
 * pour XML_SA.ExpressionXPath().
 * 
 * Si e n’est pas spécifié, document.body est utilisé à la place. Cette
 * fonction sera le plus souvent invoquée sans argument, en réponse à
 * un gestionnaire d’événements onload. Cela permet de développer
 * automatiquement tous les modèles.
 */
XML_SA.developperModeles = function(e, espacesDeNoms) {
    // Adapter les arguments.
    if (!e) e = document.body;
    else if (typeof e == "string") e = document.getElementById(e);
    if (!espacesDeNoms) espacesDeNoms = null; // "undefined" ne fonctionne pas.

    // Un élément HTML est un modèle s’il possède un attribut "datasource".
    // Rechercher et développer récursivement tous les modèles. Noter que 
    // les modèles imbriqués ne sont pas acceptés.
    if (e.getAttribute("datasource")) {
        // S’il s’agit d’un modèle, le développer.
        XML_SA.developperModele(e, espacesDeNoms);
    }
    else {
        // Sinon, parcourir récursivement chaque enfant. Nous effectuons tout
        // d’abord une copie statique de l’enfant afin que le développement
        // d’un modèle ne perturbe pas l’itération.
        var enfants = []; // Pour les copies des éléments enfants.
        for(var i = 0; i < e.childNodes.length; i++) {
            var c = e.childNodes[i];
            if (c.nodeType == 1) enfants.push(e.childNodes[i]);
        }
        
        // Parcourir chaque élément enfant.
        for(var i = 0; i < enfants.length; i++)
            XML_SA.developperModeles(enfants[i], espacesDeNoms);
    }
};

/**
 * Développer le modèle indiqué. Si les expressions XPath du modèle
 * utilisent un espace de noms, le deuxième argument doit spécifier
 * la correspondance entre le préfixe et l’URL.
 */
XML_SA.developperModele = function(modele, espacesDeNoms) {
    if (typeof modele=="string") modele=document.getElementById(modele);
    if (!espacesDeNoms) espacesDeNoms = null; // "undefined" ne fonctionne pas.

    // Nous devons commencer par déterminer la provenance 
    // des données du modèle.
    var sourceDonnees = modele.getAttribute("datasource");

    // Si la valeur de l’attribut datasource commence par '#', elle 
    // indique le nom d’un îlot de données XML. Sinon, il s’agit de l’URL 
    // d’un fichier XML externe.
    var docDonnees;
    if (sourceDonnees.charAt(0) == '#')   // Obtenir l’îlot de données.
        docDonnees = XML_SA.obtenirIlotDonnees(sourceDonnees.substring(1));
    else                                  // Ou charger le document externe.
        docDonnees = XML_SA.load(sourceDonnees);

    // Déterminer les nœuds de la source de données qui fourniront 
    // les données. Si le modèle possède un attribut foreach, 
    // nous l’utilisons comme une expression XPath pour obtenir
    // la liste des nœuds. Sinon, nous utilisons tous les éléments
    // enfants de l’élément Document.
    var noeudsDonnees;
    var pourChaque = template.getAttribute("foreach");
    if (pourChaque)
        noeudsDonnees = XML_SA.obtenirNoeuds(docDonnees, pourChaque,
                                          espacesDeNoms);
    else {
        // Si l’attribut foreach n’est pas présent, utiliser l’élément
        // enfant de documentElement.
        noeudsDonnees = [];
        for(var c=docDonnees.documentElement.firstChild; c!=null;
            c=c.nextSibling)
            if (c.nodeType == 1) noeudsDonnees.push(c);
    }

    // Retirer l’élément modèle de son parent, mais mémoriser ce dernier, 
    // ainsi que le frère suivant du modèle.
    var conteneur = modele.parentNode;
    var pointInsertion = modele.nextSibling;
    modele = conteneur.removeChild(modele);

    // Pour chaque élément du tableau noeudsDonnees, nous allons insérer
    // une copie du modèle dans le conteneur. Mais avant d’effectuer cette
    // opération, nous développons tout enfant dans la copie possédant
    // l’attribut data.
    for(var i = 0; i < noeudsDonnees.length; i++) {
        var copie = modele.cloneNode(true);            // Copier le modèle.
        developper(copie, noeudsDonnees[i],            // Développer la copie.
                   espacesDeNoms); 
        conteneur.insertBefore(copie, pointInsertion); // Insérer la copie.
    }

    // Cette fonction imbriquée trouve tous les éléments enfants de e qui
    // possèdent un attribut data. Elle traite cet attribut comme une
    // expression XPath qu’elle évalue dans le contexte de noeudDonnees.
    // Elle prend la valeur textuelle du résultat de l’évaluation et en
    // fait le contenu du nœud HTML en cours de traitement. Tout autre
    // contenu est supprimé.
    function developper(e, noeudDonnees, espacesDeNoms) {
        for(var c = e.firstChild; c != null; c = c.nextSibling) {
            if (c.nodeType != 1) continue;  // Uniquement les éléments.
            var exprDonnees = c.getAttribute("data");
            if (exprDonnees) {
                // Évaluer l’expression XPath dans le contexte.
                var n = XML_SA.obtenirNoeud(noeudDonnees, exprDonnees,
                                         espacesDeNoms);
                // Supprimer tout contenu de l’élément.
                c.innerHTML = "";
                // Insérer le texte du résultat de l’évaluation XPath.
                c.appendChild(document.createTextNode(obtenirTexte(n)));
            }
            // Si nous ne développons pas le nœud, parcourir sa hiérarchie.
            else developper(c, noeudDonnees, espacesDeNoms);
        }
    }

    // Cette fonction imbriquée extrait le texte d’un nœud DOM, avec
    // un parcours récursif si nécessaire.
    function obtenirTexte(n) {
        switch(n.nodeType) {
        case 1: /* élément */
            var s = "";
            for(var c = n.firstChild; c != null; c = c.nextSibling)
                s += obtenirTexte(c);
            return s;
        case 2: /* attribut */
        case 3: /* texte */
        case 4: /* cdata */
            return n.nodeValue;
        default: 
            return "";
        }
    }
    
};
