使用XSLT处理循环依赖项

时间:2010-08-03 23:23:50

标签: xslt exslt xslt-1.0

我正在处理一个简化的XML文件,如下所示:

<resources>
  <resource id="a">
    <dependency idref="b"/>
    <!-- some other stuff -->
  </resource>
  <resource id="b">
    <!-- some other stuff -->
  </resource>
</resources>

XSLT样式表必须处理我们感兴趣的特定资源,我称之为 root 资源,以及所有递归依赖项。依赖关系是其他资源,由id属性唯一标识。

资源是否被处理两次并不重要,尽管最好只处理一次所需的资源。处理资源的顺序也无关紧要。

root 资源及其递归依赖关系的处理非常重要。我们不能只处理所有资源并完成它。

一个天真的实现如下:

<xsl:key name="resource-id" match="resource" use="@id"/>

<xsl:template match="resource">
  <!-- do whatever is required to process the resource. -->

  <!-- then handle any dependencies -->
  <xsl:apply-templates select="key('resource-id', dependency/@idref)"/>
</xsl:template>

此实现适用于上述示例以及许多实际情况。它的缺点是它经常处理同一个资源不止一次,但如上所述,这并不是非常重要。

问题是有时资源具有循环依赖性:

<resources>
  <resource id="a">
    <dependency idref="b"/>
    <dependency idref="d"/>
  </resource>
  <resource id="b">
    <dependency idref="c"/>
  </resource>
  <resource id="c">
    <dependency idref="a"/>
  </resource>
  <resource id="d"/>
</resources>

如果您使用天真的实现来处理此示例,并且首先处理 a b c ,您将获得无限递归

不幸的是,我无法控制输入数据,在任何情况下,循环依赖都是完全有效的,并且是相关规范所允许的。

我已经提出了各种部分解决方案,但在所有情况下都没有。

理想的解决方案是防止节点被多次处理的一般方法,但我不认为这是可能的。事实上,我怀疑整个问题是不可能解决的。

如果有帮助,我可以使用大部分EXSLT(包括功能)。如果有必要,我还可以使用任意数量的其他XSLT脚本预处理输入,但最好不要对输出中不会产生的资源进行过多的预处理。

我不能做的是切换到用另一种语言处理它(至少没有大量的重新设计)。我也不能使用XSLT 2.0。

有什么想法吗?

2 个答案:

答案 0 :(得分:3)

这是一个简单的解决方案

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:param name="pRootResourceId" select="'a'"/>

 <xsl:key name="kResById" match="resource" use="@id"/>

 <xsl:template match="/">
  <resourceProcessing root="{$pRootResourceId}">
    <xsl:apply-templates select=
    "key('kResById', $pRootResourceId)"/>
  </resourceProcessing>
 </xsl:template>

 <xsl:template match="resource">
  <xsl:param name="pVisited" select="'|'"/>

  <xsl:copy>
    <xsl:copy-of select="@*"/>

    <xsl:apply-templates select=
      "key('kResById',
           dependency/@idref
                [not(contains($pVisited, concat('|', ., '|')))])">
      <xsl:with-param name="pVisited"
       select="concat($pVisited, @id, '|')"/>
    </xsl:apply-templates>
  </xsl:copy>
 </xsl:template>
</xsl:stylesheet>

应用于提供的XML文档

<resources>
  <resource id="a">
    <dependency idref="b"/>
    <dependency idref="d"/>
  </resource>
  <resource id="b">
    <dependency idref="c"/>
  </resource>
  <resource id="c">
    <dependency idref="a"/>
  </resource>
  <resource id="d"/>
</resources>

产生了想要的正确结果

<resourceProcessing root="a">
   <resource id="a">
      <resource id="b">
         <resource id="c"/>
      </resource>
      <resource id="d"/>
   </resource>
</resourceProcessing>

主要思路很简单:维护已访问资源的ID列表,只有在新资源的ID不在列表中时才允许处理。 “处理”用于演示目的,并输出请求包装其所依赖的所有其他请求(递归)。

另请注意每个request仅处理一次。

多年前,我为图遍历问题提供了类似的解决方案 - 可以在xml-dev组档案中找到 - here 。 :)

答案 1 :(得分:2)

只是为了好玩,另一种解决方案(遵循Dimitre),但增加了一个带有访问节点的节点集。我发布了两个样式表,一个是节点集逻辑,另一个是节点集比较,因为你必须对大型XML输入进行更快的测试。

所以,这个样式表:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <xsl:param name="pRootResourceId" select="'a'"/>
    <xsl:key name="kResById" match="resource" use="@id"/>
    <xsl:template match="/" name="resource">
        <xsl:param name="pVisited" select="key('kResById', $pRootResourceId)"/>
        <xsl:param name="pNew" select="key('kResById',$pVisited/dependency/@idref)"/>
        <xsl:choose>
            <xsl:when test="$pNew">
                <xsl:call-template name="resource">
                    <xsl:with-param name="pVisited" select="$pVisited|$pNew"/>
                    <xsl:with-param name="pNew" select="key('kResById',
           $pNew/dependency/@idref)[not(@id=($pVisited|$pNew)/@id)]"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <result>
                    <xsl:copy-of select="$pVisited"/>
                </result>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

这个样式表:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <xsl:param name="pRootResourceId" select="'a'"/>
    <xsl:key name="kResById" match="resource" use="@id"/>
    <xsl:template match="/" name="resource">
        <xsl:param name="pVisited" select="key('kResById', $pRootResourceId)"/>
        <xsl:param name="pNew" select="key('kResById', $pVisited/dependency/@idref)"/>
        <xsl:variable name="vAll" select="$pVisited|$pNew"/>
        <xsl:choose>
            <xsl:when test="$pNew">
                <xsl:call-template name="resource">
                    <xsl:with-param name="pVisited" select="$vAll"/>
                    <xsl:with-param name="pNew" select="key('kResById',
           $pNew/dependency/@idref)[count(.|$vAll)>count($vAll)]"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <result>
                    <xsl:copy-of select="$pVisited"/>
                </result>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

两个输出:

(首先输入)

<result>
    <resource id="a">
        <dependency idref="b" />
        <!-- some other stuff -->
    </resource>
    <resource id="b">
        <!-- some other stuff -->
    </resource>
</result>

(使用上次输入)

<result>
    <resource id="a">
        <dependency idref="b" />
        <dependency idref="d" />
    </resource>
    <resource id="b">
        <dependency idref="c" />
    </resource>
    <resource id="c">
        <dependency idref="a" />
    </resource>
    <resource id="d" />
</result>