Question

Quick Question: Is there a way to increment the predicate of an XPATH, by using a variable, like itereating through an array in C? For example /XPATH/element[i]

I am trying to use an XSL to access data from an XML using XPATHS. The XML is the output of a database where the parent node is the table name and its children are the columns. The XSL must be able to convert the text value of the children into attributes with the column name of the element of the table name.

The problem I am trying to solve is that each table can have multiple rows which is outputted to the XML as sibling nodes with the same names. There could be infinite rows in any table so I am trying to use a for-each with the XPATH of the table name to process each row. This works but when I try to use the document function with the XPATH with a predicate to the first XPATH and then the next XPATH and then the next, I do not know how to do it. I can only access the first XPATH. I want a way to be able to access the next XPATH on each iteration of the for-each. Is there anything which can increment each loop and that the predicate and use to point to the next XPATH?

The XML code below is a sample which I am using for testing, it is called DB.xml:

<?xml version="1.0"?>
<dataset>
 <rtbp>
  <cfmtype>dog</cfmtype>
  <cfmid>1</cfmid>
 </rtbp>
 <rtbp>
  <cfmtype>cat</cfmtype>
  <cfmid>2</cfmid>
 </rtbp>
 <FunctionSet> 
  <FUNCTIONSET__IDENTIFIER>1</FUNCTIONSET__IDENTIFIER>
  <RTBP__CFMID>1</RTBP__CFMID>
 </FunctionSet>
 <FunctionSet> 
  <FUNCTIONSET__IDENTIFIER>2</FUNCTIONSET__IDENTIFIER>
  <RTBP__CFMID>2</RTBP__CFMID>
 </FunctionSet>
</dataset>

Below is the XSL I am using:

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

 <xsl:template match="/">
  <xsl:for-each select="dataset/rtbp">
   <xsl:element name="RTBP">
    <xsl:attribute name="CFMtype">
     <xsl:value-of select="document('DB.xml')/dataset/rtbp[1]/cfmtype" />
    </xsl:attribute>
    <xsl:attribute name="CFMid">
     <xsl:value-of select="document('DB.xml')/dataset/rtbp[1]/cfmid" />
    </xsl:attribute>
    <xsl:text>&#xa;</xsl:text>

    <xsl:for-each select="/dataset/FunctionSet">
     <xsl:element name="FunctionSet">
      <xsl:attribute name="RTBP__CFMid">
       <xsl:value-of select="document('DB.xml')/dataset/FunctionSet[1]/FUNCTIONSET__IDENTIFIER" />
      </xsl:attribute>
      <xsl:attribute name="RTBP_FunctionSet">
       <xsl:value-of select="document('DB.xml')/dataset/FunctionSet[1]/RTBP__CFMID" />
      </xsl:attribute>
     </xsl:element>
     <xsl:text>&#xa;</xsl:text>
    </xsl:for-each>

   </xsl:element>
   <xsl:text>&#xa;</xsl:text>
  </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

The predicates are set to 1 at the moment but I wish it to be a variable which iterates on each loop so the XPATH changes to the next occurence of the table name.

The expected result is below:

<?xml version="1.0"?>
<RTBP CFMtype="dog" CFMid="1">
  <FunctionSet RTBP__CFMid="1" RTBP_FunctionSet="1"/>
</RTBP>
<RTBP CFMtype="cat" CFMid="2">
  <FunctionSet RTBP__CFMid="2" RTBP_FunctionSet="2"/>
</RTBP>

As you may be able to tell the second table (FunctionSet) is a child of the first (RTBP) hence the for-each inside the for-each. I need a method that will put the first row of the FunctionSet into the First row of the RTBP and likewise for the second rows.

I am new to XML, XSL and Posting questions.

Was it helpful?

Solution 3

For anyone who had as little knowledge as me when I posted this question and whom wish to find out the same infomation here is my solution to the question. Short answer to the quick question 'can you increment a variable'. No! But you can set a variable and move the position with the following snippet:

<xsl:for-each select="/dataset/rtbp">
   <xsl:variable name="i" select="position()" />
 </xsl:for-each> 

This snippet loops through rtbp tables in the source XML and moves the position one position more each loop. This creates an object which you can use inside a XPath to test for a condition of each occurence of the Xpath with the same URI path. Such as:

<xsl:for-each select="/dataset/rtbp">
    <xsl:variable name="i" select="position()" />
    <xsl:if test="/dataset/FunctionSet[$i]/cfmid = /dataset/rtbp[$n]/cfmid">
       <!--code if condition is true-->
 </xsl:for-each> 

The [$variable name] is how you direct an XPath to the occurence of the element name. So when i = 1 it looks for the first occurence of the element name in the XPath and then when i = 2 it looks for the second occurence of the element name in the XPath.

The Key function is a good tool to search for a key condition inside a template. However I can only use 1 key function per a template. if you wish to have a multi condition test you have to use a choose when statement with multiple if statements being anded with eeach other. for example:

This is a snippet from my advanced code which has multiple for-each loops inside each other and choose when statements to decide if a XML element is a child of the parent via its Identifiers which are child elements of the parent elements in the example XML in my question.

With the position function and the XPath predicate entry combined with choose when statements with ands you can build up a complex XSL which can re-create a flat XML table list of a database into a hierarchical XML form.

Vincent's Key function answer worked for the small complexity of this question but this answer includes an answer about the XPath predicates so I think it is more relevant of an answer to the question. Please look at Vincent's answer and consider using Key Functions for your solution because it is very useful

OTHER TIPS

The purpose is to re-create a hierarchical XML from a flat XML exported from a database using DBunit. The association could be done by cmfid

You should definitely use a key based on matching the cfmid value - especially if you are expecting a large number of rows. Try:

XSLT 1.0

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

<xsl:key name="func" match="FunctionSet" use="RTBP__CFMID" />

<xsl:template match="/">
    <root>
        <xsl:for-each select="dataset/rtbp">
            <RTBP CFMtype="{cfmtype}" CFMid="{cfmid}">
                <xsl:for-each select="key('func', cfmid)">
                    <FunctionSet RTBP__CFMid="{RTBP__CFMID}" RTBP_FunctionSet="{FUNCTIONSET__IDENTIFIER}"/>
                </xsl:for-each>
            </RTBP>
        </xsl:for-each>
    </root>
</xsl:template>

</xsl:stylesheet>

When the above is applied to the following test input:

<?xml version="1.0"?>
<dataset>
 <rtbp>
  <cfmtype>dog</cfmtype>
  <cfmid>124</cfmid>
 </rtbp>
 <rtbp>
  <cfmtype>cat</cfmtype>
  <cfmid>256</cfmid>
 </rtbp>
 <FunctionSet> 
  <FUNCTIONSET__IDENTIFIER>Canine</FUNCTIONSET__IDENTIFIER>
  <RTBP__CFMID>124</RTBP__CFMID>
 </FunctionSet>
 <FunctionSet> 
  <FUNCTIONSET__IDENTIFIER>Feline</FUNCTIONSET__IDENTIFIER>
  <RTBP__CFMID>256</RTBP__CFMID>
 </FunctionSet>
 <FunctionSet> 
  <FUNCTIONSET__IDENTIFIER>Hound</FUNCTIONSET__IDENTIFIER>
  <RTBP__CFMID>124</RTBP__CFMID>
 </FunctionSet>
</dataset>

the result is:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <RTBP CFMtype="dog" CFMid="124">
    <FunctionSet RTBP__CFMid="124" RTBP_FunctionSet="Canine"/>
    <FunctionSet RTBP__CFMid="124" RTBP_FunctionSet="Hound"/>
  </RTBP>
  <RTBP CFMtype="cat" CFMid="256">
    <FunctionSet RTBP__CFMid="256" RTBP_FunctionSet="Feline"/>
  </RTBP>
</root>

Note that your requested output format needlessly duplicates the cfmid value in both parent and child.

I think you're looking for something like (updated after quetion update) :

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
        <xsl:output indent="yes"/>
        <xsl:template match="node()|@*">
            <xsl:copy>
                <xsl:apply-templates select="node()|@*"/>
            </xsl:copy>
        </xsl:template>
        <xsl:template match="rtbp">
            <xsl:copy>
                <xsl:for-each select="*">
                    <xsl:attribute name="{local-name()}" select="."/>
                </xsl:for-each>
                <xsl:apply-templates 
                             select="//FunctionSet[RTBP__CFMID = current()/cfmid]"
                     mode="insertFunctionSet"/>
            </xsl:copy>
        </xsl:template>
        <xsl:template match="FunctionSet"/>
        <xsl:template match="FunctionSet" mode="insertFunctionSet">
            <xsl:copy>
                <xsl:for-each select="*">
                    <xsl:attribute name="{local-name()}" select="."/>
                </xsl:for-each>
            </xsl:copy>
        </xsl:template>
    </xsl:stylesheet>

The idea, here, is to handle differently the element FunctionSet in the context of rtbp element.

It should not be part of the output when you recursively loop over the whole tree (that's the goal of the <xsl:template match="FunctionSet"/>).

But it should be handled inside the rtbp element and so we apply the templates on the relevant FunctionSet in a specific mode at this point. That's the goal of the <xsl:template match="FunctionSet" mode="insertFunctionSet">...</xsl:template>

With your input:

<?xml version="1.0"?>
<dataset>
 <rtbp>
  <cfmtype>dog</cfmtype>
  <cfmid>1</cfmid>
 </rtbp>
 <rtbp>
  <cfmtype>cat</cfmtype>
  <cfmid>2</cfmid>
 </rtbp>
 <FunctionSet> 
  <FUNCTIONSET__IDENTIFIER>1</FUNCTIONSET__IDENTIFIER>
  <RTBP__CFMID>1</RTBP__CFMID>
 </FunctionSet>
 <FunctionSet> 
  <FUNCTIONSET__IDENTIFIER>2</FUNCTIONSET__IDENTIFIER>
  <RTBP__CFMID>2</RTBP__CFMID>
 </FunctionSet>
</dataset>

The result is:

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
   <rtbp cfmtype="dog" cfmid="1">
      <FunctionSet FUNCTIONSET__IDENTIFIER="1" RTBP__CFMID="1"/>
   </rtbp>
   <rtbp cfmtype="cat" cfmid="2">
      <FunctionSet FUNCTIONSET__IDENTIFIER="2" RTBP__CFMID="2"/>
   </rtbp>
</dataset>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top