XPath,XML命名空间和Java

6

我花了一整天的时间试图从以下文档中提取一个XML节点,但是无法掌握XML命名空间的细微差别使其正常工作。

XML文件太大,无法全部发布,因此这里是与我相关的部分:

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<XFDL xmlns="http://www.PureEdge.com/XFDL/6.5" xmlns:custom="http://www.PureEdge.com/XFDL/Custom" xmlns:designer="http://www.PureEdge.com/Designer/6.1" xmlns:pecs="http://www.PureEdge.com/PECustomerService" xmlns:xfdl="http://www.PureEdge.com/XFDL/6.5">
   <globalpage sid="global">
      <global sid="global">
         <xmlmodel xmlns:xforms="http://www.w3.org/2003/xforms">
            <instances>
               <xforms:instance id="metadata">
                  <form_metadata>
                     <metadataver version="1.0"/>
                     <metadataverdate>
                        <date day="05" month="Jul" year="2005"/>
                     </metadataverdate>
                     <title>
                        <documentnbr number="2062" prefix.army="DA" scope="army" suffix=""/>
                        <longtitle>HAND RECEIPT/ANNEX NUMBER </longtitle>
                     </title>

文档一直很好地形成。我试图从“documentnbr”标签(从底部数起三个)中提取“number”属性。
用于此操作的代码如下:
/***
     * Locates the Document Number information in the file and returns the form number.
     * @return File's self-declared number.
     * @throws InvalidFormException Thrown when XPath cannot find the "documentnbr" element in the file.
     */
    public String getFormNumber() throws InvalidFormException
    {
        try{
            XPath xPath = XPathFactory.newInstance().newXPath();
            xPath.setNamespaceContext(new XFDLNamespaceContext());

            Node result = (Node)xPath.evaluate(QUERY_FORM_NUMBER, doc, XPathConstants.NODE);
            if(result != null) {
                return result.getNodeValue();
            } else {
                throw new InvalidFormException("Unable to identify form.");
            }

        } catch (XPathExpressionException err) {
            throw new InvalidFormException("Unable to find form number in file.");
        }

    }

其中QUERY_FORM_NUMBER是我的XPath表达式,XFDLNamespaceContext实现了NamespaceContext并且长这样:

public class XFDLNamespaceContext implements NamespaceContext {

    @Override
    public String getNamespaceURI(String prefix) {
        if (prefix == null) throw new NullPointerException("Invalid Namespace Prefix");
        else if (prefix.equals(XMLConstants.DEFAULT_NS_PREFIX))
            return "http://www.PureEdge.com/XFDL/6.5";
        else if ("custom".equals(prefix))
            return "http://www.PureEdge.com/XFDL/Custom";
        else if ("designer".equals(prefix)) 
            return "http://www.PureEdge.com/Designer/6.1";
        else if ("pecs".equals(prefix)) 
            return "http://www.PureEdge.com/PECustomerService";
        else if ("xfdl".equals(prefix))
            return "http://www.PureEdge.com/XFDL/6.5";      
        else if ("xforms".equals(prefix)) 
            return "http://www.w3.org/2003/xforms";
        else    
            return XMLConstants.NULL_NS_URI;
    }

    @Override
    public String getPrefix(String arg0) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Iterator getPrefixes(String arg0) {
        // TODO Auto-generated method stub
        return null;
    }

}

我尝试了许多不同的XPath查询,但我仍然觉得这应该有效:

protected static final String QUERY_FORM_NUMBER = 
        "/globalpage/global/xmlmodel/xforms:instances/instance" + 
        "/form_metadata/title/documentnbr[number]";

很不幸,它不起作用,我不断得到一个空返回。

我已经阅读了相当多的内容这里, 这里, 和 这里, 但是没有什么能够足够清晰地帮助我让它工作。

我几乎可以肯定,当我弄清楚时,我会感到非常尴尬,但我真的不知道我错过了什么。

谢谢您阅读所有这些,并提前感谢您的帮助。

-Andy

3个回答

5

啊哈,我尝试调试你的表达式并且让它工作了。你错过了一些东西。这个XPath表达式应该可以解决问题:

/XFDL/globalpage/global/xmlmodel/instances/instance/form_metadata/title/documentnbr/@number
  1. 您需要包括根元素(在本例中为XFDL)
  2. 出于某种原因,我最终没有使用表达式中的任何命名空间。不确定为什么。如果是这种情况,则NamespaceContext.getNamespaceURI()永远不会被调用。如果我将instance替换为xforms:instance,则getNamespaceURI()会被调用一次,并将xforms作为输入参数,但程序会抛出异常。
  3. 属性值的语法为@attr,而不是[attr]

我的完整示例代码:

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

public class XPathNamespaceExample {
    static public class MyNamespaceContext implements NamespaceContext {
        final private Map<String, String> prefixMap;
        MyNamespaceContext(Map<String, String> prefixMap)
        {
            if (prefixMap != null)
            {
                this.prefixMap = Collections.unmodifiableMap(new HashMap<String, String>(prefixMap));
            }
            else
            {
                this.prefixMap = Collections.emptyMap();
            }
        }
        public String getPrefix(String namespaceURI) {
            // TODO Auto-generated method stub
            return null;
        }
        public Iterator getPrefixes(String namespaceURI) {
            // TODO Auto-generated method stub
            return null;
        }
        public String getNamespaceURI(String prefix) {
                if (prefix == null) throw new NullPointerException("Invalid Namespace Prefix");
                else if (prefix.equals(XMLConstants.DEFAULT_NS_PREFIX))
                    return "http://www.PureEdge.com/XFDL/6.5";
                else if ("custom".equals(prefix))
                    return "http://www.PureEdge.com/XFDL/Custom";
                else if ("designer".equals(prefix)) 
                    return "http://www.PureEdge.com/Designer/6.1";
                else if ("pecs".equals(prefix)) 
                    return "http://www.PureEdge.com/PECustomerService";
                else if ("xfdl".equals(prefix))
                    return "http://www.PureEdge.com/XFDL/6.5";      
                else if ("xforms".equals(prefix)) 
                    return "http://www.w3.org/2003/xforms";
                else    
                    return XMLConstants.NULL_NS_URI;
        }


    }

    protected static final String QUERY_FORM_NUMBER = 
        "/XFDL/globalpage/global/xmlmodel/xforms:instances/instance" + 
        "/form_metadata/title/documentnbr[number]";

    public static void main(String[] args) {
        try
        {
            DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
            Document doc = docBuilder.parse(new File(args[0]));
            System.out.println(extractNodeValue(doc, "/XFDL/globalpage/@sid"));
            System.out.println(extractNodeValue(doc, "/XFDL/globalpage/global/xmlmodel/instances/instance/@id" ));
            System.out.println(extractNodeValue(doc, "/XFDL/globalpage/global/xmlmodel/instances/instance/form_metadata/title/documentnbr/@number" ));
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        }
    }

    private static String extractNodeValue(Document doc, String expression) {
        try{

            XPath xPath = XPathFactory.newInstance().newXPath();
            xPath.setNamespaceContext(new MyNamespaceContext(null));

            Node result = (Node)xPath.evaluate(expression, doc, XPathConstants.NODE);
            if(result != null) {
                return result.getNodeValue();
            } else {
                throw new RuntimeException("can't find expression");
            }

        } catch (XPathExpressionException err) {
            throw new RuntimeException(err);
        }
    }
}

完美的解决方案,虽然我还不太理解命名空间,但至少现在代码可以工作了。非常感谢。 - MrWizard54
8
@Jason: "我最终没有因某种原因需要在表达式中使用任何命名空间。" 在标准的Java实现中,DocumentBuilderFactory默认会生成不支持命名空间的解析器。在生成DocumentBuilder之前添加 dbfac.SetNamespaceAware(true) 可能会改变结果。 - Ti Strga

3

SAX(XPath的替代方案)版本:

SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
final String[] number = new String[1];
DefaultHandler handler = new DefaultHandler()
{           
    @Override
    public void startElement(String uri, String localName, String qName,
    Attributes attributes) throws SAXException
    {
        if (qName.equals("documentnbr"))
            number[0] = attributes.getValue("number");
    }
};
saxParser.parse("input.xml", handler);
System.out.println(number[0]);

我发现在使用XPath和命名空间时,它更加复杂(这是我的观点)。以下是我的(简单)代码:

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

NamespaceContextMap contextMap = new NamespaceContextMap();
contextMap.put("custom", "http://www.PureEdge.com/XFDL/Custom");
contextMap.put("designer", "http://www.PureEdge.com/Designer/6.1");
contextMap.put("pecs", "http://www.PureEdge.com/PECustomerService");
contextMap.put("xfdl", "http://www.PureEdge.com/XFDL/6.5");
contextMap.put("xforms", "http://www.w3.org/2003/xforms");
contextMap.put("", "http://www.PureEdge.com/XFDL/6.5");

xpath.setNamespaceContext(contextMap);
String expression = "//:documentnbr/@number";
InputSource inputSource = new InputSource("input.xml");
String number;
number = (String) xpath.evaluate(expression, inputSource, XPathConstants.STRING);
System.out.println(number);

你可以从这里(GPL许可证)获取NamespaceContextMap类(不是我的)。还有6376058 bug。

如果我在应用程序的其他地方没有使用DOM/XPath,我会选择这种方法,但目前我一直朝另一个方向发展。在应用程序中混合使用是否有常规智慧? - MrWizard54
您可以在第二段代码中使用DOM/XPath(xpath.evalute也可以接受Document对象)。在我看来,最好使用NamespaceContextMap类(imho应该在JDK中)。 - Grzegorz Szpetkowski

2
请查看XPathAPI库。它是一种更简单的使用XPath的方式,而不必处理低级Java API,特别是在处理命名空间时。
获取number属性的代码如下:
String num = XPathAPI.selectSingleNodeAsString(doc, '//documentnbr/@number');

命名空间会自动从根节点(在这种情况下是doc)中提取。如果您需要明确定义其他命名空间,可以使用以下方法:
Map<String, String> nsMap = new HashMap<String, String>();
nsMap.put("xforms", "http://www.w3.org/2003/xforms");

String num =
    XPathAPI.selectSingleNodeAsString(doc, '//documentnbr/@number', nsMap);

(免责声明:我是这个库的作者。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接