web-dev-qa-db-fra.com

Comment interroger XML en utilisant des espaces de noms en Java avec XPath?

Quand mon XML ressemble à ceci (pas xmlns), je peux facilement l'interroger avec XPath comme /workbook/sheets/sheet[1]

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook>
  <sheets>
    <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
  </sheets>
</workbook>

Mais quand ça ressemble à ça, je ne peux pas

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <sheets>
    <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
  </sheets>
</workbook>

Des idées?

60
Inez

Dans le deuxième exemple de fichier XML, les éléments sont liés à un espace de noms. Votre XPath tente d'adresser des éléments liés à l'espace de noms par défaut "no namespace" afin qu'ils ne correspondent pas.

La méthode recommandée consiste à enregistrer l'espace de noms avec un préfixe d'espace de noms. Cela facilite beaucoup le développement, la lecture et la maintenance de votre XPath.

Cependant, il n'est pas obligatoire d'enregistrer l'espace de noms et d'utiliser le préfixe d'espaces de noms dans votre XPath.  

Vous pouvez formuler une expression XPath utilisant une correspondance générique pour un élément et un filtre de prédicat limitant la correspondance pour les fonctions local-name() et namespace-uri() souhaitées. Par exemple:

/*[local-name()='workbook'
    and namespace-uri()='http://schemas.openxmlformats.org/spreadsheetml/2006/main']
  /*[local-name()='sheets'
      and namespace-uri()='http://schemas.openxmlformats.org/spreadsheetml/2006/main']
  /*[local-name()='sheet'
      and namespace-uri()='http://schemas.openxmlformats.org/spreadsheetml/2006/main'][1]

Comme vous pouvez le constater, une instruction XPath extrêmement longue et détaillée est très difficile à lire (et à maintenir).

Vous pouvez également simplement faire correspondre la local-name() de l'élément et ignorer l'espace de noms. Par exemple:

/*[local-name()='workbook']/*[local-name()='sheets']/*[local-name()='sheet'][1]

Cependant, vous courez le risque de faire correspondre les mauvais éléments. Si votre XML contient des vocabulaires mixtes (qui peuvent ne pas poser problème pour cette instance) et qui utilisent le même local-name(), votre XPath peut correspondre aux mauvais éléments et sélectionner le mauvais contenu:

64
Mads Hansen

Votre problème est l'espace de noms par défaut. Consultez cet article pour savoir comment gérer les espaces de noms dans votre XPath: http://www.edankert.com/defaultnamespaces.html

L'une des conclusions qu'ils tirent est la suivante:

Donc, pour pouvoir utiliser XPath expressions sur le contenu XML défini dans un espace de noms (par défaut), nous devons spécifier un mappage de préfixe d'espace de nom

Notez que cela ne signifie pas que vous devez modifier votre document source de quelque manière que ce soit (même si vous êtes libre de placer les préfixes d'espace de nom si vous le souhaitez). Cela semble étrange, non? Ce que vous allez faites, vous créez un mappage de préfixe d'espace de nom dans votre code Java et utilisez ce préfixe dans votre expression XPath. Ici, nous allons créer un mappage de spreadsheet vers votre espace de nom par défaut.

XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();

// there's no default implementation for NamespaceContext...seems kind of silly, no?
xpath.setNamespaceContext(new NamespaceContext() {
    public String getNamespaceURI(String prefix) {
        if (prefix == null) throw new NullPointerException("Null prefix");
        else if ("spreadsheet".equals(prefix)) return "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
        else if ("xml".equals(prefix)) return XMLConstants.XML_NS_URI;
        return XMLConstants.NULL_NS_URI;
    }

    // This method isn't necessary for XPath processing.
    public String getPrefix(String uri) {
        throw new UnsupportedOperationException();
    }

    // This method isn't necessary for XPath processing either.
    public Iterator getPrefixes(String uri) {
        throw new UnsupportedOperationException();
    }
});

// note that all the elements in the expression are prefixed with our namespace mapping!
XPathExpression expr = xpath.compile("/spreadsheet:workbook/spreadsheet:sheets/spreadsheet:sheet[1]");

// assuming you've got your XML document in a variable named doc...
Node result = (Node) expr.evaluate(doc, XPathConstants.NODE);

Et voila ... Maintenant, votre élément est enregistré dans la variable result.

Caveat: Si vous analysez votre XML en tant que DOM avec les classes JAXP standard, veillez à appeler setNamespaceAware(true) sur votre DocumentBuilderFactory. Sinon, ce code ne fonctionnera pas!

56
stevevls

Tous les espaces de noms que vous souhaitez sélectionner dans le fichier XML source doivent être associés à un préfixe dans la langue de l'hôte. En Java/JAXP, cela se fait en spécifiant l'URI de chaque préfixe d'espace de nom à l'aide d'une instance de javax.xml.namespace.NamespaceContext. Malheureusement, il n’existe aucune implémentation de NamespaceContext fournie dans le SDK. 

Heureusement, il est très facile d'écrire le vôtre:

import Java.util.HashMap;
import Java.util.Iterator;
import Java.util.Map;
import javax.xml.namespace.NamespaceContext;

public class SimpleNamespaceContext implements NamespaceContext {

    private final Map<String, String> PREF_MAP = new HashMap<String, String>();

    public SimpleNamespaceContext(final Map<String, String> prefMap) {
        PREF_MAP.putAll(prefMap);       
    }

    public String getNamespaceURI(String prefix) {
        return PREF_MAP.get(prefix);
    }

    public String getPrefix(String uri) {
        throw new UnsupportedOperationException();
    }

    public Iterator getPrefixes(String uri) {
        throw new UnsupportedOperationException();
    }

}

Utilisez-le comme ceci:

XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
HashMap<String, String> prefMap = new HashMap<String, String>() {{
    put("main", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
    put("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
}};
SimpleNamespaceContext namespaces = new SimpleNamespaceContext(prefMap);
xpath.setNamespaceContext(namespaces);
XPathExpression expr = xpath
        .compile("/main:workbook/main:sheets/main:sheet[1]");
Object result = expr.evaluate(doc, XPathConstants.NODESET);

Notez que même si le premier espace de noms ne spécifie pas de préfixe dans le document source (c’est-à-dire, l’espace de noms default ) vous devez l’associer quand même à un préfixe Votre expression doit ensuite référencer les nœuds dans cet espace de noms en utilisant le préfixe que vous avez choisi, comme ceci:

/main:workbook/main:sheets/main:sheet[1]

Les noms de préfixes que vous choisissez d'associer à chaque espace de noms sont arbitraires; ils n'ont pas besoin de correspondre à ce qui apparaît dans le code XML source. Ce mappage est simplement un moyen d'indiquer au moteur XPath qu'un nom de préfixe donné dans une expression est en corrélation avec un espace de nom spécifique dans le document source.

34
Wayne Burkett

Si vous utilisez Spring, il contient déjà org.springframework.util.xml.SimpleNamespaceContext. 

        import org.springframework.util.xml.SimpleNamespaceContext;
        ...

        XPathFactory xPathfactory = XPathFactory.newInstance();
        XPath xpath = xPathfactory.newXPath();
        SimpleNamespaceContext nsc = new SimpleNamespaceContext();

        nsc.bindNamespaceUri("a", "http://some.namespace.com/nsContext");
        xpath.setNamespaceContext(nsc);

        XPathExpression xpathExpr = xpath.compile("//a:first/a:second");

        String result = (String) xpathExpr.evaluate(object, XPathConstants.STRING);
3
kasi

J'ai écrit une simple implémentation NamespaceContext ( here ), qui prend un Map<String, String> en entrée, où key est un préfixe et value est un espace de noms.

Il suit le NamespaceContext spesification, et vous pouvez voir comment cela fonctionne dans le unit tests .

Map<String, String> mappings = new HashMap<>();
mappings.put("foo", "http://foo");
mappings.put("foo2", "http://foo");
mappings.put("bar", "http://bar");

context = new SimpleNamespaceContext(mappings);

context.getNamespaceURI("foo");    // "http://foo"
context.getPrefix("http://foo");   // "foo" or "foo2"
context.getPrefixes("http://foo"); // ["foo", "foo2"]

Notez qu'il est dépendant de Google Guava

0
tomaj

Étonnamment, si je ne mets pas factory.setNamespaceAware(true);, alors le xpath que vous avez mentionné fonctionne avec et sans espaces de noms en jeu. Vous ne pouvez simplement pas sélectionner d'éléments "avec un espace de noms spécifié", mais uniquement des xpath génériques. Allez comprendre. Donc, cela peut être une option:

 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
 factory.setNamespaceAware(false);
0
rogerdpack

Assurez-vous de référencer l'espace de noms dans votre XSLT

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
             xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
             xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"       >
0
cordsen

Deux choses à ajouter aux réponses existantes:

  • Je ne sais pas si c'était le cas lorsque vous avez posé la question suivante: avec Java 10, votre XPath fonctionne réellement pour le deuxième document si vous n'utilisez pas setNamespaceAware(true) sur la fabrique de constructeurs de documents. (falseest la valeur par défaut).

  • Si vous voulez utiliser setNamespaceAware(true), d'autres réponses ont déjà montré comment faire cela en utilisant un contexte d'espace de nom. Cependant, vous n'avez pas besoin de fournir vous-même le mappage des préfixes aux espaces de noms, comme le font les réponses suivantes: Cela existe déjà dans l'élément document et vous pouvez l'utiliser pour le contexte de votre espace de noms:

import Java.util.Iterator;

import javax.xml.namespace.NamespaceContext;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

public class DocumentNamespaceContext implements NamespaceContext {
    Element documentElement;

    public DocumentNamespaceContext (Document document) {
        documentElement = document.getDocumentElement();
    }

    public String getNamespaceURI(String prefix) {
        return documentElement.getAttribute(prefix.isEmpty() ? "xmlns" : "xmlns:" + prefix);
    }

    public String getPrefix(String namespaceURI) {
        throw new UnsupportedOperationException();
    }

    public Iterator<String> getPrefixes(String namespaceURI) {
        throw new UnsupportedOperationException();
    }
}

Le reste du code est comme dans les autres réponses. Ensuite, XPath /:workbook/:sheets/:sheet[1] donne l'élément de feuille. (Vous pouvez également utiliser un préfixe non vide pour l'espace de noms par défaut, comme le font les autres réponses, en remplaçant prefix.isEmpty() par, par exemple, prefix.equals("spreadsheet") et en utilisant XPath /spreadsheet:workbook/spreadsheet:sheets/spreadsheet:sheet[1].)

PS : Je viens de trouver ici qu'il existe en fait une méthode Node.lookupNamespaceURI(String prefix), vous pouvez donc l'utiliser à la place du attribut recherche:

    public String getNamespaceURI(String prefix) {
        return documentElement.lookupNamespaceURI(prefix.isEmpty() ? null : prefix);
    }

Notez également que les espaces de noms peuvent être déclarés sur des éléments autres que l'élément de document et que ceux-ci ne seraient pas reconnus (par l'une ou l'autre version).

0
joriki