Dmitry Deryabin
Dmitry Deryabin

Reputation: 1578

XSL iteratively update the value

I'm quite new to XSL, so I would appreciate any help. Here is a sample xml that I have:

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <data>
     <element>
        <e>BenefitUsed</e>
        <p>
           <BenefitType>Fuel</BenefitType>
           <QuantityTotal>3</QuantityTotal>
           <QuantityUsed>1</QuantityUsed>
           <PlayerLevel>5</PlayerLevel>
        </p>
        <t>1395922443994</t>
        <ts>336761474</ts>
     </element>
     <element>
        <e>StartMission</e>
        <p>
           <AvatarID>123</AvatarID>
           <MissionID>2785</MissionID>
           <MissionType>TaxiBadge</MissionType>
        </p>
        <t>1395922445517</t>
        <ts>336762997</ts>
     </element>
     <element>
        <e>EndMission</e>
        <p>
           <AvatarID>123</AvatarID>
           <BestAttempt>0</BestAttempt>
           <BoostsUsed>0</BoostsUsed>
           <PlayerLevel>6</PlayerLevel>
        </p>
        <t>1395922445576</t>
        <ts>336763056</ts>
     </element>
     <element>
        <e>BenefitUsed</e>
        <p>
           <BenefitType>Fuel</BenefitType>
           <QuantityTotal>2</QuantityTotal>
           <QuantityUsed>1</QuantityUsed>
           <PlayerLevel>7</PlayerLevel>
        </p>
        <t>1395922443994</t>
        <ts>336761474</ts>
     </element>
   </data>
</root>

I want to iterate through the elements and output it's name and the current player level (tag "p/PlayerLevel"). I have a default value for the player level (set to -1), which should change if the element contains tag "p/PlayerLevel", otherwise the player level remains unchanged).

So, here is my xsl file:

<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output method='text' omit-xml-declaration="yes"/>

<xsl:param name="level" select="-1"/>

<xsl:template name="mVarUpdate">
    <xsl:param name="level">
        <xsl:choose>
            <xsl:when test="p/PlayerLevel">
            <xsl:value-of select="p/PlayerLevel"/>
            </xsl:when>
        </xsl:choose>
    </xsl:param>
</xsl:template>

<xsl:template match="/">
    <xsl:for-each select="root/data/element">
        <xsl:call-template name="mEventHandler"/>           
     </xsl:for-each>
    </xsl:template>

 <xsl:template name="mEventHandler">
    <xsl:call-template name="mVarUpdate">
        <xsl:with-param name="level" />
    </xsl:call-template>
    <xsl:choose>
        <xsl:when test="e='BenefitUsed'">
            <xsl:call-template name="mBenefitUsed"/>
        </xsl:when>
        <xsl:when test="e='StartMission'">
            <xsl:call-template name="mStartMission"/>
        </xsl:when>
        <xsl:when test="e='EndMission'">
            <xsl:call-template name="mEndMission"/>
        </xsl:when>
    </xsl:choose>
</xsl:template>

<xsl:template name="mBenefitUsed">
    <xsl:text>This is BenefitUsed event</xsl:text>
    <xsl:text>,</xsl:text>
    <xsl:copy-of select="$level"/>
    <xsl:text>,</xsl:text>
    <xsl:text>Some other data related to event BenefitUsed</xsl:text>
    <xsl:text>&#10;</xsl:text>
</xsl:template>

<xsl:template name="mStartMission">
    <xsl:text>This is StartMission event</xsl:text>
    <xsl:text>,</xsl:text>
    <xsl:copy-of select="$level"/>
    <xsl:text>,</xsl:text>
    <xsl:text>Some other data related to event StartMission</xsl:text>
    <xsl:text>&#10;</xsl:text>
</xsl:template>

<xsl:template name="mEndMission">
    <xsl:text>This is EndMission event</xsl:text>
    <xsl:text>,</xsl:text>
    <xsl:copy-of select="$level"/>
    <xsl:text>,</xsl:text>
    <xsl:text>Some other data related to event EndMission</xsl:text>
    <xsl:text>&#10;</xsl:text>
</xsl:template> 
 </xsl:stylesheet>

It works ok and produces the following:

    This is BenefitUsed event,-1,Some other data related to event BenefitUsed
    This is StartMission event,-1,Some other data related to event StartMission
    This is EndMission event,-1,Some other data related to event EndMission
    This is BenefitUsed event,-1,Some other data related to event BenefitUsed

But ideally I would like:

    This is BenefitUsed event,5,Some other data related to event BenefitUsed
    This is StartMission event,5,Some other data related to event StartMission
    This is EndMission event,6,Some other data related to event EndMission
    This is BenefitUsed event,7,Some other data related to event BenefitUsed

So can I somehow iteratively change this value?

P.S. I understand that it would be easier to write XSL without many templates, but each element based on it's name will be in future processed differently. Thank you for any help

Upvotes: 1

Views: 69

Answers (2)

Michael Kay
Michael Kay

Reputation: 163352

The description you give of the problem leads naturally to a solution involving sibling recursion, where each item is processed taking account of information obtained while processing the previous item. The general structure is like this:

Your template rule for the data element processes its first child:

<xsl:template match="data">
  <xsl:apply-templates select="element[1]">
    <xsl:with-param name="playerLevel" select="0"/>
  </xsl:apply-templates>
</xsl:template>

Your template rule for the element element processes that element, and its first following sibling, passing parameters as necessary:

<xsl:template match="element">
  <xsl:param name="playerLevel" select="0"/>
  <!-- process this element -->
  ....
  <!-- process the next element -->
  <xsl:apply-templates select="following-sibling::element[1]">
   <xsl:with-param name="playerLevel" select="(p/playerLevel, $playerLevel)[1]"/>
  </xsl:apply-templates>
</xsl:template>

I've allowed myself an XSLT 2.0 construct here in the with-param - it passes p/playerLevel if it exists, otherwise $playerLevel. If you're stuck with XSLT 1.0 you'll have to expand this into a more verbose xsl:choose instruction.

This approach is very powerful and it's capable of tackling far more complex problems than the one you describe; but it's a technique well worth mastering. One caveat is that if you have more than 500 elements or so, it may crash on processors that don't implement recursion efficiently.

Upvotes: 1

Tim C
Tim C

Reputation: 70618

In XSLT, variables (and parameters) are immutable, and so cannot be changed once initially set. All your mVarUpdate template is doing is setting the value of a parameter locally within the scope of the template (It is not the same parameter as the one defined globally).

Now, if you want to find the "player level" for an element, you could it with a named element like so:

<xsl:template name="level">
   <xsl:choose>
      <xsl:when test="p/PlayerLevel"><xsl:value-of select="p/PlayerLevel" /></xsl:when>
      <xsl:otherwise><xsl:value-of select="preceding-sibling::*/p/PlayerLevel[1]" /></xsl:otherwise>
   </xsl:choose>
</xsl:template>

(i.e. Check if the current element has a level. If not, got the first preceding sibling that does)

Then, instead of doing <xsl:copy-of select="$level"/>, you can just do <xsl:call-template name="level"/>

Now, you are also right where you say "it would easier to write XSL without many templates". What you could be doing is having a single generic template to match any element element, but you could still have more specific templates matching other element elements with a specific name.

For example, try the following XSLT

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema">
   <xsl:output method="text" omit-xml-declaration="yes"/>

   <xsl:template name="level">
      <xsl:choose>
         <xsl:when test="p/PlayerLevel"><xsl:value-of select="p/PlayerLevel" /></xsl:when>
         <xsl:otherwise><xsl:value-of select="preceding-sibling::*/p/PlayerLevel[1]" /></xsl:otherwise>
      </xsl:choose>
   </xsl:template>

   <xsl:template match="/">
      <xsl:apply-templates select="root/data/element"/>
   </xsl:template>

   <xsl:template match="element">
      <xsl:text>This is </xsl:text><xsl:value-of select="e"/><xsl:text> event</xsl:text>
      <xsl:text>,</xsl:text>
      <xsl:call-template name="level"/>
      <xsl:text>,</xsl:text>
      <xsl:text>Some other data related to event </xsl:text><xsl:value-of select="e"/>
      <xsl:text>&#10;</xsl:text>
   </xsl:template>

   <xsl:template match="element[e='EndMission']">
      <xsl:text>This is the special EndMission event</xsl:text>
      <xsl:text>,</xsl:text>
      <xsl:call-template name="level"/>
      <xsl:text>,</xsl:text>
      <xsl:text>Some different data related to event EndMission</xsl:text>
      <xsl:text>&#10;</xsl:text>
   </xsl:template>
</xsl:stylesheet>

XSLT has the concept of template priority, and a template with a condition specified for an element has an higher priority than one that just matches the element name on its own.

Upvotes: 1

Related Questions