UD AY
UD AY

Reputation: 13

Generic XSLT transform to manipulate similar XML data

We have a tool which is producing an unwanted XML element and we are using XSLT to translate it to the required format.

We are writing a different XSLT for every XML generated by the file. e.g. one for the Customers XML, one for the Orders XML and so on.

Below are a couple of XML data files produced by the tool and the actual output expected

Customers

Tool-generated XML

<Message>
  <Data>
    <CustomerArray>
      <Customer>
        <X>
          <Name>John</Name>
          <Id>100</Id>
          <Roles>
            <Role>
              <X>Manager</X>
              <X>Architect</X>
            </Role>
          </Roles>
        </X>
        <X>
          <Name>Doe</Name>
          <Id>102</Id>
          <Roles>
            <Role>
              <X>Supervisor</X>
              <X>Admin</X>
            </Role>
          </Roles>
        </X>
      </Customer>
    </CustomerArray>
  </Data>
</Message>

Required XML data

<Message>
  <Data>
    <CustomerArray>
      <Customer>
        <Name>John</Name>
        <Id>100</Id>
        <Roles>
          <Role>Manager</Role>
          <Role>Architect</Role>
        </Roles>
      </Customer>
      <Customer>
        <Name>Doe</Name>
        <Id>102</Id>
        <Roles>
          <Role>Supervisor</Role>
          <Role>Admin</Role>
        </Roles>
      </Customer>
    </CustomerArray>
  </Data>
</Message>

Orders

Tool-generated XML

<Message>
  <Orders>
    <Order>
      <X>
        <OrderNumber>O123</OrderNumber>
        <CustomerID>C100</CustomerID>
        <Quantity>100</Quantity>
        <UnitPrice>10.0</UnitPrice>
      </X>
      <X>
        <OrderNumber>O456</OrderNumber>
        <CustomerID>C107</CustomerID>
        <Quantity>100</Quantity>
        <UnitPrice>5.0</UnitPrice>
      </X>
    </Order>
  </Orders>
</Message>

Required XML data

<Message>
  <Orders>
    <Order>
      <OrderNumber>O123</OrderNumber>
      <CustomerID>C100</CustomerID>
      <Quantity>100</Quantity>
      <UnitPrice>10.0</UnitPrice>
    </Order>
    <Order>
      <OrderNumber>O456</OrderNumber>
      <CustomerID>C107</CustomerID>
      <Quantity>100</Quantity>
      <UnitPrice>5.0</UnitPrice>
    </Order>
  </Orders>
</Message>

The unwanted element X can come at any level.

Is it possible to write a generic XSLT transform to achieve this result across all XML input? For instance, where an X is found, replace it with the parent tag and then delete the parent tag.

Upvotes: 1

Views: 433

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243529

Here is a slightly simpler/shorter solution, which also handles correctly the case when a X can have non X sibling elements:

<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="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="*[X]"><xsl:apply-templates/></xsl:template>

 <xsl:template match="X">
  <xsl:element name="{name(..)}">
   <xsl:apply-templates select="node()|@*"/>
  </xsl:element>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied to the following XML document (the first of the provided ones, with one non-X sibling added to the X elements:

<Message>
  <Data>
    <CustomerArray>
      <Customer>
        <X>
          <Name>John</Name>
          <Id>100</Id>
          <Roles>
            <Role>
              <X>Manager</X>
              <X>Architect</X>
            </Role>
          </Roles>
        </X>
        <somethingElse/>
        <X>
          <Name>Doe</Name>
          <Id>102</Id>
          <Roles>
            <Role>
              <X>Supervisor</X>
              <X>Admin</X>
            </Role>
          </Roles>
        </X>
      </Customer>
    </CustomerArray>
  </Data>
</Message>

the wanted, correct result is produced:

<Message>
   <Data>
      <CustomerArray>
         <Customer>
            <Name>John</Name>
            <Id>100</Id>
            <Roles>
               <Role>Manager</Role>
               <Role>Architect</Role>
            </Roles>
         </Customer>
         <somethingElse/>
         <Customer>
            <Name>Doe</Name>
            <Id>102</Id>
            <Roles>
               <Role>Supervisor</Role>
               <Role>Admin</Role>
            </Roles>
         </Customer>
      </CustomerArray>
   </Data>
</Message>

Do note that the solution by Borodin loses the somethingElse element.

Upvotes: 1

Borodin
Borodin

Reputation: 126742

You need to write an identity transform with an explicit template for all nodes that have any X children so that they can be replicated.

This transform does what you asked. It uses the variable name to save the name of the element that is parent to the X to avoid writing the more obscure name(current()) when it comes to generating each output element.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

  <xsl:strip-space elements="*"/>
  <xsl:output method="xml" indent="yes" encoding="UTF-8" omit-xml-declaration="yes"/>

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

  <xsl:template match="*[X]">
    <xsl:variable name="name" select="name()"/>
    <xsl:for-each select="X">
      <xsl:element name="{$name}">
        <xsl:apply-templates/>
      </xsl:element>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

output

Customers

<Message>
   <Data>
      <CustomerArray>
         <Customer>
            <Name>John</Name>
            <Id>100</Id>
            <Roles>
               <Role>Manager</Role>
               <Role>Architect</Role>
            </Roles>
         </Customer>
         <Customer>
            <Name>Doe</Name>
            <Id>102</Id>
            <Roles>
               <Role>Supervisor</Role>
               <Role>Admin</Role>
            </Roles>
         </Customer>
      </CustomerArray>
   </Data>
</Message>

Orders

<Message>
   <Orders>
      <Order>
         <OrderNumber>O123</OrderNumber>
         <CustomerID>C100</CustomerID>
         <Quantity>100</Quantity>
         <UnitPrice>10.0</UnitPrice>
      </Order>
      <Order>
         <OrderNumber>O456</OrderNumber>
         <CustomerID>C107</CustomerID>
         <Quantity>100</Quantity>
         <UnitPrice>5.0</UnitPrice>
      </Order>
   </Orders>
</Message>

Upvotes: 2

Related Questions