JAXP XPath 1.0或2.0 - 如何区分空字符串和不存在的值

时间:2013-06-30 13:09:25

标签: java xpath xpath-2.0 jaxp

给出以下XML实例:

<entities>
    <person><name>Jack</name></person>
    <person><name></name></person>
    <person></person>
</entities>

我使用以下代码:(a)迭代人和(b)获取每个人的姓名:

XPathExpression expr = xpath.compile("/entities/person");
NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
for (int i = 0 ; i < nodes.getLength() ; i++) {
    Node node = nodes.item(i);
    String innerXPath = "name/text()";
    String name  = xpath.compile(innerXPath).evaluate(node);
    System.out.printf("%2d -> name is %s.\n", i, name);
}

上面的代码无法区分第二人称案例(名称为空字符串)和第三人案例(根本没有姓名元素),只是打印:

0 -> name is Jack.
1 -> name is .
2 -> name is .

有没有办法用不同的innerXPath表达来区分这两种情况?在this SO question中,似乎XPath的方式是返回一个空列表,但我也尝试过:

String innerXPath = "if (name) then name/text() else ()";

......输出仍然相同。

那么,有没有办法用不同的innerXPath表达来区分这两种情况?我在类路径上有Saxon HE,所以我也可以使用XPath 2.0功能。

更新

基于接受的答案,我能做的最好的事情如下:

XPathExpression expr = xpath.compile("/entities/person");                                                                                                                                                                                 
NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);                                                                                                                                                                   
for (int i = 0 ; i < nodes.getLength() ; i++) {                                                                                                                                                                                           
    Node node = nodes.item(i);                                                                                                                                                                                                            
    String innerXPath = "name";                                                                                                                                                                                                           
    NodeList names = (NodeList) xpath.compile(innerXPath).evaluate(node, XPathConstants.NODESET);                                                                                                                                         
    String nameValue = null;                                                                                                                                                                                                              
    if (names.getLength()>1) throw new RuntimeException("impossible");                                                                                                                                                                    
    if (names.getLength()==1)                                                                                                                                                                                                             
        nameValue = names.item(0).getFirstChild()==null?"":names.item(0).getFirstChild().getNodeValue();                                                                                                                                  
    System.out.printf("%2d -> name is [%s]\n", i, nameValue);                                                                                                                                                                             
} 

以上代码打印:

0 -> name is [Jack]
1 -> name is []
2 -> name is [null]

在我看来,这并不十分令人满意,因为逻辑在 XPath Java 代码中传播,并限制了 XPath 的有用性主机语言和API不可知的表示法。我的特殊用例是将一个XPath集合保存在一个属性文件中,并在运行时对它们进行评估,以获得我需要的信息,而无需任何特别的额外处理。显然这是不可能的。

2 个答案:

答案 0 :(得分:3)

基于XPath 1.0的JAXP API在这里非常有限。我的直觉是返回Name元素(作为NodeList)。因此,所需的XPath表达式只是“名称”。然后情况1和2将返回长度为1的节点列表,而情况3将返回长度为0的节点列表。然后,通过获取节点的值并测试它是否为零,可以在应用程序中轻松区分情况1和2。长度。

无论如何,总是最好避免使用/ text(),因为它会使您的查询对XML中注释的存在敏感。

答案 1 :(得分:0)

作为Saxon XSLT的长期用户,我很高兴再次发现我喜欢Michael Kay的推荐。通常,我喜欢为查询返回集合的模式,即使对于预期只返回最多一个实例的查询也是如此。

我不喜欢做的是必须打开一个捆绑的界面来尝试解决特定需求,然后发现必须重新实现原始界面处理的大部分内容。

因此,这是一种使用迈克尔建议的方法,同时避免了必须重新实现此线程中其他注释中建议的节点到字符串转换的成本。

@Nonnull
public Optional<String> findString( @Nonnull final String expression )
{
    try
    {
        // for XpathConstants.STRING XPath returns an empty string for both values of no length
        // and for elements that are not present.

        // therefore, ask for a NODESET and then retrieve the first Node if any

        final FluentIterable<Node> matches = 
                IterableNodeList.from( (NodeList) xpath.evaluate( expression, node, XPathConstants.NODESET ) );

        if ( matches.isEmpty() )
        {
            return Optional.absent();
        }

        final Node firstNode = matches.first().get();

        // now let XPath process a known-to-exist Node to retrieve its String value         
        return Optional.fromNullable( (String) xpath.evaluate( ".", firstNode, XPathConstants.STRING ) );
    }
    catch ( XPathExpressionException xee )
    {
        return Optional.absent();
    }
}

这里,第二次调用XPath.evaluate来做任何通常做的事情,将第一个找到的Node转换为请求的String值。如果没有这个,重新实现的风险就会产生与在同一源节点和同一表达式上直接调用XPathConstant.STRING不同的结果。

当然,此代码使用Guava Optional和FluentIterable来使意图更加明确。如果您不想要Guava,请使用Java 8或使用null和NodeList自己的集合方法重构实现。