dellair
dellair

Reputation: 437

XSLT replacing one elements with multiple elements

I would like to replace an element in XML (large) with multiple elements as below:

Original XML:

<root>
  <cr>
   <id>1</id>
   <release>A</release>
  </cr>
  <cr>
   <id>2</id>
   <release>B</release>
  </cr>
</root>

I would like the output to be:

<root>
  <cr>
   <id>1</id>
   <release>Aa</release>
   <release>Ab</release>
   <release>Ad</release>
  </cr>
  <cr>
   <id>2</id>
   <release>Bd</release>
   <release>Be</release>
  </cr>
</root>

The principle is, whenever there is //release[text()='A'], replace the element with three above, whenever there is //release[text()='B'], replace the element with two above etc. If the release text() is "C" or "D" or other values, they remain the same values.

My attempt:

<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:strip-space elements="*"/>

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

<xsl:template match="//cr/release/text()">
    <xsl:if test=".='A'">
        <xsl:value-of select="Aa"/>
    </xsl:if>
</xsl:template>
</xsl:stylesheet>

It works with one -> one replacement, but how to do multiple? Thanks a lot,

Upvotes: 1

Views: 1561

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243479

Here is a completely generic and short XSLT 1.0 solution. It uses a mapping xml file that specifies the replacements for each wanted release:

(Please, find at the end of this answer the ultimately-generic and short solution written in XSLT 2.0.)

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

 <xsl:variable name="vMap" select="document('mapping.xml')/*/*"/>

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

  <xsl:template match="release[. = document('mapping.xml')/*/release/@old]">
    <xsl:copy-of select="$vMap[@old = current()]/*"/>
  </xsl:template>
</xsl:stylesheet>

If the file mapping.xml is in the same directory as the transformation (.xsl file), and is:

<map>
    <release old="A">
        <release>Aa</release>
        <release>Ab</release>
        <release>Ad</release>
    </release>
    <release old="B">
        <release>Ba</release>
        <release>Bb</release>
        <release>Bd</release>
    </release>
</map>

then when the transformation is applied on this XML document:

<root>
  <cr>
   <id>1</id>
   <release>A</release>
  </cr>
  <cr>
   <id>2</id>
   <release>B</release>
  </cr>
  <cr>
   <id>3</id>
   <release>C</release>
  </cr>
  <cr>
   <id>4</id>
   <release>D</release>
  </cr>
</root>

the wanted, correct result is produced:

<root>
   <cr>
      <id>1</id>
      <release>Aa</release>
      <release>Ab</release>
      <release>Ad</release>
   </cr>
   <cr>
      <id>2</id>
      <release>Ba</release>
      <release>Bb</release>
      <release>Bd</release>
   </cr>
   <cr>
      <id>3</id>
      <release>C</release>
   </cr>
   <cr>
      <id>4</id>
      <release>D</release>
   </cr>
</root>

Note: Many, differently named elements may be replaced with the same transformation.

If we have this mapping file:

<map>
    <release old="A">
        <release>Aa</release>
        <release>Ab</release>
        <release>Ad</release>
    </release>
    <release old="B">
        <release>Ba</release>
        <release>Bb</release>
        <release>Bd</release>
    </release>
    <history old="p">
        <history>Pp</history>
        <history>Pq</history>
        <history>Pr</history>
    </history>
</map>

and this source XML document:

<root>
  <cr>
   <id>1</id>
   <history>p</history>
   <release>A</release>
  </cr>
  <cr>
   <id>2</id>
   <history>q</history>
   <release>B</release>
  </cr>
  <cr>
   <id>3</id>
   <history>r</history>
   <release>C</release>
  </cr>
  <cr>
   <id>4</id>
   <history>t</history>
   <release>D</release>
  </cr>
</root>

then this transformation:

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

 <xsl:variable name="vMap" select="document('mapping.xml')/*/*"/>

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

  <xsl:template match="release[. = document('mapping.xml')/*/release/@old]">
    <xsl:copy-of select="$vMap[@old = current()]/*"/>
  </xsl:template>
  <xsl:template match="history[. = document('mapping.xml')/*/history/@old]">
    <xsl:copy-of select="$vMap[@old = current()]/*"/>
  </xsl:template>
</xsl:stylesheet>

when applied on the above XML document,produces the wanted result -- where more than one differently-named elements are mapped and replaced:

<root>
   <cr>
      <id>1</id>
      <history>Pp</history>
      <history>Pq</history>
      <history>Pr</history>
      <release>Aa</release>
      <release>Ab</release>
      <release>Ad</release>
   </cr>
   <cr>
      <id>2</id>
      <history>q</history>
      <release>Ba</release>
      <release>Bb</release>
      <release>Bd</release>
   </cr>
   <cr>
      <id>3</id>
      <history>r</history>
      <release>C</release>
   </cr>
   <cr>
      <id>4</id>
      <history>t</history>
      <release>D</release>
   </cr>
</root>

Finally: An even shorter and more generic transformation can be written in XSLT 2.0:

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

 <xsl:variable name="vMap" select="document('mapping.xml')/*/*"/>

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

  <xsl:template match=
   "*[. = $vMap[name() eq name(current())]/@old]">
    <xsl:copy-of select="$vMap[name() eq name(current()) and @old eq current()]/*"/>
  </xsl:template>
</xsl:stylesheet>

Do note: No element name is hardcoded in this transformation !

An even greater advantage: We can modify the transformation, so we can pass the URI of the mapping document as a global parameter on invoking the transformation -- thus we can have a single, generic transformation that works with any, unknown in advance mapping.

Only these are the required changes:

 <xsl:param name="pmapUrl" select="'file:///c:/temp/mapping.xml'"/>

 <xsl:variable name="vMap" select="document($pmapUrl)/*/*"/>

The complete transformation (the global parameter pmapUrl can and typically will be specified dynamically by the invoker of the transformation):

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

 <xsl:param name="pmapUrl" select="'file:///c:/temp/mapping.xml'"/>

 <xsl:variable name="vMap" select="document($pmapUrl)/*/*"/>

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

  <xsl:template match=
   "*[for $name in name() 
        return
          . = $vMap[name() eq $name]/@old]">
    <xsl:copy-of select="$vMap[@old = current()]/*"/>
  </xsl:template>
</xsl:stylesheet>

Upvotes: 2

michael.hor257k
michael.hor257k

Reputation: 117003

Try it this way:

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:strip-space elements="*"/>

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

<xsl:template match="release[.='A']">
    <release>Aa</release>
    <release>Ab</release>
    <release>Ad</release>
</xsl:template>

<xsl:template match="release[.='B']">
    <release>Bd</release>
    <release>Be</release>
</xsl:template>

</xsl:stylesheet>

Test input

<root>
  <cr>
   <id>1</id>
   <release>A</release>
  </cr>
  <cr>
   <id>2</id>
   <release>B</release>
  </cr>  
  <cr>
   <id>3</id>
   <release>C</release>
  </cr>
</root>

Result

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <cr>
      <id>1</id>
      <release>Aa</release>
      <release>Ab</release>
      <release>Ad</release>
   </cr>
   <cr>
      <id>2</id>
      <release>Bd</release>
      <release>Be</release>
   </cr>
   <cr>
      <id>3</id>
      <release>C</release>
   </cr>
</root>

Upvotes: 2

Related Questions