Phil
Phil

Reputation:

XSLT 2.0 External lookup using key() and document()

I'm pulling what's left of my hair out trying to get a simple external lookup working using Saxon 9.1.0.7.

I have a simple source file dummy.xml:

<something>
   <monkey>
      <genrecode>AAA</genrecode>
   </monkey>
   <monkey>
      <genrecode>BBB</genrecode>
   </monkey>
   <monkey>
      <genrecode>ZZZ</genrecode>
   </monkey>
   <monkey>
      <genrecode>ZER</genrecode>
   </monkey>
</something>

Then the lookup file is GenreSet_124.xml:

<GetGenreMappingObjectsResponse>
   <tuple>
      <old>
         <GenreMapping DepartmentCode="AAA"
                       DepartmentName="AND - NEWS AND CURRENT AFFAIRS"
                       Genre="10 - NEWS"/>
      </old>
   </tuple>
   <tuple>
      <old>
         <GenreMapping DepartmentCode="BBB"
                       DepartmentName="AND - NEWS AND CURRENT AFFAIRS"
                       Genre="11 - NEWS"/>
      </old>
   </tuple>

   ... lots more
</GetGenreMappingObjectsResponse>

What I'm trying to achieve is simply to get hold of the "Genre" value based on the "DepartmentCode" value.

So my XSL looks like:

...
<!-- Set up the genre lookup key -->
<xsl:key name="genre-lookup" match="GenreMapping" use="@DepartmentCode"/>


<xsl:variable name="lookupDoc" select="document('GenreSet_124.xml')"/>

<xsl:template match="/something">
<stuff>
   <xsl:for-each select="monkey">
      <Genre>

       <xsl:apply-templates select="$lookupDoc"> 
         <xsl:with-param name="curr-label" select="genrecode"/> 
      </xsl:apply-templates>

      </Genre>
   </xsl:for-each>
</stuff>
</xsl:template>

<xsl:template match="GetGenreMappingObjectsResponse">
   <xsl:param name="curr-genrecode"/> 

   <xsl:value-of select="key('genre-lookup', $curr-genrecode)/@Genre"/>

</xsl:template>

...


The issue that I have is that I get nothing back. I currently just get

<?xml version="1.0" encoding="UTF-8"?>
<stuff>
   <Genre/>
   <Genre/>
   <Genre/>
   <Genre/>
</stuff>

I have moved all the lookup data to be attributes of GenreMapping, previously as child elements of GenreMapping whenever I entered the template match="GetGenreMappingObjectsResponse" it would just print out all text from every GenreMapping (DepartmentCode, DepartmentName, Genre)!

I can't for the life of me figure out what I am doing wrong. Any helpo/suggestions would be greatly appreciated.

PLease find the current actual XSLT listing:

<?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">

<!-- Define the global parameters -->
<xsl:param name="TransformationID"/>
<xsl:param name="TransformationType"/>

<!-- Specify that XML is the desired output type --> 
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>

<!-- Set up the genre matching capability -->
<xsl:key name="genre-lookup" match="GenreMapping" use="@DepartmentCode"/>

<xsl:variable name="documentPath"><xsl:value-of select="concat('GenreSet_',$TransformationID,'.xml')"/></xsl:variable>
<xsl:variable name="lookupDoc" select="document($documentPath)"/>

<!-- Start the first match on the Root level -->
<xsl:template match="/something">
<stuff>
   <xsl:for-each select="monkey">
      <Genre>
      <xsl:apply-templates select="$lookupDoc/*">
         <xsl:with-param name="curr-genrecode" select="string(genrecode)"/>
      </xsl:apply-templates>
      </Genre>
   </xsl:for-each>
</stuff>
</xsl:template >

<xsl:template match="GetGenreMappingObjectsResponse">
   <xsl:param name="curr-genrecode"/>
   <xsl:value-of select="key('genre-lookup', $curr-genrecode, $lookupDoc)/@Genre"/>
</xsl:template>
</xsl:stylesheet>

The TransformationID is alway 124 (so the correct lookup file is opened. The Type is just a name that I am currently not using but intending to.

Upvotes: 7

Views: 11484

Answers (2)

Welbog
Welbog

Reputation: 60378

In XSLT 2.0 there are two ways you can do what you want:

One is the three-parameter version of the key function. The third parameter lets you specify the root node you want the key to work on (by default it's always the root of the main document):

<xsl:value-of select="key('genre-lookup', $curr-genrecode,$lookupDoc)/@Genre"/>

Another way is to use the key function under the $lookupDoc node:

<xsl:value-of select="$lookupDoc/key('genre-lookup', $curr-genrecode)/@Genre"/>

Both of these methods are documented in the XSLT 2.0 specification on keys, and won't work in XSLT 1.0.

For the sake of completeness, you'd have to rewrite this to not use keys if you're restricted to XSLT 1.0.

<xsl:value-of select="$lookupDoc//GenreMapping[@DepartmentCode = $curr-genrecode]/@Genre"/>

Aha! The problem is the select="$lookupDoc" in your apply-templates call is calling a default template rather than the one you expect, so the parameter is getting lost.

Change it to this:

<xsl:apply-templates select="$lookupDoc/*">
  <xsl:with-param name="curr-genrecode" select="string(genrecode)"/>
</xsl:apply-templates>

That will call your template properly and the key should work.

So the final XSLT sheet should look something like this:

  <xsl:variable name="lookupDoc" select="document('XMLFile2.xml')"/>
  <xsl:key name="genre-lookup" match="GenreMapping" use="@DepartmentCode"/>
  <xsl:template match="/something">
    <stuff>
      <xsl:for-each select="monkey">
        <Genre>
          <xsl:apply-templates select="$lookupDoc/*">
            <xsl:with-param name="curr-genrecode" select="string(genrecode)"/>
          </xsl:apply-templates>
        </Genre>
      </xsl:for-each>
    </stuff>
  </xsl:template>
  <xsl:template match="GetGenreMappingObjectsResponse">
    <xsl:param name="curr-genrecode"/>
    <xsl:value-of select="key('genre-lookup',$curr-genrecode,$lookupDoc)/@Genre"/>
  </xsl:template>

Upvotes: 9

Phil
Phil

Reputation:

OK, so this is a bit mental and I don't claim to understand it but it works (sounds like a career in software).

The issue I was having is that when I call the apply-templates and pass in the external document as a variable it never matched any templates even ones called "Genremapping".

So I used a wildcard to catch it, also when calling the apply-templates I narrowed down the node set to be the child I was interested in. This was pretty bonkers a I could print out the name of the node and see "GenreMapping" yet it never went into any template I had called "GenreMapping" choosing instead to only ever go to "*" template.

What this means is that my new template match gets called for every single GenreMapping node there is so it may be a little inefficient. What I realised then was all I needed to do was print sometihng out if a predicate matched.

So it looks like this now (no key used whatsoever):

...
<xsl:template match="/something">
<stuff>
   <xsl:for-each select="monkey">
      <Genre>
         <key_value><xsl:value-of select="genrecode"/></key_value>
         <xsl:variable name="key_val"><xsl:value-of select="genrecode"/></xsl:variable>
         <code>
      <xsl:apply-templates select="$lookupDoc/*/*/*/*">
         <xsl:with-param name="curr-genrecode" select="string(genrecode)"/>
      </xsl:apply-templates>

         </code>
      </Genre>
   </xsl:for-each>
</stuff>
</xsl:template >


<xsl:template match="*">
   <xsl:param name="curr-genrecode"/>
   <xsl:value-of select=".[@DepartmentCode = $curr-genrecode]/@Genre"/>
</xsl:template>
...

All of which output: Note, the last key_value correctly doesn't have a code entry as there is no match in the lookup document.

<?xml version="1.0" encoding="UTF-8"?>
<stuff xmlns:xs="http://www.w3.org/2001/XMLSchema">
   <Genre>
      <key_value>AAA</key_value>
      <code>10 - NEWS</code>
   </Genre>
   <Genre>
      <key_value>AAA</key_value>
      <code>10 - NEWS</code>
   </Genre>
   <Genre>
      <key_value>BBB</key_value>
      <code>11 - NEWS</code>
   </Genre>
   <Genre>
      <key_value>SVVS</key_value>
      <code/>
   </Genre>
</stuff>

Answer on a postcode. Thanks for the help Welbog.

Upvotes: 0

Related Questions