it-swarm.dev

Como consultar XML usando namespaces em Java com XPath?

Quando meu XML se parece com isso (sem xmlns), então eu posso facilmente consultá-lo com XPath como /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>

Mas quando se parece com isso, então eu não posso

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

Alguma ideia?

60
Inez

No segundo arquivo XML de exemplo, os elementos são ligados a um namespace. Seu XPath está tentando endereçar elementos que estão vinculados ao namespace "sem namespace" padrão, para que eles não correspondam.

O método preferido é registrar o namespace com um prefixo de namespace. Isso torna seu XPath muito mais fácil de desenvolver, ler e manter.

No entanto, não é obrigatório registrar o namespace e usar o prefixo namespace em seu XPath. 

Você can formula uma expressão XPath que usa uma correspondência genérica para um elemento e um filtro de predicado que restringe a correspondência para o local-name() desejado e o namespace-uri(). Por exemplo:

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

Como você pode ver, ele produz uma instrução XPath extremamente longa e detalhada que é muito difícil de ler (e manter).

Você também pode combinar no local-name() do elemento e ignorar o namespace. Por exemplo:

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

No entanto, você corre o risco de combinar os elementos errados. Se o seu XML tiver vocabulários mistos (que podem não ser um problema para essa instância) que usem o mesmo local-name(), seu XPath poderá corresponder aos elementos errados e selecionar o conteúdo errado:

64
Mads Hansen

Seu problema é o namespace padrão. Confira este artigo para saber como lidar com namespaces em seu XPath: http://www.edankert.com/defaultnamespaces.html

Uma das conclusões que eles tiram é:

Portanto, para poder usar expressões XPath No conteúdo XML definido em Um namespace (padrão), precisamos especificar Um mapeamento de prefixo de namespace.

Observe que isso não significa que você tenha que alterar seu documento de origem de qualquer maneira (embora esteja livre para colocar os prefixos de espaço de nomes lá, se desejar). Soa estranho, certo? O que você will fazer é criar um mapeamento de prefixo de namespace em seu código Java e usar o dito prefixo em sua expressão XPath. Aqui, criaremos um mapeamento de spreadsheet para seu namespace padrão.

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

E voila ... Agora você tem seu elemento salvo na variável result.

Advertência: se você está analisando seu XML como um DOM com as classes JAXP padrão, lembre-se de chamar setNamespaceAware(true) na sua DocumentBuilderFactory. Caso contrário, este código não funcionará!

56
stevevls

Todos os namespaces que você pretende selecionar no XML de origem devem estar associados a um prefixo no idioma do Host. Em Java/JAXP, isso é feito especificando o URI para cada prefixo de namespace usando uma instância de javax.xml.namespace.NamespaceContext. Infelizmente, há sem implementação de NamespaceContext fornecido no SDK. 

Felizmente, é muito fácil escrever o seu próprio:

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

}

Use assim:

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

Observe que, embora o primeiro namespace não especifique um prefixo no documento de origem (isto é, o namespace padrão ) você deve associá-lo a um prefixo de qualquer maneira. Sua expressão deve então referenciar os nós nesse namespace usando o prefixo que você escolheu, assim:

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

Os nomes de prefixo que você escolhe para associar a cada namespace são arbitrários; eles não precisam corresponder ao que aparece no XML de origem. Esse mapeamento é apenas uma maneira de informar ao mecanismo XPath que um determinado nome de prefixo em um a expressão se correlaciona com um namespace específico no documento de origem.

34
Wayne Burkett

Se você estiver usando o Spring, ele já contém 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

Eu escrevi uma implementação simples de NamespaceContext ( aqui ), que toma um Map<String, String> como entrada, onde key é um prefixo, e value é um namespace.

Ele segue o NamespaceContext spesification, e você pode ver como ele funciona nos 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"]

Observe que há uma dependência em Google Guava

0
tomaj

Surpreendentemente, se eu não definir factory.setNamespaceAware(true);, o xpath que você mencionou funciona com e sem namespaces em jogo. Você simplesmente não pode selecionar itens "com namespace especificado" apenas xpaths genéricos. Vai saber. Então isso pode ser uma opção:

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

Certifique-se de referenciar o namespace no seu 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