XSLT 1.0 - How to exclude node trees that do not contain a specific child

279 views Asked by At

I've been struggling with this for the last 2 days. I'm nowhere as familiar with XPATH and XSLT as I need to be, but time has not been conducive to truly dig in and study. This is being used in BMC's TrueSight Orchestration application, and as such is limited to using XPATH/XSLT 1.0.

Some add-on questions that I wouldn't mind being redirected to resources for explanations:

  1. I'm not familiar with an "identity template" and I have seen it referred to in several posts.
  2. I'm not familiar with templates with regard to how to be more selective of the elements, nodes, and node sets for processing.
    • I'm a shell script programmer by experience, so a top-down approach is a bit foreign to me as it relates to how XML documents are processed.
  3. I'm still learning a lot of the terms, axes, relational paths vs. absolute paths, etc. when it comes to XPATH. I'm not afraid of "Googling" for answers, but sometimes having a go-to resource is better than trying to guess at what keywords might provide a result set that may lead to an answer.
    • Is there a more recommended go-to resource? (I know that this is a loaded and biased question, but I'm expecting that there will be a handful of resources that will be more recommended than others.)

I have the following input document:

<servers>
  <server os="WINDOWS" role="CENTRAL" account="*****" name="SERVP0001">
    <services>
      <service name="QlikLoggingService" start_type="AUTOMATIC" state="RUNNING">Qlik Logging Service</service>
      <service name="QlikSenseEngineService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Engine Service</service>
      <service name="QlikSensePrintingService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Printing Service</service>
      <service name="QlikSenseProxyService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Proxy Service</service>
      <service name="QlikSenseRepositoryDatabase" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Repository Database</service>
      <service name="QlikSenseRepositoryService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Repository Service</service>
      <service name="QlikSenseSchedulerService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Scheduler Service</service>
      <service name="QlikSenseServiceDispatcher" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Service Dispatcher</service>
    </services>
  </server>
  <server os="WINDOWS" role="SLAVE" account="*****" name="SERVP0002">
    <services>
      <service name="QlikLoggingService" start_type="AUTOMATIC" state="RUNNING">Qlik Logging Service</service>
      <service name="QlikSenseEngineService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Engine Service</service>
      <service name="QlikSensePrintingService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Printing Service</service>
      <service name="QlikSenseProxyService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Proxy Service</service>
      <service name="QlikSenseRepositoryService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Repository Service</service>
      <service name="QlikSenseSchedulerService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Scheduler Service</service>
      <service name="QlikSenseServiceDispatcher" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Service Dispatcher</service>
    </services>
  </server>
  <server os="WINDOWS" role="NPRINTING" account="*****" name="SERVN0001">
    <services>
      <service name="QlikNPrintingEngine" start_type="AUTOMATIC" state="RUNNING">Qlik NPrinting Engine</service>
      <service name="QlikNPrintingLicenseService" start_type="AUTOMATIC" state="RUNNING">Qlik NPrinting License Service</service>
      <service name="QlikNPrintingMessagingService" start_type="AUTOMATIC" state="RUNNING">QlikNPrintingMessagingService</service>
      <service name="QlikNPrintingRepoService" start_type="AUTOMATIC" state="RUNNING">QlikNPrintingRepoService</service>
      <service name="QlikNPrintingScheduler" start_type="AUTOMATIC" state="RUNNING">Qlik NPrinting Scheduler</service>
      <service name="QlikNPrintingWebEngine" start_type="AUTOMATIC" state="RUNNING">Qlik NPrinting Web Engine</service>
    </services>
  </server>
</servers>

I am trying to select from the root element down, including only those service nodes that match either the @name attribute or the text value.

This is the XSL that I've been able to cobble together through several hours of searching and trial and error. The only part I have not been able to figure out is how to prevent the printing of the server node and its descendants when no matching service is found.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output indent="no" />
  <xsl:template match="node() | @*">
    <xsl:copy>
      <xsl:apply-templates select="node() | @*" />
    </xsl:copy>
  </xsl:template>
  <xsl:template match="service">
    <xsl:choose>
      <xsl:when test="contains( translate( normalize-space( ./@name ), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ), translate( normalize-space( &quot;${SERVICENAME}&quot; ), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ) )">
        <xsl:copy-of select="." />
      </xsl:when>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

This is the current output I'm getting with my XSL:

<servers>
  <server os="WINDOWS" role="CENTRAL" account="*****" name="SERVP0001">
    <services>
      <service name="QlikLoggingService" start_type="AUTOMATIC" state="RUNNING">Qlik Logging Service</service>
    </services>
  </server>
  <server os="WINDOWS" role="SLAVE" account="*****" name="SERVP0002">
    <services>
      <service name="QlikLoggingService" start_type="AUTOMATIC" state="RUNNING">Qlik Logging Service</service>
    </services>
  </server>
  <server os="WINDOWS" role="NPRINTING" account="*****" name="SERVN0001">
    <services />
  </server>
</servers>

This is my desired output:

<servers>
  <server os="WINDOWS" role="CENTRAL" account="*****" name="SERVP0001">
    <services>
      <service name="QlikLoggingService" start_type="AUTOMATIC" state="RUNNING">Qlik Logging Service</service>
    </services>
  </server>
  <server os="WINDOWS" role="SLAVE" account="*****" name="SERVP0002">
    <services>
      <service name="QlikLoggingService" start_type="AUTOMATIC" state="RUNNING">Qlik Logging Service</service>
    </services>
  </server>
</servers>

In some cases, there may be more than one matching service for a server. For example, if I used "engine" as a keyword to match services, I would have 1 match in SERVP0001, 1 match in SERVP0002, and 2 matches in SERVN0001. My provided example output is using the keyword "logging" to match services.

I know that I do not have a template or Choose-When element that searches for and returns the services whose text values contain my search string. I figured that if I could get the attribute search to return the correctly formatted document, I could modify to suit for the service text.

Thank you in advance for whatever help and resources you can provide.

2

There are 2 answers

3
Dimitre Novatchev On BEST ANSWER

A simpler / shorter solution (no need to set something to 1 to remember there was a hit):

<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:param name="pKeyword" select="'logging'"/>
 <xsl:variable name="vKeywordUpper" select="translate($pKeyword, $vLower, $vUpper)"/>
 
 <xsl:variable name="vUpper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
 <xsl:variable name="vLower" select="'abcdefghijklmnopqrstuvwxyz'"/>

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>
  
  <xsl:template match="server | service">
    <xsl:if test="descendant-or-self::service
                   /@name[contains(translate(., $vLower, $vUpper), $vKeywordUpper)]">
      <xsl:element name="{name()}">
        <xsl:apply-templates select="@*|node()"/>
      </xsl:element>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided XML document (re-formatted to avoid the need for horizontal scrolling):

<servers>
  <server os="WINDOWS" role="CENTRAL" account="*****" name="SERVP0001">
    <services>
      <service name="QlikLoggingService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Logging Service</service>
      <service name="QlikSenseEngineService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Engine Service</service>
      <service name="QlikSensePrintingService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Printing Service</service>
      <service name="QlikSenseProxyService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Proxy Service</service>
      <service name="QlikSenseRepositoryDatabase" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Repository Database</service>
      <service name="QlikSenseRepositoryService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Repository Service</service>
      <service name="QlikSenseSchedulerService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Scheduler Service</service>
      <service name="QlikSenseServiceDispatcher" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Service Dispatcher</service>
    </services>
  </server>
  <server os="WINDOWS" role="SLAVE" account="*****" name="SERVP0002">
    <services>
      <service name="QlikLoggingService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Logging Service</service>
      <service name="QlikSenseEngineService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Engine Service</service>
      <service name="QlikSensePrintingService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Printing Service</service>
      <service name="QlikSenseProxyService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Proxy Service</service>
      <service name="QlikSenseRepositoryService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Repository Service</service>
      <service name="QlikSenseSchedulerService" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Scheduler Service</service>
      <service name="QlikSenseServiceDispatcher" start_type="AUTOMATIC"
      state="RUNNING">Qlik Sense Service Dispatcher</service>
    </services>
  </server>
  <server os="WINDOWS" role="NPRINTING" account="*****" name="SERVN0001">
    <services>
      <service name="QlikNPrintingEngine" start_type="AUTOMATIC"
      state="RUNNING">Qlik NPrinting Engine</service>
      <service name="QlikNPrintingLicenseService" start_type="AUTOMATIC"
      state="RUNNING">Qlik NPrinting License Service</service>
      <service name="QlikNPrintingMessagingService" start_type="AUTOMATIC"
      state="RUNNING">QlikNPrintingMessagingService</service>
      <service name="QlikNPrintingRepoService" start_type="AUTOMATIC"
      state="RUNNING">QlikNPrintingRepoService</service>
      <service name="QlikNPrintingScheduler" start_type="AUTOMATIC"
      state="RUNNING">Qlik NPrinting Scheduler</service>
      <service name="QlikNPrintingWebEngine" start_type="AUTOMATIC"
      state="RUNNING">Qlik NPrinting Web Engine</service>
    </services>
  </server>
</servers>

the wanted, correct result is produced:

<servers>
   <server os="WINDOWS" role="CENTRAL" account="*****" name="SERVP0001">
      <services>
         <service name="QlikLoggingService" start_type="AUTOMATIC" state="RUNNING">Qlik Logging Service</service>
      </services>
   </server>
   <server os="WINDOWS" role="SLAVE" account="*****" name="SERVP0002">
      <services>
         <service name="QlikLoggingService" start_type="AUTOMATIC" state="RUNNING">Qlik Logging Service</service>
      </services>
   </server>
</servers>

If we change <xsl:param name="pKeyword" select="'logging'"/> to <xsl:param name="pKeyword" select="'engine'"/> and run the transformation, again the expected correct result is produced:

<servers>
   <server os="WINDOWS" role="CENTRAL" account="*****" name="SERVP0001">
      <services>
         <service name="QlikSenseEngineService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Engine Service</service>
      </services>
   </server>
   <server os="WINDOWS" role="SLAVE" account="*****" name="SERVP0002">
      <services>
         <service name="QlikSenseEngineService" start_type="AUTOMATIC" state="RUNNING">Qlik Sense Engine Service</service>
      </services>
   </server>
   <server os="WINDOWS" role="NPRINTING" account="*****" name="SERVN0001">
      <services>
         <service name="QlikNPrintingEngine" start_type="AUTOMATIC" state="RUNNING">Qlik NPrinting Engine</service>
         <service name="QlikNPrintingWebEngine" start_type="AUTOMATIC" state="RUNNING">Qlik NPrinting Web Engine</service>
      </services>
   </server>
</servers>
10
zx485 On

Your approach was close. You successfully masked out the <service> elements you don't want, but failed to mask out the <server> element you don't want.

Just as a simple introduction: the identity template copies all nodes(elements, attributes, ...) from the input stream to the output stream - unless a more specific template rule is present in the XSLT.

In your XSLT you defined a more specific template rule for the element <service> - hence your success in that regard. But you did not define a more specific template rule for the <server> element, therefore all of these elements are simply copied by the identity template.

I do not know about the BMC's TrueSight Orchestration application, so I defined the matching strings as xsl:variables (You could also use xsl:params if necessary). Replace the strings in the single quotes with ${SERVICENAME} or whatever your program needs. The @* copies all attributes.

The following XSLT does mask out all <server> elements that do not contain the "LOGGING" text in a <service> child and also all <service> elements that do not contain the `LOGGING" text:

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

  <xsl:template match="server|service">
    <xsl:variable name="matching">
      <xsl:for-each select="descendant-or-self::service/@name">
        <xsl:if test="contains(translate(normalize-space(.), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'),'LOGGING')">1</xsl:if>
      </xsl:for-each>
    </xsl:variable>
    <xsl:if test="contains($matching,'1')">
      <xsl:copy>
        <xsl:apply-templates select="@* | node()" />
      </xsl:copy>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>

Its output is:

<?xml version="1.0"?>
<servers>
    <server os="WINDOWS" role="CENTRAL" account="*****" name="SERVP0001">
        <services>
            <service name="QlikLoggingService" start_type="AUTOMATIC" state="RUNNING">Qlik Logging Service</service>
        </services>
    </server>
    <server os="WINDOWS" role="SLAVE" account="*****" name="SERVP0002">
        <services>
            <service name="QlikLoggingService" start_type="AUTOMATIC" state="RUNNING">Qlik Logging Service</service>
        </services>
    </server>
</servers>

P.S.: I know that this suggestion will not be liked by everyone, but I recommend W3Schools as an introductory site to XML/XSLT/XPath.