Insert optional element between other optional elements in XML document... according to schema?

StackOverflow https://stackoverflow.com/questions/21190240

  •  29-09-2022
  •  | 
  •  

Question

Really hard to fit the explanation into an accurate title but I tried.

Anyhow the question is simple.

Given the following xml:

<Parent> 
  <Child/> 
  <Child/> 
  <Child/> 
  <Child/> 
  <!-- Trying to insert an element here -->
  <OtherChild/>
  <OtherChild/>
  <OtherChild/>
</Parent>

And assuming the following:

  • We know the schema and have access to it.
  • All elements are optional according to the schema but must appear in order.
  • The element we are inserting may be optional as well.
  • There are no guarantees that any of the parents or other child nodes are there or how many there will be.
  • The depth of nesting is not necessarily 1 it could be multiple nested elements.

My solution:

  1. Check to see if there are any of the child we are inserting.
  2. Recursive method to check for previous siblings (requires knowledge of the sibling that precede the element we are inserting) and then check the parent.

Results:

A. If an element that we are trying to insert exists we insert after.
B. If a preceding sibling exists we insert after.
C. If no preceding sibling exists but the parent does we prepend to the parent.
D. Walking up the tree from there we would keep going to the next parent or preceding sibling of the current parent until we find one that exists then insert the entire tree down to the element we are trying to insert.

I am currently doing this in C# code by first using the schema to discover the preceding siblings then iteratively trying to select one until I either find one or reach the end of the list. Then I try to discover the parent then repeat with the parent as the element to be inserted.

The Question:

Is it possible to do this in another, faster, more efficient, neater, better way?

I am just learning about XSLT and if it can be done with a transform that would be perfect (note must be XSLT 1.0).

Alternatively clever ways of minimising the work seeing as at present it will basically have to walk the entire tree until it reaches an existing node which could be a lot of work.

Note: For reference this is actually for use in InfoPath where I will be working on the main data source. In particular I am referring to repeating tables, it is possible for the user to delete all rows from the table at which point any code attempting to add rows to that table may have a very difficult time.

InfoPath itself is able to add new rows to the tables as easy as pie and if I could figure out how they manage it that would be great. In lieu of that I would be happy just to have a simple method for doing so.

I already have a solution to the problem, as I said I am just looking for some folk to bounce ideas off of and see if I can get some improvements. I appreciate any suggestions that you may have.

Thank you :)

Pas de solution correcte

Autres conseils

This is not a complete answer, but if you give me some clarifications I might be able to work something out.

The following given an xml like :

<root>
    <parent>
        <child>child1</child>
        <child>child2</child>
        <otherChild>other1</otherChild>
    </parent>
</root>

Will transform it into:

<?xml version="1.0" encoding="UTF-8"?> 
<root>
    <parent>
        <child>child1</child>    
        <child>child2</child>
        <insertedChild>inserted1</insertedChild>
        <otherChild>other2</otherChild>
    </parent>
</root>

This is the xsl:

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  version="1.0"> 
  <xsl:template match="/root"> 
    <xsl:for-each select=".">
      <xsl:element name="root">
          <xsl:for-each select="parent">
              <xsl:element name="parent">
                  <xsl:copy-of select="child" />
                  <xsl:element name="insertedChild">
                    inserted1
                  </xsl:element>
                 <xsl:copy-of select="otherChild" />            
              </xsl:element>
           </xsl:for-each>      
      </xsl:element>
    </xsl:for-each>
  </xsl:template> 
</xsl:stylesheet>

This can be altered for any number of distinct elements before and after the element we wish to add.

What I did not get from the problem description is whether the element to be added might be in any place; that is, we do not know beforehand the exact schema we wish to end with.

Is this a transformation you will need to run for multiple elements? If that is the case you may need to create multiple xsl files or include them in this one. Either way it will be a bit more complex.

XSL is however open to parametrization, as you can choose for example both the inserted element's name and value, like so :

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  version="1.0"> 
<xsl:param name="insertedChild"></xsl:param>
<xsl:param name="valueOfInsertedChild"></xsl:param>
  <xsl:template match="/root"> 
    <xsl:for-each select=".">
        <xsl:element name="root">
            <xsl:for-each select="parent">
                <xsl:element name="parent">
                    <xsl:copy-of select="child" />
                    <xsl:element name="{$insertedChild}">
                        <xsl:value-of select="$valueOfInsertedChild" />
                    </xsl:element>
                    <xsl:copy-of select="otherChild" />         
                </xsl:element>
            </xsl:for-each>     
        </xsl:element>
    </xsl:for-each>
  </xsl:template> 
</xsl:stylesheet>

I hope this is even a bit close to what you're looking for. Clarify the points mentioned above for me and we might work something better out.

Ok I believe I got this. Declared are 4 params, defining the inserted element's name, value, one param to define which shall be the preceding element, and one to define the parent element.

Two limitations :

  • It is required that the root (document) element is known (here "root").
  • You can not get mixed content (that is, insert a node into another that has character data instead of other nodes).

    <?xml version="1.0"?>
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  version="1.0"> 
    <xsl:param name="insertedChild">a</xsl:param>
    <xsl:param name="valueOfInsertedChild">a</xsl:param>
    <xsl:param name="appendAfterChild">child</xsl:param>
    <xsl:param name="appendIntoNode">parent</xsl:param>
    
     <xsl:template match="node()|@*">
         <xsl:copy>
           <xsl:apply-templates select="node()|@*" />
         </xsl:copy>
     </xsl:template>
    
    <xsl:template match="root">
        <xsl:choose>
        <xsl:when test="name() = $appendIntoNode">    
            <xsl:call-template name="insertNode" />
        </xsl:when>
        <xsl:otherwise>
            <xsl:call-template name="recurse" />
        </xsl:otherwise>
        </xsl:choose>
     </xsl:template>
    
    <xsl:template name="recurse">
        <xsl:copy>
        <xsl:for-each select="child::node()">
        <xsl:choose>
        <xsl:when test="name() = $appendIntoNode">    
            <xsl:call-template name="insertNode" />
        </xsl:when>
        <xsl:otherwise>
            <xsl:call-template name="recurse" />
        </xsl:otherwise>
        </xsl:choose>
        </xsl:for-each>
        </xsl:copy>
     </xsl:template>
    
      <xsl:template name="insertNode">    
            <xsl:element name="{name()}">
            <xsl:for-each select="*[(following-sibling::*[name()=$appendAfterChild] and name()!=$appendAfterChild) or name()=$appendAfterChild]">
                    <xsl:copy-of select="." />
            </xsl:for-each>
            <xsl:element name="{$insertedChild}">
                    <xsl:value-of select="$valueOfInsertedChild" />
            </xsl:element>
            <xsl:for-each select="*[not((following-sibling::*[name()=$appendAfterChild] and name()!=$appendAfterChild) or name()=$appendAfterChild)]">
                    <xsl:copy-of select="." />
            </xsl:for-each>
            </xsl:element> 
     </xsl:template>
    
    </xsl:stylesheet>
    

I am not sure how it works for performance reasons, but you can try it out. I think this is quite closer to what you asked. Let me know how this is. :)

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top