Search code examples
xmlxsltmsxml

How can I apply translate() to an attribute value in XSLT?


I have a function that returns XML data and want to write a test for it to check if it returns the expected result. The XML data looks like this:

<Session Id="OF1qxev4OSZcIbsS">
  <User Id="10001">
    <LoginName>SelfTest</LoginName>
    <RealName>SelfTest User</RealName>
  </User>
  <StartTime>2015-11-13T13:01:59Z</StartTime>
  <EndTime>2015-11-13T15:01:59Z</EndTime>
</Session>

The XML contains data that is different upon each request so before comparing against the expected result I use XSLT to replace the variable data (session ID attribute and content of StartTime/EndTime nodes) with some fixed pattern. The XSL file looks like this:

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

  <xsl:variable name="uid-chars">ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789</xsl:variable>
  <xsl:variable name="uid-mask" >IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII</xsl:variable>

  <xsl:variable name="timestamp-nums">0123456789</xsl:variable>
  <xsl:variable name="timestamp-mask">DDDDDDDDDD</xsl:variable>

  <xsl:template match="*">
    <xsl:copy>
      <!-- copy attributes -->
      <xsl:apply-templates select="@*"/>
      <!-- copy child elements -->
      <xsl:apply-templates select="*|text()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="text()">
    <!-- trim whitespace -->
    <xsl:value-of select="normalize-space(.)"/>
  </xsl:template>

  <!-- normalize uids -->
  <xsl:template match="@Id">
    <xsl:copy>
      <xsl:attribute name="Id">
        <xsl:value-of select="translate(., $uid-chars, $uid-mask)"/>
      </xsl:attribute>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="StartTime|EndTime">
    <xsl:copy>
      <xsl:apply-templates mode="timestamp"/>
    </xsl:copy>
  </xsl:template>

  <!-- normalize timestamps -->
  <xsl:template mode="timestamp" match="text()">
    <xsl:value-of select="translate(., $timestamp-nums, $timestamp-mask)"/>
  </xsl:template>

</xsl:stylesheet>

And the expected output would look like this:

<Session Id="IIIIIIIIIIIIIIII">
  <User Id="10001">
    <LoginName>SelfTest</LoginName>
    <RealName>SelfTest User</RealName>
  </User>
  <StartTime>DDDD-DD-DDTDD:DD:DDZ</StartTime>
  <EndTime>DDDD-DD-DDTDD:DD:DDZ</EndTime>
</Session>

Unfortunately with the current XSL above the normalization only works for StartTime/EndTime values but not for the session ID:

<Session Id="OF1qxev4OSZcIbsS">
  <User Id="10001">
    <LoginName>SelfTest</LoginName>
    <RealName>SelfTest User</RealName>
  </User>
  <StartTime>DDDD-DD-DDTDD:DD:DDZ</StartTime>
  <EndTime>DDDD-DD-DDTDD:DD:DDZ</EndTime>
</Session>

What is the correct way to apply the translate() function to attribute values?

Note: I used the XSLT engine from MSXML6 in my tests.


Solution

  • You shouldn't use xsl:copy in your template that matches the Id attribute, as that will just copy the attribute as-is. Your existing xsl:attribute is just being ignored because you can't add an attribute onto this copied attribute.

    Just remove the xsl:copy from the template, and just create a whole new attribute

    <xsl:template match="@Id">
       <xsl:attribute name="Id">
         <xsl:value-of select="translate(., $uid-chars, $uid-mask)"/>
       </xsl:attribute>
    </xsl:template>
    

    Note that this would update by the Id of the User element as well as the Session element. If you did want to restrict it to just the Session attribute, you could use these two templates instead:

    <xsl:template match="Session/@Id">
       <xsl:attribute name="Id">
          <xsl:value-of select="translate(., $uid-chars, $uid-mask)"/>
       </xsl:attribute>
    </xsl:template>
    
    <xsl:template match="@Id">
        <xsl:copy />
    </xsl:template>