Вопрос-ответ

Generate/get Xpath from XML in Java

Генерировать / получить Xpath из XML в Java

Мне интересен совет/псевдокод код / объяснение вместо реальной реализации.


  • Я хотел бы просмотреть XML-документ, все его узлы

  • Проверьте узел на наличие атрибута

Случай, если у узла нет атрибута, get/generate String with value of its xpath
В случае, если у узла действительно есть атрибуты, выполните итерацию по списку атрибутов и создайте xpath для каждого атрибута, включая также узел.

Редактировать

Причина, по которой я это делаю: я пишу автоматические тесты в Jmeter, поэтому для каждого запроса мне нужно убедиться, что запрос действительно выполнил свою работу, поэтому я утверждаю результаты, получая значения узлов с помощью Xpath.

Когда запрос небольшой, создать утверждения вручную не проблема, но для больших запросов это действительно мучительно.

Я ищу подход Java.

Цель

Моя цель - добиться следующего из этого примера XML-файла :

<root>
<elemA>one</elemA>
<elemA attribute1='first' attribute2='second'>two</elemA>
<elemB>three</elemB>
<elemA>four</elemA>
<elemC>
<elemB>five</elemB>
</elemC>
</root>

для создания следующего :

//root[1]/elemA[1]='one'
//root[1]/elemA[2]='two'
//root[1]/elemA[2][@attribute1='first']
//root[1]/elemA[2][@attribute2='second']
//root[1]/elemB[1]='three'
//root[1]/elemA[3]='four'
//root[1]/elemC[1]/elemB[1]='five'

Объяснено :


  • Если значение узла / текст не равен null / нулю, получите xpath , add = 'nodevalue' для целей утверждения

  • Если у узла есть атрибуты, создайте assert и для них

Обновить

Я нашел этот пример, он не дает правильных результатов, но я ищу что-то вроде этого:

http://www.coderanch.com/how-to/java/SAXCreateXPath

Переведено автоматически
Ответ 1

Обновить:

@c0mrade обновил свой вопрос. Вот его решение.:

Это преобразование XSLT:

<xsl:stylesheet version="1.0"  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>

<xsl:variable name="vApos">'</xsl:variable>

<xsl:template match="*[@* or not(*)] ">
<xsl:if test="not(*)">
<xsl:apply-templates select="ancestor-or-self::*" mode="path"/>
<xsl:value-of select="concat('=',$vApos,.,$vApos)"/>
<xsl:text>&#xA;</xsl:text>
</xsl:if>
<xsl:apply-templates select="@*|*"/>
</xsl:template>

<xsl:template match="*" mode="path">
<xsl:value-of select="concat('/',name())"/>
<xsl:variable name="vnumPrecSiblings" select=
"count(preceding-sibling::*[name()=name(current())])"/>

<xsl:if test="$vnumPrecSiblings">
<xsl:value-of select="concat('[', $vnumPrecSiblings +1, ']')"/>
</xsl:if>
</xsl:template>

<xsl:template match="@*">
<xsl:apply-templates select="../ancestor-or-self::*" mode="path"/>
<xsl:value-of select="concat('[@',name(), '=',$vApos,.,$vApos,']')"/>
<xsl:text>&#xA;</xsl:text>
</xsl:template>
</xsl:stylesheet>

when applied on the provided XML document:

<root>
<elemA>one</elemA>
<elemA attribute1='first' attribute2='second'>two</elemA>
<elemB>three</elemB>
<elemA>four</elemA>
<elemC>
<elemB>five</elemB>
</elemC>
</root>

produces exactly the wanted, correct result:

/root/elemA='one'
/root/elemA[2]='two'
/root/elemA[2][@attribute1='first']
/root/elemA[2][@attribute2='second']
/root/elemB='three'
/root/elemA[3]='four'
/root/elemC/elemB='five'

When applied to the newly-provided document by @c0mrade:

<root>
<elemX serial="kefw90234kf2esda9231">
<id>89734</id>
</elemX>
</root>

again the correct result is produced:

/root/elemX[@serial='kefw90234kf2esda9231']
/root/elemX/id='89734'

Explanation:


  • Only elements that have no children elements, or have attributes are matched and processed.



  • For any such element, if it doesn't have children-elements all of its ancestor-or self elements are processed in a specific mode, named 'path'. Then the "='theValue'" part is output and then a NL character.



  • All attributes of the matched element are then processed.



  • Then finally, templates are applied to all children-elements.



  • Обработка элемента в 'path' режиме проста: выводится / символ и имя элемента. Затем, если есть предыдущие родственные файлы с таким же именем, выводится часть "[numPrecSiblings +1]`.



  • Обработка атрибутов проста: сначала в ancestor-or-self:: режиме обрабатываются все 'path' элементы родительского элемента, затем выводится часть [attrName=attrValue], за которой следует символ NL.



Обратите внимание:


  • Имена, находящиеся в пространстве имен, отображаются без каких-либо проблем и в их первоначальном удобочитаемом виде.



  • Для удобства чтения индекс [1] никогда не отображается.




Ниже приведен мой первоначальный ответ (может быть проигнорирован)

Вот чистое решение XSLT 1.0:

Ниже приведен образец XML-документа и таблица стилей, которая принимает параметр, установленный для узла, и создает одно допустимое выражение XPath для каждого узла-члена.

таблица стилей (buildPath.xsl):


<xsl:stylesheet version='1.0'
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
>


<xsl:output method="text"/>
<xsl:variable name="theParmNodes" select="//namespace::*[local-name() =
'myNamespace']"
/>

<xsl:template match="/">
<xsl:variable name="theResult">
<xsl:for-each select="$theParmNodes">
<xsl:variable name="theNode" select="."/>
<xsl:for-each select="$theNode |
$theNode/ancestor-or-self::node()[..]"
>

<xsl:element name="slash">/</xsl:element>
<xsl:choose>
<xsl:when test="self::*">
<xsl:element name="nodeName">
<xsl:value-of select="name()"/>
<xsl:variable name="thisPosition"
select="count(preceding-sibling::*[name(current()) =
name()])"
/>

<xsl:variable name="numFollowing"
select="count(following-sibling::*[name(current()) =
name()])"
/>

<xsl:if test="$thisPosition + $numFollowing > 0">
<xsl:value-of select="concat('[', $thisPosition +
1, ']')"
/>

</xsl:if>
</xsl:element>
</xsl:when>
<xsl:otherwise> <!-- This node is not an element -->
<xsl:choose>
<xsl:when test="count(. | ../@*) = count(../@*)">
<!-- Attribute -->
<xsl:element name="nodeName">
<xsl:value-of select="concat('@',name())"/>
</xsl:element>
</xsl:when>
<xsl:when test="self::text()"> <!-- Text -->
<xsl:element name="nodeName">
<xsl:value-of select="'text()'"/>
<xsl:variable name="thisPosition"
select="count(preceding-sibling::text())"/>

<xsl:variable name="numFollowing"
select="count(following-sibling::text())"/>

<xsl:if test="$thisPosition + $numFollowing > 0">
<xsl:value-of select="concat('[', $thisPosition +
1, ']')"
/>

</xsl:if>
</xsl:element>
</xsl:when>
<xsl:when test="self::processing-instruction()">
<!-- Processing Instruction -->
<xsl:element name="nodeName">
<xsl:value-of select="'processing-instruction()'"/>
<xsl:variable name="thisPosition"
select="count(preceding-sibling::processing-instruction())"/>

<xsl:variable name="numFollowing"
select="count(following-sibling::processing-instruction())"/>

<xsl:if test="$thisPosition + $numFollowing > 0">
<xsl:value-of select="concat('[', $thisPosition +
1, ']')"
/>

</xsl:if>
</xsl:element>
</xsl:when>
<xsl:when test="self::comment()"> <!-- Comment -->
<xsl:element name="nodeName">
<xsl:value-of select="'comment()'"/>
<xsl:variable name="thisPosition"
select="count(preceding-sibling::comment())"/>

<xsl:variable name="numFollowing"
select="count(following-sibling::comment())"/>

<xsl:if test="$thisPosition + $numFollowing > 0">
<xsl:value-of select="concat('[', $thisPosition +
1, ']')"
/>

</xsl:if>
</xsl:element>
</xsl:when>
<!-- Namespace: -->
<xsl:when test="count(. | ../namespace::*) =
count(../namespace::*)"
>


<xsl:variable name="apos">'</xsl:variable>
<xsl:element name="nodeName">
<xsl:value-of select="concat('namespace::*',
'[local-name() = ', $apos, local-name(), $apos, ']')"
/>


</xsl:element>
</xsl:when>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
<xsl:text>&#xA;</xsl:text>
</xsl:for-each>
</xsl:variable>
<xsl:value-of select="msxsl:node-set($theResult)"/>
</xsl:template>
</xsl:stylesheet>

источник xml (buildPath.xml):


<!-- top level Comment -->
<root>
<nodeA>textA</nodeA>
<nodeA id="nodeA-2">
<?myProc ?>
xxxxxxxx
<nodeB/>
<nodeB xmlns:myNamespace="myTestNamespace">
<!-- Comment within /root/nodeA[2]/nodeB[2] -->
<nodeC/>
<!-- 2nd Comment within /root/nodeA[2]/nodeB[2] -->
</nodeB>
yyyyyyy
<nodeB/>
<?myProc2 ?>
</nodeA>
</root>
<!-- top level Comment -->

Результат:

/root/nodeA[2]/nodeB[2]/namespace::*[local-name() = 'myNamespace']
/root/nodeA[2]/nodeB[2]/nodeC/namespace::*[local-name() =
'myNamespace']
Ответ 2

Вот как это можно сделать с помощью SAX:

import java.util.HashMap;
import java.util.Map;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

public class FragmentContentHandler extends DefaultHandler {

private String xPath = "/";
private XMLReader xmlReader;
private FragmentContentHandler parent;
private StringBuilder characters = new StringBuilder();
private Map<String, Integer> elementNameCount = new HashMap<String, Integer>();

public FragmentContentHandler(XMLReader xmlReader) {
this.xmlReader = xmlReader;
}

private FragmentContentHandler(String xPath, XMLReader xmlReader, FragmentContentHandler parent) {
this(xmlReader);
this.xPath = xPath;
this.parent = parent;
}

@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
Integer count = elementNameCount.get(qName);
if(null == count) {
count = 1;
} else {
count++;
}
elementNameCount.put(qName, count);
String childXPath = xPath + "/" + qName + "[" + count + "]";

int attsLength = atts.getLength();
for(int x=0; x<attsLength; x++) {
System.out.println(childXPath + "[@" + atts.getQName(x) + "='" + atts.getValue(x) + ']');
}

FragmentContentHandler child = new FragmentContentHandler(childXPath, xmlReader, this);
xmlReader.setContentHandler(child);
}

@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
String value = characters.toString().trim();
if(value.length() > 0) {
System.out.println(xPath + "='" + characters.toString() + "'");
}
xmlReader.setContentHandler(parent);
}

@Override
public void characters(char[] ch, int start, int length) throws SAXException {
characters.append(ch, start, length);
}

}

Это можно протестировать с помощью:

import java.io.FileInputStream;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

public class Demo {

public static void main(String[] args) throws Exception {
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser();
XMLReader xr = sp.getXMLReader();

xr.setContentHandler(new FragmentContentHandler(xr));
xr.parse(new InputSource(new FileInputStream("input.xml")));
}
}

Это приведет к желаемому результату:

//root[1]/elemA[1]='one'
//root[1]/elemA[2][@attribute1='first]
//root[1]/elemA[2][@attribute2='
second]
//root[1]/elemA[2]='two'
//root[1]/elemB[1]='three'
//root[1]/elemA[3]='four'
//root[1]/elemC[1]/elemB[1]='five'
Ответ 3

С помощью jOOX (порт jquery API для Java, отказ от ответственности - я работаю в компании, создающей библиотеку) вы можете практически достичь желаемого одним оператором:

// I'm assuming this:
import static org.joox.JOOX.$;

// And then...
List<String> coolList = $(document).xpath("//*[not(*)]").map(
context -> $(context).xpath() + "='" + $(context).text() + "'"
);

Если document является вашим образцом документа:

<root>
<elemA>one</elemA>
<elemA attribute1='first' attribute2='second'>two</elemA>
<elemB>three</elemB>
<elemA>four</elemA>
<elemC>
<elemB>five</elemB>
</elemC>
</root>

Это приведет к

/root[1]/elemA[1]='one'
/root[1]/elemA[2]='two'
/root[1]/elemB[1]='three'
/root[1]/elemA[3]='four'
/root[1]/elemC[1]/elemB[1]='five'

Под "почти" я подразумеваю, что jOOX (пока) не поддерживает атрибуты сопоставления. Следовательно, ваши атрибуты не будут выдавать никаких выходных данных. Однако это будет реализовано в ближайшем будущем.

Ответ 4
private static void buildEntryList( List<String> entries, String parentXPath, Element parent ) {
NamedNodeMap attrs = parent.getAttributes();
for( int i = 0; i < attrs.getLength(); i++ ) {
Attr attr = (Attr)attrs.item( i );
//TODO: escape attr value
entries.add( parentXPath+"[@"+attr.getName()+"='"+attr.getValue()+"']");
}
HashMap<String, Integer> nameMap = new HashMap<String, Integer>();
NodeList children = parent.getChildNodes();
for( int i = 0; i < children.getLength(); i++ ) {
Node child = children.item( i );
if( child instanceof Text ) {
//TODO: escape child value
entries.add( parentXPath+"='"+((Text)child).getData()+"'" );
} else if( child instanceof Element ) {
String childName = child.getNodeName();
Integer nameCount = nameMap.get( childName );
nameCount = nameCount == null ? 1 : nameCount + 1;
nameMap.put( child.getNodeName(), nameCount );
buildEntryList( entries, parentXPath+"/"+childName+"["+nameCount+"]", (Element)child);
}
}
}

public static List<String> getEntryList( Document doc ) {
ArrayList<String> entries = new ArrayList<String>();
Element root = doc.getDocumentElement();
buildEntryList(entries, "/"+root.getNodeName()+"[1]", root );
return entries;
}

Этот код работает с двумя допущениями: вы не используете пространства имен и в нем нет элементов смешанного содержимого. Ограничение пространства имен несерьезно, но оно значительно усложнило бы чтение вашего выражения XPath, поскольку каждый элемент был бы чем-то вроде *:<name>[namespace-uri()='<nsuri>'][<index>], но в остальном это легко реализовать. С другой стороны, смешанный контент сделал бы использование xpath очень утомительным, поскольку вы должны были бы иметь возможность индивидуально обращаться ко второму, третьему и так далее текстовому узлу внутри элемента.

java xml