user3248211
user3248211

Reputation: 3

XSLT - Remove Nodes with no child node matching specific values

I am very new to XSLT, I started reading about it yesterday and I'm stuck. I have the following XML

<?xml version="1.0" encoding="utf-8" ?>
<Menu>
  <Function>
    <Name>ViewHome</Name>
    <Roles>
      <Role>Finance</Role>
      <Role>Advisor</Role>
      <Role>Admin</Role>
    </Roles>
  </Function>
  <Function>
    <Name>ViewStaff</Name>
    <ChildFunctions>
      <Function>
        <Name>StaffFunction1</Name>
        <Roles>
          <Role>Finance</Role>
        </Roles>
      </Function>
      <Function>
        <Name>StaffFunction2</Name>
        <Roles>
          <Role>Admin</Role>
        </Roles>
      </Function>
      <Function>
        <Name>StaffFunction3</Name>
        <Roles>
          <Role>Advisor</Role>
        </Roles>
      </Function>
    </ChildFunctions>
  </Function>
  <Function>
    <Name>ViewDiary</Name>
    <Roles>
      <Role>Advisor</Role>
    </Roles>
  </Function>
</Menu>

Which I would like transformed to show only those functions (parent or child) that contain a specified role or roles. So that if Finance and Advisor roles are applied the output would be like:

<?xml version="1.0" encoding="utf-8" ?>
<Menu>
  <Function>
    <Name>ViewHome</Name>
    <Roles>
      <Role>Finance</Role>
      <Role>Advisor</Role>
    </Roles>
  </Function>
  <Function>
    <Name>ViewStaff</Name>
    <ChildFunctions>
      <Function>
        <Name>StaffFunction1</Name>
        <Roles>
          <Role>Finance</Role>
        </Roles>
      </Function>
      <Function>
        <Name>StaffFunction3</Name>
        <Roles>
          <Role>Advisor</Role>
        </Roles>
      </Function>
    </ChildFunctions>
  </Function>
  <Function>
    <Name>ViewDiary</Name>
    <Roles>
      <Role>Advisor</Role>
    </Roles>
  </Function>
</Menu>

I have looked at other posts on this site, but can't generate the required output. The xsl I have been using is:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

 <xsl:output method="xml" />

    <xsl:variable name="rolesList">Advisor Finance</xsl:variable>

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

    <xsl:template match="Function[not(descendant::Role[contains($rolesList, .)])]"/>

<!--
    <xsl:template match="Function[not(descendant::Role='Finance')]"/>
-->

</xsl:stylesheet>

Ultimately the rolesList variable will be supplied through code. I just wanted to get the transform working. If I use the commented out template match for just 'Finance' role I get the expected results. Any help greatly appreciated.

Upvotes: 0

Views: 1855

Answers (4)

Erlock
Erlock

Reputation: 1968

Use an internal lookup list:

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xmlns:my="your-urn-here">

  <my:rolesList>
    <my:role>Advisor</my:role>
    <my:role>Finance</my:role>
  </my:rolesList>


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

  <xsl:template match="Function[not(descendant::Role = document('')/*/my:rolesList/my:role)]"
/>

</xsl:stylesheet>

Upvotes: 1

michael.hor257k
michael.hor257k

Reputation: 116992

Which I would like transformed to show only those functions (parent or child) that contain a specified role or roles.

I believe your stylesheet does exactly that (although it shouldn't, and with some processors only - see the comments to the question for more details).

However it does not remove the "other" roles from the list of roles descendant to the functions that have passed the test. For that, you would need to add another template matching Role.

Ultimately the rolesList variable will be supplied through code

Ideally, the list would be supplied as a node-set (or something that can be turned into a node-set), so that you can make the comparison without resorting to string functions. In XSLT 2.0 there is more flexibility in this aspect, though.

Upvotes: 0

Martin Honnen
Martin Honnen

Reputation: 167516

With XSLT 1.0 using string comparison you could use

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

 <xsl:output method="xml" />

    <xsl:param name="roleList" select="'|Advisor|Finance|'"/>

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

    <xsl:template match="Function">
      <xsl:if test="descendant::Role[contains($roleList, concat('|', ., '|'))]">
         <xsl:call-template name="identity"/>
      </xsl:if>
    </xsl:template>

    <xsl:template match="Role">
      <xsl:if test="contains($roleList, concat('|', ., '|'))">
         <xsl:call-template name="identity"/>
      </xsl:if>
    </xsl:template>

</xsl:stylesheet>

Upvotes: 1

Mathias M&#252;ller
Mathias M&#252;ller

Reputation: 22617

I suspect that contains() does not work properly if there is more than one Role element, because the function does not expect lists as its second argument.

Note that you provide the list of roles as a whitespace-separated list. Yet, without a Schema it is not recognized as a list type. That is why you have to rely on string functions, as pointed out by @michael.hor257k.

This (i.e. template matches that contain variables) only works with XSLT 2.0, as pointed out by @Tim C.

Use the following stylesheet:

<?xml version="1.0" encoding="utf-8"?>

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

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

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

   <xsl:variable name="roles">Finance Advisor</xsl:variable>

   <xsl:template match="Function[count(Roles/Role) = 1 and not(matches($roles,Roles/Role))]"/>

   <xsl:template match="Role[not(matches($roles,.))]"/>

</xsl:stylesheet>

Output

<?xml version="1.0" encoding="UTF-8"?>
<Menu>
   <Function>
      <Name>ViewHome</Name>
      <Roles>
         <Role>Finance</Role>
         <Role>Advisor</Role>
      </Roles>
   </Function>
   <Function>
      <Name>ViewStaff</Name>
      <ChildFunctions>
         <Function>
            <Name>StaffFunction1</Name>
            <Roles>
               <Role>Finance</Role>
            </Roles>
         </Function>
         <Function>
            <Name>StaffFunction3</Name>
            <Roles>
               <Role>Advisor</Role>
            </Roles>
         </Function>
      </ChildFunctions>
   </Function>
   <Function>
      <Name>ViewDiary</Name>
      <Roles>
         <Role>Advisor</Role>
      </Roles>
   </Function>
</Menu>

Upvotes: 0

Related Questions