65.9K
CodeProject 正在变化。 阅读更多。
Home

重写 XML 的设计模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (6投票s)

2008年10月28日

CPOL

3分钟阅读

viewsIcon

37596

一个关于如何覆盖基础 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:本文的第一个版本已发布。
© . All rights reserved.