Search code examples
xmlxsltxpathapply-templates

Output two separate nodegroups based on a shared property/value


I'd like to start by saying I'm not super good at XPATH and that is the main reason I come to you guys for some assistance.

So I was working today on trying to "group", or so to say, some data from an XML file based on both of them sharing an ID. I managed, with help from a friend, to do this but it was rather long winded and I'm certain there must be an easier/cleaner way. Below is the XML, XSLT I used and the desired output:

 <Dude>
     <ID>768</ID>
     <Name>Mr Dude Man</Name>
 </Dude>
 ...
 <Basket>
      <CustomerID>768</CustomerID>
      <Purchases>
          <PurchasedItem>
              <ItemID>736383-2</ItemID>
              <ItemName>XSLT Training</ItemName>
              <ItemType>Book</ItemType>
              <ItemQuantity>2</ItemQuantity>
          </PurchasedItem>
          <PurchasedItem>
              <ItemID>736383-2</ItemID>
              <ItemName>Candy</ItemName>
              <ItemType>Consumable</ItemType>
              <ItemQuantity>1</ItemQuantity>
          </PurchasedItem>
      </Purchases>
 </Basket>

XSLT I used:

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

<xsl:template match="Dude">
    {Name} has purchased: 
    <xsl:apply-templates select="Basket[Basket/CustomerID = ../Dude/ID]"/>
</xsl:template>

<xsl:template match="Basket">
    {ItemName}
</xsl:template>

In the above example, each Dude can have a single basket and the basket has a customerID connected to it to identify the basket owner. Assume that both nodes are as deep as each other. How would I go about, using xpath on an <apply-templates/>, to produce the following result:

ps. Don't worry to much about the actual output, I just want to know the proper way of traversing an XML tree while matching on one of the nodes using apply-templates

Mr Dude Man has purchased: XSLT Training, Candy 

EDIT: Forgot the XSLT I Used... Now the thing I am confused about is that is this the best way of doing this? With two separate matches. Also inside of a predicate do I need the ../ or does the predicate assume I'm starting at where I was matched eg: Dude


Solution

  • An efficient way to do cross-references like this is to use a key:

    <xsl:key name='kBasket' match="Basket" use="CustomerID" />
    

    Then you can do this:

    <xsl:template match="/">
        <xsl:apply-templates select="/Full/Path/To/Dude" />
    </xsl:template>
    
    <xsl:template match="Dude">
        {Name} has purchased: 
        <xsl:apply-templates select="key('kBasket', ID)"/>
    </xsl:template>
    
    <xsl:template match="Basket">
        <-- Any per-basket stuff could be output here -->
        <xsl:apply-templates select="Purchases/PurchasedItem" />
    </xsl:template>
    
    <xsl:template match="PurchasedItem">
        <xsl:value-of select="ItemName" />
    </xsl:template>
    


    The problem with your original attempt was that all the paths within your predicate were relative to Basket (and you didn't have the requisite path to get to Basket so the node-set was already empty at that point). The correct way to do that would be something like this:

    <xsl:apply-templates select="/Absolute/Path/To/Basket[CustomerID = current()/ID]"/>
    

    but the key approach is preferable because it is more efficient.