0
votes

Taking the following XML as an example:

<?xml version="1.0" encoding="UTF-8"?>
<HelloXml>
  <Greeting>Hello</Greeting>
  <Name>World</Name>
  <FriendList>
    <Friend>
      <Number>1</Number>
      <BestFriend id='start' />
    </Friend>
    <Friend>
      <Number>2</Number>
      <Name>Mercury</Name>
    </Friend>
    <Friend>
      <Number>3</Number>
      <BestFriend id='end' />
    </Friend>
    <Friend>
      <Number>4</Number>
      <Name>Venus</Name>
    </Friend>
    <!-- ... -->
    <Friend>
      <Number>7</Number>
      <BestFriend id='start' />
    </Friend>
    <Friend>
      <Number>8</Number>
      <Name>Saturn</Name>
    </Friend>
    <!-- ... -->
    <Friend>
      <Number>11</Number>
      <BestFriend id='end' />
    </Friend>
    <!-- ... -->
    <Friend>
      <Number>12</Number>
      <Name>Neptune</Name>
    </Friend>
  </FriendList>
</HelloXml>

I need to accomplish a couple of things:

  • Get all the "Friends" that are between the "BestFriend" start/end flags (note that they are siblings, not child nodes).
  • Make a sequence of all the "BestFriends" that I got there.

The idea is to make an output similar to this:

<BestFriends>
  <Friend number='1'>
    <Name>Mercury</Name>
  </Friend>
  <Friend number='2'>
    <Name>Saturn</Name>
  </Friend>
</BestFriends>

I've tried using the next-sibling, preceeding-sibling and other logics, but so far, no luck.

Any advise is welcome.

Thanks.

EDIT: Sorry, I messed up with an "end" flag. Also, we are using XSLT 1.0.

EDIT: Now the requirement has changed, and it's more confusing than before. The situation is the same, but now the output should look like this:

<?xml version="1.0" encoding="UTF-8"?>
<FriendList>
  <Friend BestFriend="true">
    <Name>Mercury</Name>
  </Friend>
  <Friend BestFriend="false">
    <Name>Venus</Name>
  </Friend>
  <Friend BestFriend="true">
    <Name>Saturn</Name>
  </Friend>
  <Friend BestFriend="false">
    <Name>Neptune</Name>
  </Friend>
</FriendList>

To put the requirement in words, I need to output all the "friends" without discrimination, but I need to add an attribute to differentiate the "BestFriends" from the normal ones. These BestFriends are all of those that are inside the "start/end" flags.

The previous solution provided by @michael.hor257k is really cool and helpful (thanks again man!), but my user changed the requirement :/

I have tried some modifications to the "sibling recursion" approach, also tried some "loops" (but this is not java and of course, it didn't work), and working with some variables (that in XSLT are constants, so it doesn't work either).

Any help is welcome!

1
do you want to get all data in bestfriends node ?TechnicalKalsa
Can you explain why there are several <BestFriend id='start' /> but only one <BestFriend id='end' />? What does "between the "BestFriend" start/end flags" mean if these elements do not appear pair-wise?Martin Honnen
Should friend 3 have id="end" instead?Jim Garrison
Also consider to tell us if you can use XSLT 2.0 or are restricted to XSLT 1.0.Martin Honnen
I have just fixed the flags, sorry about that... Also, it needs to be done with XSLT 1.0dabug

1 Answers

0
votes

Assuming you are using XSLT 1.0, and further assuming that your input example is incorrect, you could use the following stylesheet based on a technique known as "sibling recursion":

XSLT 1.0

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

<xsl:template match="/HelloXml">
    <BestFriends>
        <xsl:apply-templates select="FriendList/Friend[BestFriend/@id='start']/following-sibling::Friend[1]"/>
    </BestFriends>
</xsl:template>

<xsl:template match="Friend">
    <Friend number="{Number}">
        <xsl:copy-of select="Name"/>
    </Friend>
    <xsl:apply-templates select="following-sibling::Friend[1][not(BestFriend/@id='end')]" />
</xsl:template>

</xsl:stylesheet>

Applied to the following test input:

<HelloXml>
   <Greeting>Hello</Greeting>
   <Name>World</Name>
   <FriendList>
       <Friend>
         <Number>0</Number>
         <Name>Sun</Name>
      </Friend>
     <Friend>
         <Number>1</Number>
         <BestFriend id="start"/>
      </Friend>
      <Friend>
         <Number>2</Number>
         <Name>Mercury</Name>
      </Friend>
      <Friend>
         <Number>3</Number>
         <BestFriend id="end"/>
      </Friend>
      <Friend>
         <Number>7</Number>
         <BestFriend id="start"/>
      </Friend>
      <Friend>
         <Number>8</Number>
         <Name>Saturn</Name>
      </Friend>
      <Friend>
         <Number>9</Number>
         <Name>Pluto</Name>
      </Friend>
      <Friend>
         <Number>10</Number>
         <BestFriend id="end"/>
      </Friend>
       <Friend>
         <Number>11</Number>
         <Name>Moon</Name>
      </Friend>
   </FriendList>
</HelloXml>

the result will be:

<?xml version="1.0" encoding="UTF-8"?>
<BestFriends>
   <Friend number="2">
      <Name>Mercury</Name>
   </Friend>
   <Friend number="8">
      <Name>Saturn</Name>
   </Friend>
   <Friend number="9">
      <Name>Pluto</Name>
   </Friend>
</BestFriends>

Note that this assumes there will be at least one Friend node in-between the start and end flag nodes.


Added in response to changed requirement:

I need to output all the "friends" without discrimination, but I need to add an attribute to differentiate the "BestFriends" from the normal ones. These BestFriends are all of those that are inside the "start/end" flags.

There is a very simple method to achieve that, but it's not very efficient:

XSLT 1.0

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

<xsl:template match="/HelloXml">
    <FriendList>
        <xsl:for-each select="FriendList/Friend[Name]">
            <Friend BestFriend="{count(preceding-sibling::Friend[BestFriend[@id='start']]) != count(preceding-sibling::Friend[BestFriend[@id='end']])}">
                <xsl:copy-of select="Name"/>
            </Friend>
        </xsl:for-each>
    </FriendList>
</xsl:template>

</xsl:stylesheet>

If you have a large input, you may want to consider pre-processing only best friends (or not best friends) first, then using the list to determine the value of the BestFriend attribute on the output.