views:

264

answers:

2

My problem:
I have a wealth of atom RSS feed files which have many different atom entries in them and a few overlapping entries between files. I need to find and return an entry based on a URL from any one of the RSS feeds.

Technologies:
This code is being run through PHP 5.2.10's XLSTProcessor extension, which uses XSLT 1, has support for EXSLT and ability to run built in PHP functions. Saxan, Xalan or other similar solutions are not too helpful in this particular situation.

The following code is greatly simplified, but represents my situation.

rss-feed-names.xml:

<feeds>
    <feed name="travel.xml"/>
    <feed name="holidays.xml"/>
    ...
    <feed name="summer.xml"/>
    <feed name="sports.xml"/>
</feeds>

stylesheet.xsl

<xsl:stylesheet ...>
...

<func:function name="cozi:findPost">
    <xsl:param name="post-url"/>  

    <xsl:variable name="blog-feeds" select="document('rss-feed-names.xml')/feeds"/>  

    <xsl:for-each select="$blog-feeds/feed">
        <xsl:variable name="feed-file" select="document(@name)/atom:feed"/>
        <xsl:variable name="feed-entry" select="$feed-file/atom:entry[atom:link[contains(@href, $post-url)]]"/>
        <xsl:if test="$feed-entry">
            <func:result select="$feed-entry"/><!-- this causes errors if more than one result is found -->
        </xsl:if>
    </xsl:for-each>
</func:function>
</xsl:stylesheet>
...

This code works just fine iff the atom entry that we're looking for appears in ONE of the files we look through. It may appear multiple times within that file, but as soon as it appears in two or more files, the code breaks because func:result was already instantiated and is being over-written, which is a no-no in XSLT.

If there is a way to ACTUALLY exit an EXSLT function or xsl:for-each "loop" (you can assign a return variable for a function, but the function continues; and for-each's are actually not loops, but more similar to function maps), that would be ideal but I have not found a way yet.

I have considered combining all feeds into one variable and removing the for-each loop altogether, but have had problems getting this to work from the beginning.

Any other possible solutions, ideas or pointers are much appreciated! The file relationship here and XML is pretty hard to change, so solutions suggesting such a change are not ideal.

Thanks in advance,
Tristan Eastburn

A: 

Since you can't force exit from the loop, you have to build the complete list and then return only the first element:

<func:function name="cozi:findPost">
    <xsl:param name="post-url"/>  

    <xsl:variable name="blog-feeds" select="document('rss-feed-names.xml')/feeds"/>  

     <xsl:variable name="feedList">
         <xsl:for-each select="$blog-feeds/feed">
            <xsl:variable name="feed-file" select="document(@name)/atom:feed"/>
            <xsl:variable name="feed-entry" select="$feed-file/atom:entry[atom:link[contains(@href, $post-url)]]"/>
            <xsl:if test="$feed-entry">
                <xsl:value-of select="$feed-entry"/>
            </xsl:if>
        </xsl:for-each>
    </xsl:variable>
    <func:result select="$feedList[1]"/>
</func:function>

Handling of the empty-list condition is left as an exercise :-)

Jim Garrison
A: 

The general answer (as with Jim's response) is that you shouldn't put <func:result> inside <xsl:for-each>. My more specific solution for this case doesn't require you to use <xsl:for-each> or even <xsl:variable>. You can use just XPath alone:

<func:result select="(document
                       (document('rss-feed-names.xml')/feeds/@name)
                       /atom:feed
                       /atom:entry
                             [atom:link
                                   [contains(@href, $post-url)]]
                     )[1]"/>

This works because document() can take a node-set. When it does, it goes and gets the document that's referenced by each node in the argument. Multiple input/multiple output.

That said, judicious use of explaining variables would be good to help readability. Still, you don't need to use <xsl:for-each>.

Evan Lenz
Quick question that I'm having problems with. What if @name is not in the final format you want? E.g. I have to prepend "../" to all of the @names but the concat() function will convert the node set into a string and append the "../" to the beginning of the whole string. Is there a way to do this (maybe edit content in a node-set while retaining it as a node-set) via XPATH and not use something more similar to Jim's reply? Thanks!
teastburn
In XPath 2.0 you could use a sequence of strings, e.g., /feeds/@name/concat('../',.), but unfortunately not in XSLT/XPath 1.0 which only supports collections of nodes (and in which that syntax is not allowed, i.e. a function call after "/"). So I'm afraid you'd have to go back to using for-each or apply-templates to do the iteration in XSLT 1.0. Of course, you could pre-process the input so that it has "../", but I think that defeats the purpose of keeping the code concise.
Evan Lenz