George Macgregor
George Macgregor

Reputation: 11

Transforming repeated XML elements containing different values using XSLT

Hello Stackoverflowers --

This is my first time posting. I have encountered a frustrating XSLT problem which, despite various supposed solutions, has failed to resolve. The scenario is a basic transformation of one XML document (#1) into the format of another (#2) to enable interoperability between two systems.

XML doc #1 contains a number of repeated elements, each of which contains a separate value. These repeated elements need to be transformed into an alternative element for doc #2 and include the corresponding value from doc #1.

Despite a lot of experimentation I can't seem to get this to work. The relevant elements in doc #1 are as follows:

 <mods:extension> 
  <rx:funder>Funder A</rx:funder> 
  <rx:projectid>Funder A code number</rx:projectid> 
  <rx:funder>Funder B</rx:funder> 
  <rx:projectid>Funder B code number</rx:projectid> 
 </mods:extension> 

I need to the XML to transform in doc #2 to transform as follows:

<project_input>
  <item>
    <project>Funder A code number</project>
    <funder_name>Funder A</funder_name>
  </item>
  <item>
    <project>Funder B code number</project>
    <funder_name>Funder B</funder_name>
  </item>
</project_input>

But unfortunately the output of my transformation is always a variant of this:

<project_input>
  <item>
    <project>Funder A code numberFunder B code number</project>
    <funder_name>Funder AFunder B</funder_name>
  </item>
</project_input>

...with the values stuffed into a single element.

The transformation at the moment is like this:

<xsl:if test="v3:extension">
<project_input>
<item>      
<xsl:for-each select="v3:extension/rx:projectid">
    <project>
        <xsl:value-of select="."/>   
    </project>
</xsl:for-each> 
<xsl:for-each select="v3:extension/rx:funder">
    <funder_name>
        <xsl:value-of select="."/>      
    </funder_name>
</xsl:for-each>
</item> 
</project_input>
</xsl:if>

So the problem is that although the correct values are looped over successfully in doc #1 they are not output properly in doc #2.

I have tried using variants incorporating distinct-values but this was unsuccessful too. The frustrating this is I have done stuff like this before but can't for the life of me remember! There are also similar scenarios described on stack already but the solution proposed there didn't seem to work in this use case.

Can anyone point out something obvious I'm missing? Thanks

UPDATED question -- see comments below. This needs to work in XSLT 1.0. I have managed to cobble something together. It half works insofar as the correct data are extracted, ordered and grouped, but the specified XML elements are missing. There is probably something really obvious I have omitted.

Can anyone help me, please? Here is the XSLT 1.0:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
xmlns:mods="http://www.loc.gov/mods/v3" xmlns:rx="http://example.com/rx" 
version="1.0">
<xsl:output indent="yes"/>
<xsl:key use="rx:funder" match="mods:mods/mods:extension" name="groups" />
<xsl:template match="/">
<xsl:apply-templates select="mods:mods/mods:extension" />
</xsl:template>
<xsl:template match="mods:mods">
<rioxx2_project_output>
  <xsl:for-each select="/mods:extension[generate-id(.)=generate- 
  id(key('groups', rx:funder))]">
    <item>
      <funder_name>
        <xsl:value-of select="mods:extension/rx:funder/text()" />
      </funder_name>
      <project>
        <xsl:value-of select="mods:extension/rx:projectid/text()" />
      </project>
     </item>
     </xsl:for-each>
     </project_input>
   </xsl:template>
  </xsl:stylesheet>

Here is in XSLT Fiddle: https://xsltfiddle.liberty-development.net/bFN1y8S/1

Upvotes: 1

Views: 432

Answers (2)

Martin Honnen
Martin Honnen

Reputation: 167426

It seems like a classic example of using for-each-group group-starting-with or group-ending-with so in your case to use e.g.

  <xsl:template match="mods:extension">
      <project_input>
          <xsl:for-each-group select="*" group-starting-with="rx:funder">
              <item>
                  <project>
                      <xsl:value-of select="current-group()[2]"/>
                  </project>
                  <funder_name>
                      <xsl:value-of select="."/>
                  </funder_name>
              </item>
          </xsl:for-each-group>
      </project_input>
  </xsl:template>

That assumes there is some regularity to the sequence of child elements of mods:extension with rx:funder to be able to identify a group.

Complete example https://xsltfiddle.liberty-development.net/bFN1y8S using XSLT 3 (for an XSLT 2 processor you would need to spell out the identity transformation template instead of using xsl:mode)

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:mods="http://example.com/mods"
    xmlns:rx="http://example.com/rx"
    exclude-result-prefixes="#all"
    version="3.0">

  <xsl:output indent="yes"/>

  <xsl:mode on-no-match="shallow-copy"/>

  <xsl:template match="mods:extension">
      <project_input>
          <xsl:for-each-group select="*" group-starting-with="rx:funder">
              <item>
                  <project>
                      <xsl:value-of select="current-group()[2]"/>
                  </project>
                  <funder_name>
                      <xsl:value-of select="."/>
                  </funder_name>
              </item>
          </xsl:for-each-group>
      </project_input>
  </xsl:template>

</xsl:stylesheet>

To achieve a similar approach in XSLT 1 one way is to use use a key that keys any non rx:funder elements on the id of the preceding sibling rx:funder:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:mods="http://example.com/mods"
    xmlns:rx="http://example.com/rx"
    exclude-result-prefixes="mods rx"
    version="1.0">

  <xsl:output indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:key name="group" match="mods:extension/*[not(self::rx:funder)]" 
     use="generate-id(preceding-sibling::rx:funder[1])"/>

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

  <xsl:template match="mods:extension">
      <project_input>
          <xsl:for-each select="rx:funder">
              <item>
                  <project>
                      <xsl:value-of select="key('group', generate-id())[self::rx:projectid]"/>
                  </project>
                  <funder_name>
                      <xsl:value-of select="."/>
                  </funder_name>
              </item>
          </xsl:for-each>
      </project_input>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/bFN1y8S/3

Upvotes: 2

imran
imran

Reputation: 461

    <xsl:strip-space elements="*"/>
        <xsl:template match="node()">
            <xsl:copy>
                <xsl:apply-templates />
            </xsl:copy>
        </xsl:template>
        <xsl:template match="mods:extension">
            <xsl:element name="project_input">
                <xsl:for-each-group select="node()" group-starting-with="rx:funder">

                       <item>
                           <project>
                               <xsl:value-of select="./following-sibling::*[1][self::rx:projectid]"/>
                       </project>
                           <funder_name>
                               <xsl:value-of select="."/>
                           </funder_name>

                       </item>

                </xsl:for-each-group>
            </xsl:element>  
        </xsl:template>
Kindly chek it.

Upvotes: 0

Related Questions