0

I've got an XML file with the following structure (multiple "entity" nodes):

<!-- entities.xml -->
<root>
    <entity template="foo-template" kind="foo" name="bar">
        <groups>
            <group id="1">
                <definition id="1" name="foobar" />
            </group>
        </groups>
    </entity>
</root>

Many entity nodes have similar attributes and children nodes. I'd like to allow users to create entity templates in a separate file. Referencing the template will be done as follows:

<entity template="foo-template" kind="foo" ... />

Every attribute and child node from "foo-template" should be copied into the entity, except for those that already exist (i.e. allow overriding the template).

I'm not very familiar with XSLT. Is it the right tool for this task, or am I better off implementing this without it?

I'm using C++ and RapidXml, but can use other XML libraries.


Edit: example.

Template file:

<!-- templates.xml -->
<templates>
    <entity template="foo-template" name="n/a" model="baz">
        <groups>
            <group id="1">
                <definition id="1" name="def1" />
                <definition id="2" name="def2" />
            </group>
            <group id="2">
                <definition id="1" name="def3" />
                <definition id="2" name="def4" />
            </group>
        </groups>
    </entity>
</templates>

Output file:

<!-- output.xml -->
<root>
    <entity kind="foo" name="bar" model="baz">
        <groups>
            <group id="1">
                <definition id="1" name="foobar" />
            </group>
            <group id="2">
                <definition id="1" name="def3" />
                <definition id="2" name="def4" />
            </group>
        </groups>
    </entity>
</root>

So the output contains group 1 from "entities.xml" and group 2 from "templates.xml". No need to merge group nodes with the same id.

kshahar
  • 10,423
  • 9
  • 49
  • 73
  • I've provided an answer that will handle attributes on the entity template but re-reading the question I see you also want to allow child elements in the templates as well. What are the criteria for "except for those that already exist" in the case of elements? Can you edit the question to include some detail about what would be considered a duplicate and what wouldn't (ideally some concrete examples of input, template, and the corresponding expected output). – Ian Roberts Mar 05 '13 at 14:20
  • Thanks for the detailed answer! I've added an example, hope this helps to understand my question better. – kshahar Mar 05 '13 at 14:55
  • OK, I've edited my answer to work with this. – Ian Roberts Mar 05 '13 at 15:18
  • Excellent, it works, thanks for the effort :) – kshahar Mar 05 '13 at 16:05

2 Answers2

2

If you have a file templates.xml that looks like

<templates>
  <entity template="foo-template" kind="foo" name="bar" model="baz" />
  <!-- and other entity elements with different template="..." values -->
</templates>

then an XSLT such as the following would achieve what you're after

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

  <xsl:key name="kEntityTemplate" match="entity" use="@template" />

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

  <xsl:template match="entity[@template]">
    <xsl:variable name="thisEntity" select="." />
    <!-- switch to templates doc -->
    <xsl:for-each select="document('templates.xml')">
      <xsl:variable name="template"
         select="key('kEntityTemplate', $thisEntity/@template)" />

      <entity>
        <!-- copy template attributes that are not overridden -->
        <xsl:for-each select="$template/@*">
          <xsl:if test="not($thisEntity/@*[name() = name(current())])">
            <!-- if not, copy the one from the template -->
            <xsl:apply-templates select="." />
          </xsl:if>
        </xsl:for-each>

        <!-- copy source attributes -->
        <xsl:apply-templates select="$thisEntity/@*[name() != 'template']" />

        <!-- deal with elements -->
        <xsl:if test="$thisEntity/groups/group | $template/groups/group">
          <groups>
            <!-- here we select all group elements from the source plus
                 those group elements from the template that do not also exist
                 in the source, and sort the whole lot by id -->
            <xsl:apply-templates select="$thisEntity/groups/group
              | $template/groups/group[not(@id = $thisEntity/groups/group/@id)]">
              <xsl:sort select="@id" data-type="number" />
            </xsl:apply-templates>
          </groups>
        </xsl:if>
      </entity>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

The templates.xml file needs to be in the same directory as the stylesheet.

Ian Roberts
  • 120,891
  • 16
  • 170
  • 183
0

One option that you have outside of doing any kind of XML transformation is importing the other XML file and then referencing it from within the tags. See here for an example.

This would require your users to have separate template files for each template type which you may not want. However I would prefer the import approach because of the kiss principle. If you're not familiar with XSLT then importing is probably a better way to go as well.

I hope this helps!

Community
  • 1
  • 1
nattyddubbs
  • 2,085
  • 15
  • 24