#java #xpath #attributes #element #jdom-2
#java #xpath #атрибуты #элемент #jdom-2
Вопрос:
Программе должно быть разрешено чтение из XML-файла с использованием выражений XPath. Я уже запустил проект с использованием JDOM2, переключение на другой API нежелательно. Сложность в том, что программа заранее не знает, нужно ли ей читать элемент или атрибут. Предоставляет ли API какую-либо функцию для получения содержимого (строки), просто задав ему выражение XPath? Из того, что я знаю о XPath в JDOM2, он использует объекты разных типов для вычисления выражений XPath, указывающих на атрибуты или элементы. Меня интересует только содержимое атрибута / элемента, на который указывает выражение XPath.
Вот пример XML-файла:
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book category="COOKING">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="CHILDREN">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="WEB">
<title lang="en">XQuery Kick Start</title>
<author>James McGovern</author>
<author>Per Bothner</author>
<author>Kurt Cagle</author>
<author>James Linn</author>
<author>Vaidyanathan Nagarajan</author>
<year>2003</year>
<price>49.99</price>
</book>
<book category="WEB">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>
Вот как выглядит моя программа:
package exampleprojectgroup;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.filter.Filters;
import org.jdom2.input.SAXBuilder;
import org.jdom2.input.sax.XMLReaders;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;
public class ElementAttribute2String
{
ElementAttribute2String()
{
run();
}
public void run()
{
final String PATH_TO_FILE = "c:\readme.xml";
/* It is essential that the program has to work with a variable amount of XPath expressions. */
LinkedList<String> xPathExpressions = new LinkedList<>();
/* Simulate user input.
* First XPath expression points to attribute,
* second one points to element.
* Many more expressions follow in a real situation.
*/
xPathExpressions.add( "/bookstore/book/@category" );
xPathExpressions.add( "/bookstore/book/price" );
/* One list should be sufficient to store the result. */
List<Element> elementsResult = null;
List<Attribute> attributesResult = null;
List<Object> objectsResult = null;
try
{
SAXBuilder saxBuilder = new SAXBuilder( XMLReaders.NONVALIDATING );
Document document = saxBuilder.build( PATH_TO_FILE );
XPathFactory xPathFactory = XPathFactory.instance();
int i = 0;
for ( String string : xPathExpressions )
{
/* Works only for elements, uncomment to give it a try. */
// XPathExpression<Element> xPathToElement = xPathFactory.compile( xPathExpressions.get( i ), Filters.element() );
// elementsResult = xPathToElement.evaluate( document );
// for ( Element element : elementsResult )
// {
// System.out.println( "Content of " string ": " element.getText() );
// }
/* Works only for attributes, uncomment to give it a try. */
// XPathExpression<Attribute> xPathToAttribute = xPathFactory.compile( xPathExpressions.get( i ), Filters.attribute() );
// attributesResult = xPathToAttribute.evaluate( document );
// for ( Attribute attribute : attributesResult )
// {
// System.out.println( "Content of " string ": " attribute.getValue() );
// }
/* I want to receive the content of the XPath expression as a string
* without having to know if it is an attribute or element beforehand.
*/
XPathExpression<Object> xPathExpression = xPathFactory.compile( xPathExpressions.get( i ) );
objectsResult = xPathExpression.evaluate( document );
for ( Object object : objectsResult )
{
if ( object instanceof Attribute )
{
System.out.println( "Content of " string ": " ((Attribute)object).getValue() );
}
else if ( object instanceof Element )
{
System.out.println( "Content of " string ": " ((Element)object).getText() );
}
}
i ;
}
}
catch ( IOException ioException )
{
ioException.printStackTrace();
}
catch ( JDOMException jdomException )
{
jdomException.printStackTrace();
}
}
}
Другая мысль заключается в поиске символа «@» в выражении XPath, чтобы определить, указывает ли оно на атрибут или элемент.
Это дает мне желаемый результат, хотя я хотел бы, чтобы было более элегантное решение.
Предоставляет ли JDOM2 API что-нибудь полезное для решения этой проблемы?
Можно ли переработать код в соответствии с моими требованиями?
Заранее благодарю вас!
Ответ №1:
Выражения XPath сложно вводить / приводить, потому что их необходимо компилировать в системе, чувствительной к типу возвращаемых функций / значений XPath, содержащихся в выражении. Для этого JDOM использует сторонний код, и этот сторонний код не имеет механизма для сопоставления этих типов во время компиляции вашего кода JDOM. Обратите внимание, что выражения XPath могут возвращать несколько различных типов содержимого, включая строковое, логическое значение, число и содержимое, подобное списку узлов.
В большинстве случаев тип возвращаемого выражения XPath известен до вычисления выражения, и программист имеет «правильное» приведение / ожидания для обработки результатов.
В вашем случае вы этого не делаете, и выражение является более динамичным.
Я рекомендую вам объявить вспомогательную функцию для обработки содержимого:
private static final Function extractValue(Object source) {
if (source instanceof Attribute) {
return ((Attribute)source).getValue();
}
if (source instanceof Content) {
return ((Content)source).getValue();
}
return String.valueOf(source);
}
Это, по крайней мере, улучшит ваш код, и, если вы используете потоки Java8, может быть довольно компактным:
List<String> values = xPathExpression.evaluate( document )
.stream()
.map(o -> extractValue(o))
.collect(Collectors.toList());
Обратите внимание, что спецификация XPath для узлов элемента заключается в том, что string-value
это объединение text()
содержимого элемента, а также содержимого всех дочерних элементов. Таким образом, в следующем фрагменте XML:
<a>bilbo <b>samwise</b> frodo</a>
getValue()
в a
элементе будет возвращено bilbo samwise frodo
, но getText()
вернется bilbo frodo
. Тщательно выбирайте, какой механизм вы используете для извлечения значения.
Комментарии:
1. Есть ли
Attribute
в JDOM2 подклассContent
? jdom.org/docs/apidocs/org/jdom2/Attribute.html не показывает этого, поэтому я в замешательстве, почему ваш ответ, похоже, предполагает, чтоXPathExpression<Content> xPathExpression = xPathFactory.compile( xPathExpressions.get( i ), Filters.content() )
обрабатывает элементы и атрибуты.2. Аааа …. дерьмо. Я забыл, что атрибуты не являются содержимым. У него есть
getValue()
метод, который я предполагал. Позвольте мне немного подумать об этом.3. Я не могу придумать лучшего способа обработки неоднозначных результатов XPath, кроме их проверки. JDOM мог бы немного упростить задачу, если бы оба узла элемента и атрибута имели общего предка, но есть другие причины, по которым это невозможно. Я отредактировал ответ, чтобы рекомендовать извлечение функции для уточнения кода, а не изменять базовый механизм, описанный OP.
4. Большое тебе спасибо за ответ, Рольф 🙂 Ваш ответ многое проясняет для меня. Спасибо, что указали на наличие объекта «Content» в JDOM и что выражение XPath может иметь множество различных возвращаемых типов.
Ответ №2:
У меня была точно такая же проблема, и я использовал подход распознавания, когда атрибут находится в центре внимания Xpath. Я решил с помощью двух функций. Первое соответствует выражению XPath для последующего использования:
XPathExpression xpExpression;
if (xpath.matches( ".*/@[\w] $")) {
// must be an attribute value we're after..
xpExpression = xpfac.compile(xpath, Filters.attribute(), null, myNSpace);
} else {
xpExpression = xpfac.compile(xpath, Filters.element(), null, myNSpace);
}
Второй вычисляет и возвращает значение:
Object target = xpExpression.evaluateFirst(baseEl);
if (target != null) {
String value = null;
if (target instanceof Element) {
Element targetEl = (Element) target;
value = targetEl.getTextNormalize();
} else if (target instanceof Attribute) {
Attribute targetAt = (Attribute) target;
value = targetAt.getValue();
}
Я подозреваю, что это вопрос стиля кодирования, предпочитаете ли вы вспомогательную функцию, предложенную в предыдущем ответе, или этот подход. Либо то, либо другое сработает.