XSLT Create a hierarchical structure from a flat s

2019-03-03 12:50发布

问题:

I want to create a XSD structure from an Excel output using XSLT. But my XSLT does not generate the hierarchical structure, correctly. It has some additional nodes in an element group, if these nodes with same matching parameters are defined in a later group, which has the same level in the hierarchy.

The Excel XML output looks like following:

 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <nodes>
    <node typeTag="Test" nodeTag="GGG" minOccurs="0" segmentMaxOccurs="1" groupMaxOccurs="1000000">
        <metaInfo><ID>40</ID><Level>1</Level></metaInfo>
    </node>
    <node typeTag="Test" nodeTag="BBB" minOccurs="0" segmentMaxOccurs="1" groupMaxOccurs="1000000">
        <metaInfo><ID>60</ID><Level>2</Level></metaInfo>
    </node>
    <node typeTag="Test" nodeTag="XXX" minOccurs="0" segmentMaxOccurs="1000000" groupMaxOccurs="">
        <metaInfo><ID>80</ID><Level>2</Level></metaInfo>
    </node>
    <node typeTag="Test" nodeTag="AAA" minOccurs="0" segmentMaxOccurs="1" groupMaxOccurs="1000000">
        <metaInfo><ID>90</ID><Level>1</Level></metaInfo>
    </node>
    <node typeTag="Test" nodeTag="WWW" minOccurs="0" segmentMaxOccurs="1" groupMaxOccurs="1000000">
        <metaInfo><ID>110</ID><Level>1</Level></metaInfo>
    </node>
    <node typeTag="Test" nodeTag="OOO" minOccurs="0" segmentMaxOccurs="1" groupMaxOccurs="1000000">
        <metaInfo><ID>130</ID><Level>1</Level></metaInfo>
    </node>
    <node typeTag="Test" nodeTag="AAA" minOccurs="0" segmentMaxOccurs="1" groupMaxOccurs="1000000">
        <metaInfo><ID>140</ID><Level>2</Level></metaInfo>
    </node>
    <node typeTag="Test" nodeTag="WWW" minOccurs="0" segmentMaxOccurs="1" groupMaxOccurs="1000000">
        <metaInfo><ID>160</ID><Level>2</Level></metaInfo>
    </node>
 </nodes>

Each node should be an element, whereby @segmentMaxOccurs != 0 will generate leaf elementa and @groupMaxOccurs will generate group elements. The element "Level" defines the hierarchy of the structure (it should be a nested structure) and the element "ID" is an ordered unique identifier.

Based on this input, I would like to get the following XSD structure. In order to just focus on my issue, I stripped out all not relevant items such as "xs:sequence" or "xs:complexType":

<?xml version="1.0" encoding="UTF-8"?>
<xs:element xmlns:xs="http://www.w3.org/2001/XMLSchema" name="Test">
    <xs:element name="GGG" minOccurs="0" maxOccurs="1000000" id="40">
        <xs:element name="GGG" minOccurs="0" maxOccurs="1" id="40"/>
        <xs:element name="BBB" minOccurs="0" maxOccurs="1000000" id="60">
            <xs:element name="BBB" minOccurs="0" maxOccurs="1" id="60"/>
            <xs:element name="XXX" minOccurs="0" maxOccurs="1000000" id="80"/>
        </xs:element>
    </xs:element>
    <xs:element name="AAA" minOccurs="0" maxOccurs="1000000" id="90">
        <xs:element name="AAA" minOccurs="0" maxOccurs="1" id="90"/>
    </xs:element>
    <xs:element name="WWW" minOccurs="0" maxOccurs="1000000" id="110">
        <xs:element name="WWW" minOccurs="0" maxOccurs="1" id="110"/>
    </xs:element>
    <xs:element name="OOO" minOccurs="0" maxOccurs="1000000" id="130">
        <xs:element name="OOO" minOccurs="0" maxOccurs="1" id="130"/>
        <xs:element name="AAA" minOccurs="0" maxOccurs="1000000" id="140">
            <xs:element name="AAA" minOccurs="0" maxOccurs="1" id="140"/>
        </xs:element>
        <xs:element name="WWW" minOccurs="0" maxOccurs="1000000" id="160">
            <xs:element name="WWW" minOccurs="0" maxOccurs="1" id="160"/>
        </xs:element>
    </xs:element>
</xs:element>

But I just get the following output:

<?xml version="1.0" encoding="UTF-8"?>
<xs:element xmlns:xs="http://www.w3.org/2001/XMLSchema" name="Test">
    <xs:element name="GGG" minOccurs="0" maxOccurs="1000000" id="40">
        <xs:element name="GGG" minOccurs="0" maxOccurs="1" id="40"/>
        <xs:element name="BBB" minOccurs="0" maxOccurs="1000000" id="60">
            <xs:element name="BBB" minOccurs="0" maxOccurs="1" id="60"/>
            <xs:element name="XXX" minOccurs="0" maxOccurs="1000000" id="80"/>
        </xs:element>
        <xs:element name="AAA" minOccurs="0" maxOccurs="1000000" id="140">
            <xs:element name="AAA" minOccurs="0" maxOccurs="1" id="140"/>
        </xs:element>
        <xs:element name="WWW" minOccurs="0" maxOccurs="1000000" id="160">
            <xs:element name="WWW" minOccurs="0" maxOccurs="1" id="160"/>
        </xs:element>
    </xs:element>
    <xs:element name="AAA" minOccurs="0" maxOccurs="1000000" id="90">
        <xs:element name="AAA" minOccurs="0" maxOccurs="1" id="90"/>
    </xs:element>
    <xs:element name="WWW" minOccurs="0" maxOccurs="1000000" id="110">
        <xs:element name="WWW" minOccurs="0" maxOccurs="1" id="110"/>
    </xs:element>
    <xs:element name="OOO" minOccurs="0" maxOccurs="1000000" id="130">
        <xs:element name="OOO" minOccurs="0" maxOccurs="1" id="130"/>
        <xs:element name="AAA" minOccurs="0" maxOccurs="1000000" id="140">
            <xs:element name="AAA" minOccurs="0" maxOccurs="1" id="140"/>
        </xs:element>
        <xs:element name="WWW" minOccurs="0" maxOccurs="1000000" id="160">
            <xs:element name="WWW" minOccurs="0" maxOccurs="1" id="160"/>
        </xs:element>
    </xs:element>
</xs:element>

The group "GGG" has the two addtional groups AAA (id = 140) and WWW (id = 160), which should be just underneath the group OOO. Do you have any idea, how just the correct nodes of GGG will be processed until the group AAA (id = 90), which is at the same level like element group "GGG". I don't have idea, how these nodes will be consumed that just belong to element group "GGG".

This is the part of the XSLT, which is relevant for generating the hierarchical structure:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:nestor="http://sap.com/ia" xmlns:ns1="http://www.sap.com/ia" xmlns:functx="http://sap.com/xslt/functions">
    <xsl:output method="xml" indent="yes"/>
    <xsl:param name="pRootNode" select="'Test'"/>
    <xsl:template match="nodes">
        <xsl:variable name="vRootNode" select="$pRootNode"/>
        <xsl:element name="xs:element">
            <xsl:attribute name="name" select="$vRootNode"/>
            <xsl:variable name="vLevel" select="0"/>
            <xsl:variable name="vNextLevel" select="1"/>
            <xsl:variable name="vGroupCounter" select="node[@typeTag = $vRootNode and @groupMaxOccurs != '']"/>
            <xsl:apply-templates select="node[@typeTag = $vRootNode and ((@groupMaxOccurs != '' and ./metaInfo/Level = $vNextLevel) or (@groupMaxOccurs = '' and ./metaInfo/Level = $vLevel))]" mode="MessageStructure">
                <xsl:with-param name="pRootNode" select="$vRootNode"/>
                <xsl:with-param name="pLevel" select="$vLevel"/>
                <xsl:with-param name="pGroupCounter" select="$vGroupCounter"/>
            </xsl:apply-templates>
        </xsl:element>
    </xsl:template>
    <xsl:template match="node[@groupMaxOccurs = '']" mode="MessageStructure">
        <xsl:param name="pRootNode"/>
        <xsl:param name="pLevel"/>
        <xsl:param name="pGroupCounter"/>
        <xsl:variable name="vActGroupCounter" select="count(following-sibling::node[@typeTag = $pRootNode and @groupMaxOccurs != ''])"/>
        <xsl:if test="./metaInfo/Level = $pLevel and ($vActGroupCounter = $pGroupCounter or $pLevel = 0)">
            <xsl:element name="xs:element">
                <xsl:attribute name="name" select="@nodeTag"/>
                <xsl:attribute name="minOccurs" select="@minOccurs"/>
                <xsl:attribute name="maxOccurs" select="@segmentMaxOccurs"/>
                <xsl:attribute name="id" select="./metaInfo/ID"/>
            </xsl:element>
        </xsl:if>
    </xsl:template>
    <xsl:template match="node[@groupMaxOccurs != '']" mode="MessageStructure">
        <xsl:param name="pRootNode"/>
        <xsl:param name="pLevel"/>
        <xsl:param name="pGroupCounter"/>
        <xsl:variable name="vNextLevel" select="$pLevel + 1"/>
        <xsl:variable name="vActGroupCounter" select="count(following-sibling::node[@typeTag = $pRootNode and @groupMaxOccurs != ''])"/>
        <xsl:variable name="vNextGroupLevel" select="following-sibling::node[@typeTag = $pRootNode and @groupMaxOccurs != ''][1]/metaInfo/Level"/>
        <xsl:element name="xs:element">
            <xsl:attribute name="name" select="@nodeTag"/>
            <xsl:attribute name="minOccurs" select="@minOccurs"/>
            <xsl:attribute name="maxOccurs" select="@groupMaxOccurs"/>
            <xsl:attribute name="id" select="./metaInfo/ID"/>
            <xsl:element name="xs:element">
                <xsl:attribute name="name" select="@nodeTag"/>
                <xsl:attribute name="minOccurs" select="@minOccurs"/>
                <xsl:attribute name="maxOccurs" select="@segmentMaxOccurs"/>
                <xsl:attribute name="id" select="./metaInfo/ID"/>
            </xsl:element>
            <xsl:apply-templates select="following-sibling::node[@typeTag = $pRootNode and ((@groupMaxOccurs != '' and ./metaInfo/Level = $vNextLevel + 1 and ./metaInfo/Level = $vNextGroupLevel) or (@groupMaxOccurs = '' and ./metaInfo/Level = $vNextLevel))]" mode="MessageStructure">
                <xsl:with-param name="pRootNode" select="$pRootNode"/>
                <xsl:with-param name="pLevel" select="$vNextLevel"/>
                <xsl:with-param name="pGroupCounter" select="$vActGroupCounter"/>
            </xsl:apply-templates>
        </xsl:element>
    </xsl:template>
</xsl:stylesheet>

Any support is really appreciated. Many thanks in advance.

回答1:

Here is a suggestion using a recursive function:

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

  <xsl:output indent="yes"/>

  <xsl:function name="mf:group" as="element()*">
      <xsl:param name="nodes" as="element(node)*"/>
      <xsl:param name="level" as="xs:integer"/>
      <xsl:for-each-group select="$nodes" group-starting-with="node[metaInfo/Level = $level and @groupMaxOccurs != '']">
        <xs:element name="{@nodeTag}" minOccurs="{@minOccurs}" maxOccurs="{@groupMaxOccurs}" id="{metaInfo/ID}">
            <xs:element name="{@nodeTag}" minOccurs="0" maxOccurs="1" id="{metaInfo/ID}"/>
            <xsl:choose>
                <xsl:when test="(current-group() except .)/metaInfo/Level = $level + 1">
                    <xsl:sequence select="mf:group(current-group() except ., $level + 1)"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:apply-templates select="current-group() except ."/>
                </xsl:otherwise>
            </xsl:choose>
        </xs:element>
      </xsl:for-each-group>
  </xsl:function>

  <xsl:template match="nodes">
      <xs:schema>
          <xsl:sequence select="mf:group(node, 1)"/>
      </xs:schema>
  </xsl:template>

  <xsl:template match="node">
      <xs:element name="{@nodeTag}" minOccurs="0" maxOccurs="{@segmentMaxOccurs}" id="{metaInfo/ID}"/>
  </xsl:template>

</xsl:transform>

You will have to test yourself with more levels or provide some input samples with deeper nesting to allow us to test.



回答2:

Let me suggest a different approach to creating the nested hierarchy. To demonstrate, I will use the following minimized input:

XML

<nodes>
   <node nodeTag="A">
      <metaInfo>
         <Level>1</Level>
      </metaInfo>
   </node>
   <node nodeTag="Ab">
      <metaInfo>
         <Level>2</Level>
      </metaInfo>
   </node>
   <node nodeTag="Ab1">
      <metaInfo>
         <Level>3</Level>
      </metaInfo>
   </node>
   <node nodeTag="Ab2">
      <metaInfo>
         <Level>3</Level>
      </metaInfo>
   </node>
   <node nodeTag="Ac">
      <metaInfo>
         <Level>2</Level>
      </metaInfo>
   </node>
   <node nodeTag="B">
      <metaInfo>
         <Level>1</Level>
      </metaInfo>
   </node>
   <node nodeTag="C">
      <metaInfo>
         <Level>1</Level>
      </metaInfo>
   </node>
   <node nodeTag="D">
      <metaInfo>
         <Level>1</Level>
      </metaInfo>
   </node>
   <node nodeTag="Da">
      <metaInfo>
         <Level>2</Level>
      </metaInfo>
   </node>
   <node nodeTag="Db">
      <metaInfo>
         <Level>2</Level>
      </metaInfo>
   </node>
</nodes>

Applying the following stylesheet:

XSLT

<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:key name="child-by-parent" 
         match="node" 
         use="generate-id(preceding-sibling::node[metaInfo/Level=current()/metaInfo/Level - 1][1])" />

<xsl:template match="nodes">
    <root>
        <xsl:apply-templates select="node[metaInfo/Level=1]"/>
    </root>
</xsl:template>

<xsl:template match="node">
    <element name="{@nodeTag}">
        <xsl:apply-templates select="key('child-by-parent', generate-id())"/>
    </element>
</xsl:template>

</xsl:stylesheet>

will return:

Result

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <element name="A">
      <element name="Ab">
         <element name="Ab1"/>
         <element name="Ab2"/>
      </element>
      <element name="Ac"/>
   </element>
   <element name="B"/>
   <element name="C"/>
   <element name="D">
      <element name="Da"/>
      <element name="Db"/>
   </element>
</root>

This works recursively with any number of levels.