Rubén
Rubén

Reputation: 427

XSLT xml to xml grouping nodes and transform to other structure

I'm having a xml document which I would like to transform from:

<?xml version="1.0" encoding="UTF-8"?>
<import:configuration xmlns:import="http://schemas.company.com/wsdl/domain/v2/import">
    <import:input>
        <import:file headers="1" group="MAPPING">
            <import:name>file001.txt</import:name>
            <import:separator><![CDATA[;]]></import:separator>
            <import:table>TAB00008_TECSPEC</import:table>
            <import:field primary="true">
                <import:name>VEMAR</import:name>
                <import:target>VEMAR</import:target>
                <import:type dbs="oracle" type="text">VARCHAR(3)</import:type>
                <import:reference>
                    <import:table>TAB00006_TECSPEC</import:table>
                    <import:field>VEMAR</import:field>
                </import:reference>
                <import:description><![CDATA[]]></import:description>
            </import:field>
            <import:field primary="true">
                <import:name>VENR</import:name>
                <import:target>VENR</import:target>
                <import:type dbs="oracle" type="numeric">NUMBER(7)</import:type>
                <import:reference>
                    <import:table>TAB00006_TECSPEC</import:table>
                    <import:field>VENR</import:field>
                </import:reference>
                <import:description><![CDATA[]]></import:description>
            </import:field>
            <import:field primary="true">
                <import:name>KNR</import:name>
                <import:target>KNR</import:target>
                <import:type dbs="oracle" type="numeric">NUMBER(9)</import:type>
                <import:reference>
                    <import:table>TAB00003_TECSPEC</import:table>
                    <import:field>KNR</import:field>
                </import:reference>
                <import:description><![CDATA[]]></import:description>
            </import:field>
            <import:field>
                <import:name>DATNEU</import:name>
                <import:target>DATNEU</import:target>
                <import:type dbs="oracle" type="date" format="YYYY.MM.DD">DATE</import:type>
                <import:description><![CDATA[]]></import:description>
            </import:field>
            <import:field>
                <import:name>VTYPE</import:name>
                <import:target>VTYPE</import:target>
                <import:type dbs="oracle" type="numeric">NUMBER(1)</import:type>
                <import:description><![CDATA[]]></import:description>
            </import:field>
            <import:description><![CDATA[]]></import:description>
        </import:file>
    </import:input>
</import:configuration>

To:

<?xml version="1.0" encoding="UTF-8"?>
<import:configuration xmlns:import="http://schemas.company.com/wsdl/domain/v2/import">
    <import:input>
        <import:file headers="1" group="MAPPING">
            <import:name>file001.txt</import:name>
            <import:separator><![CDATA[;]]></import:separator>
            <import:table>TAB00008_TECSPEC</import:table>
            <import:field primary="true">
                <import:name>VEMAR</import:name>
                <import:target>VEMAR</import:target>
                <import:type dbs="oracle" type="text">VARCHAR(3)</import:type>
                <import:description><![CDATA[]]></import:description>
            </import:field>
            <import:field primary="true">
                <import:name>VENR</import:name>
                <import:target>VENR</import:target>
                <import:type dbs="oracle" type="numeric">NUMBER(7)</import:type>
                <import:description><![CDATA[]]></import:description>
            </import:field>
            <import:field primary="true">
                <import:name>KNR</import:name>
                <import:target>KNR</import:target>
                <import:type dbs="oracle" type="numeric">NUMBER(9)</import:type>
                <import:description><![CDATA[]]></import:description>
            </import:field>
            <import:field>
                <import:name>DATNEU</import:name>
                <import:target>DATNEU</import:target>
                <import:type dbs="oracle" type="date" format="YYYY.MM.DD">DATE</import:type>
                <import:description><![CDATA[]]></import:description>
            </import:field>
            <import:field>
                <import:name>VTYPE</import:name>
                <import:target>VTYPE</import:target>
                <import:type dbs="oracle" type="numeric">NUMBER(1)</import:type>
                <import:description><![CDATA[]]></import:description>
            </import:field>
            <import:reference>
                <import:table>TAB00006_TECSPEC</table>
                <import:link>
                    <source>VEMAR</source>
                    <target>VEMAR</target>
                </import:link>
                <import:link>
                    <source>VENR</source>
                    <target>VENR</target>
                </import:link>              
            </import:reference>
            <import:reference>
                <import:table>TAB00003_TECSPEC</table>
                <import:link>
                    <source>KNR</source>
                    <target>KNR</target>
                </import:link>          
            </import:reference>         
            <import:description><![CDATA[]]></import:description>
        </import:file>
    </import:input>
</import:configuration>

I want to group all references (</import:reference> under <import:field>) and grouped by table transform them to one element as described above.

I was reading this posting/question: XML to CSV with XSLT - Grouping nodes but I can't get this working to get the desired output.

My knowledge in xslt is not so deep. Can anybody give a hint how I could do this?

Upvotes: 0

Views: 612

Answers (2)

Tomalak
Tomalak

Reputation: 338128

Your attempt is too complicated. Things I've noted:

  • You can use <xsl:copy> and <xsl:copy-of> to make copies of input nodes. You don't need to re-create them manually.
  • You are overusing XPath functions. There is no need to be this specific.
  • If you find yourself solving a complete task in a single template, you are misusing XSLT. (That's true for every programming language - if you cram everything into one function then there's something wrong.)

Your task is of the "I need a copy of the input file, but with a small modification" variety.

The basis for these task always is the identity transform.

<xsl:transform
    version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
</xsl:transform>

This makes a verbatim copy of the input. Now to your small modifications. XSLT works through template matching, so we need an <xsl:template> for everything we want to make a modification to.

  1. You want the <import:reference> removed from <import:field> in the final output. That's easy, write a template for them that produces no output:

    <xsl:template match="import:reference" />
    
  2. You want a new <import:reference> inside the <import:file>, one per <import:table>. That's not so difficult, either. Write a template that matches import:file, copies most of it and appends something for each import:table group.

    <xsl:template match="import:file">
      <xsl:copy>
        <xsl:apply-templates select="@* | node()" />
        <xsl:for-each-group select=".//import:reference" group-by="import:table">
          <xsl:copy>
            <xsl:copy-of select="import:table" />
            <import:link>
              <source><xsl:value-of select="../import:target" /></source>
              <target><xsl:value-of select="import:field" /></target>
            </import:link>
          </xsl:copy>
        </xsl:for-each-group>
      </xsl:copy>
    </xsl:template>
    

Putting it together:

<xsl:transform
  version="2.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:import="http://schemas.company.com/wsdl/domain/v2/import"
>
  <xsl:output method="xml" encoding="UTF-8" indent="yes" />
  <xsl:strip-space elements="*" />

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

  <xsl:template match="import:file">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()" />
      <xsl:for-each-group select=".//import:reference" group-by="import:table">
        <xsl:copy>
          <xsl:copy-of select="import:table" />
          <import:link>
            <source><xsl:value-of select="../import:target" /></source>
            <target><xsl:value-of select="import:field" /></target>
          </import:link>
        </xsl:copy>
      </xsl:for-each-group>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="import:reference" />
</xsl:transform>

Produces pretty much exactly your desired result:

<import:configuration xmlns:import="http://schemas.company.com/wsdl/domain/v2/import">
   <import:input>
      <import:file headers="1" group="MAPPING">
         <import:name>file001.txt</import:name>
         <import:separator>;</import:separator>
         <import:table>TAB00008_TECSPEC</import:table>
         <import:field primary="true">
            <import:name>VEMAR</import:name>
            <import:target>VEMAR</import:target>
            <import:type dbs="oracle" type="text">VARCHAR(3)</import:type>
            <import:description/>
         </import:field>
         <import:field primary="true">
            <import:name>VENR</import:name>
            <import:target>VENR</import:target>
            <import:type dbs="oracle" type="numeric">NUMBER(7)</import:type>
            <import:description/>
         </import:field>
         <import:field primary="true">
            <import:name>KNR</import:name>
            <import:target>KNR</import:target>
            <import:type dbs="oracle" type="numeric">NUMBER(9)</import:type>
            <import:description/>
         </import:field>
         <import:field>
            <import:name>DATNEU</import:name>
            <import:target>DATNEU</import:target>
            <import:type dbs="oracle" type="date" format="YYYY.MM.DD">DATE</import:type>
            <import:description/>
         </import:field>
         <import:field>
            <import:name>VTYPE</import:name>
            <import:target>VTYPE</import:target>
            <import:type dbs="oracle" type="numeric">NUMBER(1)</import:type>
            <import:description/>
         </import:field>
         <import:description/>
         <import:reference>
            <import:table>TAB00006_TECSPEC</import:table>
            <import:link>
               <source>VEMAR</source>
               <target>VEMAR</target>
            </import:link>
         </import:reference>
         <import:reference>
            <import:table>TAB00003_TECSPEC</import:table>
            <import:link>
               <source>KNR</source>
               <target>KNR</target>
            </import:link>
         </import:reference>
      </import:file>
   </import:input>
</import:configuration>

This does not include the xmlns:xsi attribute in the output (wasn't part of the original question). You can add that in the same fashion, by writing a template that modifies import:configuration:

<xsl:template match="import:configuration">
  <xsl:copy>
    <xsl:attribute name="xsi:schemaLocation" namespace="http://www.w3.org/2001/XMLSchema-instance">http://schemas.company.com/wsdl/domain/v2/import C:/Users/Ruben/Downloads/tmp/new/xsd/import_config.xsd</xsl:attribute>
    <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
</xsl:template>

You could move the xsi namespace prefix declaration to the root level in the XSLT.

Upvotes: 1

Rub&#233;n
Rub&#233;n

Reputation: 427

This worked (it brings my near to my desired target):

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ns0="http://schemas.company.com/wsdl/domain/v2/import" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions" exclude-result-prefixes="ns0 xs fn">
    <xsl:output method="xml" encoding="UTF-8" byte-order-mark="no" indent="yes"/>
    <xsl:template match="/">
        <configuration xmlns="http://schemas.company.com/wsdl/domain/v2/import">
            <xsl:attribute name="xsi:schemaLocation" namespace="http://www.w3.org/2001/XMLSchema-instance" select="'http://schemas.company.com/wsdl/domain/v2/import C:/Users/Ruben/Downloads/tmp/new/xsd/import_config.xsd'"/>
            <xsl:for-each select="ns0:configuration">
                <input>
                    <xsl:for-each select="ns0:input/ns0:file">
                        <xsl:variable name="var1_group" as="node()?" select="@group"/>
                        <file>
                            <xsl:attribute name="headers" namespace="" select="xs:string(xs:integer(fn:string(@headers)))"/>
                            <xsl:if test="fn:exists($var1_group)">
                                <xsl:attribute name="group" namespace="" select="fn:string($var1_group)"/>
                            </xsl:if>
                            <name>
                                <xsl:sequence select="fn:string(ns0:name)"/>
                            </name>
                            <xsl:for-each select="ns0:directory">
                                <directory>
                                    <xsl:sequence select="fn:string(.)"/>
                                </directory>
                            </xsl:for-each>
                            <separator>
                                <xsl:sequence select="fn:string(ns0:separator)"/>
                            </separator>
                            <table>
                                <xsl:sequence select="fn:string(ns0:table)"/>
                            </table>
                            <xsl:for-each select="ns0:field">
                                <xsl:variable name="var2_primary" as="node()?" select="@primary"/>
                                <field>
                                    <xsl:if test="fn:exists($var2_primary)">
                                        <xsl:attribute name="primary" namespace="" select="xs:string(xs:boolean(fn:string($var2_primary)))"/>
                                    </xsl:if>
                                    <name>
                                        <xsl:sequence select="fn:string(ns0:name)"/>
                                    </name>
                                    <target>
                                        <xsl:sequence select="fn:string(ns0:target)"/>
                                    </target>
                                    <xsl:for-each select="ns0:type">
                                        <type>
                                            <xsl:sequence select="(./@node(), ./node())"/>
                                        </type>
                                    </xsl:for-each>
                                    <description>
                                        <xsl:sequence select="fn:string(ns0:description)"/>
                                    </description>
                                </field>
                            </xsl:for-each>
                            <xsl:for-each select="ns0:field">
                                <xsl:variable name="var3_current" as="node()" select="."/>
                                <xsl:for-each select="ns0:reference">
                                    <reference>
                                        <table>
                                            <xsl:sequence select="fn:string(ns0:table)"/>
                                        </table>
                                        <link>
                                            <source>
                                                <xsl:sequence select="fn:string($var3_current/ns0:target)"/>
                                            </source>
                                            <target>
                                                <xsl:sequence select="fn:string(ns0:field)"/>
                                            </target>
                                        </link>
                                    </reference>
                                </xsl:for-each>
                            </xsl:for-each>
                            <description>
                                <xsl:sequence select="fn:string(ns0:description)"/>
                            </description>
                        </file>
                    </xsl:for-each>
                </input>
                <description>
                    <xsl:sequence select="fn:string(ns0:description)"/>
                </description>
            </xsl:for-each>
        </configuration>
    </xsl:template>
</xsl:stylesheet>

Upvotes: 0

Related Questions