views:

1565

answers:

2

I have a source document:

<?xml version="1.0"?>
<source>
  <ItemNotSubstituted/>
  <ItemToBeSubstituted Id='MatchId' />
</source>

And a stylesheet containing content I want to substitute into the source:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
  <xsl:output indent="yes" method="xml" omit-xml-declaration="no" version="1.0"/>
  <xsl:preserve-space elements="//*"/>

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

  <xsl:template match="ItemToBeSubstituted[@Id = 'MatchId']">
    <xsl:copy>
      <xsl:copy-of select="@*|*"/>
      <Element1/>
      <Element2 Value="foo"/>
      <Element3 Value="bar"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

This stylesheet succesfuly copies <Element1/><Element2 Value="foo"/><Element3 Value="bar"/> into ItemToBeSubstituted. But when I use a different source document, in which ItemToBeSubstituted already has content:

<?xml version="1.0"?>
<source>
  <ItemNotSubstituted/>
  <ItemToBeSubstituted Id='MatchId'>
    <Element3 Value="baz"/>
  </ItemToBeSubstituted>
</source>

I get this output:

<?xml version="1.0"?>
<source>
  <ItemNotSubstituted/>
  <ItemToBeSubstituted Id="MatchId">
    <Element3 Value="baz"/>
    <Element1/>
    <Element2 Value="foo"/>
    <Element3 Value="bar"/>
  </ItemToBeSubstituted>
</source>

I would like to only substitute elements from the stylesheet that do not already exist in the source document. This is the output I'm looking for after applying the stylesheet to the second document, with only the <Element3> element from the source document:

<?xml version="1.0"?>
<source>
  <ItemNotSubstituted/>
  <ItemToBeSubstituted Id="MatchId">
    <Element3 Value="baz"/>
    <Element1/>
    <Element2 Value="foo"/>
  </ItemToBeSubstituted>
</source>

What is the best approach for doing this with XSL? The stylesheet may contain many elements to be substituted. So I don't want to use an approach that requires an <xsl:if> around every single element. Is there a better way than using one stylesheet to insert the content, then having a second stylesheet that removes duplicate elements?

+2  A: 

I would use something like that:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
  <xsl:output indent="yes" method="xml" omit-xml-declaration="no" version="1.0"/>
  <xsl:preserve-space elements="//*"/>

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

  <xsl:template match="ItemToBeSubstituted[@Id = 'MatchId']">
    <xsl:variable name="node" select="." />
    <xsl:copy>
      <xsl:copy-of select="@*|*"/>

      <xsl:for-each select="document('elements.xml')/elements/*">
        <xsl:if test="not($node/*[name() = name(current())])">
          <xsl:copy-of select="." />
        </xsl:if>
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

Where elements.xml is the file in which you store which elements to add by default

<?xml version="1.0" encoding="utf-8" ?>
<elements>
  <Element1/>
  <Element2 Value="foo"/>
  <Element3 Value="bar"/>
</elements>

Using <for-each> we iterate over default elements, check if there's an element by that name as a child to current node, then add it if there's none.

Josh Davis
+1 This is a good start at the problem. It could use a little more generalization around the fact that there may obviously be more elements to be substituted than `ItemToBeSubstituted[@Id = 'MatchId']`, but that's not too difficult to do.
Tomalak
+2  A: 

This XSLT 1.0 solution does what you intend:

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

  <!-- expand this section to contain all your default elements/values -->
  <subst:defaults>
    <subst:element name="ItemToBeSubstituted" id="MatchId">
      <subst:Element1/>
      <subst:Element2 Value="foo"/>
      <subst:Element3 Value="bar"/>
    </subst:element>
  </subst:defaults>

  <!-- this makes the above available as a variable -->
  <xsl:variable name="defaults" select="document('')/*/subst:defaults" />

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

  <!-- expand the match expression to contain all elements 
       names that need default values -->
  <xsl:template match="ItemToBeSubstituted">
    <xsl:copy>
      <xsl:copy-of select="@*|*"/>
      <xsl:call-template name="create-defaults" />
    </xsl:copy>
  </xsl:template>

  <!-- this does all the heavy lifting -->
  <xsl:template name="create-defaults">
    <xsl:variable name="this" select="." />

    <xsl:for-each select="
      $defaults/subst:element[@name = name($this) and @id = $this/@Id]/*
    ">
      <xsl:if test="not($this/*[name() = local-name(current())])">
        <xsl:apply-templates select="." />
      </xsl:if>
    </xsl:for-each>
  </xsl:template>

  <!-- create the default nodes without namespaces -->
  <xsl:template match="subst:*">
    <xsl:element name="{local-name()}">
      <xsl:apply-templates select="subst:*|@*" />
    </xsl:element>
  </xsl:template>

</xsl:stylesheet>

The use of a separate namespace ("subst") enables you to keep the defaults within the stylesheet. Whether this is a good thing or not depends, at least you don't have to have two files lying around.

If you prefer to have the stylesheet decoupled from the default values, put them in an extra file and use this line instead.

<xsl:variable name="defaults" select="document('defaults.xml')/subst:defaults" />

You could drop all the the extra namespace handling once you do this, and would end up with the solution Josh Davis proposed, more or less.

Tomalak