Генерация / получение xpath из XML-узла Java - PullRequest
36 голосов
/ 20 января 2011

Меня интересует совет / псевдокод код / ​​объяснение , а не фактическая реализация .

  • Я бы хотел пройти через XML-документ со всеми его узлами
  • Проверка узла на наличие атрибута

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

Слово совета? Надеюсь, вы предоставите некоторые полезные сведения

EDIT:

Причина для этого заключается в том, что ... я пишу автоматизированные тесты в jmeter, поэтому для каждого запроса мне нужно проверять, действительно ли запрос выполнял свою работу, поэтому я утверждаю результаты, получая значения узлов с помощью xpath. (Дополнительная информация - не имеет значения)

Когда запрос маленький, создание заявлений вручную не проблема, но для более крупных это очень неприятно .. (дополнительная информация - не имеет значения)

BOUNTY:

Я ищу подход Java

Цель

Моя цель - добиться следующего из этого ex 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'

Объяснил:

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

ОБНОВЛЕНИЕ BOUNTY:

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

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

Ответы [ 8 ]

41 голосов
/ 20 января 2011

Обновление :

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

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

<xsl:stylesheet version="1.0"  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <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>

при применении к предоставленному документу 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/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'

При применении к вновь предоставленному документу @ c0mrade :

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

снова получается правильный результат :

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

Объяснение :

  • Только элементы, которые не имеют дочерних элементов или имеют атрибуты, сопоставляются и обрабатываются.

  • Для любого такого элемента, если он не имеет дочерних элементов всехего предковые или собственные элементы обрабатываются в определенном режиме с именем 'path'.Затем выводится часть "='theValue'", а затем символ NL.

  • Затем обрабатываются все атрибуты соответствующего элемента .

  • Затем, наконец, шаблоны применяются ко всем дочерним элементам .

  • Обработка элемента в режиме '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 source (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']
14 голосов
/ 24 января 2011

Вот как это можно сделать с помощью 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'
12 голосов
/ 20 января 2012

С 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() + "'"
);

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

<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 не (пока)Поддержка сопоставления / сопоставления атрибутов.Следовательно, ваши атрибуты не будут производить никакого вывода.Это будет реализовано в ближайшее время.

3 голосов
/ 23 января 2011
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 очень утомительным, так как вам придется иметь возможность индивидуально обращаться ко второму, третьему и т. Д. Текстовому узлу внутри элемента.

2 голосов
/ 20 января 2011
  1. использовать w3c.dom
  2. идти рекурсивно вниз
  3. для каждого узла есть простой способ получить его xpath: либо путем сохранения его в виде массива / списка в то время как # 2, либо с помощью функции, которая рекурсивно поднимается до нуля, а затем инвертирует массив / список обнаруженных узлов. *

что-то в этом роде.

UPD: и объединить окончательный список, чтобы получить окончательный xpath. не думаю, что атрибуты будут проблемой.

1 голос
/ 23 января 2011

На прошлой неделе я сделал то же самое для обработки моего xml в solr-совместимом формате.

Так как вы хотели псевдокод: вот как я это сделал.

// Вы можете пропустить ссылку на parent и child.

1_ Инициализировать объект пользовательского узла: NodeObjectVO {String nodeName, String path, List attr, NodeObjectVO parent, List child}

2_ Создать пустой список

3_ Создать dom-представление xml и выполнить итерацию по узлу. Для каждого узла получите соответствующую информацию. Вся информация, такая как имя узла, имена атрибутов и значение, должна быть легко доступна из объекта dom. (Необходимо проверить dom NodeType, код должен игнорировать инструкцию обработки и текстовые узлы.)

// Предупреждение о блоках кода. 4_ Единственная сложная часть - получить путь. Я создал метод итеративной утилиты для получения строки xpath из NodeElement. (Пока (node.Parent! = Null) {путь + = node.parent.nodeName}.

(Этого также можно добиться, поддерживая глобальную переменную пути, которая отслеживает родительский путь для каждой итерации.)

5_ В методе установки setAttributes (List) я добавлю путь к объекту со всеми доступными атрибутами. (один путь со всеми доступными атрибутами. Не список путей с каждой возможной комбинацией атрибутов. Возможно, вы захотите сделать другой путь.)

6_ Добавить NodeObjectVO в список.

7_ Теперь у нас есть плоский (не иерархический) список пользовательских объектов узлов, в которых есть вся необходимая информация.

(Примечание. Как я уже говорил, я поддерживаю родительские и дочерние отношения, вам, вероятно, следует пропустить эту часть. Существует вероятность вздутия кода, особенно при использовании getparentpath. Для небольших XML это не проблема, но это проблема большой xml).

1 голос
/ 20 января 2011

Я написал метод для возврата абсолютного пути к элементу в библиотеке Practical XML .Чтобы дать вам представление о том, как это работает, вот выдержка из одного из модульных тестов :

assertEquals("/root/wargle[2]/zargle",
             DomUtil.getAbsolutePath(child3a)); 

Итак, вы можете просмотреть документ, применить тесты иэто вернуть XPath.Или, что, вероятно, лучше, так это то, что вы можете использовать утверждения на основе XPath из той же библиотеки.

1 голос
/ 20 января 2011

Я выполнил похожую задачу один раз. Основная идея заключалась в том, что вы можете использовать индексы элемента в xpath. Например, в следующем xml

<root>
    <el />
    <something />
    <el />
</root>

xpath ко второму <el/> будет /root[1]/el[2] (индексы xpath основаны на 1). Это читается как «взять первый корень, затем взять второй из всех элементов с именем el». Поэтому элемент something не влияет на индексацию элементов el. Таким образом, теоретически вы можете создать xpath для каждого конкретного элемента в xml. На практике я добился этого, обходя дерево рекурсивно и запоминая информацию об элементах и ​​их индексах.
Создание xpath, ссылающегося на определенный атрибут элемента, просто добавляло '/ @ attrName' к xpath элемента.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...