XSLT - Tricky Transformation

2019-04-14 22:57发布

I'm having trouble with an XSLT transformation of XML fragments. The source XML looks like so:

<XXX>
    <Name>Sample</Name>
    <MMM>
        <AAA ID="A"/>
        <MMM>
            <BBB ID="B"/>
            <MMM>
                <AA ID="C"/>
                <BB ID="D"/>
            </MMM>
        </MMM>
    </MMM>
</XXX>

But it needs to be transformed into:

<XXX>
    <Name>Sample</Name>
    <MMM>
        <MMM>
            <MMM>
                <AAA ID="A"/>
                <BBB ID="B"/>
            </MMM>
            <AA ID="C"/>            
        </MMM>
        <BB ID="D"/>
    </MMM>
</XXX>

The rule is simple, the MMM element can only have two child element nodes. If only one of those nodes happen to be another MMM, it needs to occupy the first position.

It is easy using code, but these XML fragments are values to XML columns in an SQL database, and I want to use SQL along with XSLT to update those values.

Any pointer or suggestions?

标签: xml xslt
1条回答
beautiful°
2楼-- · 2019-04-14 23:35

One way to solve this is to first extract the MMM tree on the one hand and the other nodes on the other hand into separate structures and then to merge them again.

This was really a nice challenge! Made me stay up until 4:00 AM.

The following XSLT (almost! see below) does the job:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt="http://exslt.org/common" version="1.0" exclude-result-prefixes="exslt">
   <xsl:output method="xml" encoding="ISO-8859-1" />
   <!-- handling of MMM extraction -->
   <xsl:template match="MMM" mode="extract_mmm">
      <MMM>
         <xsl:apply-templates mode="extract_mmm" />
      </MMM>
   </xsl:template>
   <xsl:template match="*" mode="extract_mmm" />
   <!-- handling of extraction of other nodes -->
   <xsl:template match="MMM" mode="extract_other">
      <xsl:apply-templates mode="extract_other" />
   </xsl:template>
   <xsl:template match="*" mode="extract_other">
      <xsl:copy-of select="." />
   </xsl:template>
   <!-- handling of merging the two partial result sets -->
   <xsl:template match="MMM" mode="dump">
      <xsl:param name="others" />
      <xsl:choose>
         <!-- this handles the case of an MMM being a leaf node -->
         <xsl:when test="count(MMM) = 0">
            <xsl:variable name="nodes_in_next_sibling" select="2*count(following-sibling::MMM)" />
            <MMM>
               <xsl:copy-of select="$others[count($others) - $nodes_in_next_sibling - 1]" />
               <xsl:copy-of select="$others[count($others) - $nodes_in_next_sibling]" />
            </MMM>
         </xsl:when>
         <!-- this handles the case of an inner MMM with a sibling -->
         <xsl:when test="count(../MMM) = 2">
            <xsl:variable name="free_positions_in_second_child" select="count(MMM[position() = 2 and count(MMM) = 0])*2 + count(MMM[2]//MMM[count(MMM) = 0])*2 + count(MMM[position() = 2 and count(MMM) = 1]) + count(MMM[2]//MMM[count(MMM) = 1])" />
            <MMM>
               <xsl:apply-templates mode="dump" select="MMM[1]">
                  <xsl:with-param name="others" select="$others[position() &lt; count($others)- $free_positions_in_second_child + 1]" />
               </xsl:apply-templates>
               <xsl:apply-templates mode="dump" select="MMM[2]">
                  <xsl:with-param name="others" select="$others[position() &gt;= count($others) - $free_positions_in_second_child + 1]" />
               </xsl:apply-templates>
            </MMM>
         </xsl:when>
         <!-- this handles the case of an inner MMM without sibling -->
         <xsl:when test="count(../MMM) = 1">
            <MMM>
               <xsl:apply-templates mode="dump">
                  <xsl:with-param name="others" select="$others[position() &lt; count($others)]" />
               </xsl:apply-templates>
            </MMM>
            <xsl:copy-of select="$others[position() = count($others)]" />
         </xsl:when>
      </xsl:choose>
   </xsl:template>
   <xsl:template match="XXX">
      <XXX>
         <xsl:copy-of select="Name" />
         <xsl:variable name="mmm_structure">
            <xsl:apply-templates mode="extract_mmm" select="MMM" />
         </xsl:variable>
         <xsl:variable name="other_structure_tmp">
            <xsl:apply-templates mode="extract_other" select="MMM" />
         </xsl:variable>
         <!-- http://stackoverflow.com/questions/4610921/how-to-concatenate-two-node-sets-such-that-order-is-respected -->
         <!-- http://www.exslt.org/exsl/ -->
         <xsl:variable name="other_structure" select="exslt:node-set($other_structure_tmp/*)" />
         <xsl:apply-templates select="$mmm_structure" mode="dump">
            <xsl:with-param name="others" select="$other_structure" />
         </xsl:apply-templates>
      </XXX>
   </xsl:template>
</xsl:stylesheet>

Notes:

  • This is still pretty much XSLT 1.0 with EXSLT extensions.
  • MMM nodes with two children are also handled. The rule of placing the other nodes is such that the positions are filled from the bottom to the top.
  • In this version the other nodes are moved down as far as possible without violating the 2 children max rule. So the output is not exactly as display in the question but rather as follows.

This is the output:

<?xml version="1.0" encoding="ISO-8859-1"?>
<XXX>
  <Name>Sample</Name>
  <MMM>
    <MMM>
      <MMM>
        <AAA ID="A"/>
        <AAA ID="B"/>
      </MMM>
    </MMM>
    <BBB ID="C"/>
  </MMM>
  <BBB ID="D"/>
</XXX>

If this is a problem let me know.

查看更多
登录 后发表回答