bigtech
bigtech

Reputation: 484

How to reformat XML with related element groups using XSLT

I'm improving some XML I have inherited by using XSLT to clean things up, but I'm struggling with one section. Which looks like this:

    <rules>
        <if condition="equals" arg1="somevar" arg2="1"/>
        <elseif condition="equals" arg1="somevar" arg2="2"/>
        <elseif condition="equals" arg1="somevar" arg2="3"/>
        <else/>
        <if condition="equals" arg1="somevar" arg2="4"/>
        <else/>
    </rules>

This looks to be difficult to validate with XSD, so I'd like to transform it into something like this -- ideas?

    <rules>
        <conditionSet>
            <if condition="equals" arg1="somevar" arg2="1"/>
            <elseif condition="equals" arg1="somevar" arg2="2"/>
            <elseif condition="equals" arg1="somevar" arg2="3"/>
            <else/>
        </conditionSet>
        <conditionSet>
            <if condition="equals" arg1="somevar" arg2="4"/>
            <else/>
        </conditionSet>
    </rules>

Upvotes: 4

Views: 356

Answers (4)

C. M. Sperberg-McQueen
C. M. Sperberg-McQueen

Reputation: 25034

It's an interesting XSLT challenge. But, uh, why are you changing the XML again? The pattern in the input can easily be defined by a regular expression, namely

(if, elseif*, else)*

and for that reason it is easy to validate with XSD.

It may be worth while to change -- a veteran vocabulary designer (Lynne A. Price) once told me that any repetition operator on group was automatically suspect and often meant that the group should be replaced by an element. She would, I guess, approve of your change. But to make sense, the rationale for the change has to be greater ease of processing, not easier validation.

Upvotes: 1

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243449

I. Much simpler and shorter XSLT 1.0 solution:

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

 <xsl:key name="kFollowing" match="elseif|else"
  use="generate-id(preceding-sibling::if[1])"/>

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

 <xsl:template match="if">
  <conditionSet>
   <xsl:copy-of select=".|key('kFollowing', generate-id())"/>
  </conditionSet>
 </xsl:template>
</xsl:stylesheet>

when applied on the provided XML document:

<rules>
    <if condition="equals" arg1="somevar" arg2="1"/>
    <elseif condition="equals" arg1="somevar" arg2="2"/>
    <elseif condition="equals" arg1="somevar" arg2="3"/>
    <else/>
    <if condition="equals" arg1="somevar" arg2="4"/>
    <else/>
</rules>

the wanted, correct result is produced:

<rules>
   <conditionSet>
      <if condition="equals" arg1="somevar" arg2="1"/>
      <elseif condition="equals" arg1="somevar" arg2="2"/>
      <elseif condition="equals" arg1="somevar" arg2="3"/>
      <else/>
   </conditionSet>
   <conditionSet>
      <if condition="equals" arg1="somevar" arg2="4"/>
      <else/>
   </conditionSet>
</rules>

II. Even simpler and shorter XSLT 2.0 solution:

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/*">
  <rules>
   <xsl:for-each-group select="*" group-starting-with="if">
    <conditionSet>
     <xsl:sequence select="current-group()"/>
    </conditionSet>
   </xsl:for-each-group>
  </rules>
 </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the same XML document (above), the same correct result is produced:

<rules>
   <conditionSet>
      <if condition="equals" arg1="somevar" arg2="1"/>
      <elseif condition="equals" arg1="somevar" arg2="2"/>
      <elseif condition="equals" arg1="somevar" arg2="3"/>
      <else/>
   </conditionSet>
   <conditionSet>
      <if condition="equals" arg1="somevar" arg2="4"/>
      <else/>
   </conditionSet>
</rules>

Upvotes: 0

Kirill Polishchuk
Kirill Polishchuk

Reputation: 56162

One more:

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

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

  <xsl:template match="if">
    <conditionSet>
      <xsl:copy-of select="."/>
        <xsl:apply-templates select="
                             following-sibling::*[not(self::if) 
                             and generate-id(preceding-sibling::if[1]) 
                                = generate-id(current())]
                             "/>
    </conditionSet>
  </xsl:template>

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

</xsl:stylesheet>

Upvotes: 0

Wayne
Wayne

Reputation: 60414

Group elseif and else elements by their immediately preceding if element:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <xsl:key name="block" match="elseif|else" 
             use="generate-id(preceding-sibling::if[1])"/>
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="rules">
        <xsl:copy>
            <xsl:apply-templates select="@*|
                    node()[not(self::elseif or self::else)]"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="if">
        <conditionSet>
            <xsl:copy>
                <xsl:apply-templates select="@*|node()"/>
            </xsl:copy>
            <xsl:apply-templates select="key('block', generate-id())"/>
        </conditionSet>
    </xsl:template>
</xsl:stylesheet>

This stylesheet produces the requested output.

Explanation: The xsl:key associates each if element with its following, related elements, so that, later, when we match an if, we can simply wrap and copy the entire set.

Upvotes: 1

Related Questions