Hello, this is not a Web Development question, but this is the only XML forum on Daniweb. I need to copy a specific number of nodes in an XSLT transform. For example, I need to copy the first 5 <student> nodes, with all child nodes and attribute nodes, even though the original XML file may contain 100 student nodes. Thanks in advance.

A more general question would be how to copy a specific range of nodes based on their position/index within the original XML file. For example, copy student nodes 50 through 88.

Recommended Answers

All 8 Replies

There's a couple of ways to do this; probably the most imperative is:

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

  <xsl:template match="/">
    <xsl:apply-templates>
      <xsl:with-param name="first" select="1"/>
      <xsl:with-param name="last" select="8"/>
    </xsl:apply-templates>
  </xsl:template>
  
  <xsl:template match="students">
    <xsl:param name="first"/>
    <xsl:param name="last"/>    
    <students>
      <xsl:for-each select="student[position()&gt;=$first and position()&lt;=$last]">
        <xsl:copy-of select="."/>
      </xsl:for-each>
    </students>
  </xsl:template>

</xsl:stylesheet>

That's assuming an input like:

<students>
  <student a="1">data</student>
  <student a="2">data</student>
   ....
  <student a="20">data</student>
</students>

No, this still copies everything... perhaps my processor doesn't support position().

Which processor are you using? If your processor doesn't support position( ).. that's strange; because it's been around a long time. Anyway, try this replacement for the 2nd template:

<xsl:template match="students">
    <xsl:param name="first"/>
    <xsl:param name="last"/>    
    <students>
      <xsl:for-each select="student">
        <xsl:variable name="position"><xsl:number/></xsl:variable>
        <xsl:if test="( number( $position ) &gt;= $first ) and ( number( $position ) &lt;= $last )">
          <xsl:copy-of select="."/>
        </xsl:if>
      </xsl:for-each>
    </students>
  </xsl:template>

Something like this should also work:

<xsl:template match="students">
    <xsl:param name="first"/>
    <xsl:param name="last"/>    
    <students>
      <xsl:for-each select="student">
        <xsl:variable name="num_preceding" select="count( preceding-sibling::student )"/>
        <xsl:if test="( $num_preceding &gt;= ( $first - 1 ) ) and ( $num_preceding &lt; $last )">
          <xsl:copy-of select="."/>
        </xsl:if>
      </xsl:for-each>
    </students>
  </xsl:template>

Also, what's the structure of your input document? Is it as simple as the example I gave ( i.e. all student elements are sibling level within the root node ) or more complex?

Altova.

It's a bit more complex. I need to do a deep copy of a second-level node, but it's the only second level node. Something like this:

<?xml version="1.0" encoding="utf-8"?>
<Students xmlns="myNameSpace" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<student>
  <firstname>Thomas</firstname>
  <lastname>Greer</lastname>
</student>
..
..
</Students>

The <student> node is what I want to copy, all attributes, child nodes, and attributes of child nodes. I need to copy a range of them. The "student" node contains child-nodes, and those nodes may have their own children, and so on. Essentially, I'm dividing the XML file into smaller chunks based on the total number student nodes.

I will test your other examples. Thanks.

I'm only getting the text nodes, not the element nodes or attributes, and I'm getting the entire file not just the first few students.

It sounds like the `students' template isn't being matched atall ( that would cause the output to end up only as text nodes ). Silly question - did you change the capitalization of the match="students" in the 2nd template to match the capitalization of the `Students' node in your document?

The only other thing I can think of is changing the <apply-templates> in the first template to explicitly select only the Students ( root ) node, i.e:

<xsl:template match="/">
  <xsl:apply-templates [b]select="Students"[/b]>
    <xsl:with-param name="first" select="1"/>
    <xsl:with-param name="last" select="8"/>
  </xsl:apply-templates>
</xsl:template>

If the root node is `Students', that should be the same as <apply-templates> without an explicit select, but, I guess all xslt processors are a bit different..

commented: Thanks for your help. +7

Yes, I changed my example. I'm not really dealing with Students and student, but the structure is the same: root node, second-level node with attributes and child nodes. I will thrash around a bit more and let you know. Thanks again for taking a look.

Namespaces were messing me up. The techniques above work just fine. In order to split the file into several groups, I ended up using this:

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:variable name="nodes-to-group" select="ceiling(count(Students/student) div 2)" />

<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
  <xsl:for-each-group select="Students/student" group-by="ceiling(position() div $nodes-to-group)">
    <xsl:result-document href="{TEMPFOLDER}{format-number(position(),'000000000')}.xml">
      <Students>
        <xsl:apply-templates select=".|following::student[position() &lt; $nodes-to-group]"/>
      </Students>
    </xsl:result-document>
  </xsl:for-each-group>
</xsl:template>

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

</xsl:stylesheet>

Use any hard-coded value or calculation to determine group size in the nodes-to-group variable.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.