Hierarchial xml to Flat xml using XSLT

2019-02-14 06:03发布

问题:

I have xml which is structed like following

<root>
  <PNode>
    <node1>
      <node1Child>data</node1Child>
      <node2Child>data</node2Child>
    </node1>
  </PNode>
  <SecondNode>
    <node1>
      <node1Child>
        <child>data</child>
      </node1Child>
    </node1>
  </SecondNode>
</root>

I want output using genric xslt because i have many xml to convert to this format.

<root>
  <Pnode-node1-node1Child>data</Pnode-node1-node1Child>
  <Pnode-node1-node2Child>data</Pnode-node1-node2Child>
  <SecondNode-node1-node1child-child>data</SecondNode-node1-node1child-child>
</root>

It could be more deep or less.
can I do it by XSLT pls give any example or reference

I want to do it to generate PDF from sql server 2k8 r2 rdl. because rdl do not accept nested xml so need to flatten it.

回答1:

This stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:strip-space elements="*"/>
    <xsl:template match="/*">
        <xsl:copy>
            <xsl:apply-templates/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="*">
        <xsl:param name="pName"/>
        <xsl:apply-templates>
            <xsl:with-param name="pName" select="concat($pName,name(),'-')"/>
        </xsl:apply-templates>
    </xsl:template>
    <xsl:template match="text()">
        <xsl:param name="pName"/>
        <xsl:element name="{substring($pName,1,string-length($pName)-1)}">
            <xsl:value-of select="."/>
        </xsl:element>
    </xsl:template>
</xsl:stylesheet>

Output:

<root>
    <PNode-node1-node1Child>data</PNode-node1-node1Child>
    <PNode-node1-node2Child>data</PNode-node1-node2Child>
    <SecondNode-node1-node1Child-child>data</SecondNode-node1-node1Child-child>
</root>

Update: If there could be empy nodes...

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:strip-space elements="*"/>
    <xsl:template match="/*">
        <xsl:copy>
            <xsl:apply-templates/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="*">
        <xsl:param name="pName"/>
        <xsl:apply-templates>
            <xsl:with-param name="pName" select="concat($pName,name(),'-')"/>
        </xsl:apply-templates>
    </xsl:template>
    <xsl:template match="*[not(*)]">
        <xsl:param name="pName"/>
        <xsl:element name="{$pName}{name()}">
            <xsl:value-of select="."/>
        </xsl:element>
    </xsl:template>
</xsl:stylesheet>

Note: Matching innermost element.



回答2:

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="/*">
     <root>
      <xsl:apply-templates/>
     </root>
 </xsl:template>

 <xsl:template match="text()">
  <xsl:variable name="vCompName">
   <xsl:for-each select="ancestor::*[not(position() =last())]">
    <xsl:value-of select="translate(name(), ':', '_')"/>
    <xsl:if test="not(position()=last())">-</xsl:if>
   </xsl:for-each>
  </xsl:variable>

  <xsl:element name="{$vCompName}">
   <xsl:value-of select="."/>
  </xsl:element>
 </xsl:template>
</xsl:stylesheet>

when applied on the provided XML document:

<root>
    <PNode>
        <node1>
            <node1Child>data</node1Child>
            <node2Child>data</node2Child>
        </node1>
    </PNode>
    <SecondNode>
        <node1>
            <node1Child>
                <child>data</child>
            </node1Child>
        </node1>
    </SecondNode>
</root>

produces the wanted, correct result:

<root>
   <PNode-node1-node1Child>data</PNode-node1-node1Child>
   <PNode-node1-node2Child>data</PNode-node1-node2Child>
   <SecondNode-node1-node1Child-child>data</SecondNode-node1-node1Child-child>
</root>

Explanation:

  1. Besides wrapping the document in a root top element, there is just a single template. It matches any non-whitespace-only text node.

  2. The nodeset of all element-ancestors except the first one in document order (which is the last in the reverse axis ancestor::) are string-joined with a '-' character and an element is constructed with this string as a name.

  3. Before the string-join operation in 2. above, every name is modified so that any ':' character in it is substituted by an underline character. This makes the transformation not produce invalid compound names if there are namespace prefixes in some names.

  4. Finally, the current text node is copied as a child of the dynamically-constructed element.



回答3:

Given this input:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <PNode>
        <node1>
            <node1Child>data</node1Child>
            <node2Child>data</node2Child>
        </node1>
    </PNode>
    <SecondNode>
        <node1>
            <node1Child>
                <child>data</child>
            </node1Child>
        </node1>
    </SecondNode>
</root>

The following stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:strip-space elements="*"/>
    <xsl:output indent="yes" method="xml"/>

    <xsl:template match="root">
        <xsl:copy>
            <xsl:apply-templates select="*" mode="flatten"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="*[normalize-space(text())]" mode="flatten">
        <xsl:param name="name-prefix" select="''"/>
        <xsl:variable name="name">
            <xsl:call-template name="construct-name">
                <xsl:with-param name="name-prefix" select="$name-prefix"/>
            </xsl:call-template>
        </xsl:variable>

        <xsl:element name="{$name}">
            <xsl:apply-templates select="text()"/>
        </xsl:element>

        <xsl:apply-templates select="node()" mode="flatten">
            <xsl:with-param name="name-prefix" select="$name"/>
        </xsl:apply-templates>
    </xsl:template>

    <xsl:template match="*[not(normalize-space(text()))]" mode="flatten">
        <xsl:param name="name-prefix" select="''"/>

        <xsl:variable name="prefix">
            <xsl:call-template name="construct-name">
                <xsl:with-param name="name-prefix" select="$name-prefix"/>
            </xsl:call-template>
        </xsl:variable>

        <xsl:apply-templates select="node()" mode="flatten">
            <xsl:with-param name="name-prefix" select="$prefix"/>
        </xsl:apply-templates>
    </xsl:template>

    <xsl:template name="construct-name">
        <xsl:param name="name-prefix"/>
        <xsl:choose>
            <xsl:when test="$name-prefix">
                <xsl:value-of select="concat($name-prefix, '-', local-name(.))"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="local-name(.)"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <xsl:template match="text()" mode="flatten"/>
</xsl:stylesheet>

Produces wanted result:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <PNode-node1-node1Child>data</PNode-node1-node1Child>
    <PNode-node1-node2Child>data</PNode-node1-node2Child>
    <SecondNode-node1-node1Child-child>data</SecondNode-node1-node1Child-child>
</root>


回答4:

In your template, test for a child node.

If there is a child node, pass it previous parameter value concatenated with the name of this element.

If there is only #text, output a new element using the parameter as its name, and set its contents to the #text