web-dev-qa-db-de.com

Wie fragt man XML mit Namespaces in Java mit XPath ab?

Wenn mein XML so aussieht (keine xmlns), kann ich es einfach mit XPath abfragen wie /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>

Aber wenn es so aussieht, kann ich nicht

<?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>

Irgendwelche Ideen?

60
Inez

In der zweiten XML-Beispieldatei sind die Elemente an einen Namespace gebunden. Ihr XPath versucht, Elemente anzusprechen, die an den Standard-Namespace "no Namespace" gebunden sind, sodass sie nicht übereinstimmen.

Die bevorzugte Methode besteht darin, den Namespace mit einem Namespace-Präfix zu registrieren. Dadurch wird es einfacher, Ihren XPath zu entwickeln, zu lesen und zu warten.

Es ist jedoch nicht zwingend erforderlich, dass Sie den Namespace registrieren und in Ihrem XPath das Namespace-Präfix verwenden.  

Sie can formulieren einen XPath-Ausdruck, der eine generische Übereinstimmung für ein Element und einen Prädikatfilter verwendet, der die Übereinstimmung für die gewünschte local-name() und die namespace-uri() einschränkt. Zum Beispiel:

/*[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]

Wie Sie sehen, erzeugt sie eine extrem lange und ausführliche XPath-Anweisung, die sehr schwer zu lesen (und zu verwalten) ist.

Sie können auch einfach auf die local-name() des Elements passen und den Namespace ignorieren. Zum Beispiel:

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

Es besteht jedoch die Gefahr, dass die falschen Elemente übereinstimmen. Wenn Ihr XML gemischte Vokabulare enthält (was für diese Instanz möglicherweise kein Problem ist), die dieselbe local-name() verwenden, könnte Ihr XPath auf die falschen Elemente passen und den falschen Inhalt auswählen:

64
Mads Hansen

Ihr Problem ist der Standard-Namespace. Informationen zum Umgang mit Namespaces in XPath finden Sie in diesem Artikel: http://www.edankert.com/defaultnamespaces.html

Eine der Schlussfolgerungen, die sie daraus ziehen, lautet:

Um XPath verwenden zu können, __. Ausdrücke für XML-Inhalte, definiert in einen (Standard-) Namespace, müssen wir Geben Sie eine Namensraumpräfixzuordnung an

Beachten Sie, dass dies nicht bedeutet, dass Sie Ihr Quelldokument in irgendeiner Weise ändern müssen (Sie können jedoch die Namespace-Präfixe dort einfügen, wenn Sie dies wünschen). Klingt komisch, richtig? Was Sie will tun, ist das Erstellen einer Namespacepräfixzuordnung in Ihrem Java-Code und die Verwendung des Präfixes in Ihrem XPath-Ausdruck. Hier erstellen wir eine Zuordnung von spreadsheet zu Ihrem Standard-Namespace.

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);

Und voila ... Jetzt haben Sie Ihr Element in der Variable result gespeichert.

Caveat: Wenn Sie Ihr XML als DOM mit den Standard-JAXP-Klassen analysieren, müssen Sie unbedingt setNamespaceAware(true) in Ihrer DocumentBuilderFactory aufrufen. Ansonsten funktioniert dieser Code nicht!

56
stevevls

Alle Namespaces, aus denen Sie im Quell-XML auswählen möchten, müssen mit einem Präfix in der Host-Sprache verknüpft werden. In Java/JAXP wird dies durch Angabe des URI für jedes Namespacepräfix mithilfe einer Instanz von javax.xml.namespace.NamespaceContext ausgeführt. Leider ist keine Implementierung von NamespaceContext im SDK enthalten. 

Zum Glück ist es sehr einfach, eigene zu schreiben:

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();
    }

}

Verwenden Sie es so:

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);

Beachten Sie, dass der erste Namespace zwar kein Präfix im Quelldokument angibt (d. H. Der Standard-Namespace ) Sie müssen es trotzdem einem Präfix zuordnen. Ihr Ausdruck sollte dann auf Knoten in diesem Namespace mit dem von Ihnen gewählten Präfix verweisen, wie folgt:

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

Die Präfixnamen, die Sie für die Zuordnung zu jedem Namespace auswählen, sind willkürlich. Sie müssen nicht mit dem übereinstimmen, was in der Quell-XML angezeigt wird. Diese Zuordnung ist nur ein Weg, um der XPath-Engine mitzuteilen, dass ein bestimmter Präfixname in einem Ausdruck mit einem bestimmten Namespace im Quelldokument korreliert.

34
Wayne Burkett

Wenn Sie Spring verwenden, enthält es bereits 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

Ich habe eine einfache NamespaceContext-Implementierung ( hier ) geschrieben, die einen Map<String, String> als Eingabe verwendet, wobei key ein Präfix und value ein Namespace ist.

Es folgt der NamespaceContext -Spezifikation und Sie können sehen, wie es in den Unit-Tests funktioniert.

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"]

Beachten Sie, dass es eine Abhängigkeit von Google Guava gibt.

0
tomaj

Wenn ich factory.setNamespaceAware(true); nicht setze, funktioniert der von Ihnen erwähnte Xpath erstaunlicherweise mit und ohne Namespaces. Sie können nur Dinge mit "angegebenem Namensraum" nur generische xpaths auswählen. Stelle dir das vor. Das kann also eine Option sein:

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

Stellen Sie sicher, dass Sie den Namespace in Ihrem XSLT referenzieren

<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

Zwei Dinge, die zu den vorhandenen Antworten hinzugefügt werden sollten:

  • Ich weiß nicht, ob dies der Fall war, als Sie die Frage stellten: Mit Java 10 funktioniert Ihr XPath tatsächlich für das zweite Dokument, wenn Sie setNamespaceAware(true) in der Document Builder-Factory nicht verwenden (falseist die Standardeinstellung).

  • Wenn Sie setNamespaceAware(true) verwenden möchten, haben bereits andere Antworten gezeigt, wie dies unter Verwendung eines Namespace-Kontexts durchgeführt wird. Sie müssen jedoch die Zuordnung von Präfixen zu Namespaces nicht selbst vornehmen, wie dies bei den folgenden Antworten der Fall ist: Es ist bereits im document-Element vorhanden, und Sie können dies für Ihren Namespace-Kontext verwenden:

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();
    }
}

Der Rest des Codes ist wie in den anderen Antworten. Dann liefert der XPath /:workbook/:sheets/:sheet[1] das Sheet-Element. (Sie können auch ein nicht leeres Präfix für den Standard-Namespace verwenden, wie es die anderen Antworten tun, indem Sie prefix.isEmpty() durch beispielsweise prefix.equals("spreadsheet") ersetzen und den XPath /spreadsheet:workbook/spreadsheet:sheets/spreadsheet:sheet[1] verwenden.)

PS : Ich habe gerade hier festgestellt, dass es tatsächlich eine Methode Node.lookupNamespaceURI(String prefix) gibt, sodass Sie diese anstelle der verwenden können Attributsuche:

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

Beachten Sie außerdem, dass Namespaces für andere Elemente als das document-Element deklariert werden können und diese (von beiden Versionen) nicht erkannt werden.

0
joriki