views:

281

answers:

1

I'm trying to transform certain parts of an XML file to another XML file. The source file:

<CUSTOMERS>
  <CUSTOMER>
    <CUSTOMER_NUMBER>12345678</CUSTOMER_NUMBER>
    <CUSTOMER_ADDRESS>
      <CUSTOMER_ADDRESS_NAME>John Doe</CUSTOMER_ADDRESS_NAME>
      <CUSTOMER_ADDRESS_STREET>Street 1</CUSTOMER_ADDRESS_STREET>
      <CUSTOMER_ADDRESS_CITY>Amsterdam</CUSTOMER_ADDRESS_CITY>
    </CUSTOMER_ADDRESS>
    <CUSTOM_FIELDS>
      <CUSTOM_FIELD>
        <CUSTOM_FIELD_NAME>Cellphone</CUSTOM_FIELD_NAME>
        <CUSTOM_FIELD_VALUE>443209432</CUSTOM_FIELD_VALUE>
      </CUSTOM_FIELD>
      <CUSTOM_FIELD>
        <CUSTOM_FIELD_NAME>Geo</CUSTOM_FIELD_NAME>
        <CUSTOM_FIELD_VALUE>323932121,31231233,0</CUSTOM_FIELD_VALUE>
      </CUSTOM_FIELD>
    </CUSTOM_FIELDS>
  </CUSTOMER>
</CUSTOMERS>

The rules:

  • Not every CUSTOMER has CUSTOM_FIELDS, those who don't have CUSTOM_FIELDS should not be processed.
  • When a CUSTOMER has CUSTOM_FIELDS, the number and order of all the CUSTOM_FIELDS may vary.
  • A CUSTOMER should only be processed if it has a CUSTOM_FIELD which name is 'Geo'

So in my XSL I've attempted to loop through all CUSTOMERS, and for each CUSTOMER loop through the CUSTOM_FIELDS. When it finds one called 'Geo' it should output data.

<xsl:for-each select="CUSTOMERS/CUSTOMER">
  <xsl:for-each select="CUSTOM_FIELDS/CUSTOM_FIELD">
    <xsl:if test=".[CUSTOM_FIELD_NAME='Geo']">
      <Placemark>
        <name>
          <xsl:value-of select="CUSTOMER_NUMBER" />
          <xsl:text> </xsl:text>
          <xsl:value-of select="CUSTOMER_ADDRESS/CUSTOMER_ADDRESS_NAME" />
        </name>
        <styleUrl>#msn_ylw-pushpin</styleUrl>
        <Point>
          <coordinates>
            <xsl:value-of select="CUSTOM_FIELD_NAME" />
          </coordinates>
        </Point>
      </Placemark>
    </xsl:if>
  </xsl:for-each>
</xsl:for-each>

But of course at this point I am in CUSTOMERS/CUSTOMER/CUSTOM_FIELDS/CUSTOM_FIELD so it's not possible to output data from the nodes that are a few levels up.

I've tried setting a variable in the if-block, which should then read after the if-block to see if any data should be returned but I understand variables can only be set once so they can't be used for this goal either.

So my questions are these:

  • Do I really need a loop to see if there is a CUSTOM_FIELD who's CUSTOM_FIELD_NAME = 'Geo'?
  • If the loop is needed, how can I use a result from inside the loop, outside the loop?
  • Or if that is not possible, how can I return values from nodes that are a few levels above the node I'm currently in?
+2  A: 

Your problem is easily solved with XPath. Consider this solution:

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

  <xsl:template match="CUSTOMERS">
    <Placemarks>
      <!-- apply templates to every customer -->
      <xsl:apply-templates select="CUSTOMER" />
    </Placemarks>
  </xsl:template>

  <xsl:template match="CUSTOMER">  
    <!-- process only customers that have a "Geo" custom field -->
    <xsl:if test="CUSTOM_FIELDS/CUSTOM_FIELD[CUSTOM_FIELD_NAME = 'Geo']">
      <Placemark>
        <name>
          <xsl:value-of select="CUSTOMER_NUMBER"/>
          <xsl:text> </xsl:text>
          <xsl:value-of select="CUSTOMER_ADDRESS/CUSTOMER_ADDRESS_NAME"/>
        </name>
        <styleUrl>
          <xsl:text>#msn_ylw-pushpin</xsl:text>
        </styleUrl>
        <Point>
          <coordinates>
            <xsl:value-of select="
              CUSTOM_FIELDS/CUSTOM_FIELD[
                CUSTOM_FIELD_NAME = 'Geo'
              ]/CUSTOM_FIELD_VALUE
            "/>
          </coordinates>
        </Point>
      </Placemark>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>

The first template (match="CUSTOMERS") processes every <CUSTOMER> node.

They are handed over to the second template (match="CUSTOMER"), which decides if the customer will be processed or not.

It does that by checking for the existence of a custom field with name the 'Geo'. If such a node exists, the <Placemark> is created. The output of the above is:

<Placemarks>
  <Placemark>
    <name>12345678 John Doe</name>
    <styleUrl>#msn_ylw-pushpin</styleUrl>
    <Point>
      <coordinates>323932121,31231233,0</coordinates>
    </Point>
  </Placemark>
</Placemarks>

So there's no need for any loops or extensive checks, they indicate that you are not looking at the problem from an XSLT viewpoint. ;-)

Now to your questions:

[…] so it's not possible to output data from the nodes that are a few levels up.

Of course it is - you can always go up with the '..' XPath operator, or move along the ancestor:: axis. I suggest you read some more about XPath first, you seem to be missing a lot.

I've tried setting a variable in the if-block, which should then read after the if-block to see if any data should be returned but I understand variables can only be set once so they can't be used for this goal either.

Variables are not just read-only, they also go out of scope immediately if the processing flow leaves their parent element. So checking a variable that is inside of an if-block from outside the if-block is impossible to begin with.

Do I really need a loop to see if there is a CUSTOM_FIELD who's CUSTOM_FIELD_NAME = 'Geo'?

No. You need an XPath check like shown above.

If the loop is needed, how can I use a result from inside the loop, outside the loop?

No. As explained above.

Or if that is not possible, how can I return values from nodes that are a few levels above the node I'm currently in?

Also explained above. ;-)

Tomalak
Thanks for this correct answer, and the in depth explanation ! I knew there had to be a more effective way to achieve this, but all examples and documentation i found only listed really simple cases.Anyway, thanks again :-)
Rene
You are welcome. :-)
Tomalak