John Royal
John Royal

Reputation: 371

XSLT 1 Exclude parent element if not in a list

I want to perform an xslt transformation where I split products up by type. As an example:

Source XML:

 <Products>
    <Product>
       <Name>Cheese</Name>
       <Value>30</Value>
    </Product>
    <Product>
       <Name>Bread</Name>
       <Value>10</Value>
    </Product>
    <Product>
       <Name>Bacon</Name>
       <Value>100</Value>
    </Product>
 </Products>

Required Output XML:

<Products>
    <AnimalProducts>
    <Product>
        <Name>Cheese</Name>
        <Value>30</Value>
    </Product>
    <Product>
        <Name>Bacon</Name>
        <Value>100</Value>
    </Product>
    </AnimalProducts>
    <VeganProducts>
    <Product>
        <Name>Bread</Name>
        <Value>10</Value>
    </Product>
    </VeganProducts>
</Products>

If there are no animal products, or no vegan products then the parent elements should not be included. I have it half working with:

<xsl:variable name="veganProducts" select="'Bread:Lettuce'" />

<xsl:if test="Products/Product[count(*) &gt; 0]">
            <AnimalProducts>
                <xsl:for-each select="Products/Product">
                    <xsl:if test="not(contains(concat(':', $veganProducts, ':'), concat(':', Name, ':')))">     
                        <Product>
                            <Name>
                                <xsl:value-of select="Name" />
                            </Name>
                            <Value>
                                <xsl:value-of select="Value" />
                            </Value>
                        </Product>
                    </xsl:if>
                </xsl:for-each>
            </AnimalProducts>

            <VeganProducts>
                <xsl:for-each select="Products/Product">
                    <xsl:if test="contains(concat(':', $veganProducts, ':'), concat(':', Name, ':'))">     
                        <Product>
                            <Name>
                                <xsl:value-of select="Name" />
                            </Name>
                            <Value>
                                <xsl:value-of select="Value" />
                            </Value>
                        </Product>
                    </xsl:if>
                </xsl:for-each>
            </VeganProducts>
</xsl:if>

The problem is I am getting empty parent elements if there are no vegan or animal products in certain cases. I am unsure how I can test for this.

Upvotes: 1

Views: 769

Answers (2)

michael.hor257k
michael.hor257k

Reputation: 117043

I would prefer to solve this the XML way:

XSLT 1.0

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="http://www.example.com/my"
exclude-result-prefixes="my">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<my:vegan-items>
    <item>Bread</item>
    <item>Lettuce</item>
</my:vegan-items>

<xsl:variable name="vegan-items" select="document('')/xsl:stylesheet/my:vegan-items/item" />

<xsl:template match="/Products">
    <xsl:variable name="vegan-products" select="Product[Name=$vegan-items]" />
    <xsl:variable name="animal-products" select="Product[not(Name=$vegan-items)]" />
    <xsl:copy>
        <xsl:if test="$animal-products">
            <AnimalProducts>
                <xsl:copy-of select="$animal-products"/>
            </AnimalProducts>
        </xsl:if>
        <xsl:if test="$vegan-products">
            <VeganProducts>
                <xsl:copy-of select="$vegan-products"/>
            </VeganProducts>
        </xsl:if>
    </xsl:copy>                             
 </xsl:template>

</xsl:stylesheet>

Here we are assuming that anything not "vegan" is "animal" (since no list of "animal" items has been provided).

Keeping an external XML document listing the items in each category would probably be even better.

Upvotes: 0

JLRishe
JLRishe

Reputation: 101710

This is a good time to make use of variables:

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

  <xsl:variable name="veganProductNames" select="'Bread:Lettuce'" />
  <xsl:variable name="veganProductNamesPadded" 
                select="concat(':', $veganProductNames, ':')" />

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

  <xsl:template match="/*">
    <xsl:copy>
      <xsl:variable name="animalProducts"
                    select="Product[not(contains($veganProductNamesPadded,
                                                 concat(':', Name, ':')))]" />
      <xsl:variable name="veganProducts"
                    select="Product[contains($veganProductNamesPadded,
                                             concat(':', Name, ':'))]" />

      <xsl:if test="$animalProducts">
        <AnimalProducts>
          <xsl:apply-templates select="$animalProducts" />
        </AnimalProducts>
      </xsl:if>
      <xsl:if test="$veganProducts">
        <VeganProducts>
          <xsl:apply-templates select="$veganProducts" />
        </VeganProducts>
      </xsl:if>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

When run on your sample input, the result is:

<Products>
  <AnimalProducts>
    <Product>
       <Name>Cheese</Name>
       <Value>30</Value>
    </Product>
    <Product>
       <Name>Bacon</Name>
       <Value>100</Value>
    </Product>
  </AnimalProducts>
  <VeganProducts>
    <Product>
       <Name>Bread</Name>
       <Value>10</Value>
    </Product>
  </VeganProducts>
</Products>

Upvotes: 1

Related Questions