0

How would I create a collapsed xml structure from of an inconsistent list of xpaths? xslt 3.0 / 2.0 is preferred.

Input xml

<root>
    <accepted>
        <x xp="vehicle/car/models/model/part/partnumber"/>
        <x xp="vehicle/car/models/model/part/vendor"/>
        <x xp="vehicle/car/models/model/part/vendor/name"/>
        <x xp="vehicle/car/models/model/part/vendor/email"/>
    </accepted>
    <rejected>
        <x xp="vehicle/car/models/model/part/partnumber"/>
        <x xp="vehicle/car/models/model/part/vendor"/>
        <x xp="vehicle/car/models/model/part/vendor/name"/>
        <x xp="vehicle/car/models/model/part/vendor/email"/>
        <x xp="vehicle/car/models/model/part/vendor/telephone"/>
    </rejected>
    <offices>
        <x xp="country/city/name"/>
        <x xp="country/city/district/name"/>
        <x xp="country/city/district/numberofstores"/>
        <x xp="country/city/district/totalrevenue"/>
    </offices>
</root>

Desired output:

<xml>
    <vehicle>
        <car>
            <models>
                <model>
                    <part>
                        <partnumber/>
                        <vendor>
                            <name/>
                            <email/>
                            <telephone/>
                        </vendor>
                    </part>
                </model>
            </models>
        </car>
    </vehicle>
    <country>
        <city>
            <district>
                <name/>
                <numberofstores/>
                <totalrevenue/>
            </district>
        </city>
    </country>
</xml>

What I tried: I removed the duplicate xpaths using distinct-values() and then looped over this list of unique strings. For each unique string I applied tokenize() and created a nested xml element for each delimited portion of the string. The result is an xml node which I stored in a variable. But the problem now is that I end up with with a child node for each unique xpath and I couldn't figure out how to merge these nodes. The alternative question would be how would I merge the below xml structure into a collapsed tree? (keeping in mind that this source xml comes from a variable)

<xml>
    <vehicle>
        <car>
            <models>
                <model>
                    <part>
                        <partnumber/>
                    </part>
                </model>
            </models>
        </car>
    </vehicle>
    <vehicle>
        <car>
            <models>
                <model>
                    <part>
                        <vendor>
                            <name/>
                        </vendor>
                    </part>
                </model>
            </models>
        </car>
    </vehicle>
    ...
    <country>
        <city>
            <district>
                <name/>
                <numberofstores/>
                <totalrevenue/>
            </district>
        </city>
    </country>
</xml>

1 Answer 1

2

You can use a recursive grouping function:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:mf="http://example.com/mf"
    exclude-result-prefixes="#all"
    version="3.0">

  <xsl:function name="mf:group" as="element()*">
      <xsl:param name="paths" as="xs:string*"/>
      <xsl:for-each-group select="$paths" group-by="if (contains(., '/')) then substring-before(., '/') else if (. != '') then . else ()">
          <xsl:element name="{current-grouping-key()}">
              <xsl:sequence select="mf:group(current-group() ! substring-after(., '/'))"/>
          </xsl:element>              
      </xsl:for-each-group>
  </xsl:function>

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

  <xsl:template match="/">
    <xml>
        <xsl:sequence select="mf:group(//@xp)"/>
    </xml>
  </xsl:template>
  
</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/pNvtBGK

Sign up to request clarification or add additional context in comments.

2 Comments

@Martin Honnen. Great answer, I'm trying to figure out what "current-group() ! substring-after(., '/')" does exactly?
@Sebastien, the map operator ! is a compact XPath 3 form usable instead of for $path in current-group() return substring-after($path, '/') so in this case each string in the group is mapped to the part after /, to recurse down the levels of nesting.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.