Insert XML node at a specific position of an existing document

20,026

[Replaced my last answer. Now I understand better what you need.]

Here's an XSLT 2.0 solution:

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

  <xsl:template match="/root">
    <xsl:variable name="elements-after" select="t|u|v|w|x|y|z"/>
    <xsl:copy>
      <xsl:copy-of select="* except $elements-after"/>
      <s>new node</s>
      <xsl:copy-of select="$elements-after"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

You have to explicitly list either the elements that come after or the elements that come before. (You don't have to list both.) I would tend to choose the shorter of the two lists (hence "t" - "z" in the above example, instead of "a" - "r").

OPTIONAL ENHANCEMENT:

This gets the job done, but now you need to maintain the list of element names in two different places (in the XSLT and in the schema). If it changes much, then they might get out of sync. If you add a new element to the schema but forget to add it to the XSLT, then it won't get copied through. If you're worried about this, you can implement your own sort of schema awareness. Let's say your schema looks like this:

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="root">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="a" type="xs:string"/>
        <xs:element name="r" type="xs:string"/>
        <xs:element name="s" type="xs:string"/>
        <xs:element name="t" type="xs:string"/>
        <xs:element name="z" type="xs:string"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>

</xs:schema>

Now all you need to do is change your definition of the $elements-after variable:

  <xsl:variable name="elements-after" as="element()*">
    <xsl:variable name="root-decl" select="document('root.xsd')/*/xs:element[@name eq 'root']"/>
    <xsl:variable name="child-decls" select="$root-decl/xs:complexType/xs:sequence/xs:element"/>
    <xsl:variable name="decls-after" select="$child-decls[preceding-sibling::xs:element[@name eq 's']]"/>
    <xsl:sequence select="*[local-name() = $decls-after/@name]"/>
  </xsl:variable>

This is obviously more complicated, but now you don't have to list any elements (other than "s") in your code. The script's behavior will automatically update whenever you change the schema (in particular, if you were to add new elements). Whether this is overkill or not depends on your project. I offer it simply as an optional add-on. :-)

Share:
20,026
MyKey_
Author by

MyKey_

Updated on May 26, 2020

Comments

  • MyKey_
    MyKey_ almost 4 years

    I have an existing XML document with some optional nodes and I want to insert a new node, but at a certain position.

    The document looks something like this:

    <root>
      <a>...</a>
      ...
      <r>...</r>
      <t>...</t>
      ...
      <z>...</z>
    </root>
    

    The new node (<s>...</s>) should be inserted between node <r> and <t>, resulting in:

    <root>
      <a>...</a>
      ...
      <r>...</r>
      <s>new node</s>
      <t>...</t>
      ...
      <z>...</z>
    </root>
    

    The problem is, that the existing nodes are optional. Therefore, I can't use XPath to find node <r> and insert the new node after it.

    I would like to avoid the "brute force method": Search from <r> up to <a> to find a node that exists.

    I also want to preserve the order, since the XML document has to conform to a XML schema.

    XSLT as well as normal XML libraries can be used, but since I'm only using Saxon-B, schema aware XSLT processing is not an option.

    Does anyone have an idea on how to insert such a node?

    thx, MyKey_

  • Evan Lenz
    Evan Lenz about 15 years
    SAX processing will work as you suggest. But XSLT is quite capable for the task as well (see my answer).
  • Aaron Digulla
    Aaron Digulla about 15 years
    This doesn't work when there is no 'r' node (as per the original question: All nodes are optional). How would the template look when you can't rely on any node to exist?
  • Evan Lenz
    Evan Lenz about 15 years
    Oops, you're right. I had mis-read the original post. Now I've completely replaced the answer. Thanks.
  • 13ren
    13ren about 15 years
    That's really cool. Slight refinement: in deriving $elments-after, use a variable instead of 's', so you can automatically handle inserting after any child of <root>.
  • Evan Lenz
    Evan Lenz almost 15 years
    Agreed. The "s" is pretty hidden among all those implementation details.
  • MyKey_
    MyKey_ almost 15 years
    Wow, nice solution. I would never have thought of parsing the schema. Thanks a lot.
  • London
    London almost 14 years
    @Evan Lenz is it possible to do this somehow from java using this schema?