views:

446

answers:

5

I want to tightly grasp the hair on the back of a Microsoft employee's head, using it as leverage to pound his head forcefully and repeatedly against a hard surface! That would make me feel nearly as good as solving this problem right now.

I've got a simple XML message that looks like this:

<?xml version="1.0" encoding="utf-8"?>
<message>
    <cmd id="instrument_status">
        <status_id>1</status_id>
    </cmd>
</message>

A web service on the device I'm working with returns several such messages and I'm converting them to a different format. For the above message the new format would look like the following:

<?xml version="1.0" encoding="UTF-8"?>
<grf:message xmlns:grf="http://www.company.com/schemas/device/version001"&gt;
    <grf:messageHeader>
        <grf:messageType>instrumentStatus</grf:messageType>
    </grf:messageHeader>
    <grf:messageBody>
        <grf:instrumentStatusBody>
            <grf:statusId>Running</grf:statusId>
        </grf:instrumentStatusBody>
    </grf:messageBody>
</grf:message>

There is a mapping for status_id integer values in the XML as follows:

status-id    Meaning  
=========    =======
0            Ready  
1            Running  
2            NotReady  
3            PoweringUp  
4            PoweringDown  
5            PoweredUp  
6            PoweredDown  
7            Tuning  
8            Error  

My XSLT is working correctly and giving me the correct output when I use Altova XMLSpy, but when I run my .NET application, I'm getting a failure at the point where the mapping for the status_id integer is converted to one of the allowable enumerated strings. Instead of getting the enumerated value, the MS XSLT processor returns an empty string and I get an empty <status_id/> element in the output XML.

The following is my XSLT code with some sections removed to reduce the amount of space:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:fo="http://www.w3.org/1999/XSL/Format"
        xmlns:xs="http://www.w3.org/2001/XMLSchema"
        xmlns:fn="http://www.w3.org/2005/xpath-functions"
        xmlns:msxsl="urn:schemas-microsoft-com:xslt"
        xmlns:grf="http://www.company.com/schemas/device/version001"
        exclude-result-prefixes="#default">

 <xsl:template match="/">
  <xsl:apply-templates select="message"/>
 </xsl:template>

 <xsl:template match="message">
  <xsl:element name="grf:message">
   <xsl:apply-templates select="/message/cmd/@id"/>
  </xsl:element>
 </xsl:template>

 <xsl:template match="/message/cmd/@id">
  <xsl:variable name="_commandType" select="/message/cmd/@id"/>
        <!-- Following line works in Altova XMLSpy, but fails in .NET app. ??? -->
  <xsl:variable name="_statusIdValue" select="/message/cmd/status_id"/>
  <xsl:element name="grf:messageHeader">
   <xsl:element name="grf:messageType">
    <xsl:choose>
     <xsl:when test="$_commandType = 'api_info'">
      <xsl:text>apiInfo</xsl:text>
     </xsl:when>
     <xsl:when test="$_commandType = 'instrument_status'">
      <xsl:text>instrumentStatus</xsl:text>
     </xsl:when>
    </xsl:choose>
   </xsl:element>
  </xsl:element>
  <xsl:element name="grf:messageBody">
   <xsl:choose>
    <xsl:when test="$_commandType = 'api_info'">
     <xsl:element name="grf:apiInfoBody">
      <xsl:element name="grf:apiVersion">
       <xsl:value-of select="/message/cmd/api-version"/>
      </xsl:element>
      <xsl:element name="grf:apiBuild">
       <xsl:value-of select="/message/cmd/api-build"/>
      </xsl:element>
     </xsl:element>
    </xsl:when>
    <xsl:when test="$_commandType = 'instrument_status'">
     <xsl:element name="grf:instrumentStatusBody">
      <xsl:element name="grf:statusId">
       <xsl:choose>
        <xsl:when test="$_statusIdValue = '0'">
         <xsl:text>Ready</xsl:text>
        </xsl:when>
        <xsl:when test="$_statusIdValue = '1'">
         <xsl:text>Running</xsl:text>
        </xsl:when>
        <xsl:when test="$_statusIdValue = '2'">
         <xsl:text>NotReady</xsl:text>
        </xsl:when>
        <xsl:when test="$_statusIdValue = '3'">
         <xsl:text>PoweringUp</xsl:text>
        </xsl:when>
        <xsl:when test="$_statusIdValue = '4'">
         <xsl:text>PoweringDown</xsl:text>
        </xsl:when>
        <xsl:when test="$_statusIdValue = '5'">
         <xsl:text>PoweredUp</xsl:text>
        </xsl:when>
        <xsl:when test="$_statusIdValue = '6'">
         <xsl:text>PoweredDown</xsl:text>
        </xsl:when>
        <xsl:when test="$_statusIdValue = '7'">
         <xsl:text>Tuning</xsl:text>
        </xsl:when>
        <xsl:when test="$_statusIdValue = '8'">
         <xsl:text>Error</xsl:text>
        </xsl:when>
       </xsl:choose>
      </xsl:element>
     </xsl:element>
    </xsl:when>
   </xsl:choose>
  </xsl:element>
 </xsl:template>
</xsl:stylesheet>

Is there XSLT 1.0 code that will behave the same in both Altova XMLSpy and the MS XSLT processor?

Thanks,

AlarmTripper

A: 

It's been a long time since I've coded up any xslt's but based on what I'm seeing you might be able to change this line:

<xsl:variable name="_statusIdValue" select="/message/cmd/status_id"/>

to

<xsl:variable name="_statusIdValue" select="/message/cmd/status_id/."/>

That should tell it to select the content of the element vs the node itself.

Kind of like when you do a value-of operation and you want the node's text content. you would do the same thing.

For example if you wanted to spit back out the status id number you could use the following:

<xsl:value-of select="/message/cmd/status_id/."/>
Joshua Cauble
Same result as I was getting previously. :(
AlarmTripper
What version of .net are you using? I just pulled down your xml and your xsl and ran it in my xsl test tool and it converted just fine. It converted your sample xml from 1 to Running I had no errors and changed nothing. The test app I have is written in .net 2.0 version 2003
Joshua Cauble
A: 

This is such terrible coding I'm actually glad it doesn't work in .NET, I suggest you rewrite your stylesheet.

Try this:

<xsl:stylesheet version="1.0"
   xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
   xmlns:grf="http://www.company.com/schemas/device/version001"&gt;

<xsl:template match="message">
   <xsl:variable name="commmand-type" select="cmd/@id"/>
   <xsl:variable name="status-id" select="cmd/status_id/text()"/>

      <grf:message>
         <grf:messageHeader>
            <grf:messageType>
               <xsl:choose> 
                  <xsl:when test="$commmand-type = 'api_info'">apiInfo</xsl:when> 
                  <xsl:when test="$commmand-type = 'instrument_status'">instrumentStatus</xsl:when> 
               </xsl:choose> 
            </grf:messageType>
         </grf:messageHeader>

         <grf:messageBody>
            <xsl:choose> 
               <xsl:when test="$commmand-type = 'api_info'"> 
                  <grf:apiInfoBody>
                     <grf:apiVersion>
                        <xsl:value-of select="cmd/api-version"/>
                     </grf:apiVersion>
                  </grf:apiInfoBody>
                  <grf:apiBuild> 
                     <xsl:value-of select="cmd/api-build"/> 
                  </grf:apiBuild> 
               </xsl:when> 
               <xsl:when test="$commmand-type = 'instrument_status'"> 
                  <grf:instrumentStatusBody>
                     <grf:statusId>
                        <xsl:choose> 
                           <xsl:when test="$status-id = '0'">Ready</xsl:when> 
                           <xsl:when test="$status-id = '1'">Running</xsl:when> 
                           <xsl:when test="$status-id = '2'">NotReady</xsl:when> 
                           <xsl:when test="$status-id = '3'">PoweringUp</xsl:when> 
                           <xsl:when test="$status-id = '4'">PoweringDown</xsl:when> 
                           <xsl:when test="$status-id = '5'">PoweredUp</xsl:when> 
                           <xsl:when test="$status-id = '6'">PoweredDown</xsl:when> 
                           <xsl:when test="$status-id = '7'">Tuning</xsl:when> 
                           <xsl:when test="$status-id = '8'">Error</xsl:when> 
                        </xsl:choose>
                     </grf:statusId>
                  </grf:instrumentStatusBody>
               </xsl:when> 
            </xsl:choose>
        </grf:messageBody>
    </grf:message>
</xsl:template>

</xsl:stylesheet>

I'm using only one <xsl:template>, you can refactor if you feel it's appropiate.

Max Toro
Are you a Microsoft employee?I'm new to the XSLT stuff so. If you have some concrete suggestions about what could be done to improve my code, I'm certainly open to considering your suggestions, but a broad spectrum negative comment isn't very helpful.
AlarmTripper
In all fairness you are the one talking about pounding people's heads.
Max Toro
You're right, I was. Part of my issue was general frustration, and the other part is that Microsoft appears to have no intention of supporting XSLT 2.0 - which I did not say. I've found myself rewriting numerous things to be 1.0 compliant and have wondered whether or not the current issue relates to 1.0 vs. 2.0 compliance. Having said all that, what are your suggestions for improving my XSLT example. I'm all for learning how to make it better. Thanks.
AlarmTripper
A: 

OK, I found out that if I use the following line to assign the _statusIdValue variable, then the code will function correctly with the XslCompiledTransform class:

<xsl:variable name="_statusIdValue" select="msxsl:node-set(/message/cmd/*)/text()"/>

This replaces the original line which was:

<xsl:variable name="_statusIdValue" select="/message/cmd/status_id"/>

However, the assignment that works in for the XslCompiledTransform class doesn't work with Altova XMLSpy. Is there a variant of the assignment that will work correctly in both the Altova XMLSpy editor and with the XslCompiledTransform class?

Thanks,

AlarmTripper

AlarmTripper
Rather than rely on `msxsl:node-set()` (non-standard) have you tried just using `/message/cmd/status_id/text()` ?
Mads Hansen
Yes, tried that. It works in everything except the XslCompiledTransform class. There it simply comes back empty.
AlarmTripper
+1  A: 

One thing to note is that in the template that matches the "message" element, you do this

<xsl:apply-templates select="/message/cmd/@id"/> 

This will actually try to match the very first message in the XML relative to the document root, regardless of what message you are currently on. It is not selecting relative to the current node. In your case, it looks like there will only ever be one message, so it won't be an issue here, but it would be in other cases.

It is also probably more common to match on elements, rather than attributes, especially where you want to process child elements of an element. So, you would probably replace the above line with this instead

<xsl:apply-templates select="cmd"/> 

Then, for the template that matches it, instead of doing this currently

<xsl:template match="/message/cmd/@id"> 

You would do this instead

<xsl:template match="cmd"> 

Next, within this template, you could try replacing your variables with simpler select statements

<xsl:variable name="_commandType" select="@id"/> 
<xsl:variable name="_statusIdValue" select="status_id"/> 

See if that makes a difference.

Tim C
Thanks Tim. I see what you're saying.I'm working with a device that is sending it's data back as XML messages and I'm not at liberty to alter the way the device creates the XML responses. I do wish the device manufacturer had used elements instead of attributes. Unfortunately, the design of the returned XML doesn't have any structure besides the id attribute that distinguishes radically different types of messages. In my example, I only showed two of the simplest messages that the device sends back.
AlarmTripper
+1  A: 

You are way over-complicating your transformation. Try this considerably simpler stylesheet:

<xsl:stylesheet 
  version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns="http://www.company.com/schemas/device/version001"
>
  <xsl:output indent="yes" encoding="utf-8" />

  <!-- main template / entry point -->
  <xsl:template match="message">
    <message>
      <messageHeader>
        <xsl:apply-templates select="cmd" mode="head" />
      </messageHeader>
      <messageBody>
        <xsl:apply-templates select="cmd" mode="body" />
      </messageBody>
    </message>
  </xsl:template>

  <!-- header templates -->
  <xsl:template match="cmd[@id = 'api_info']" mode="head">
    <messageType>apiInfo</messageType>
  </xsl:template>

  <xsl:template match="cmd[@id = 'instrument_status']" mode="head">
    <messageType>instrumentStatus</messageType>
  </xsl:template>

  <!-- body templates -->
  <xsl:template match="cmd[@id = 'api_info']" mode="body">
    <apiInfoBody>
      <apiVersion><xsl:value-of select="api-version" /></apiVersion>
      <apiBuild><xsl:value-of select="api-build" /></apiBuild>
    </apiInfoBody>
  </xsl:template>

  <xsl:template match="cmd[@id = 'instrument_status']" mode="body">
    <instrumentStatusBody>
      <statusId>
        <xsl:choose>
          <xsl:when test="status_id = 0">Ready</xsl:when>
          <xsl:when test="status_id = 1">Running</xsl:when>
          <xsl:when test="status_id = 2">NotReady</xsl:when>
          <xsl:when test="status_id = 3">PoweringUp</xsl:when>
          <xsl:when test="status_id = 4">PoweringDown</xsl:when>
          <xsl:when test="status_id = 5">PoweredUp</xsl:when>
          <xsl:when test="status_id = 6">PoweredDown</xsl:when>
          <xsl:when test="status_id = 7">Tuning</xsl:when>
          <xsl:when test="status_id = 8">Error</xsl:when>
          <!-- just in case… -->
          <xsl:otherwise>
            <xsl:text>Unknown status_id: </xsl:text>
            <xsl:value-of select="status_id" />
          </xsl:otherwise>
        </xsl:choose>
      </statusId>
    </instrumentStatusBody>
  </xsl:template>

</xsl:stylesheet>

I got rid of all your seemingly superfluous namespace definitions (add them back as you need them) and put your stylesheet into a default namespace. This means you don't need a 'grf:' prefix on every element anymore, without changing the actual result:

<?xml version="1.0" encoding="utf-8"?>
<message xmlns="http://www.company.com/schemas/device/version001"&gt;
  <messageHeader>
    <messageType>instrumentStatus</messageType>
  </messageHeader>
  <messageBody>
    <instrumentStatusBody>
      <statusId>Running</statusId>
    </instrumentStatusBody>
  </messageBody>
</message>

Note how I use different match expressions and different template modes to output the appropriate elements in the right situations. This way any <xsl:variable> or <xsl:choose> become unnecessary, making for a cleaner and more maintainable stylesheet.

Also, usually there is no need to define <xsl:element> explicitly, unless you want to output elements with dynamic names. In all other cases, you can write the element straight-away.

I'm sorry that I can't say for sure why your stylesheet does not run as intended. It works for me, and it looks okay(ish).

Do not hesitate to ask if any of the above is unclear.

Tomalak
Thanks Tomalak! This does look much simpler! I'll see if I can convert my larger XSLT stylesheet to look more like the version you've suggested. This is my first go at XSLT and so I don't have a lot of experience and design patterns to fall back on like some of you more experienced folks. When you're one design pattern is a hammer, you of course view every problem as a nail. Now maybe you've given me a pair of pliers to use as well.Thanks much,AlarmTripper
AlarmTripper
Glad to help. :-)
Tomalak
Tomalak, I thought the code you submitted resolved the problem I was having. I converted my entire stylesheet to use the template approach you showed me and it is much easier to follow and understand, but I still have the problem with the XslCompiledTransform class not accepting any of the test statements. The only way I can get the XslCompiledTransform function to work is to change the test="status_id = x" statements to "msxsl:node-set(*)/text() = x" statements. And that doesn't parse with the XMLSpy product. But I'm accepting your answer because it helped clean up my code so much.
AlarmTripper