views:

397

answers:

1

Hi,

I have an XML like this

<ContractInfo  ContractNo="12345">
                <Details LastName="Goodchild">                        
                        <Filedata  FileName="File1"/>
                </Details>
</ContractInfo>

<ContractInfo  ContractNo="12345">
                <Details LastName="Goodchild">                        
                        <Filedata  FileName="File2"/>
                </Details>
</ContractInfo>

<ContractInfo  ContractNo="123456">
                <Details LastName="Goodchild">                        
                        <Filedata  FileName="File2"/>
                </Details>
</ContractInfo>

I want my output XML to be like this

<ContractInfo  ContractNo="12345">
                <Details LastName="Goodchild">                        
                        <Filedata  FileName="File1"/>
                        <Filedata  FileName="File2"/>
                </Details>
</ContractInfo>

<ContractInfo  ContractNo="123456">
                <Details LastName="Goodchild">                        
                        <Filedata  FileName="File2"/>
                </Details>
</ContractInfo>

Here, the 'FileData' pertaining to matching "contractNo" needs to be combined at the output. Can this transformation be achieved with XSLT?

Thanks in advance.

Srini

+5  A: 

The following XSLT 1.0 transformation produces the right result:

<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

  <xsl:output method="xml" indent="yes" />

  <xsl:key name="contract" match="ContractInfo" use="@ContractNo" />
  <xsl:key name="filedata" match="Filedata" use="../../@ContractNo" />

  <xsl:template match="ContractInfo">
    <xsl:if test="generate-id() = 
                  generate-id(key('contract', @ContractNo)[1])">
      <xsl:copy>
        <xsl:apply-templates select="key('contract', @ContractNo)/Details | @*" />
      </xsl:copy>
    </xsl:if>
  </xsl:template>

  <xsl:template match="Details">
    <xsl:if test="generate-id(..) = 
                  generate-id(key('contract', ../@ContractNo)[1])">
      <xsl:copy>
        <xsl:apply-templates select="key('filedata', ../@ContractNo) | @*" />
      </xsl:copy>
    </xsl:if>
  </xsl:template>

  <!-- copy everything else (root node, Filedata nodes and @attributes) -->
  <xsl:template match="* | @*">
    <xsl:copy>
      <xsl:apply-templates select="* | @*" />
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

Note the use of <xsl:key> in conjunction with generate-id() to identify the first node of a matching node set, effectively grouping equal nodes together.

You can force an ordered result by using <xsl:sort> within the <xsl:apply-templates>. I did not include that for the sake of clarity.

My test output is:

<root>
  <ContractInfo ContractNo="12345">
    <Details LastName="Goodchild">
      <Filedata FileName="File1"></Filedata>
      <Filedata FileName="File2"></Filedata>
    </Details>
  </ContractInfo>
  <ContractInfo ContractNo="123456">
    <Details LastName="Goodchild">
      <Filedata FileName="File2"></Filedata>
    </Details>
  </ContractInfo>
</root>
Tomalak
It is relatively easy to extend the transformation to exclude duplicate <Filedata> nodes as well. Since that was not part of the original requirement I left it as an exercise for the reader. ;-) Hint: One additional <xsl:key> and another <xsl:template> is required.
Tomalak
hi, brilliant stuff..is there any link to study these concepts in much detail? Can you suggest one that covers these transformation from the basics?
There are loads of XSLT/XPath related web sites and tutorials on the internet. Just google the parts you don't understand. Depending on your current level of XSLT knowledge, the solution may take a while to become transparent, though. ;-)
Tomalak
great..when doing such a merge, is it straightforward to filter out the duplicates? or otherwise?
I'm afraid I don't understand the question?
Tomalak
I just voted up. One small remark: the value of the @match attribute of <xsl:key> is a "match pattern". As such, it does not need the "//" abbreviation. using just "match='//Filedata'" is better and more readable than "match='//Filedata'"
Dimitre Novatchev
Thanks for the hint (and the upvote). Redundant "//" removed.
Tomalak