重写 XML 的设计模式






4.83/5 (6投票s)
一个关于如何覆盖基础 XML 中元素的设计模式。
引言
由于 XML 被广泛用于定义各种系统配置,有时需要能够定义一些基本配置,然后为每个特定上下文在单独的配置文件中覆盖某些 XML 元素。
以下文章介绍了一种用于覆盖 XML 元素的通用设计模式,并提供了一个基于此模式的通用 XSL 样式表,该样式表允许将覆盖 XML 文件与相应的基本 XML 结构合并。
XML 覆盖设计模式
此设计模式背后的想法是让覆盖 XML 重复基本 XML 的结构,以覆盖基本值并指定要使用的新值。 覆盖 XML 的模式可以与基本 XML 的模式几乎相同,只是每个元素都不需要启用,以便仅指定 XML 树中覆盖所需的部分。 此外,每个元素都可以有一个可选的额外属性 `overrideMode`,用于指定该元素如何覆盖相应的基本元素。 此属性可以采用以下三个值之一
- `delete` – 此值可用于任何元素,以指示应从 XML 树中删除此元素(包括其所有子元素)。
- `update` – 此值可用于任何非叶子覆盖元素,以指示必须使用覆盖元素更新当前基本元素,但除非在 XML 中进一步显式覆盖,否则不应更改所有子元素。 这是因为覆盖 XML 中的非叶子元素可能具有双重目的:为该元素提供新值,并为其子覆盖元素提供结构。 没有子元素的覆盖 XML 元素根本不需要额外的属性,因为它们的存在会自动指示覆盖现有元素或分别添加新元素。
- `replace` – 此值可用于非叶子元素,以完全替换元素(包括其所有子元素)。
有了这个额外的属性,就可以很容易地以一种非常简单的方式覆盖基本 XML 的任何部分。 为了演示它是如何工作的,让我们考虑一个示例基本 XML,它由第一级和第二级元素、属性和文本节点组成,如下所示
<root>
<lvl1 id="elem11" value="val11">
<lvl2 name="elem21" value="val21">txt21</lvl2>
<lvl2 name="elem22" value="val22">txt22</lvl2>txt11</lvl1>
<lvl1 id="elem12" value="val12">
<lvl2 name="elem21" value="val21">txt21</lvl2>
<lvl2 name="elem22" value="val22">txt22</lvl2>txt12</lvl1>
<lvl1 id="elem13" value="val13">
<lvl2 name="elem21" value="val21">txt21</lvl2>
<lvl2 name="elem22" value="val22">txt22</lvl2>txt13</lvl1>
<lvl1 id="elem14" value="val14">
<lvl2 name="elem21" value="val21">txt21</lvl2>
<lvl2 name="elem22" value="val22">txt22</lvl2>txt14</lvl1>
<lvl1 id="elem15" value="val15">
<lvl2 name="elem21" value="val21">txt21</lvl2>
<lvl2 name="elem22" value="val22">txt22</lvl2>txt15</lvl1>
</root>
现在,以下示例覆盖 XML 显示了一些如何以不同方式覆盖不同基本元素的示例。 XML 中的注释解释了如何覆盖每个元素。
<root>
<!-- update elem11 and the two child elements respectively -->
<lvl1 id="elem11" value="oval11" overrideMode="update">
<lvl2 name="elem21" value="oval21">otxt21</lvl2>
<lvl2 name="elem22" value="oval22">otxt22</lvl2>otxt1</lvl1>
<!-- update elem12, since it has no children -->
<lvl1 id="elem12" value="oval12">otxt2</lvl1>
<!-- update children of elem13 but not the element itself,
since the overrideMode is not specified -->
<lvl1 id="elem13" value="oval13(ignored)">
<lvl2 name="elem21" value="oval21"/>
<lvl2 name="elem22" value="oval22"/>
<lvl2 name="elem23" value="oval23"/>otxt3</lvl1>
<!-- delete elem14 -->
<lvl1 id="elem14" overrideMode="delete"/>
<!-- replace elem15 with the following element -->
<lvl1 id="elem15" value="oval12" overrideMode="replace">
<lvl2 name="elem21" value="oval21"/>otxt5</lvl1>
</root>
通用 XSLT 实现
关于此设计模式的一大优点是,它允许创建适用于大多数情况的通用转换模板。 它使用一个名为 *merge* 的 XSLT 模板,该模板可以被其他模板用作转换的一部分,或者在独立的 XSL 中使用,将覆盖 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>
结论
本文说明了如何通过使用与原始 XML 几乎相同的结构加上每个元素的附加属性来实现定义覆盖 XML 的常见模式。 此模式可以利用通用 XSLT 实现,该实现可以将覆盖 XML 应用于基本 XML 并将其转换为生成的 XML。
历史
- 2008/10/28:本文的第一个版本已发布。