Search code examples
xmlxsltxslt-1.0hierarchicalflat

Flat to Hierarchical XSLT Transform


I'm having pretty significant trouble figuring out how to convert a flat XML into a hierarchical result using an XSLT V1 transform. I would post my XSLT, but I'm not sure I'm even approaching it correctly. I know I need to create and use Keys, but, regardless of attempting to apply every example I've found here and elsewhere (many, many, many...), it simply doesn't work.

Here is the input:

<env:Envelope xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
    <env:Header/>
    <env:Body>
        <tns:getClientRS xmlns:tns="http://services.xxx.com/ClientService">
            <tns:Acknowledgement>Process completed successfully.</tns:Acknowledgement>
            <tns:client>
                <tns:ClientID>515164</tns:ClientID>
                <tns:episodeID>1</tns:episodeID>
                <tns:guarantorID>1</tns:guarantorID>
            </tns:client>
            <tns:client>
                <tns:ClientID>515164</tns:ClientID>
                <tns:episodeID>1</tns:episodeID>
                <tns:guarantorID>2</tns:guarantorID>
            </tns:client>
            <tns:plan>
                <tns:ClientID>515164</tns:ClientID>
                <tns:episodeID>1</tns:episodeID>
                <tns:guarantorID>1</tns:guarantorID>
                <tns:guarantorPlan>1</tns:guarantorPlan>
            </tns:plan>
            <tns:plan>
                <tns:ClientID>515164</tns:ClientID>
                <tns:episodeID>1</tns:episodeID>
                <tns:guarantorID>2</tns:guarantorID>
                <tns:guarantorPlan>2</tns:guarantorPlan>
            </tns:plan>
        </tns:getClientRS>
    </env:Body>
</env:Envelope>

and desired response XML:

<env:Envelope xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
    <env:Header/>
    <env:Body>
        <tns:getClientRS xmlns:tns="http://services.xxx.com/ClientService">
            <tns:Acknowledgement>Process completed successfully.</tns:Acknowledgement>
            <tns:clients>
                <tns:ClientID>515164</tns:ClientID>
                <tns:episodes>
                    <tns:Episode>
                        <tns:episodeID>1</tns:episodeID>
                        <tns:Guarantors>
                            <tns:Guarantor>
                                <tns:guarantorID>1</tns:guarantorID>
                                <tns:Plan>
                                    <tns:guarantorPlan>1</tns:guarantorPlan>
                                </tns:Plan>
                            </tns:Guarantor>
                            <tns:Guarantor>
                                <tns:guarantorID>2</tns:guarantorID>
                                <tns:Plan>
                                    <tns:guarantorPlan>2</tns:guarantorPlan>
                                </tns:Plan>
                            </tns:Guarantor>
                        </tns:Guarantors>
                    </tns:Episode>
                </tns:episodes>
            </tns:clients>
        </tns:getClientRS>
    </env:Body>
</env:Envelope>

Just trying to keep things simple, but there could be multiple clients and multiple episodes. I suspect that if I can figure out the guarantors, I can figure out the episodes and clients.

I really need help here. No matter what I do, I get only the first of each key... that is, guarantor 1 and plan 1. That's it.


Solution

  • I suggest you try it this way:

    XSLT 1.0 + EXSLT set:distinct()

    <xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tns="http://services.xxx.com/ClientService"
    xmlns:set="http://exslt.org/sets"
    extension-element-prefixes="set">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    
    <xsl:key name="client" match="tns:client" use="tns:ClientID" />
    <xsl:key name="plan" match="tns:plan" use="concat(tns:ClientID, '|', tns:episodeID)" />
    
    <xsl:template match="/">
        <env:Envelope xmlns:wsa="http://www.w3.org/2005/08/addressing">
            <env:Header/>
            <env:Body>
                <tns:getClientRS>
                    <xsl:copy-of select="tns:Acknowledgement"/>
                    <tns:clients>
                        <!-- client -->
                        <xsl:for-each select="set:distinct(env:Envelope/env:Body/tns:getClientRS/tns:client/tns:ClientID)">
                            <xsl:variable name="clientID" select="." />
                            <xsl:copy-of select="."/>
                             <tns:episodes>
                                <!-- episode -->
                                <xsl:for-each select="set:distinct(key('client', .)/tns:episodeID)">
                                    <tns:Episode>
                                        <xsl:variable name="episodeID" select="." />
                                        <xsl:copy-of select="."/>
                                            <!-- plans -->
                                            <tns:Guarantors>
                                                <xsl:for-each select="key('plan', concat($clientID, '|', $episodeID))">
                                                    <tns:Guarantor>
                                                        <xsl:copy-of select="tns:guarantorID"/>
                                                        <tns:Plan>
                                                            <xsl:copy-of select="tns:guarantorPlan"/>
                                                        </tns:Plan>
                                                    </tns:Guarantor>
                                                </xsl:for-each> 
                                            </tns:Guarantors>
                                    </tns:Episode>
                                </xsl:for-each> 
                            </tns:episodes> 
                        </xsl:for-each> 
                    </tns:clients>
                </tns:getClientRS>
            </env:Body>
        </env:Envelope>
    </xsl:template>
    
    </xsl:stylesheet>
    

    This returns the expected result for your input example - though it could be partly a matter of coincidence. If you have multiple plans per guarantor, you'll need to add one more level.