xsl-list
[Top] [All Lists]

Re: Retaining value of a Global variable

2004-07-17 05:18:21
Hi Vivek,

Vivek Shinde wrote:
Hi Folks,
Thanks a lot for all your responses. Michael Kay has provided an alternative of 
using position() function. What I am trying to do is display the category 
heading only once if the article of that category is found.
Thanks again.
Vivek

It's always interesting for me to see what questions stem mass response from the big guns on the list. As sure as Christmas will come in December this is one of the questions that will always generate a response from the likes of Michael Kay, David Carlisle, Wendell Piez, etc... etc... etc... It seems Santa can begin his travel preperations as, once again, the "global variable" question has proven itself worthy of it's own spot on the master list of Cosmic Constant's (right alongside the speed of light, Pi, and, oh yes, Christmas in December ;)

What you are attempting is an interesting problem to solve with XSLT 1.0 which didnt include a lot of built-in string processing capabilities. As such parsing strings and "flagging" the current instance of an element in a way to tell the processor to handle this particular node different than the nodes following it is not as straight forward as I know a lot of people would like it to be. There is a way to solve your problem however... in fact there are two that I can think of off the top of my head. The first is the least expensive of the two but utilizes the node-set() functionality that, although almost every processor inmplements some sort of support for, is not part of the 1.0 spec (a point that has not gone overlooked since the release of the spec). The second is supported in the 1.0 spec but is REALLY expensive and should be avoided at all costs for any implementation other than very small XML source documents.

Solution 1 - By creating a temporary tree (method show below) you can create an exact copy of the original elements of interest with one difference - adding a new attribute that "flags" the elements that match the string test in a way that will allow a simple check of an attribute during a second run through the now copied elements to determine if the flag exists and if so if its the first instance in the tree in which it does exist. Take a look at this code and it should be pretty easy to see what I am referring to. (if it looks all mangled in your email client you may want to view it with "wrap" turned off or set to a higher number)

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"; xmlns:exslt="http://exslt.org/common"; version="1.0" exclude-result-prefixes="exslt"> <xsl:template match="/newsletter/content"><!-- Selects all "content" elements. By default all of the "content" elements children will be selected as well --> <xsl:variable name="temp"><!-- creates a variable that will contain a copy of the original "article" elements plus a new attribute that we can later test for existence and determine if its the first of its kind --> <xsl:apply-templates select="article" mode="temp"/><!-- selects all "article" elements and then begins a recursive parse through each element and finds the template that best matches the criteria set forth - in this case we all "article" elements will match the template set to mode="temp". See this template below for more information as to what happens within the template to determine what gets copied to the Result Tree Fragement we are creating -->
    </xsl:variable>
<xsl:apply-templates select="exslt:node-set($temp)" mode="writeContents"/><!-- selects the $temp tree we have created, converts it to a node-set using the "exslt:node-set()" function, and then takes each immediate child element and searches for the best matching template for further processing. Notice: You need to remember to make a reference to the exslt namespace in the stylesheet element. See above for example. To be able to use this namespace you need a processor that supports it... Saxon was used in this instance but there are others that support exslt by default as well as extensions that can be refered to and used. See "http://www.exslt.org"; for more information or "http://www.Saxonica.com"; for information about Saxon's implementation -->
  </xsl:template>
<xsl:template match="article" mode="temp"><!-- all "article" elements from the temporary tree creation will match this template -->
    <xsl:copy><!-- copies just the "article" element to the output tree -->
      <xsl:if test="contains(metadata/article-classification, 'Biz-Webcast')">
        <xsl:attribute name="passStringTest">true</xsl:attribute>
</xsl:if><!-- tests for the 'Biz-Webcast' string contained in the "metadata/article-classification" element and if it is found creates a new attribute for the current element in context (in this case "article") called "passStringTest" and sets it value to "true". We could have used any value here as later on all we are going to test for is the existince of the "passStringTest" attribute as this is enough information to determine if this element originally passed the string test --> <xsl:copy-of select="* | @*"/><!-- copies all children and attributes that are a part of the current element back into the current element. There are no attributes in the sample XML I created but I dont know if that is true so I showed the method to copy the attributes just in case in your source XML they do exist -->
      </xsl:copy>
  </xsl:template>
<xsl:template match="article[(_at_)passStringTest][1]" mode="writeContents"><!-- This template will be used if and only if an attribute called "passStringTest" exists AND this element is in the first position of all elements that pass the attribute test --> <hr/><!-- simple hard rule so we can see where each output stopes and starts --> <p><xsl:value-of select="'This is the first instance of an element that passed the string test'"/></p><!-- this can of course be anything you want --> <p><xsl:value-of select="metadata/article-classification"/></p><!-- As can this --> <hr/><!-- simple hard rule so we can see where each output stopes and starts -->
  </xsl:template>
<xsl:template match="article" mode="writeContents"><!-- all templates that havent matched to an previous templates with attributes match="article" and mode="writeContents" will match to this template to be further processed --> <hr/><!-- simple hard rule so we can see where each output stopes and starts --> <p><xsl:value-of select="metadata/article-classification"/></p><!-- again, this could be anything you want it to be --> <hr/><!-- simple hard rule so we can see where each output stopes and starts -->
  </xsl:template>
</xsl:stylesheet>

Heres the sample XML I used:

<?xml version="1.0" encoding="UTF-8"?>
<newsletter>
  <content>
    <article>
      <metadata>
        <article-classification>This is some test text</article-classification>
      </metadata>
    </article>
    <article>
      <metadata>
        <article-classification>This is some test text</article-classification>
      </metadata>
    </article>
    <article>
      <metadata>
<article-classification>This is some test text with the string Biz-Webcast in it</article-classification>
      </metadata>
    </article>
    <article>
      <metadata>
        <article-classification>This is some test text</article-classification>
      </metadata>
    </article>
    <article>
      <metadata>
<article-classification>This is some test text with the string Biz-Webcast in it</article-classification>
      </metadata>
    </article>
    <article>
      <metadata>
<article-classification>This is some test text with the string Biz-Webcast in it</article-classification>
      </metadata>
    </article>
  </content>
</newsletter>

And then the output of the transformation:

<hr>
<p>This is some test text</p>
<hr>
<hr>
<p>This is some test text</p>
<hr>
<hr>
<p>This is the first instance of an element that passed the string test</p>
<p>This is some test text with the string Biz-Webcast in it</p>
<hr>
<hr>
<p>This is some test text</p>

<hr>
<hr>
<p>This is some test text with the string Biz-Webcast in it</p>
<hr>
<hr>
<p>This is some test text with the string Biz-Webcast in it</p>
<hr>

Solution 2 - The second solution requires utilization of the preceding-sibling axis in XPath. The problem with using preceding-sibling in any sort of test is as the processor continues through the tree another preceding-sibling is added to the list of preceding-siblings. This test will evaluate each and every preceding-sibling so it doesnt take long for performance to become a BIG issue. Depending on the amount of text contained in each article-classification element you could copy all of the preceding-siblings into one variable and test that variable for existence of the test string. But if the text in this element is fairly long and the XML tree big then you can imagine the size of the string thats created by the time its get to the end once again bringing the processor to its knees. Still, of the two I would probably take the variable string concat test as, overall, there's a better chance of your performance being somewhat manageable when doing one boolean test instead of 100. Here's what the code would look like.

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"; version="1.0">
  <xsl:template match="/newsletter/content/article">
    <hr/>
    <xsl:if test="contains(metadata/article-classification, 'Biz-Webcast')">
      <xsl:variable name="checkString">
<xsl:for-each select="preceding-sibling::article/metadata/article-classification">
          <xsl:value-of select="."/>
        </xsl:for-each>
      </xsl:variable>
      <xsl:if test="not(contains($checkString, 'Biz-Webcast'))">
This is the first instance of the element article/metadata/article-classification that contains the string 'Biz-Webcast'.
      </xsl:if>
    </xsl:if>
    <xsl:value-of select="metadata/article-classification"/>
    <hr/>
  </xsl:template>
</xsl:stylesheet>

Which outputs:

<hr>This is some test text
<hr>
<hr>This is some test text
<hr>
<hr>
This is the first instance of the element article/metadata/article-classification that contains the string 'Biz-Webcast'.
This is some test text with the string Biz-Webcast in it
<hr>
<hr>This is some test text
<hr>
<hr>This is some test text with the string Biz-Webcast in it
<hr>
<hr>This is some test text with the string Biz-Webcast in it
<hr>

When using the same source XML.

The # of lines of code using this solution is a lot less but I'll leave it to you to decide if lines of code is more important than overall performance or if using strict XSLT 1.0 without use of the node-set() function is something that is important.


Best of luck to you!

<M:D/>

 :: The Saxon.NET early beta has proven to be a great success! ::
 :: Stay tuned for the announcment of the general public beta release soon ::
 :: Visit http://www.x2x2x.org/x2x2x/home for more information ::
 :: RSS & ATOM feeds -> http://www.x2x2x.org/x2x2x/home/xml_feeds/ ::