tags:

views:

128

answers:

3

I have an XML document with a section similar to the following:

<release_list>
  <release>
    <id>100</id>
    <file_list>
      <file>
        <id>20</id>
      </file>
      <file>
        <id>21</id>
      </file>
    </file_list>
  </release>
  <release>
    <id>101</id>
    <file_list>
      <file>
        <id>22</id>
      </file>
      <file>
        <id>21</id>
      </file>
    </file_list>
  </release>
  <release>
    <id>102</id>
    <file_list>
      <file>
        <id>22</id>
      </file>
      <file>
        <id>23</id>
      </file>
    </file_list>
  </release>
</release_list>

At some point I have XSL for-each statements looping through each release, and in turn through each list of files. While I'm looping through each file, I want to get information about which other releases DO NOT contain that same file ID. (So for example, when I'm iterating through file 20 in release 100, I want to get pointers to both releases 101 and 102, but when I'm iterating through file 21 in that same package I want only a pointer to release 102.)

Is there a way to do this with XPath? The closest thing I've come up with is:

../../../release[ not( file_list/file/id = ./id ) ]

...which of course fails because within the square brackets the './' refers to the release in front of the brackets, not the file that the XPath statement is being called from.

Anything I'm missing here?

+2  A: 

A good approach in XSLT to things like this is to use a variable at the outer level, and then you can compare against it in your selection path. So, for instance, something like

<xsl:variable name="id" select="id" />
<xsl:apply-templates select="/release_list/release[not(file_list/file/id = $id)]" />

may help you.

Peter Cooper Jr.
For some reason I had the impression that variables in XSLT were write-once, and that they were therefore of limited utility--that's demonstrably not the case, though. Thanks a lot!
Chris
The write-only holds true, though. ;-)
Tomalak
+2  A: 

Within a predicate (the expression in square brackets) you can get the current node (as opposed to the context node returned by .) using the current() function.

However you should change your approach to something like Peter Cooper Jr's answer. In XSLT, it's almost always better to filter at the outermost level, so as to only process what you need, than to try to process everything and then impose exclusions from the inside.

It's also worth understanding that you aren't using a loop at all; for-each in XSLT is a mapping, not a loop. In other words, there is no concept of doing the first element, then the second element, then the third... in a conceptual sense, all matched elements are processed simultaneously. This is why the oft-asked question "How do I break out of a loop in XSLT?" can only be answered with the Matrix-inspired words: "There is no loop."

NickFitz
I wish I could add two accepted answers, because this is every bit as helpful as Peter Cooper Jr's answer. The current() function did do exactly what I wanted it to in the code, but I can see that you're also both right about using the template.
Chris
A: 

Here is a complete solution that does what you seem to want. When applied to your sample input XML, the following XSLT 1.0 stylesheet:

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

  <xsl:template match="release">
    <xsl:copy>
      <xsl:copy-of select="id" />
      <xsl:apply-templates select="file_list/file" mode="show-other" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="file" mode="show-other">
    <xsl:copy>
      <xsl:copy-of select="id" />

      <!-- select all releases that do not contain any file with the same
           id as the current file -->
      <xsl:variable name="other" select="
        /release_list/release[not(current()/id = file_list/file/id)]
      " />
      <releases-without-that-file>
        <xsl:copy-of select="$other/id" />
      </releases-without-that-file>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

produces:

<release>
  <id>100</id>
  <file>
    <id>20</id>
    <releases-without-that-file>
      <id>101</id>
      <id>102</id>
    </releases-without-that-file>
  </file>
  <file>
    <id>21</id>
    <releases-without-that-file>
      <id>102</id>
    </releases-without-that-file>
  </file>
</release>
<release>
  <id>101</id>
  <file>
    <id>22</id>
    <releases-without-that-file>
      <id>100</id>
    </releases-without-that-file>
  </file>
  <file>
    <id>21</id>
    <releases-without-that-file>
      <id>102</id>
    </releases-without-that-file>
  </file>
</release>
<release>
  <id>102</id>
  <file>
    <id>22</id>
    <releases-without-that-file>
      <id>100</id>
    </releases-without-that-file>
  </file>
  <file>
    <id>23</id>
    <releases-without-that-file>
      <id>100</id>
      <id>101</id>
    </releases-without-that-file>
  </file>
</release>

Note that

/release_list/release[not(current()/id = file_list/file/id)]

is not the same as

/release_list/release[current()/id != file_list/file/id]

since the = compares against any node in a node set (and returns true if all nodes match only), whereas the != operator compares against the first node of a node-set only.

Tomalak