Monday, November 12, 2007

Java pas m'énerver ...

Quand il s'agit de manipuler un document XML, moi, mon interface préférée, c'est XPath. Pas besoin de code bien compliqué: on fait comme si tous les éléments étaient des petits fichiers dans une arborescence, et on donne un chemin pour trouver les éléments qui nous intéressent. J'avais déjà pu m'en servir avec succès en PERL, je m'attendais à quelque-chose d'ultra-simple en Java, langage de l'XML par excellence.

J'y vais donc de mon petit test débile:

import java.lang.*;
import java.util.*;
import javax.xml.xpath.*;
import org.xml.sax.InputSource;

public class XpathTest {
public static void main(String[] args) {
try {
System.err.println("creating xpath...");
XPath xpath = XPathFactory.newInstance().newXPath();
String expression = args[0];
InputSource src =
new InputSource(new java.io.FileInputStream(new java.io.File(args[1])));
System.err.println("search for "+args[0]);
String content = xpath.evaluate(expression, src);
System.out.println("here: "+content);

xpath.reset();
src = new InputSource(new java.io.FileInputStream(new java.io.File(args[1])));
Object node = xpath.evaluate(expression, src, XPathConstants.NODESET);
System.out.println("here: ("+node+")");
} catch (javax.xml.xpath.XPathExpressionException ex) {
System.err.println("oops: "+ex);
} catch (java.io.FileNotFoundException ex) {
System.err.println("no file : "+ex);
} catch (java.io.IOException ex) {
System.err.println("doh: "+ex);
}
}
}

Tout ça à l'aide de la première référence que je trouve: la doc de sun bien claire, sauf sur un point "c'est quoi cette 'nodelist' que je suis supposé recevoir mais qui m'est transmise sous la simple forme d'un 'Object' ?". Au bout de quelques minutes de tests, je me rends compte que la seule chose qui marche, c'est le chemin "/" qui reprend tout le texte du document. le reste ("/feed/entry[1]/title" ou "//author", ce genre de choses chouettes et utiles), ne donne aucun résultat. Pas une exception ou un message d'erreur. non. rien, tout simplement. que le chemin corresponde à un noeud existant dans le document ou pas, d'ailleurs.

grmbl.

Puis à force d'essayer, je finis par constater que le fameux objet que je récupère est de la classe com.sun.org.apache.xml.internal.dtm.ref.DTMNodeList.

Et là, je dis: ça pue. pourquoi m'ont-ils renvoyé un objet interne non-documenté et (de plus) qu'est-ce que c'est que ce "sun...apache..." ou bien c'est dans le standard, ou bien ça n'y est pas, non ?


allons bon ...
Xerces does not provide an XPath implementation (it implements only support for the limited expressions allowed by XML Schema constraints). Xalan provides an XPath 1.0 implementation and from the com.sun.org.apache... package name it seems there is the Sun (derived from Xalan) implementation that is included with the JVM.ref

Tout de suite, ça m'étonne moins. Reprenons calmement avec la doc d'apache, donc.
  1. on installe le package "xalan", un traducteur XSLT qui est sensé fournir XPath pour de vrai
  2. on redéfini sa variable d'environnement CLASSPATH pour pointer sur /usr/share/java/xalan2.jar
  3. on google sur "org.apache.xpath example" et pas "xpath java example"
Voyons voir. Ouais. des tonnes de Factory et docBuilder.parse() ... 'fallait s'y attendre. Et naturellement, rien n'aide dans le code java ou dans JavaDoc à retrouver comment il serait bien possible de construire un objet de telle ou telle classe à partir d'un nom de fichier, ce qui rend toujours les tutoriels Java pénibles (très 'magie noire', si vous voyons c'que j'voulions dire).

Bref, le code le plus simple auquel j'arrive ressemble à ceci:
import org.apache.xpath.*;   // for XPathAPI, obviously!
import org.apache.xpath.objects.*; // for items, nodelists, etc. (XObject)
import org.w3c.dom.*; // for Document and friends
import java.lang.*; // for String and the like.

/** see also:
http://xml.apache.org/xalan-j/apidocs/javax/xml/parsers/DocumentBuilder.html
*/
class xpath2 {
public static void main(String args[]) {
Document doc=FileToDoc(args[0]);
String xpath = args[1];
System.out.println("\nQuerying DOM using xpath string:" + xpath);
try {
XObject o = XPathAPI.eval(doc, xpath);
System.out.println(o.getTypeString()+"> "+o);
} catch (Exception e) {
System.err.println("doh:" +e);
}
}
}
mais pour ça, il me faut écrire une horrible fonction capable de passer d'un nom de fichier à un Document, parce que visiblement, les gens qui ont écrit XPathAPI n'ont pas pensé à rendre ce cas-là simple :(
    static Document FileToDoc(String fname) {
/* well, Document is an interface and cannot be built simply */
/* -- grmbl -- */
try {
javax.xml.parsers.DocumentBuilderFactory docBuilderFactory =
javax.xml.parsers.DocumentBuilderFactory.newInstance();
javax.xml.parsers.DocumentBuilder docBuilder =
docBuilderFactory.newDocumentBuilder();

// Parse the XML file and build the Document object in RAM
return docBuilder.parse(new java.io.File(fname));
} catch (Exception e) {
System.err.println("parse error: "+e);
return null;
}
}
Notez que si vous avez déjà un InputStream prêt sur le contenu (p.ex. une connexion réseau), inutile de passer par un fichier: docBuilder.parse(InputStream) existe également. Il y a même docBuilder.parse(URL) pour ceux qui ne sont pas occupés à faire un exercice sur l'écriture d'un client HTTP :P
(soupir)

Et pour être sûr, ils ont *aussi* rendu la réécriture de documents XML vers un fichier tordue:
 import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
DOMSource source = new DOMSource(document);
StreamResult result = new StreamResult(System.out);
transformer.transform(source, result);


Pour la postérité, la javadoc de l'API XPath sur le projet Apache.

3 comments:

PypeBros said...

Bon. Ca, c'est dans le cas où on prend un File. Si on veut partir d'un ByteArray, il y a http://java.sun.com/j2se/1.4.2/docs/api/java/io/ByteArrayInputStream.html#ByteArrayInputStream(byte[])

PypeBros said...

voir aussi http://www.w3.org/TR/DOM-Level-3-LS/load-save.html

PypeBros said...

Préférer XPathAPI.eval(item, "@rel").str(); à XPathAPI.eval(item, "@rel") .nodelist().item(0).getTextContent();, vu que le 2eme génèrera une Null Pointer Exception s'il n'y a pas de noeud correspondant (attribut rel=... absent de l'item).