Re: [xsl] The notion of Inheritance?

Subject: Re: [xsl] The notion of Inheritance?
From: Jeni Tennison <mail@xxxxxxxxxxxxxxxx>
Date: Fri, 30 Mar 2001 10:00:11 +0100
Hi William,

> Now lets add a couple of wrinkles to it...

Oh good ;)

> 1. Suppose I'm using person[id = 1] as the default, but now
> person[id = 2] (or any person for that matter) can contain nodes
> that are not in the default. I want all the nodes of person[id = 2],
> plus the nodes from person[id = 1] (I guess this is the union in
> mathematical terms).

OK.  The solution I gave you works through the default structure
(person[id = 1]) and the override structure (person[id = 2]) side by
side.  The normal processing is working through the default structure
(by applying templates to the elements in 'inherit' mode), and the
override structure is being worked through by passing elements as
parameters.

In the 'inherit' templates then the current node is an element in the
default structure (e.g. person[id = 1]/first-name) and the $override
parameter is the same-named element in the override structure (e.g.
person[id = 2]/first-name).

When you have one of the 'wrapper' elements (ones that only have
element children, like 'person' or 'physical'), then you can go
through the children of the overriding element and copy any that don't
have a corresponding element in the children of the default element:

<xsl:template match="*[*]" mode="inherit">
   <xsl:param name="override" />
   <xsl:variable name="default" select="." />
   <xsl:copy>
      <xsl:for-each select="$default/*">
         <xsl:apply-templates select="." mode="inherit">
            <xsl:with-param name="override"
               select="$override/*[name() = name(current())]" />
         </xsl:apply-templates>
      </xsl:for-each>
      <xsl:for-each select="$override/*">
         <xsl:if test="not($default/*[name() = name(current())])">
            <xsl:copy-of select="current()" />
         </xsl:if>
      </xsl:for-each>
   </xsl:copy>
</xsl:template>

One thing about this is that it does change the ordering of the
elements - all the elements that are new in the overriding structure
will be added at the bottom of the default structure. No doubt you
will return asking how to fix this, but it's fairly complicated, so
I'd avoid needing it if you can ;)

> 2. Suppose I add an <address> node that looks like this:
[snip]
> and I want to be able to get the defaults for only that persons'
> country (which must be specified). This is looking (and feeling)
> more and more like a Relational DB, I guess, but is this possible?

Almost everything is *possible*.  Again, this is a matter of expanding
the template above.  You only want to apply templates to the address
element under the person element if the $override element's child
address element's child country element has the same value as the
default address element's child country element.  So you need:

<xsl:template match="*[*]" mode="inherit">
   <xsl:param name="override" />
   <xsl:variable name="default" select="." />
   <xsl:copy>
      <xsl:for-each select="$default/*">
         <xsl:if test="not(self::address) or
                       country = $override/address/country">
            <xsl:apply-templates select="." mode="inherit">
               <xsl:with-param name="override"
                  select="$override/*[name() = name(current())]" />
            </xsl:apply-templates>
         </xsl:if>
      </xsl:for-each>
      <xsl:for-each select="$override/*">
         <xsl:if test="not($default/*[name() = name(current())])">
            <xsl:copy-of select="current()" />
         </xsl:if>
      </xsl:for-each>
   </xsl:copy>
</xsl:template>

*Or* you could leave the template as it is, and instead have a
template specifically for the address element, so that it only copies
itself if the country matches:

<xsl:template match="address" mode="inherit">
   <xsl:param name="override" />
   <xsl:variable name="default" select="." />
   <xsl:if test="country = $override/country">
      <xsl:copy>
         <xsl:for-each select="$default/*">
            <xsl:apply-templates select="." mode="inherit">
               <xsl:with-param name="override"
                  select="$override/*[name() = name(current())]" />
            </xsl:apply-templates>
         </xsl:for-each>
         <xsl:for-each select="$override/*">
            <xsl:if test="not($default/*[name() = name(current())])">
               <xsl:copy-of select="current()" />
            </xsl:if>
         </xsl:for-each>
      </xsl:copy>
   </xsl:if>
</xsl:template>

> Or is there a better way to structure the XML in the first place? I
> don't even know if a DTD will allow this sort of thing.

Things are a lot easier if *one* of the default or the override has
*the* structure that you want.  Given the latter complication, it
makes more sense if this is the child.  So, something like (just
taking the sample XML that you first posted):

<people>
        <person>
                <id>1</id>
                <first-name>John</first-name>
                <last-name>Smith</last-name>
                <physical>
                        <eyes>brown</eyes>
                        <hair>black</hair>
                        <height>6'1"</height>
                </physical>
                <occupation>Software Developer</occupation>
        </person>
        <person>
                <id>2</id>
                <first-name>Mary</first-name>
                <last-name>Jacobs</last-name>
                <physical>
                        <eyes />
                        <hair />
                        <height />
                </physical>
                <occupation>Welder</occupation>
        </person>
        <person>
                <id>3</id>
                <first-name>Joe</first-name>
                <last-name />
                <physical>
                        <eyes>blue</eyes>
                        <hair />
                        <weight />
                </physical>
                <occupation />
        </person>
</people>

With this, you just need to have three templates: one that deals with
elements with element children by copying the element and moving on to
the content:

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

One that deals with elements with text inside them by copying them:

<xsl:template match="*[text()]">
   <xsl:copy-of select="." />
</xsl:template>

And finally one that deals with elements that are empty by referring
to the default and copying the relevant element from that default. If
all your elements are named differently, this is really easy. I'd set
up the default structure in a variable so that it can be accessed
anywhere:

<xsl:variable name="default" select="/people/person[id = 1]" />

Then it's just a matter of finding the descendant of the $default
element with the relevant name:

<xsl:template match="*[not(node())]">
   <xsl:copy-of select="$default//*[name() = name(current())][1]" />
</xsl:template>

(You might be able to make this slightly more efficient with a key.)

This is getting very long so I'll stop now.  I hope you'll follow up
if you have questions.

Cheers,

Jeni

---
Jeni Tennison
http://www.jenitennison.com/



 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list


Current Thread