1
votes

I want to create nested XML nodes using a single node from an XML structure, I tried but unable to bring the nested XML structure, Can some please help me, I am unable to get the expected behavior.

XML Sructure:

<job[n]/> -> 0...n
<person-non-active/> -> 1
<removed> -> 1
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <person>
        <job1/>
        <job2/>
        <person-non-active>
            <removed>
                <job3/>
                <job4/>
            </removed>
        </person-non-active>
    </person>
    <person>
      <bio>
        <job1/>
        <job2/>
        <person-non-active>
            <removed>
                <job3/>
            </removed>
        </person-non-active>
      </bio>
    </person>
   <person>
        <person-non-active>
            <removed>
                <job3/>
                <job4/>
            </removed>
        </person-non-active>
    </person>
</root>

XSL Used:

<xsl:template match="person-non-active">
        <person>            
            <xsl:if test="(count(../*) &gt; 1)">
                <job>
                    <xsl:attribute name="status"><xsl:text>active</xsl:text></xsl:attribute>
                    <xsl:attribute name="jobs">
                        <xsl:for-each select="../*">
                            <xsl:if test="not(name() = 'person-non-active')">
                                <xsl:value-of select="name()"/><xsl:text>, </xsl:text>
                            </xsl:if>                            
                        </xsl:for-each>                
                    </xsl:attribute>
                </job>
            </xsl:if>
            <xsl:if test="(count(./removed/*) &gt; 0)">
                <job>
                    <xsl:attribute name="status"><xsl:text>non-acitve</xsl:text></xsl:attribute>
                    <xsl:attribute name="jobs">
                        <xsl:for-each select="./removed/*">
                            <xsl:value-of select="name()"/><xsl:text>, </xsl:text>
                        </xsl:for-each>                
                    </xsl:attribute>
                </job>
            </xsl:if>
        </person>
    </xsl:template>

Output Received:

<person>
 <job status="active" jobs="job1, job2, "/>
 <job status="non-acitve" jobs="job3, job4, "/>
</person>

Expected Output

<person>
    <job status="active" jobs="job1, job2, ">
        <job status="non-acitve" jobs="job3, job4, "/>
    </job>
</person>
<person>
 <bio>
    <job status="active" jobs="job1, job2, ">
        <job status="non-acitve" jobs="job3, "/>
    </job>
 </bio>
</person>
<person>
    <job status="non-acitve" jobs="job3, job4, "/>
</person>

"person-non-active" element should be used for match

2
Which XSLT version (and which parser) are you using? Your expected output is not really clear... you want for each person to have a <job> to list active jobs and to also contain a nest <job> to list non-actives jobs?Sebastien
I used XSLT 3.0, yes i need each <person> to have the <job> and nest the non-active.Jason

2 Answers

1
votes

Here's a way this could be done. I think all that is missing are the separators for the values in job/@jobs.

I have based some of the code on this answer : How to apply a function to a sequence of nodes in XSLT

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    version="2.0">

  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="person">
      <root>
        <xsl:copy>
            <job>
                <xsl:attribute name="status" select="'active'"/>
                <xsl:variable name="seqj"><xsl:sequence select="for $j in *[starts-with(local-name(.),'job')] return local-name($j)"/></xsl:variable>
                <xsl:attribute name="jobs"><xsl:value-of select="$seqj"/></xsl:attribute>
                <job>
                    <xsl:attribute name="status" select="'non-active'"/>
                    <xsl:variable name="seqj"><xsl:sequence select="for $j in person-non-active/removed/*[starts-with(local-name(.),'job')] return local-name($j)"/></xsl:variable>
                    <xsl:attribute name="jobs"><xsl:value-of select="$seqj"/></xsl:attribute>
                </job>
            </job>
        </xsl:copy>
      </root>
  </xsl:template>

</xsl:stylesheet>

You can see it working here : https://xsltfiddle.liberty-development.net/pNmC4J3

1
votes

Couldn't you do simply:

<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="person">
    <person>
        <job status="active">
            <xsl:attribute name="jobs">
                <xsl:for-each select="*[not(self::person-non-active)]">
                    <xsl:value-of select="name()"/>
                    <xsl:text>, </xsl:text>
                 </xsl:for-each>                
            </xsl:attribute>
            <job status="non-active">
                <xsl:attribute name="jobs">
                    <xsl:for-each select="person-non-active/removed/*">
                        <xsl:value-of select="name()"/>
                        <xsl:text>, </xsl:text>
                     </xsl:for-each>                
                </xsl:attribute>
            </job>
        </job>
    </person>
</xsl:template>

</xsl:stylesheet>

Note that I have changed status="non-acitve" to status="non-active".


Added in response to expanded question:

This is considerably more complicated, esp. if you want to preserve the wrapper elements of active jobs (such as bio in your example), without knowing their names in advance.

If jobs are the only elements with no child elements, you could do something like:

<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="*"/>

<!-- identity transform -->
<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

<!-- parent of active jobs -->
<xsl:template match="*[*[not(*)]]">
    <xsl:copy>
        <job status="active">
            <xsl:attribute name="jobs">
                <xsl:for-each select="*[not(*)]">
                    <xsl:value-of select="name()"/>
                    <xsl:text>, </xsl:text>
                 </xsl:for-each>                
            </xsl:attribute>
            <xsl:apply-templates select="*[*]"/>
        </job>  
    </xsl:copy>
</xsl:template>

<!-- ancestor of non-active jobs -->
<xsl:template match="person-non-active">
    <job status="non-active">
        <xsl:attribute name="jobs">
            <xsl:for-each select=".//*[not(*)]">
                <xsl:value-of select="name()"/>
                <xsl:text>, </xsl:text>
             </xsl:for-each>                
        </xsl:attribute>
    </job>  
</xsl:template>

</xsl:stylesheet>