XSLT Insert element only if it doesn't exist

六眼飞鱼酱① 提交于 2020-02-01 06:20:40

问题


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">
  <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?


回答1:


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.




回答2:


I would use something like that:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <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.



来源:https://stackoverflow.com/questions/1229641/xslt-insert-element-only-if-it-doesnt-exist

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!