tags:

views:

318

answers:

3

I'm new to XML and XSLT and have spent a bit of time on what should be a pretty simple search-and-replace case. I just can't seem to get the syntax correct.

The overall goal of this exercise is to replace the values of 'Y' and 'N' in element 'NewCustomer' with 'true' or 'false' respectively.

Here's my sample data.

<?xml version="1.0"?>
<CustomerList>
  <Customer>
    <CustomerID>1111</CustomerID>
    <CompanyName>Sean Chai</CompanyName>
    <City>New York</City>
    <NewCustomer>N</NewCustomer>
    </Customer>
  <Customer>
    <CustomerID>1112</CustomerID>
    <CompanyName>Tom Johnston</CompanyName>
    <City>Los Angeles</City>
    <NewCustomer>N</NewCustomer>
  </Customer>
  <Customer>
    <CustomerID>1113</CustomerID>
    <CompanyName>Institute of Art</CompanyName>
    <City>Chicago</City>
    <NewCustomer>Y</NewCustomer>
  </Customer>
</CustomerList>

Here's the transformation stylesheet.

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

  <!-- Identity Template (applies to all nodes and will copy all nodes -->
  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="Customer">
    <xsl:choose>
      <xsl:when test="NewCustomer = 'Y'">
        <xsl:text>true</xsl:text>
      </xsl:when>
      <xsl:when test="NewCustomer = 'N'">
        <xsl:text>false</xsl:text>
      </xsl:when>
    </xsl:choose>
  </xsl:template>

</xsl:stylesheet>

Here's my output.

  <?xml version="1.0" encoding="utf-8" ?> 
  <CustomerList>false false true</CustomerList>

Here's what I want it to output.

<?xml version="1.0"?>
<CustomerList>
  <Customer>
    <CustomerID>1111</CustomerID>
    <CompanyName>Sean Chai</CompanyName>
    <City>New York</City>
    <NewCustomer>false</NewCustomer>
  </Customer>
  <Customer>
    <CustomerID>1112</CustomerID>
    <CompanyName>Tom Johnston</CompanyName>
    <City>Los Angeles</City>
    <NewCustomer>false</NewCustomer>
  </Customer>
  <Customer>
    <CustomerID>1113</CustomerID>
    <CompanyName>Institute of Art</CompanyName>
    <City>Chicago</City>
    <NewCustomer>true</NewCustomer>
  </Customer>
</CustomerList>

What am I missing and why? I see that if I leave out the clauses where I examine NewCustomer, the entire output gets output. However, choosing to output the properly changed values for NewCustomer results in only them displayed. Is there a reference to the previous template that I have to make in the second template?

+2  A: 

By having your template match Customer, you are intercepting all processing for that tag.

Try changing the template to match NewCustomer only, making the corresponding changes in the test conditions (test=". = 'Y'").

Also note that you'll have to create the NewCustomer tag in the output since by matching it in a custom template it doesn't get handled by the identity transform. You're very close.

Here's the updated template:

<xsl:template match="Customer/NewCustomer">
    <xsl:copy>
        <xsl:choose>
            <xsl:when test=". = 'Y'">
                <xsl:text>true</xsl:text>
            </xsl:when>
            <xsl:when test=". = 'N'">
                <xsl:text>false</xsl:text>
            </xsl:when>
        </xsl:choose>
    </xsl:copy>
</xsl:template>

First, it matches on NewCustomer as a child of Customer. Then, it uses xsl:copy to make a copy of the node (but not attributes or children). Then it uses your xsl:choose to convert the N and Y values to false and true respectively.

The key concept is that when a template matches an input element, the input element is effectively removed from the output, and replaced with the content of the matching template. In your case when you matched on Customer, the Customer tag and everything inside it was replaced with the content of the template, which just generated 'true' or 'false'.

Jim Garrison
Nice explanation, +1.
Tomalak
A: 

The nice explanation in Jim Garrison's answer still applies. Here is a condensed/alternative approach:

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:choose>
        <xsl:when test="self::NewCustomer">
          <xsl:value-of select="boolean(number(translate(., 'YN', '10')))" />
        </xsl:when>
        <xsl:otherwise>
          <xsl:apply-templates select="@* | node()"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

The expression boolean(number(translate(., 'YN', '10'))) changes 'Y' to '1' and 'N' to '0', which then is converted first to a number, then to a boolean. The boolean will be represented as 'true' or 'false', respectively.

This is a bit dirty, since it does not really handle values other than 'Y' or 'N' - but it will output 'false' for any value other than 'Y'.

It is a mere space-saver. If you want, you can replace it with an <xsl:choose> like Jim Garrison used.

Tomalak
+2  A: 

Jim Garrison's will strip the attributes off of any NewCustomer element that has them. And Tomalak's is, as he put it, a bit dirty.

This version is an almost literal translation of your requirement into XSLT:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

  <xsl:template match="node() | @*">
    <xsl:copy>
      <xsl:apply-templates select="node() | @*"/>
    </xsl:copy>
 </xsl:template>

  <xsl:template match="NewCustomer/text()[.='Y']">
   <xsl:text>true</xsl:text>
  </xsl:template>

 <xsl:template match="NewCustomer/text()[.='N']">
    <xsl:text>false</xsl:text>
  </xsl:template>

</xsl:stylesheet>

The only nodes in the source tree that it doesn't copy exactly into the result set are text nodes that are children of NewCustomer elements and whose value is either Y or N; for those, it emits true and false respectively instead.

Robert Rossney
+1 There are indeed many ways to accomplish this task. I "assumed" the schema from the given data and didn't bother to copy attributes.
Jim Garrison