Hi, I'm new to these forums and have been working with xslt for about a month. So far I've been able to do just about everything I need with it, but my most recent task has me confused.
I have a relatively flat xml structure that I'm using XSLT to transform into html that looks like a tree. I'm not concerned about the html part, I'm concerned about how to process the xml. My xml looks like this:

<system>
    <device attr1='0' attr2='0' attr3='0'>
        <sub-device range='24' value='0'/>
        <sub-device range='25' value='1'/>
    </device>
    <device attr1='0' attr2='1' attr3='0'/>
    <device attr1='0' attr2='2' attr3='0'/>
    <device attr1='1' attr2='0' attr3='0'/>
    <device attr1='1' attr2='1' attr3='0'/>
</system>

So when a device has sub-devices with range 24 and 25 then I know this device has children. The sub-device with a range of 25 holds the key to finding the children. Any device with an attr1 equal to the value associated with the sub-device range='25' is a child. The xslt I'm using to find these is:

<xsl:template match="system">
    <xsl:result-document href="resdoc.html">
    <html>
        <body>
        <xsl:apply-templates select="device"/> 
        </body>
    </html>
   </xsl:result-document>
</xsl:template>

<!-- this matches the parents -->
<xsl:template match="device[sub-device/@range='24' sub-device/@range='25']">
    <xsl:variable name="node-def">
        <xsl:value-of select="@attr1"><xsl:text>:</xsl:text>
        <xsl:value-of select="@attr2"><xsl:text>:</xsl:text>
        <xsl:value-of select="@attr3">
    </xsl:variable>
    <xsl:value-of select="$node-def"/><xsl:text> has children.</xsl:text> <br/>
    <xsl:apply-templates select="../device[@attr1=current()/sub-device[range='25']/@value]"/>
</xsl:template>

<!-- this matches everything else -->
<xsl:template match="device">
    <xsl:variable name="node-def">
        <xsl:value-of select="@attr1"><xsl:text>:</xsl:text>
        <xsl:value-of select="@attr2"><xsl:text>:</xsl:text>
        <xsl:value-of select="@attr3">
    </xsl:variable>
    <xsl:value-of select="$node-def"/><xsl:text> has no children.</xsl:text> <br/>
</xsl:template>

The problem is, I'm processing the children twice.
The above xml would produce the following output:

0:0:0 has children.
1:0:0 has no children.
1:1:0 has no children.
0:1:0 has no children.
0:2:0 has no children.
1:0:0 has no children.
1:1:0 has no children.

What I'm looking for at this point is:
0:0:0 has children.
1:0:0 has no children.
1:1:0 has no children.
0:1:0 has no children.
0:2:0 has no children.

I want to only process the children once. I've been trying to figure out if grouping items using for-each-group is possible but I can't seem to figure out how to get the parent and children grouped together. The only other thing I could think of would be to keep a list of all the nodes I have processed and then check before re-processing, but I don't understand recursion enough to figure this out.

Is this possible? It has to be right? I'd really appreciate any ideas, suggestions or thoughts on this problem.

Thanks!

Recommended Answers

All 12 Replies

You would got an errors while using the above xslt code.

I closed all the <xsl:value-of> in the above XSLT snippet and changed

device[sub-device/@range='24' sub-device/@range='25']

to

device[sub-device/@range='24' and sub-device/@range='25']

I got the below output

<html>
   <body>0:0:0 has children.
   <br>0:1:0 has no children.
   <br>0:2:0 has no children.
   <br>1:0:0 has no children.
   <br>1:1:0 has no children.<br></body>
</html>

Sorry, I botched the code:

The xml is correct. But the code looks like this:

<xsl:template match="system">
        <xsl:result-document href="resdoc.html">
            <html>
                <body>
                    <xsl:apply-templates select="device"/> 
                </body>
            </html>
        </xsl:result-document>
    </xsl:template>
    
    <!-- this matches the parents -->
    <xsl:template match="device[sub-device/@range='24' and sub-device/@range='25']">
        <xsl:variable name="node-def">
            <xsl:value-of select="@attr1"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr2"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr3"/>
        </xsl:variable>
        <xsl:value-of select="$node-def"/><xsl:text> has children.</xsl:text> <br/>
        <xsl:apply-templates select="../device[@attr1=current()/sub-device[@range='25']/@value]"/>
    </xsl:template>
    
    <!-- this matches everything else -->
    <xsl:template match="device">
        <xsl:variable name="node-def">
            <xsl:value-of select="@attr1"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr2"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr3"/>
        </xsl:variable>
        <xsl:value-of select="$node-def"/><xsl:text> has no children.</xsl:text> <br/>
    </xsl:template>

this produces:

<html>
   <body>
0:0:0 has children.
1:0:0 has no children.
1:1:0 has no children.
0:1:0 has no children.
0:2:0 has no children.
1:0:0 has no children.
1:1:0 has no children.
</body>
</html>

I need the nodes that are processed as children, to not be reprocessed later.

Thanks! Sorry for the errors earlier!

what are you trying to do with

<xsl:apply-templates select="../device[@attr1=current()/sub-device[range='25']/@value]"/>

This is the line which makes the difference. If you run the stylesheet without this line, you would get the desired output.

what are you trying to do with

<xsl:apply-templates select="../device[@attr1=current()/sub-device[range='25']/@value]"/>

This is the line which makes the difference. If you run the stylesheet without this line, you would get the desired output.

My desired output is:
0:0:0 has children.
1:0:0 has no children.
1:1:0 has no children.
0:1:0 has no children.
0:2:0 has no children.

Not:
0:0:0 has children.
0:1:0 has no children.
0:2:0 has no children.
1:0:0 has no children.
1:1:0 has no children.

Order matters. If you take that line out then how do I put the 1:0:0, and 1:1:0 items under 0:0:0? They do print down at the bottom but that's not where I need them to be processed.

I think I'm not explaining this right. I'm trying to unflatten the xml.
I need to eventually show that 1:0:0 and 1:1:0 are children of 0:0:0

0:0:0 has children.
-> 1:0:0
-> 1:1:0
0:1:0 has no children.
0:2:0 has no children.

Eventually the html will show the items with children as collapsible. Does that make sense? Sorry I'm definitely not an html expert so I don't know how to word what it will look like. I'm just trying to get the xml to process in the right order. Theoretically 1:0:0 and/or 1:1:1 could have children as well making the tree even deeper.

This is how I could help you. But I'm still confused:

<xsl:template match="device[sub-device/@range='24' and sub-device/@range='25']">
		<xsl:variable name="attr1" select="sub-device[@range='25']/@value"/>
        <xsl:variable name="node-def">
            <xsl:value-of select="@attr1"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr2"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr3"/>
        </xsl:variable>
        <xsl:value-of select="$node-def"/><xsl:text> has children.</xsl:text> <br/>
		<xsl:apply-templates select="../device[@attr1=$attr1]" mode="attr1"/>
		<xsl:apply-templates select="../device[@attr1 != $attr1][not(sub-device/@range='24' and sub-device/@range='25')]" mode="attr2"/>
    </xsl:template>
    
	<xsl:template match="device" mode="attr1">
        <xsl:variable name="node-def">
            <xsl:value-of select="@attr1"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr2"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr3"/>
        </xsl:variable>
        <xsl:value-of select="$node-def"/><xsl:text> has no children.</xsl:text> <br/>
    </xsl:template>
	
    <!-- this matches everything else -->
    <xsl:template match="device" mode="attr2">
        <xsl:variable name="node-def">
            <xsl:value-of select="@attr1"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr2"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr3"/>
        </xsl:variable>
        <xsl:value-of select="$node-def"/><xsl:text> has no children.</xsl:text> <br/>
    </xsl:template>

Thanks so much for your help. Your code produces this output:

0:0:0 has children.
1:0:0 has no children.
1:1:0 has no children.
0:1:0 has no children.
0:2:0 has no children.
1:0:0 has no children.
1:1:0 has no children.

which is what my initial (corrected) code produced. I want to avoid the second:
1:0:0 has no children.
1:1:0 has no children.
entries.

I know it's confusing, so I really do appreciate your patience and help. I'm sorry I have been unable to explain what it is I need. In a nutshell, I will have an xml structure full of <device> nodes, all of which are siblings. Some of these <device> siblings are actually children of other <device> nodes but the xml is not structured that way. If a <device> node has sub-devices with @range '24' and '25' then it's a parent node. It's children are those <device>s that have an @attr1 equal to the parent <device> sub-device[@range='25']/@value -- clear as mud, right? :)

Anyway, ideally I'd like to be able to figure out how to group parent and children together using group-by but I've failed miserably at attempting that so far. The working attempt I posted gets me partially right output but duplicate children entries. It's those duplicates I'm trying to avoid while still processing the children right after the parents.

It may be that the answer is going to end up in the xml itself. I think it would certainly be more clear to me if the xml either looked more like the tree structure or at least was in the proper order with children directly following the parents.

I'm clear now and the my previous code itself gives you the correct output. With the below input xml

<system>
    <device attr1='0' attr2='0' attr3='0'>
        <sub-device range='24' value='0'/>
        <sub-device range='25' value='1'/>
    </device>
    <device attr1='0' attr2='1' attr3='0'/>
    <device attr1='0' attr2='2' attr3='0'/>
    <device attr1='1' attr2='0' attr3='0'/>
    <device attr1='1' attr2='1' attr3='0'/>
</system>

and xslt 2.0 code

<xsl:template match="system">
        <xsl:result-document href="resdoc.html">
            <html>
                <body>
                    <xsl:apply-templates select="device"/> 
                </body>
            </html>
        </xsl:result-document>
    </xsl:template>
    
    <!-- this matches the parents -->
    <xsl:template match="device[sub-device/@range='24' and sub-device/@range='25']">
		<xsl:variable name="attr1" select="sub-device[@range='25']/@value"/>
        <xsl:variable name="node-def">
            <xsl:value-of select="@attr1"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr2"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr3"/>
        </xsl:variable>
        <xsl:value-of select="$node-def"/><xsl:text> has children.</xsl:text> <br/>
		<xsl:apply-templates select="../device[@attr1=$attr1]" mode="attr1"/>
		<xsl:apply-templates select="../device[@attr1 != $attr1][not(sub-device/@range='24' and sub-device/@range='25')]" mode="attr2"/>
    </xsl:template>
    
	<xsl:template match="device" mode="attr1">
        <xsl:variable name="node-def">
            <xsl:value-of select="@attr1"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr2"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr3"/>
        </xsl:variable>
        <xsl:value-of select="$node-def"/><xsl:text> has no children.</xsl:text> <br/>
    </xsl:template>
	
    <!-- this matches everything else -->
    <xsl:template match="device" mode="attr2">
        <xsl:variable name="node-def">
            <xsl:value-of select="@attr1"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr2"/><xsl:text>:</xsl:text>
            <xsl:value-of select="@attr3"/>
        </xsl:variable>
        <xsl:value-of select="$node-def"/><xsl:text> has no children.</xsl:text> <br/>
    </xsl:template>

I got the below output

<html>
   <body>0:0:0 has children.<br>1:0:0 has no children.<br>1:1:0 has no children.<br>0:1:0 has no children.<br>0:2:0 has no children.<br></body>
</html>

Could you please check if you are using the same code I have posted here.

Yes your code gets the right output for this input file. But if the first entry is not a parent then it doesn't provide the right output. Thanks for your help. I think I need to figure out how to keep track of what nodes I've processed. grrrr.

for testing

<?xml version="1.0"?>
<system>
    <device attr1='0' attr2='0' attr3='0'>
        <sub-device range='24' value='0'/>
        <sub-device range='25' value='1'/>
    </device>
    <device attr1='0' attr2='1' attr3='0'/>
    <device attr1='0' attr2='2' attr3='0'/>
    <device attr1='1' attr2='0' attr3='0'/>
    <device attr1='1' attr2='1' attr3='0'/>
    <device attr1='0' attr2='0' attr3='0'>
        <sub-device range='24' value='0'/>
    </device>
</system>
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
	<xsl:output indent="yes" method="html"/>
	<xsl:template match="/">
		<xsl:apply-templates select="system"/>
	</xsl:template>
	<xsl:template match="system">
		<html>
			<body>
				<xsl:for-each select="descendant::*">
					<xsl:choose>
						<xsl:when test="local-name(.)!='device'">
							<!-- do nothing -->
							<!--
							
							<p>
								<xsl:value-of select="concat(local-name(.),'  ',@range,':',@value,' has ',count(child::*),' children')"/>
							</p>
							-->
						</xsl:when>
						<xsl:when test="count(child::*)&gt;1">
							<p>
								<xsl:value-of select="concat(local-name(.),'  ',@attr1,':',@attr2,':',@attr3,' has ',count(child::*),' children')"/>
							</p>
						</xsl:when>
						<xsl:when test="count(child::*) = 1">
							<p>
								<xsl:value-of select="concat(local-name(.),'  ',@attr1,':',@attr2,':',@attr3,' has ',count(child::*),'  child')"/>
							</p>
						</xsl:when>
						<xsl:when test="count(child::node())= 0">
							<p>
								<xsl:value-of select="concat(local-name(.),'  ',@attr1,':',@attr2,':',@attr3,' has no children')"/>
							</p>
						</xsl:when>
					</xsl:choose>
				</xsl:for-each>
			</body>
		</html>
	</xsl:template>
</xsl:stylesheet>

result

<html>
  <body>
    <p>device  0:0:0 has 2 children</p>
    <p>device  0:1:0 has no children</p>
    <p>device  0:2:0 has no children</p>
    <p>device  1:0:0 has no children</p>
    <p>device  1:1:0 has no children</p>
    <p>device  0:0:0 has 1  child</p>
  </body>
</html>

Thanks for your response but this is not what i'm looking for. I understand that the <sub-device>s are physical children of the <device> node. That's not how I need it to be. See my previous explanation earlier as to what I'm trying to get the children to be.

so in this case, 0:0:0 has two children, 1:0:0 and 1:1:0 (sub-device[@range=25]/@value gives me the attr1 of the children) I need to process those as children and display them under the 0:0:0 and then nowhere else. -- there won't be a second 0:0:0 object. Children can also be parents so theoretically 1:0:0 or 1:1:0 could have children too, and so on.

It's confusing.

So to clarify, basically i'm trying to look ahead in the document, process sibling nodes out of order, and then not reprocess them again later. I'm beginning to think it's not possible without re-arranging the xml.

new trial

xml to test

<?xml version="1.0"?>
<system>
	<device attr1="0" attr2="0" attr3="0">
		<sub-device range="24" value="0"/>
		<sub-device range="25" value="1"/>
	</device>
	<device attr1="0" attr2="1" attr3="0"/>
	<device attr1="0" attr2="2" attr3="0"/>
	<device attr1="1" attr2="0" attr3="0"/>
	<device attr1="1" attr2="1" attr3="0">
		<sub-device range="25" value="0"/>
	</device>
	<device attr1="0" attr2="0" attr3="0">
		<sub-device range="24" value="0"/>
		<sub-device range="25" value="2"/>
	</device>
</system>

xsl

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
	<xsl:output indent="yes" method="html"/>
	<xsl:variable name="system" select="/system"/>
	<xsl:template match="/">
		<xsl:apply-templates select="system"/>
	</xsl:template>
	<xsl:template match="system">
		<html>
			<body>
				<xsl:for-each select="descendant::*">
					<xsl:choose>
						<xsl:when test="local-name(.)!='device'">
							<!-- do nothing -->
							
						</xsl:when>
						<xsl:when test="count(child::*)&gt;0">
							<p>
								<xsl:value-of select="concat(local-name(.),'  ',@attr1,':',@attr2,':',@attr3,' has ')"/>
								<xsl:variable name="r25" select="./sub-device/@range[.=25]/../@value"/>
								<xsl:variable name="count" select="count($system/device[@attr1=$r25])"/>
								<xsl:choose>
									<xsl:when test="$count &gt; 1">
										<xsl:value-of select="concat($count ,' [with Range = 25 Value = ',$r25,'] children')"/>
									</xsl:when>
									<xsl:when test="$count = 1">
										<xsl:value-of select="concat($count ,' [with Range = 25 Value = ',$r25,'] child')"/>
									</xsl:when>
									<xsl:when test="$count = 0">
										<xsl:value-of select="concat($count ,' no [with Range = 25 Value = ',$r25,'] child')"/>
									</xsl:when>
								</xsl:choose>
							</p>
						</xsl:when>
						<xsl:when test="count(child::node())= 0">
							<p>
								<xsl:value-of select="concat(local-name(.),'  ',@attr1,':',@attr2,':',@attr3,' has no children')"/>
							</p>
						</xsl:when>
					</xsl:choose>
				</xsl:for-each>
			</body>
		</html>
	</xsl:template>
</xsl:stylesheet>

I think that there is an error because a device is counted again

<html>
  <body>
    <p>device  0:0:0 has 2 [with Range = 25 Value = 1] children</p>
    <p>device  0:1:0 has no children</p>
    <p>device  0:2:0 has no children</p>
    <p>device  1:0:0 has no children</p>
    <p>device  1:1:0 has 4 [with Range = 25 Value = 0] children</p>
    <p>device  0:0:0 has 0 no [with Range = 25 Value = 2] child</p>
  </body>
</html>
Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.