在许多应用程序中,经常需要定义一些基础配置,然后针对特定上下文覆盖某些XML元素。本文提出了一种通用的XML元素覆盖设计模式,并提供了基于此模式的通用XSL样式表,允许合并覆盖XML文件与相应的基础XML结构。
该设计模式的核心思想是,覆盖XML重复基础XML的结构,以覆盖基础值并指定新的值。覆盖XML的模式几乎可以与基础XML相同,但每个元素不必要求,以便仅指定需要覆盖的XML树的一部分。此外,每个元素可以有一个可选的额外属性overrideMode
,用于指定元素如何覆盖相应的基础元素。这个属性可以是以下三个值之一:
通过这个额外的属性,可以轻松覆盖基础XML的任何部分。为了演示其工作原理,考虑一个包含一级和二级元素、属性和文本节点的示例基础XML:
<root>
<lvl1 id="elem11" value="val11">
<lvl2 name="elem21" value="val21">
txt21
</lvl2>
<lvl2 name="elem22" value="val22">
txt22
</lvl2>
txt11
</lvl1>
</root>
以下示例覆盖XML展示了如何以不同的方式覆盖基础元素。XML中的注释解释了每个元素是如何被覆盖的:
<root>
<lvl1 id="elem11" value="oval11" overrideMode="update">
<lvl2 name="elem21" value="oval21">
otxt21
</lvl2>
<lvl2 name="elem22" value="oval22">
otxt22
</lvl2>
otxt1
</lvl1>
<lvl1 id="elem12" value="oval12">
otxt2
</lvl1>
<lvl1 id="elem13" value="oval13(ignored)">
<lvl2 name="elem21" value="oval21"/>
<lvl2 name="elem22" value="oval22"/>
<lvl2 name="elem23" value="oval23"/>
otxt3
</lvl1>
<lvl1 id="elem14" overrideMode="delete"/>
<lvl1 id="elem15" value="oval12" overrideMode="replace">
<lvl2 name="elem21" value="oval21"/>
otxt5
</lvl1>
</root>
这种设计模式的一个优点是,它允许创建一个通用的转换模板,适用于大多数情况。它使用了一个名为merge
的XSLT模板,可以作为转换的一部分由其他模板使用,或者作为一个独立的XSLT应用覆盖XML到基础XML。
这个XSLT的唯一限制是,每个元素要么是唯一的,或者其第一个属性作为键,可以在其父元素中唯一标识该元素。这是一个相当合理的限制,应该覆盖大多数情况,因为为了允许覆盖XML元素,它们需要有一个键,除非它们是其父元素的唯一子元素,并且第一个元素似乎是定义键的好选择。以下是实现此模式的通用XSLT模板:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output indent="yes"/>
<xsl:param name="overrideFile"/>
<xsl:template match="/">
<xsl:call-template name="merge">
<xsl:with-param name="std" select="."/>
<xsl:with-param name="ovrd" select="document($overrideFile)"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="merge">
<xsl:param name="std"/>
<xsl:param name="ovrd"/>
<xsl:for-each select="$std/*">
<xsl:variable name="key" select="@*[1]"/>
<xsl:variable name="ovr" select="$ovrd/*[local-name() = local-name(current()) and (not($key) or @*[1] = $key)]"/>
<xsl:if test="count($ovr) = 0">
<xsl:copy-of select="."/>
</xsl:if>
<xsl:if test="count($ovr) = 1 and (not($ovr/@overrideMode) or $ovr/@overrideMode != 'delete')">
<xsl:choose>
<xsl:when test="count($ovr/*) = 0 or $ovr/@overrideMode = 'update' or $ovr/@overrideMode = 'replace'">
<xsl:variable name="current" select="."/>
<xsl:for-each select="$ovr">
<xsl:copy>
<xsl:for-each select="@*[name() != 'overrideMode'] | text()[string-length(normalize-space(.))>0]">
<xsl:copy/>
</xsl:for-each>
<xsl:choose>
<xsl:when test="$ovr/@overrideMode = 'replace'">
<xsl:copy-of select="*"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="merge">
<xsl:with-param name="std" select="$current"/>
<xsl:with-param name="ovrd" select="."/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:copy>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:for-each select="@*|text()[string-length(normalize-space(.))>0]">
<xsl:copy/>
</xsl:for-each>
<xsl:call-template name="merge">
<xsl:with-param name="std" select="."/>
<xsl:with-param name="ovrd" select="$ovr"/>
</xsl:call-template>
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:for-each>
<xsl:for-each select="$ovrd/*">
<xsl:variable name="key" select="@*[1]"/>
<xsl:if test="count($std/*[local-name() = local-name(current()) and (not($key) or @*[1] = $key)]) = 0">
<xsl:copy-of select="."/>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>