如何使用XSLT基于属性值递归嵌套XML节点?

时间:2016-04-21 20:28:09

标签: xml xslt recursion

我需要在Visual Studio 2013中使用XLST 1.0转换一些XML。

我有以下XML:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <MessageTemplates>
    <MessageTemplate>
      <Segment name="Uno" cardinality="first">
        <value>something</value>
      </Segment>
      <Segment name="Dos" cardinality="second">
        <value>something</value>
      </Segment>
      <Segment name="Tres" cardinality="third">
        <value>something</value>
      </Segment>
      <Segment name="Quatro" cardinality="third">
        <value>something</value>
      </Segment>
      <Segment name="Cinco" cardinality="second">
        <value>something</value>
      </Segment>
      <Segment name="Seis" cardinality="third">
        <value>something</value>
      </Segment>
      <Segment name="Siete" cardinality="first">
        <value>something</value>
      </Segment>
    </MessageTemplate>
  </MessageTemplates>
</root>

cardinality节点的Segment属性为序数,first为最高,third为最低。我需要根据cardinality创建嵌套级别,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <MessageTemplates>
    <MessageTemplate>
      <Cardinality type="first">
        <Segment name="Uno">
          <value>something</value>
        </Segment>
        <Cardinality type="second">
          <Segment name="Dos">
            <value>something</value>
          </Segment>
          <Cardinality type="third">
            <Segment name="Tres">
              <value>something</value>
            </Segment>
            <Segment name="Quatro">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Segment name="Cinco">
            <value>something</value>
          </Segment>
          <Cardinality type="third">
            <Segment name="Seis">
              <value>something</value>
            </Segment>
          </Cardinality>
        </Cardinality>
        <Segment name="Siete">
          <value>something</value>
        </Segment>
      </Cardinality>
    </MessageTemplate>
  </MessageTemplates>
</root>

我尝试过几种方法来转换此文件,但都失败了。我搜索了SO并阅读了几十篇帖子,但没有发现任何符合我要做的事情。我也尝试过搜索增量方法来实现我的目标,例如一次只使用递归模板调用处理一个Segment,等等。我最接近的是使用以下XSLT:

<xsl:template match="MessageTemplates/MessageTemplate">
  <MessageTemplate>
    <xsl:copy-of select="@*"/>
    <xsl:call-template name="cardinality"/>
  </MessageTemplate>
</xsl:template>

<xsl:template name="cardinality" match="MessageTemplates/MessageTemplate/Segment">
  <xsl:choose>
    <xsl:when test="position() = 1">
      <Cardinality type="{Segment/@cardinality}">
        <Segment>
          <xsl:apply-templates select="@*[name() != 'cardinality'] | node()" />
        </Segment>
      </Cardinality>
    </xsl:when>

    <xsl:when test="position() != last() and following-sibling::Segment/@cardinality != @cardinality">
      <Cardinality type="{@cardinality}">
        <Segment>
          <xsl:apply-templates select="@*[name() != 'cardinality'] | node()" />
        </Segment>
      </Cardinality>
    </xsl:when>

    <xsl:when test="position() = last()">
      <Segment>
        <xsl:apply-templates select="@*[name() != 'cardinality'] | node()" />
      </Segment>
    </xsl:when>
  </xsl:choose>
</xsl:template>

其中产生了以下XML:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <Version>1.0</Version>
  <MessageTemplates>
    <MessageTemplate>
      <Cardinality type="first">
        <Segment>
          <Cardinality type="">
            <Segment name="Uno">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="second">
            <Segment name="Dos">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="third">
            <Segment name="Tres">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="third">
            <Segment name="Quatro">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="second">
            <Segment name="Cinco">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="third">
            <Segment name="Seis">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Segment name="Siete">
            <value>something</value>
          </Segment>
        </Segment>
      </Cardinality>
    </MessageTemplate>
  </MessageTemplates>
</root>

基本上,我想要的是在单个Segment节点中包装所有 Cardinality个节点。然后,如果下一个cardinality的{​​{1}}值低于当前Segment的{​​{1}}值,我想将所有包装在之后cardinality节点中的Segment个节点,只要Segment值相同即可。我希望每个Cardinality级别都能实现这一点。最后,我想将cardinality的{​​{1}}值移动到cardinality节点的cardinality属性。必须保持Segment个节点的顺序。

非常感谢任何帮助。

2 个答案:

答案 0 :(得分:1)

Here's something you could use as your starting point.

It uses the Muenchian grouping method to generate a distinct list of cardinalities, in the order in which they appear in the source XML document.

Starting with the first cardinality in the list, each cardinality fetches the matching segments, then recurses to the next cardinality on the list - thus the desired nesting is achieved.

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:strip-space elements="*"/>
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>

<xsl:key name="segment-by-cardinality" match="Segment" use="@cardinality" />

<xsl:variable name="cardinalities">
    <!-- generate a distinct list of cardinalities -->
    <xsl:for-each select="root/MessageTemplates/MessageTemplate/Segment[count(. | key('segment-by-cardinality', @cardinality)[1]) = 1]">
        <Cardinality type="{@cardinality}"/>
    </xsl:for-each>
</xsl:variable>
<xsl:variable name="cardinalities-set" select="exsl:node-set($cardinalities)/Cardinality" />

<xsl:variable name="source-doc" select="/" />

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

<xsl:template match="MessageTemplate">
    <xsl:copy>
        <!-- start with the top-level cardinality -->
        <xsl:apply-templates select="$cardinalities-set[1]"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="Cardinality">
    <xsl:variable name="type" select="@type" />
    <xsl:copy>  
        <xsl:copy-of select="@*"/>
        <!-- switch the context back to the XML source in order to use key -->
        <xsl:for-each select="$source-doc">
            <xsl:apply-templates select="key('segment-by-cardinality', $type)"/>
        </xsl:for-each>
        <!-- proceed to the next cardinality in the list -->
        <xsl:apply-templates select="following-sibling::Cardinality[1]"/>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet> 

Applied to your example input, the result will be:

<?xml version="1.0" encoding="utf-8"?>
<root>
   <MessageTemplates>
      <MessageTemplate>
         <Cardinality type="first">
            <Segment name="Uno" cardinality="first">
               <value>something</value>
            </Segment>
            <Segment name="Siete" cardinality="first">
               <value>something</value>
            </Segment>
            <Cardinality type="second">
               <Segment name="Dos" cardinality="second">
                  <value>something</value>
               </Segment>
               <Segment name="Cinco" cardinality="second">
                  <value>something</value>
               </Segment>
               <Cardinality type="third">
                  <Segment name="Tres" cardinality="third">
                     <value>something</value>
                  </Segment>
                  <Segment name="Quatro" cardinality="third">
                     <value>something</value>
                  </Segment>
                  <Segment name="Seis" cardinality="third">
                     <value>something</value>
                  </Segment>
               </Cardinality>
            </Cardinality>
         </Cardinality>
      </MessageTemplate>
   </MessageTemplates>
</root>

Note that this does not match your requirement that "The order of the Segment nodes must be maintained". I don't fully understand this requirement. If you have some criteria by which the children of a Cardinality (i.e. its Segments and the next-higher Cardinality) should be sorted, you could do that in another pass. But since the next-higher Cardinality can contain several Segments, some of which may preceed some of the current Segments, and some not, I don't quite see what the "correct" order is.

答案 1 :(得分:1)

这是一种递归方法。它确实产生了所需的输出,至少对于给定的例子。我对此并不满意。它不是真的可靠,也不是快速,也不可维护,但至少它为您提供了基本的想法。 (如果没有更好的那个)

<xsl:stylesheet version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes" />


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

    <xsl:template match="Segment/@cardinality"  />

    <xsl:template match="MessageTemplate">
        <xsl:copy>
            <Cardinality type="first">
                <xsl:apply-templates select="Segment[1]" mode="nested" >
                    <xsl:with-param name="currentcardinality" select="'first'" />
                </xsl:apply-templates>
            </Cardinality>
        </xsl:copy>
    </xsl:template>

    <xsl:template name="comapreNext">
        <xsl:variable name="this" select="@cardinality" />
        <xsl:variable name="next" select="following-sibling::Segment[1]/@cardinality" />
        <xsl:choose>
            <xsl:when test="$this= $next" >
                <xsl:text>eq</xsl:text>
            </xsl:when>
            <xsl:when test="($this='first' and ($next = 'second' or $next = 'third') ) or
                                ($this='second' and ( $next = 'third') )" >
                <xsl:text>lt</xsl:text>
            </xsl:when>
            <xsl:otherwise>
                <xsl:text>gt</xsl:text>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <xsl:template match="Segment"  mode="nested">
        <xsl:param name="currentcardinality"/>
        <xsl:variable name="this" select="." />
        <xsl:variable name="next">
            <xsl:call-template name="comapreNext"/>
        </xsl:variable>
        <xsl:variable name="next_le" select="$next='lt' or $next = 'eq'" />
        <xsl:choose>
            <xsl:when test="@cardinality = $currentcardinality  ">
                <!-- copy Segment without cardinality -->
                <xsl:apply-templates select="."  />
                <xsl:apply-templates select="following-sibling::Segment[1][$next_le]" mode="nested" >
                    <xsl:with-param name="currentcardinality" select="@cardinality" />
                </xsl:apply-templates>
            </xsl:when>
            <xsl:otherwise>
                <Cardinality type="{@cardinality}" >
                    <xsl:apply-templates select="."  />
                    <xsl:apply-templates select="following-sibling::Segment[1][$next_le]" mode="nested" >
                        <xsl:with-param name="currentcardinality" select="@cardinality" />
                    </xsl:apply-templates>
                    <xsl:if test="@cardinality = 'second'  ">
                        <!-- find same cardinality but not next -->
                        <xsl:apply-templates select="(following-sibling::Segment[position() != 1][not(@cardinality ='third')])[1][@cardinality = $this/@cardinality]" mode="nested" >
                            <xsl:with-param name="currentcardinality" select="@cardinality" />
                        </xsl:apply-templates>
                    </xsl:if>
                </Cardinality>
            </xsl:otherwise>
        </xsl:choose>
        <xsl:if test="@cardinality = 'first'  ">
            <!-- find same cardinality but not next -->
            <xsl:apply-templates select="(following-sibling::Segment[position() != 1])[@cardinality = $this/@cardinality][1]" mode="nested" >
                <xsl:with-param name="currentcardinality" select="@cardinality" />
            </xsl:apply-templates>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

生成以下输出:

<MessageTemplates>
  <MessageTemplate>
  <Cardinality type="first">
    <Segment name="Uno">
      <value>something</value>
    </Segment>
    <Cardinality type="second">
      <Segment name="Dos">
        <value>something</value>
      </Segment>
      <Cardinality type="third">
        <Segment name="Tres">
          <value>something</value>
        </Segment>
        <Segment name="Quatro">
          <value>something</value>
        </Segment>
      </Cardinality>
      <Segment name="Cinco">
        <value>something</value>
      </Segment>
      <Cardinality type="third">
        <Segment name="Seis">
          <value>something</value>
        </Segment>
      </Cardinality>
    </Cardinality>
    <Segment name="Siete">
      <value>something</value>
    </Segment>
  </Cardinality>
 </MessageTemplate>
</MessageTemplates>