使用XSLT将具有相同id的非空元素合并到单个项目中

时间:2011-05-09 15:41:01

标签: xml xslt xslt-2.0

我想将具有相同 id 的项目合并到单个项目中。结果项目需要具有项目组中的 第一个 非空元素。如果项目组中没有非空元素,则元素本身将从项目组中忽略,并且不应出现在结果项目中。

示例XML输入:

<?xml version="1.0" encoding="UTF-8" ?>
<shiporder orderid="orderid106">
   <orderperson id="123">orderperson107</orderperson>
   <shipto>
      <name>name108</name>
      <address>address109</address>
      <city>city110</city>
      <country>country111</country>
   </shipto>

   <!--Item Group 100-->
   <item>
      <id>100</id>
      <title>
         <first>
            <a></a>
         </first>
         <last>item100_lastTitle1</last>
      </title>
      <note></note>
      <quantity></quantity>
   </item>
   <item>
      <id>100</id>
      <title>
         <first>
            <a>a_100_2</a>
         </first>
         <last>item100_lastTitle2</last>
      </title>
      <note>note100_2</note>
      <quantity></quantity>
   </item>
   <item>
      <id>100</id>
      <title>
         <first>
            <a att1="abc" att2='cde'>a_100_3</a>
         </first>
         <last id="1" attr="2">item100_lastTitle3</last>
      </title>
      <note>note100_3</note>
      <quantity>1</quantity>
   </item>

   <!--Item Group 101-->
   <item>
      <id>101</id>
      <title>
         <first>
            <a>a_101_1</a>
         </first>
         <last></last>
      </title>
      <note>note101_1</note>
      <quantity>10</quantity>
   </item>
   <item>
      <id>101</id>
      <title>
         <first>
            <a>a_101_2</a>
         </first>
         <last>item101_lastTitle2</last>
      </title>
      <note>note101_2</note>
      <quantity>5</quantity>
   </item>

   <!--Item Group 103-->
   <item>
      <id>103</id>
      <title>
         <first>
            <a>a_103_2</a>
         </first>
         <last>item103_lastTitle2</last>
      </title>
      <note>note103_1</note>
      <quantity></quantity>
   </item>
</shiporder>

示例XML输出:

<?xml version = '1.0' encoding = 'UTF-8'?>
<shiporder orderid="orderid106">
  <item>
    <id>100</id>
    <title>
      <first>
        <a>a_100_2</a>
      </first>
      <last>item100_lastTitle1</last>
    </title>
    <note>note100_2</note>
    <quantity>6</quantity>
  </item>

  <item>
    <id>101</id>
    <title>
      <first>
        <a>a_101_1</a>
      </first>
      <last>item101_lastTitle2</last>
    </title>
    <note>note101_1</note>
    <quantity>10</quantity>
  </item>

  <item>
    <id>103</id>
    <title>
      <first>
        <a>a_103_2</a>
      </first>
      <last>item103_lastTitle2</last>
    </title>
    <note>note103_1</note>
  </item>
</shiporder>

我尝试使用以下XSL代码来获取上述输出:

<?xml version="1.0" encoding="windows-1252" ?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:template match="/shiporder">
      <shiporder>
         <xsl:if test="@orderid">
            <xsl:attribute name="orderid">
               <xsl:value-of select="@orderid"/>
            </xsl:attribute>
         </xsl:if>

         <!-- Grouping each item based on its ID element value-->
         <xsl:for-each-group select="item" group-by="id">
            <item-group>
               <!-- For Each group search for first non empty child element-->
               <xsl:for-each select="current-group()[id/text()][1]">
                  <xsl:copy-of select="id"/>
               </xsl:for-each>
               <title>
                  <first>
                     <!-- for 'a' element -->
                     <xsl:variable name="temp1">
                        <xsl:for-each select="current-group()/title/first[a/text()][1]">
                           <elm>
                              <xsl:copy-of select="a"/>
                           </elm>
                        </xsl:for-each>
                     </xsl:variable>
                     <xsl:copy-of select="$temp1/elm[1]/a"/>
                  </first>

                  <!-- for 'last' element -->
                  <xsl:variable name="temp2">
                     <xsl:for-each select="current-group()/title[last/text()][1]">
                        <elm>
                           <xsl:copy-of select="last"/>
                        </elm>
                     </xsl:for-each>
                  </xsl:variable>
                  <xsl:copy-of select="$temp2/elm[1]/last"/>
               </title>

               <!-- for 'note' element -->
               <xsl:for-each select="current-group()[note/text()][1]">
                  <xsl:copy-of select="note"/>
               </xsl:for-each>

               <!-- for 'quantity' element -->
               <xsl:for-each select="current-group()[quantity/text()][1]">
                  <xsl:copy-of select="quantity"/>
               </xsl:for-each>

            </item-group>
         </xsl:for-each-group>
      </shiporder>
   </xsl:template>
</xsl:stylesheet>

代码工作正常但最糟糕的是,我无法概括。对于项目下的每个元素,我需要编写特定于特定元素的xpath的xsl代码。我甚至无法根据用户定义的模板编写代码。

我在“Recursively combine identical sibling elements in XSLT”标题中找到了已回答的问题。但是我无法根据我的要求对其进行定制。

我在每个项目下都有数百个这样的子元素或者decedent元素,每个元素的深度是任意的。编写xsl代码对应于每个子或后代是非常荒谬的。任何专家都可以指导我编写通用xsl代码(无论元素数量及其深度)或优化现有代码吗?

提前致谢,
卡锡尔

1 个答案:

答案 0 :(得分:0)

这实际上是一个XSLT 1.0解决方案(我手头没有XSLT2处理器),但它使用了一个通用的“组合”命名模板,可以满足你的需要。

<?xml version="1.0" encoding="windows-1252" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:key name="item" use="id" match="item" />

  <xsl:template match="item[generate-id() = generate-id(key('item',id)[1])]">
    <xsl:call-template name="combine">
      <xsl:with-param name="list" select="../*[id = current()/id]" />
    </xsl:call-template>
  </xsl:template>

  <xsl:template match="item" />

  <xsl:template name="combine">
    <xsl:param name="list" />
    <xsl:element name="{name($list[1])}">
      <xsl:for-each select="*[not(preceding-sibling::*[name() = name(current())])]" >
        <xsl:call-template name="combine">
          <xsl:with-param name="list" select="$list/*[name() = name(current())]" />
        </xsl:call-template>
      </xsl:for-each>
      <xsl:value-of select="$list[text()][1]/text()" />
    </xsl:element>
  </xsl:template>

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

combine模板获取节点列表,并将它们递归地组合成一个节点,获取每个节点的第一个可用值。此模板也不会合并属性,但由于您提供的项目XML中没有任何属性,因此这应该没问题。模板中间的<xsl:for-each使用XSLT1技术挑选出唯一的元素名称;它只选择那些没有同名兄弟的兄弟。在XSLT2中使用for-each-group可能有一种方法可以做到这一点,但我不能在这里查看。