HeisenBerg
HeisenBerg

Reputation: 127

Rearranging XML nodesets using xslt

I want to rearrange an xml with xslt.

The source xml is as follows:

<?xml version="1.0" encoding="UTF-8" ?>
<studentDetails>
<departments>
    <name>IT</name>
    <name>CSE</name>
    <name>EEE</name>
</departments>
<students>                   // Want to arrange students and marks alternatively
    <student>
        <name>aaa</name>
        <age>20</age>
    </student>
    <student>
        <name>bbb</name>
        <age>25</age>
    </student>
    <student>
        <name>ccc</name>
        <age>27</age>
    </student>
    <student>
        <name>ddd</name>
        <age>23</age>
    </student>
    <marks>
        <maths>60</maths>
        <english>65</english>
    </marks>
    <marks>
        <maths>70</maths>
        <english>75</english>
    </marks>
    <marks>
        <maths>80</maths>
        <english>85</english>
    </marks>
    <marks>
        <maths>90</maths>
        <english>95</english>
    </marks>
</students>
</studentDetails>

I want the output in this way:

<?xml version="1.0" encoding="UTF-8" ?>
<studentDetails>
<departments>
    <name>IT</name>
    <name>CSE</name>
    <name>EEE</name>
</departments>
<students>
    <student>
        <name>aaa</name>
        <age>20</age>
    </student>
    <marks>
        <maths>60</maths>
        <english>65</english>
    </marks>        
    <student>
        <name>bbb</name>
        <age>25</age>
    </student>
    <marks>
        <maths>70</maths>
        <english>75</english>
    </marks>    
    <student>
        <name>ccc</name>
        <age>27</age>
    </student>
    <marks>
        <maths>80</maths>
        <english>85</english>
    </marks>
    <student>
        <name>ddd</name>
        <age>23</age>
    </student>
    <marks>
        <maths>90</maths>
        <english>95</english>
    </marks>
</students>
</studentDetails>

1). The number of <student> tags are equal to the no of <marks> 2). The sequence of <student> and <marks> are linear i.e, the first occurance of <student> should be followed by first occurance of <marks>

I tried the following xslt:

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
  <xsl:variable name="countVar" select="count(studentDetails/students/student)"></xsl:variable>
  <xsl:variable name="i" select="0"></xsl:variable>
  <xsl:template match="/">
  <xsl:for-each select="1 to $countVar">
  <xsl:variable name="counter" select="$i + 1"></xsl:variable>
  <xsl:choose>
  <xsl:when test="(position() mod 2) = 0">
    <xsl:copy-of select="studentDetails/students/marks[$counter]"></xsl:copy-of>
  </xsl:when>
  <xsl:otherwise>
 <xsl:copy-of select="studentDetails/students/student[$counter]"></xsl:copy-of>
  </xsl:otherwise>
</xsl:choose>
  </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

I got the following error:

Error on line 11 XPTY0020: Required item type of the context item for the child axis is node(); supplied value has item type xs:integer

Online version here: http://xsltransform.net/gWmuiJw

Please tell me where I went wrong. Thanks in advance.! :)

Upvotes: 0

Views: 373

Answers (1)

michael.hor257k
michael.hor257k

Reputation: 116992

When you do:

<xsl:for-each select="1 to $countVar">

you are switching the context from the XML input to the sequence of generated integers. In this context, the XPath you try to use, "studentDetails/students/marks", is meaningless.

Why don't you try something much simpler:

<xsl:stylesheet version="2.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

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

<xsl:template match="student">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
    <xsl:variable name="i" select="position()" />
    <xsl:copy-of select="../marks[$i]"/>
</xsl:template>

<xsl:template match="marks"/>

</xsl:stylesheet>

Note:
IMHO, a format that wraps all details of a student under a single element, for example:

<students>
    <student>
        <name>aaa</name>
        <age>20</age>
        <marks>
            <maths>60</maths>
            <english>65</english>
        </marks>
    </student>
    ...
<students>

would be much more useful.


Caveat:

The identity transform template, when applied to the <students> element, applies templates to all child nodes of <students> - not only the <student> nodes.

In your example, the <students> element also has a comment node among its children (actually, it's a text node, but it makes no difference for the purposes of this explanation). The position of each child node is calculated in the context of the node-set selected by the xsl:apply templates instruction (this is known as the current node list). As a result, the position of the first <student> node in your example is actually #2 - since position #1 is taken by the comment node.

This shouldn't be a problem in actual production, if in your real XML input there won't be any child nodes of <students> other than <student>. If you cannot be sure of that, add one more template to the stylesheet:

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

to exclude any nodes other than <student> from the current node list.

Upvotes: 2

Related Questions