I am looking for an XSLT usage pattern that will reparent nodes

Viewed 122

I am pretty skilled at XSLT, but I've not encountered before the need to transform an XML document and implement reparenting. The input XML is in OPML format:

<opml>
  <body>
    <outline text="root">
      <outline text="child 1">
        <outline text="some text A">
          <outline text="some text B" />
          <outline text="some text C" />
      ...
      <outline text="child 2">
        <outline text="some text D">
          <outline text="some text E" />
          <outline text="some text F" />
  ....

I have another xml doc which is used in the transform, and lists nodes and new parent nodes, and it has entries such as:

<row>
  <node>some text A</node>
  <newParent>child 2</newParent>
</row>

Has anyone tackled such programmatic reparenting using XSLT?

5 Answers

I am guessing (!!) you mean something like this:

XML

<opml>
  <body>
    <outline text="root">
      <outline text="child 1">
        <outline text="some text A">
          <outline text="some text B"/>
          <outline text="some text C"/>
        </outline>
      </outline>
      <outline text="child 2">
        <outline text="some text D">
          <outline text="some text E"/>
          <outline text="some text F"/>
        </outline>
      </outline>
    </outline>
  </body>
</opml>

external.xml

<root>
    <row>
        <node>some text A</node>
        <newParent>child 2</newParent>
    </row>
</root>

XSLT 2.0

<xsl:stylesheet version="2.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="reparent" match="row" use="node" />
<xsl:key name="children" match="outline" use="@parent" />

<xsl:template match="/opml">
    <xsl:variable name="outlines">
        <xsl:for-each select="//outline">
            <xsl:variable name="reparent" select="key('reparent', @text, document('path/to/external.xml'))" />
            <outline parent="{if ($reparent) then $reparent/newParent else ../@text}" text="{@text}"/>
        </xsl:for-each>
    </xsl:variable>
    <!-- output -->
    <opml>
        <body>
            <xsl:apply-templates select="$outlines/outline[not(string(@parent))]"/>
        </body>
    </opml>
</xsl:template>

<xsl:template match="outline">
    <outline text="{@text}">
        <xsl:apply-templates select="key('children', @text)"/>
    </outline>
</xsl:template>

</xsl:stylesheet>

Result

<?xml version="1.0" encoding="UTF-8"?>
<opml>
   <body>
      <outline text="root">
         <outline text="child 1"/>
         <outline text="child 2">
            <outline text="some text A">
               <outline text="some text B"/>
               <outline text="some text C"/>
            </outline>
            <outline text="some text D">
               <outline text="some text E"/>
               <outline text="some text F"/>
            </outline>
         </outline>
      </outline>
   </body>
</opml>

Note that there is an implied assumption here that the text of an outline is unique.


Added:

In view of the confusion expressed in the comments below*, here's an explanation of my method:

This transformation takes place in two steps:

  • In step one we construct a "flat" list of all outlines, where each outline has a parent attribute. The default parent is the existing parent, but if the outline is listed in the external document, then the new parent overrides.

    At the end of this step we have an $outlines variable containing:

       <outline parent="" text="root"/>
       <outline parent="root" text="child 1"/>
       <outline parent="child 2" text="some text A"/>
       <outline parent="some text A" text="some text B"/>
       <outline parent="some text A" text="some text C"/>
       <outline parent="root" text="child 2"/>
       <outline parent="child 2" text="some text D"/>
       <outline parent="some text D" text="some text E"/>
       <outline parent="some text D" text="some text F"/>
    
  • In step two we reconstruct the hierarchy by outputting the "progenitor" outlines (outlines with no parent) first, and then populating each outline recursively with its children.

It might be worth mentioning that in this method each outline performs exactly one lookup from the external document - and this lookup is performed using a key.


(*) These comments, being now deleted, are reproduced here:

enter image description here

While my first tool of choice is invariably XSLT, I might be inclined to tackle this one by transforming the input

<row>
  <node>some text A</node>
  <newParent>child 2</newParent>
</row>

into XQuery Update statements:

let $node := //outline[@text='some text A']
let $newParent := //outline[@text='child 2']
return (
    insert nodes $node into $newParent,
    delete nodes $node
) 

and then execute the resulting XQuery Update script.

I. XSLT 2.0 solution

As simple as this (no variables, no conditionals, no literal-result elements, no intermediate-result document, no xsl:for-each) :

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>
 <xsl:key name="kOutlineByText" match="outline" use="@text"/>
 <xsl:key name="kUpdatedNodesByText" match="node" use="."/>
 <xsl:param name="pupdSpec">
    <row>
      <node>some text A</node>
      <newParent>child 2</newParent>
    </row>
 </xsl:param>

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

  <xsl:template match="outline[@text = $pupdSpec/*/newParent]">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
      <xsl:apply-templates mode="copy" select="key('kOutlineByText', $pupdSpec/*/node)"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="outline[key('kUpdatedNodesByText', @text, $pupdSpec)]" />
</xsl:stylesheet>

When this transformation is applied on the provided XML document:

<opml>
  <body>
    <outline text="root">
      <outline text="child 1">
        <outline text="some text A">
          <outline text="some text B"/>
          <outline text="some text C"/>
        </outline>
      </outline>
      <outline text="child 2">
        <outline text="some text D">
          <outline text="some text E"/>
          <outline text="some text F"/>
        </outline>
      </outline>
    </outline>
  </body>
</opml>

the wanted, correct result is produced:

<opml>
   <body>
      <outline text="root">
         <outline text="child 1"/>
         <outline text="child 2">
            <outline text="some text D">
               <outline text="some text E"/>
               <outline text="some text F"/>
            </outline>
            <outline text="some text A">
               <outline text="some text B"/>
               <outline text="some text C"/>
            </outline>
         </outline>
      </outline>
   </body>
</opml>

II. XSLT 1.0 Solution:

<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:key name="kOutlineByText" match="outline" use="@text"/>
 <xsl:param name="pupdSpec">
    <row>
      <node>some text A</node>
      <newParent>child 2</newParent>
    </row>
 </xsl:param>

 <xsl:variable name="vupdSpec" select="document('')/*/xsl:param[@name='pupdSpec']/*"/>

  <xsl:template match="node()|@*">
    <xsl:if test="not(current()/@text = $vupdSpec/node)">
       <xsl:copy>
         <xsl:apply-templates select="node()|@*"/>
           <xsl:copy-of select="key('kOutlineByText', $vupdSpec/node)
                             [current()[self::outline and @text = $vupdSpec/newParent]]"/>
       </xsl:copy>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the same (above) XML document, the same correct, wanted result is produced:

<opml>
   <body>
      <outline text="root">
         <outline text="child 1"/>
         <outline text="child 2">
            <outline text="some text D">
               <outline text="some text E"/>
               <outline text="some text F"/>
            </outline>
            <outline text="some text A">
               <outline text="some text B"/>
               <outline text="some text C"/>
            </outline>
         </outline>
      </outline>
   </body>
</opml>

Thank you all for your suggestions. Posing problems like this is a great way for me and others to learn what XSLT is capable of. My specific problem turned out to be a bit more complex because what turned out be the case is that the users had edited the test and production xml at the same time - using a graphical editor that I had built for the business. Below is the solution that I came up with. I would be interested to hear if it can be simplified. And also if XSLT 2.0 solution would be more succinct.

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

  <!-- 
       Merge/refactor the OPML that is in test. 
       Both test and production were modified and now need to be reconsiled. 
       The input to this transform is the test xml.
  -->

  <xsl:output method="xml" encoding="utf-8" indent="yes" />

  <xsl:param name="imported" />
  <xsl:param name="imported2" />

  <!-- xml from Excel changes sheet -->
  <xsl:variable name="changes" select="document($imported)/table" />

  <!-- production opml body -->
  <xsl:variable name="prod" select="document($imported2)/opml/body" />

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

  <!-- match and recurse on outline nodes -->
  <xsl:template match="outline">
    <xsl:variable name="nodeName" select="@name" />
    <!-- row in change doc that has this node as a parent -->
    <xsl:variable name="changeParentRow" select="$changes/row[productInsertionParent = $nodeName and parentProductInNewStuff != 'NULL']" />

    <xsl:copy>
      <xsl:apply-templates select="@*"/>
      <xsl:apply-templates select="node()"/>
      <!-- if there is a row in changes doc that has this node as a parent -->
      <xsl:if test="$changeParentRow">
        <!--  from change doc, name of node to insert from prod version -->
        <xsl:variable name="changeNodeName" select="$changeParentRow/productToInsert" />
        <!--  node in prod version -->
        <xsl:variable name="changeNodeProd" select="$prod//outline[@name = $changeNodeName]" />
        <!-- if there is such a node in prod, copy it -->
        <xsl:if test="$changeNodeProd">
          <xsl:apply-templates select="$changeNodeProd" mode="merge" />
        </xsl:if>
      </xsl:if>
    </xsl:copy>

  </xsl:template>

  <xsl:template match="outline" mode="merge">
    <xsl:copy>
      <xsl:apply-templates select="@*" />
      <xsl:apply-templates select="node()" mode="check" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="outline" mode="check">
    <xsl:variable name="nodeName" select="@name" />
    <!-- copy this node if it wasn't already present in test -->
    <xsl:if test="not(/opml//outline[@name=$nodeName])">
      <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
      </xsl:copy>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>

If the <row>s are to be executed consecutively, I suggest you use a transformation that processes one row, like

<xsl:param name="node"/>
<xsl:param name="newParent"/>
<xsl:template match="outline">
  <xsl:if test="not(@text=$node)">
    <xsl:copy>
      <xsl:copy-of select="@text"/>
      <xsl:apply-templates select="outline"/>
      <xsl:if test="self::outline[@text=$newParent]">
        <xsl:if test="ancestor-or-self::outline[@text=$node]">
          <xsl:message terminate="yes">Forbidden cycle</xsl:message>
        </xsl:if>
        <xsl:copy-of select="//outline[@text=$node]"/>
      </xsl:if>
    </xsl:copy>
  </xsl:if>
</xsl:template>

and use non-XSLT means to loop over the <row> list and invoke that transformation once per row.

A "forbidden cycle" would be caused by

<row>
  <node>child 1</node>
  <newParent>some text A</newParent>
</row>

or by

<row>
  <node>some text A</node>
  <newParent>child 2</newParent>
</row>
<row>
  <node>child 2</node>
  <newParent>some text A</newParent>
</row>

If the <row>s are not to be executed consecutively, can you specify the rules in more detail?

Related