Получение информации xml из другой структуры xml через xslt

Я использую XSLT 1.0 и пытаюсь сделать следующее:

У меня есть файл 1.xml:

<root>
    <elem1>value1</elem1>
    <elem2>
        <elem3>
            <param1>value2</param1>
            <param2>value3</param2>
        </elem3>
    </elem2>
    <elem4>
        <param3>value4</param3>
    </elem4>
</root>

Теперь клиент передает мне другой XML-файл, в котором сообщается, какие элементы он хочет, чтобы я ему вернул (может измениться между клиентами), то есть:

<root>
  <RequiredElements>
    <elementName>elem1</elementName>
    <elementName>elem2/elem3/param1</elementName>
  </RequiredElements>
</root>

Это означает, что в этом случае я должен сделать еще один файл xml со следующей структурой:

<root>
  <elem1>value1</elem1>
  <elem2>
    <elem3>
      <param1>value2</param1>
    </elem3>
  </elem2>
</root>

Я пытался придумать что-то в xslt (кроме этого языка программирования), чтобы получить нужную мне структуру, но не смог этого сделать.

Любые идеи или указатели, что мне делать?

Спасибо за помощь.


person evyatar_m    schedule 30.10.2011    source источник


Ответы (2)


Это простое преобразование (менее 30 строк, если параметр-doc не встроен):

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:my="my:my">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <my:paramDoc>
        <root>
          <RequiredElements>
            <elementName>elem1</elementName>
            <elementName>elem2/elem3/param1</elementName>
          </RequiredElements>
        </root>
 </my:paramDoc>

 <xsl:variable name="vPaths" select=
   "document('')/*/my:paramDoc/*/*/*"/>

 <xsl:template match="node()|@*" name="identity">
         <xsl:copy>
           <xsl:apply-templates select="node()|@*"/>
         </xsl:copy>
 </xsl:template>

 <xsl:template match="*/*">
  <xsl:variable name="vPath">
    <xsl:for-each select=
      "ancestor-or-self::*[not(position()=last())]">
      <xsl:value-of select="concat(name(), '/')"/>
    </xsl:for-each>
  </xsl:variable>

   <xsl:if test=
   "$vPaths[starts-with(concat(.,'/'), $vPath)]">
    <xsl:call-template name="identity"/>
   </xsl:if>
 </xsl:template>
</xsl:stylesheet>

при применении к предоставленному XML-документу:

<root>
    <elem1>value1</elem1>
    <elem2>
        <elem3>
            <param1>value2</param1>
            <param2>value3</param2>
        </elem3>
    </elem2>
    <elem4>
        <param3>value4</param3>
    </elem4>
</root>

выдает желаемый правильный результат:

<root>
   <elem1>value1</elem1>
   <elem2>
      <elem3>
         <param1>value2</param1>
      </elem3>
   </elem2>
</root>

Пояснение:

  1. правило идентификации копирует каждый узел "как есть".

  2. Существует один переопределяющий шаблон, который соответствует любому элементу, родительским элементом которого является element. В этом шаблоне выполняется следующее:

  3. Строка, представляющая собой относительное выражение XPath с узлом контекста, являющимся верхним элементом документа, создается для текущего (совпадающего) узла.

  4. Если этот относительный путь является префиксом одного из выражений, указанных в документе, переданном в качестве параметра, то мы выполняем преобразование идентичности для этого элемента.

person Dimitre Novatchev    schedule 30.10.2011
comment
Красивый! Я даже не знал, что можно создавать переменные, используя элементы, встроенные в xsl:variable. XSLT может быть таким элегантным в умелых руках. - person G_H; 31.10.2011
comment
@G_H: Да, XSLT может быть чрезвычайно мощным и элегантным. Просто взгляните на FXSL (2.0 для XSLT 2.0). - person Dimitre Novatchev; 31.10.2011

Это лучшее, что я смог придумать до сих пор:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output method="xml" encoding="UTF-8" indent="yes" version="1.0"/>

    <xsl:template match="/root">
        <xsl:copy>
            <xsl:for-each select="*">
                <xsl:call-template name="check-if-allowed">
                    <xsl:with-param name="path" select="local-name(.)"/>
                </xsl:call-template>
            </xsl:for-each>
        </xsl:copy>
    </xsl:template>

    <xsl:template name="check-if-allowed">

        <xsl:param name="path"/>

        <xsl:copy>

            <xsl:if test="$path = document('filter.xml')//RequiredElements/elementName/text()">
                <xsl:attribute name="flagged-by-filter">true</xsl:attribute>
            </xsl:if>

            <xsl:choose>
                <xsl:when test="*">
                    <xsl:for-each select="*">
                        <xsl:call-template name="check-if-allowed">
                            <xsl:with-param name="path" select="concat($path, '/', local-name(.))"/>
                        </xsl:call-template>
                    </xsl:for-each>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:copy-of select="text()"/>
                </xsl:otherwise>
            </xsl:choose>

        </xsl:copy>

    </xsl:template>

</xsl:stylesheet>

Давайте рассмотрим это: первый шаблон соответствует вашему элементу /root. Он сделает поверхностную копию, а затем вызовет шаблон check-if-allowed для каждого дочернего элемента, передав локальное имя этого дочернего элемента в качестве параметра path.

Шаблон check-if-allowed принимает параметр с именем path. Он делает поверхностную копию своего узла, а затем проверяет, содержится ли параметр path в выборе, сделанном из документа filter.xml. Это должен быть путь к вашему второму документу, который содержит список разрешенных путей. Если тест прошел успешно (т. е. если параметр path отображается как текстовое содержимое elementName в filter.xml), то он также добавит атрибут с именем flagged-by-filter и значением true.

После этого xsl:choose сделает одно из двух. Если для текущего есть какие-либо дочерние элементы, он будет вызывать для них тот же шаблон check-if-allowed, но каждый раз с параметром path, к которому было добавлено локальное имя этого элемента. Если нет дочерних элементов, он просто скопирует любой текст, который мог быть в текущем элементе.

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

Эту вторую таблицу стилей можно применить к результату первой для фактической фильтрации:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

    <xsl:template match="/">
        <xsl:copy><xsl:apply-templates select="*"/></xsl:copy>
    </xsl:template>

    <xsl:template match="*[//*[@flagged-by-filter='true']]">
        <xsl:copy><xsl:apply-templates select="*"/></xsl:copy>
    </xsl:template>

    <xsl:template match="*[* and not(//*[@flagged-by-filter='true']) and @flagged-by-filter='true']">
        <xsl:copy></xsl:copy>
    </xsl:template>

    <xsl:template match="*[not(*) and @flagged-by-filter='true']">
        <xsl:copy-of select="."/>
    </xsl:template>

    <xsl:template match="*[not(*) and not(@flagged-by-filter='true')]"/>

</xsl:stylesheet>

Опять же, здесь очень простая работа. Он не обрабатывает атрибуты, и все еще есть проблема, поскольку elem4 по какой-то причине всегда проходит. Не знаю почему, отладчик показывает мне, что он всегда соответствует второму шаблону, но я не могу представить, как это сделать.

Это более обычный декларативный стиль XSLT. Первый шаблон соответствует корню. Он просто копирует его, а затем применяет шаблоны к своим дочерним элементам. Второй шаблон соответствует любому элементу, у которого есть потомок с атрибутом flagged-by-filter="true". Третий шаблон соответствует любому элементу, который имеет хотя бы один дочерний элемент, имеет атрибут, отмеченный фильтром, но не имеет потомков с указанным атрибутом. Четвертый шаблон соответствует любому элементу, у которого нет дочернего элемента, но который сам помечен. Окончательный шаблон соответствует любому элементу, который не имеет дочернего элемента и не помечен сам по себе.

Хотя это не полное решение вашей проблемы, я надеюсь, что этого достаточно, чтобы вы могли двигаться дальше. Если вы не можете применить два последовательных преобразования XSLT, вам нужно будет найти способ применить материал из первого XSLT по мере необходимости. Я не могу придумать, как это можно сделать, но, возможно, у кого-то есть хорошая идея.

Тем не менее, для такой проблемы либо не будет использоваться XSLT, либо будет просто сгенерирована таблица стилей программно на основе XML-файла вашего фильтра. Вышеприведенное будет очень плохо с точки зрения производительности, поскольку мы постоянно применяем выражения XPath к дополнительному документу. Мало того, его придется каждый раз полностью разбирать. У меня когда-то была похожая установка, и я обнаружил, что производительность очень низкая. Поэтому я изменил доступ ко второму документу на функцию расширения, которая будет вызывать метод Java с использованием предварительно загруженных данных.

XSLT отлично подходит для некоторых вещей, но когда вы сталкиваетесь с такой сложностью, я думаю, что лучше всего использовать его в сочетании с другим языком.

person G_H    schedule 30.10.2011
comment
@_G_H: Кажется, в конце вашего ответа вы подразумеваете, что каждый вызов функции document() вызывает (повторный) анализ XML-документа. Это неверно, и на самом деле, если данный XSLT-процессор выполняет повторный синтаксический анализ, он не соответствует требованиям, потому что, как это определено в спецификациях W3C XSLT, функция document() должна быть стабильной. -- то есть создавать один и тот же узел каждый раз, когда он вызывается с одними и теми же аргументами. - person Dimitre Novatchev; 31.10.2011
comment
@DimitreNovatchev Интересно ... Однако я заметил снижение производительности при использовании document(). Хотя этого, вероятно, следует ожидать, когда вы перетаскиваете второй документ в процесс, в котором оцениваются выражения XPath. Это так, что второй документ был в основном парами ключ-значение, что означает поиск по значениям атрибутов, а не по именам элементов. Не очень хороший подход. Я изменил его на чтение документа свойств в объект Properties и простой доступ к нему через функцию расширения. Производительность немного улучшилась. - person G_H; 31.10.2011
comment
@_G_H: Если документ с парами ключ-значение не маленький, поиск по нему должен привести к узким местам в производительности. Правильным решением здесь является использование функции <xsl:key> и key() — это приведет к значительному повышению производительности, если будет выполняться несколько поисков. - person Dimitre Novatchev; 31.10.2011
comment
@DimitreNovatchev Это, вероятно, тоже сработало бы. Впрочем, это не имеет большого значения. Он используется для представления сообщений EDI-XML на пользовательском портале, но я скоро его переработаю. В новой версии используется гораздо более понятный формат XML. - person G_H; 31.10.2011