JAXP XPath 1.0 или 2.0 - как отличить пустые строки от несуществующих значений
Учитывая следующий экземпляр XML:
<entities>
<person><name>Jack</name></person>
<person><name></name></person>
<person></person>
</entities>
Я использую следующий код для: (а) итерации по лицам и (б) получения имени каждого человека:
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);
}
Приведенный выше код не может различить регистр от 2-го лица (пустая строка для имени) и регистр от 3-го лица (без элемента name) и просто печатает:
0 -> name is Jack.
1 -> name is .
2 -> name is .
Есть ли способ отличить эти два случая, используя разные innerXPath
выражение? В этом SO вопросе кажется, что XPath мог бы вернуть пустой список, но я тоже это пробовал:
String innerXPath = "if (name) then name/text() else ()";
... и вывод остается прежним.
Итак, есть ли способ отличить эти два случая с innerXPath
выражение? У меня Saxon HE на моем classpath, поэтому я также могу использовать функции 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 ответа
JAXP API, основанный на XPath 1.0, здесь довольно ограничен. Мой инстинкт должен был бы вернуть элемент Name (как NodeList). Таким образом, требуемое выражение XPath - это просто "Имя". Тогда в случаях 1 и 2 будет возвращен список узлов длины 1, а в случае 3 будет возвращен список узлов длины 0. В этом случае случаи 1 и 2 можно легко различить в приложении, получив значение узла и проверив, равен ли он нулю. длина.
В любом случае всегда лучше избегать использования /text(), так как это делает ваш запрос чувствительным к присутствию комментариев в XML.
Как давний пользователь Saxon XSLT, я рад снова узнать, что мне здесь нравится рекомендация Майкла Кея. Обычно мне нравится шаблон возврата коллекции для запросов, даже для запросов, которые, как ожидается, будут возвращать не более одного экземпляра.
Что мне не нравится делать, так это открывать пакетный интерфейс, чтобы попытаться решить конкретную задачу, а затем найти, что нужно переопределить большую часть того, что обрабатывал оригинальный интерфейс.
Поэтому, вот метод, который использует рекомендацию Майкла, избегая затрат на необходимость переопределения преобразования Node-to-String, что рекомендуется в других комментариях в этой теме.
@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 вызывается второй раз, чтобы делать то, что обычно делает для преобразования первого найденного узла в запрошенное значение String. Без этого существует риск того, что повторная реализация даст другой результат, чем прямой вызов XPathConstant.STRING для одного и того же исходного узла и для того же выражения.
Конечно, этот код использует Guava Optional и FluentIterable, чтобы сделать намерение более явным. Если вы не хотите использовать Guava, используйте Java 8 или реорганизуйте реализацию, используя null и собственные методы сбора NodeList.