XSLT - How to map elements to name value pairs?

3.4k Views Asked by At

I need to translate an XML from the source format to the target name value pairs for generic processing. Any tips on how to achieve this please? I am trying to use MapForce if it's easier.

From

<products>
    <product>
        <type>Monitor</type>
        <size>22</size>
        <brand>EIZO</brand>
    </product>
    <product>
        ......
    </product>
</products>

to

<products>
    <product num="1">
        <attribute name="type">Monitor</attribute>
        <attribute name="size">22</attribute>
        <attribute name="brand">EIZO</attribute>
    </product>
    <product num="2">
        ....
    </product>
</products>

I presume I need to use xsl:for-each in the element to generate the element?

How about the "num" attribute, it's just a counter basically. could it be position()?

Many Thanks!!

5

There are 5 best solutions below

0
On

For problems like this you often start off by building the XSLT identity template

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

On its own this copies all nodes as-is, which means you only need to write matching templates for nodes you wish to transform.

To start with, you wish to add the num attribute to the product, so have a template matching product where you simply output it with the attribute and continue processing its children.

<xsl:template match="product">
   <product num="{position()}">
      <xsl:apply-templates select="@*|node()"/>
   </product>
</xsl:template>

Do note the use of Attribute Value Templates here in creating the num attribute. The curly braces indicate an expression to be evaluated, not output literally.

Then, you want a template to match the children of the product elements, and turn these into attribute nodes. This is done with a pattern to match any such child, like so

<xsl:template match="product/*">
   <attribute name="{local-name()}">
      <xsl:apply-templates />
   </attribute>
</xsl:template>

Note that <xsl:apply-templates /> could be replaced with <xsl:value-of select="." /> here if you are only ever going to have text nodes within the child elements.

Try this XSLT

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="xml" indent="yes"/>
   <xsl:template match="@*|node()">
      <xsl:copy>
         <xsl:apply-templates select="@*|node()"/>
      </xsl:copy>
   </xsl:template>

   <xsl:template match="product">
      <product num="{position()}">
         <xsl:apply-templates select="@*|node()"/>
      </product>
   </xsl:template>

   <xsl:template match="product/*">
      <attribute name="{local-name()}">
         <xsl:apply-templates />
      </attribute>
   </xsl:template>
</xsl:stylesheet>

When applied to your XML the following is output

<products>
  <product num="1">
    <attribute name="type">Monitor</attribute>
    <attribute name="size">22</attribute>
    <attribute name="brand">EIZO</attribute>
  </product>
  <product num="2">
        ......
  </product>
</products>

Of course, if do actually want to turn the child elements into proper attributes, as opposed to elements named "attribute", you would use the xsl:attribute command. Replace the last template with this

<xsl:template match="product/*">
   <xsl:attribute name="{local-name()}">
      <xsl:value-of select="." />
   </xsl:attribute>
</xsl:template>

When using this template instead, the following is output (Well, it would include product 2 if your sample has child elements for it!)

<products>
  <product num="1" type="Monitor" size="22" brand="EIZO"></product>
  <product num="2">
    ......
  </product>
</products>
3
On

I think I have what you are looking for. No for-each clauses. Just the magic of recursive templates:

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

<xsl:template match="products">
    <products>
        <xsl:call-template name="transformProducts">
            <xsl:with-param name="nodes" select="product"/>
        </xsl:call-template>
    </products>
</xsl:template>

<xsl:template name="transformProducts">
    <xsl:param name="nodes"/>
    <xsl:param name="counter" select="0"/>

    <xsl:choose>
        <xsl:when test="not($nodes)"></xsl:when>
        <xsl:otherwise>
            <product>
                <xsl:attribute name="num">
                    <xsl:value-of select="$counter + 1"/>
                </xsl:attribute>
                <xsl:attribute name="type">
                    <xsl:value-of select="$nodes[1]/type"/>
                </xsl:attribute>
                <xsl:attribute name="size">
                    <xsl:value-of select="$nodes[1]/size"/>
                </xsl:attribute>
                <xsl:attribute name="brand">
                    <xsl:value-of select="$nodes[1]/brand"/>
                </xsl:attribute>
            </product>
            <xsl:call-template name="transformProducts">
                <xsl:with-param name="nodes" select="$nodes[position() > 1]"/>
                <xsl:with-param name="counter" select="$counter + 1"/>
            </xsl:call-template>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

</xsl:stylesheet>

According to my test, here's the starting XML:

<products>    
 <product>
    <type>Monitor</type>
    <size>22</size>
    <brand>EIZO</brand>
</product>
<product>
    <type>MonitorTwo</type>
    <size>32</size>
    <brand>SONY</brand>
</product>
<product>
    <type>what</type>
    <size>12</size>
    <brand>VGA</brand>
</product>
</products>

And here's the output. I assume by naming your elements "attribute" you actually meant for them to be transformed into attribute nodes of the product element?

<products>
 <product num="1" type="Monitor" size="22" brand="EIZO"/>
 <product num="2" type="MonitorTwo" size="32" brand="SONY"/>
 <product num="3" type="what" size="12" brand="VGA"/>
</products>
0
On

Thanks all,

Both Tim and Phillips approaches are very different but both work perfectly. Am I right of thinking

  • Tim's approach is based on the Source XML, then using templates to tweak whatever specified. Say if I have 100 elements and only want to tweak a a few, then this is a good one.

  • Phillip approach is building a new XML from scratch, but XPath-ing/extracting out what do I want. Say If I have 100 elements and I only want to extract and transform a few as for output, then this is a good one.

Many Thanks!

0
On

Haven't tested this, but you could do something like the following:

<products>
    <xsl:for-each select="//product">
        <product num="{position()}">
           <xsl:for-each select="current()/*">
             <attribute name="{name()}">
                <xsl:value-of select="self::node()/text()"/>
             </attribute>
           </xsl>
        </product>
    </xsl>
</products>
0
On

Of course, heavens forbid that this should be short and simple, because... why really?

Version A (elements named "attribute"):

<xsl:template match="/">
<products>
    <xsl:for-each select="products/product">
        <product num="{position()}">
           <xsl:for-each select="*">
             <attribute name="{name()}">
                <xsl:value-of select="."/>
             </attribute>
           </xsl:for-each>
        </product>
    </xsl:for-each>
</products>
</xsl:template>

Version B (real attributes):

<xsl:template match="/">
<products>
    <xsl:for-each select="products/product">
        <product num="{position()}">
           <xsl:for-each select="*">
             <xsl:attribute name="{name()}">
                <xsl:value-of select="."/>
             </xsl:attribute>
           </xsl:for-each>
        </product>
    </xsl:for-each>
</products>
</xsl:template>