Search code examples
xmlxsltxslt-grouping

Move child Nodes to attributes conditionally


I am trying to transform this document but am fairly new to xslt and having tons of fun trying to get it right. The core node(truncated for simplicity) looks like this

<Product prod_id="6352">
    <brandId>221</brandId>
    <brand>Oscar Mayer</brand>
    <images>
       <smallimage>text</simage>
       <medimage>text</medimage>
       <largeimage>text</limage>
    </images>
    <nutrition>
        <nutritionShow>Y</nutritionShow>
        <servingSize>1 SLICE</servingSize>
        <servingsPerContainer>12</servingsPerContainer>
        <totalCalories>60</totalCalories>
        <fatCalories>35</fatCalories>
        <totalFat>4</totalFat>
        <totalFatPercent>6</totalFatPercent>
        <totalFatUnit>g</totalFatUnit>
        <saturatedFat>1.5</saturatedFat>
        <saturatedFatPercent>8</saturatedFatPercent>
        <saturatedFatUnit>g</saturatedFatUnit>
        <transFat>0</transFat>
        <transFatUnit>g</transFatUnit>
        <cholesterolUnit>mg</cholesterolUnit>
    </nutrition>
    <prodId>6352</prodId>
</Product>

In the end I want to sub-nodes that are grouped logically to be a single node with appropriate attribute names.

The end result should look like this

<Product prod_id="6352">
<brandId>221</brandId>
<brand>Oscar Mayer</brand>
<images>
   <smallimage>text</smallimage>
   <medimage>text</medimage>
   <largeimage>text</largeimage>
</images>
<nutrition>
    <nutritionShow>Y</nutritionShow>
    <servingSize>1 SLICE</servingSize>
    <servingsPerContainer>12</servingsPerContainer>
    <totalCalories>60</totalCalories>
    <fatCalories>35</fatCalories>
    <totalFat amount="4" percent="6" unit="g" />
    <saturatedFat amount="1.5" percent="8" unit="g"/>
    <transFat amount="0" unit="g"</>
</nutrition>
<prodId>6352</prodId>

Some key features are

  1. group the similar attributes(notice saturatedFat and transFat ... slightly different)I have a discrete list of these sets. You could use a list or something more dynamic based on relationships but notice the variance.
  2. leave other(non group-able) attributes be
  3. ignore groups that lack the amount attribute/only have unit attribute(notice cholesterol)

Thanks in advance for helping me to understand this fairly complex transformation.


Solution

  • One possible solution is following XSLT:

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="html" encoding="UTF-8" indent="yes" />
     <xsl:strip-space elements="*"/>
      <xsl:template match="@*|node()">
        <xsl:copy>
          <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
      </xsl:template>
      <xsl:template match="nutrition/*">
        <xsl:variable name="cName" select="name()"/>
        <xsl:choose>
          <xsl:when test="following-sibling::node()[name()=concat($cName,'Unit')]">
            <xsl:copy>
              <xsl:attribute name="amount">
                <xsl:value-of select="."/>
              </xsl:attribute>
              <xsl:if test="following-sibling::node()[name()=concat($cName,'Percent')]">
                <xsl:attribute name="percent">
                  <xsl:value-of select="following-sibling::node()[name()=concat($cName,'Percent')]"/>
                </xsl:attribute>
              </xsl:if>
              <xsl:attribute name="unit">
                <xsl:value-of select="following-sibling::node()[name()=concat($cName,'Unit')]"/>
              </xsl:attribute> 
            </xsl:copy>
          </xsl:when>
          <xsl:when test="contains(name() ,'Unit') or contains(name() ,'Percent')"/>
          <xsl:otherwise>
            <xsl:copy>
              <xsl:apply-templates />
            </xsl:copy>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:template>
    </xsl:stylesheet>
    

    when applied to your input XML produces the ouput

    <Product prod_id="6352">
      <brandId>221</brandId>
      <brand>Oscar Mayer</brand>
      <images>
        <smallimage>text</smallimage>
        <medimage>text</medimage>
        <largeimage>text</largeimage>
      </images>
      <nutrition>
        <nutritionShow>Y</nutritionShow>
        <servingSize>1 SLICE</servingSize>
        <servingsPerContainer>12</servingsPerContainer>
        <totalCalories>60</totalCalories>
        <fatCalories>35</fatCalories>
        <totalFat amount="4" percent="6" unit="g"></totalFat>
        <saturatedFat amount="1.5" percent="8" unit="g"></saturatedFat>
        <transFat amount="0" unit="g"></transFat>
      </nutrition>
      <prodId>6352</prodId>
    </Product>
    

    The first template is an Identity transform and copies all nodes and attributes without any changes.
    The second temmplate matches all child elements/nodes of nutrition.
    In case the current element has a following sibling with a local name matching the current local name and ending with Unit

    <xsl:when test="following-sibling::node()[name()=concat($cName,'Unit')]">
    

    the current node has to be a node containing the amount.
    The value of the current node is written as value of the amount attribute

    <xsl:attribute name="amount">
        <xsl:value-of select="."/>
    </xsl:attribute>
    

    and in case a following sibling with matching Percent exists

    <xsl:if test="following-sibling::node()[name()=concat($cName,'Percent')]">
    

    the Percent attribute is written accordingly:

    <xsl:attribute name="percent">
        <xsl:value-of select="following-sibling::node()[name()=concat($cName,'Percent')]"/>
      </xsl:attribute>
    

    Same applies to Unit without previously checking if a matching Unit exists (which could be added if necessary).
    The empty

    <xsl:when test="contains(name() ,'Unit') or contains(name() ,'Percent')"/>
    

    removes the Unit and Percent nodes that has been written as attributes as well as the cholesterolUnit.
    Finally, all other non groupable nutrition elements are just copied:

    <xsl:otherwise>
      <xsl:copy>
        <xsl:apply-templates/>
      </xsl:copy>
    </xsl:otherwise>