在内存中的XQuery中多次编辑相同的文档节点

时间:2016-01-04 13:22:11

标签: xml xpath functional-programming xquery saxon

我们说我有这样的文件:

    <root>
    <content>
        <z>valZ</z>
        <a>
            <b>
                <c>valC</c>
            </b>
            <b>
                <c>valC</c>
            </b>
        </a>
        <a>
            <d>valD</d>
        </a>
    </content>
</root>

节点数&#34; a&#34;可以从1到某个未定义的数字不高于30 &#34; b&#34; ,&#34; c&#34;和&#34; d&#34;节点可以是0到某个未定义的数字,也不高于20

我在XQuery中需要做的是获取节点的价值&#34; z&#34;并将其复制到每个现有节点&#34; b&#34;所以每次结构看起来都像这样:

   <root>
    <content>
        <z>valZ</z>
        <a>
            <b>
                <c>valC</c>
                <z>valZ</z>
            </b>
            <b>
                <c>valC</c>
                <z>valZ</z>
            </b>
        </a>
        <a>
            <d>valD</d>
            <b>             <!-- <b> was not present here before -->
                <z>valZ</z>
            </b>
        </a>
    </content>
</root>

如果甚至有一个区块&#34; b&#34;,我不需要再创建另一个区块&#34; z&#34;它内部(或多个&#34; b&#34;如果存在多于1个),否则在每个&#34; a&#34;我需要创建一个新的。

好像很容易?在XQuery以外的任何其他语言中,我都同意 - 你只需要获取文档,复制&#34; z&#34; ,遍历整个文档,寻找每一个&#34; a&#34;, 检查这些是否发现&#34; a&#34;有&#34; b&#34;,如果没有创建&#34; b&#34;并且在&#34; b&#34;创造新的&#34; z&#34;复制值。

但是我在迭代同一个文档并使用inmemupdate.xq库更新它时很困难。以下是我用于它的代码片段:

    declare function changeSourceForFieldZ($rootDoc as document-node()) as document-node()?  {
    let $value := getValueOfFieldZ($rootDoc)
    return populateBLevelIfValueExists($rootDoc, $value)
    };

    declare %private function getValueOfFieldZ($rootDoc as document-node()) as text()? {
    $rootDoc/*:root/*:content/*:z/text()
    };

    declare %private function populateBLevelIfValueExists($rootDoc as document-node(), $value as text()?) as document-node()? {
    if(fn:exists($value)) then
        addField($enrichment, $value)
    else
        $rootDoc
    };


    declare %private function addField($rootDoc as document-node(), $value as text()) as document-node()? {
    if (hasBLevel($rootDoc)) then
        insertNodeInBLevel($rootDoc, $value)
    else if (hasALevel($rootDoc)) then
        insertNodeInALevel($rootDoc, $value)
    else ()
    };

    declare %private function hasBLevel($rootDoc as document-node()) as xs:boolean {
    fn:exists($rootDoc/*:root/*:content/*:a/*:b)
    };


    declare %private function hasALevel($rootDoc as document-node()) as xs:boolean {
    fn:exists($rootDoc/*:root/*:content/*:a)
    };

    declare %private function createTagWithZField($value as text()) {
    <z>{$value}</z>
    };

    declare %private function createWholeBtagBlock($value as text()) {
    (
        <b>
            <z>{$value}</z>
        </b>
    )
    };

问题是显然插入方法。如前所述,我使用mem库,更确切地说是一个函数:

   declare function mem:node-insert-child(
    $parentNode as element(),
    $newNode as node()*
    ) as node()

如果我像这样编写insertNodeInBLevel和insertNodeInALevel:

    declare %private function insertNodeInBLevel($rootDoc as document-node(), $value as text()) {
    mem:node-insert-child($rootDoc/*:root/*:content/*:a/*:b, createTagWithZField($value))
    };

    declare %private function insertNodeInALevel($rootDoc as document-node(), $value as text()) {
    mem:node-insert-child($rootDoc/*:root/*:content/*:a, createWholeBtagBlock($value))
    };

它返回了rootDoc文档的多个副本,其中的值被添加到不同的位置,而不是一个文档,并且在所有位置都添加了节点。

我尝试了许多解决方案,包括递归和循环:

   declare %private function insertNodeInBLevel($rootDoc as document-node(), $value as text()) {
    if(fn:exists($rootDoc/*:root/*:content/*:a/*:b)) then
        let $nodes := $rootDoc/*:root/*:content/*:a/*:b
        for $node at $index in $nodes
            let $rootDoc := exampleInsertWithIndex($rootDoc, $value, $index)

        return $rootDoc 
    };

    declare %private function exampleInsertWithIndex($rootDoc as document-node(), $value as text(), $index) {
    mem:node-insert-child($rootDoc/*:root/*:content/*:a/*:b[$index], createTagWithZField($value))
    };

但是,嗯,值是不可变的,所以不能第二次保存到同一个rootDoc等等......任何想法如何解决这个问题,所以我将编辑同一文档的多个节点并仅返回这一个,不是它的副本?我是面向对象语言的开发者,功能性语言对我来说很新,他们遵循不同的范例,因此我对解决方案的思考方式可能存在缺陷......

3 个答案:

答案 0 :(得分:4)

这可以通过typeswitch和这些元素的一些自定义逻辑来实现

declare function local:transform($nodes as node()*) as item()* {
    for $node in $nodes
    return 
        typeswitch($node)
            case text() return $node
            case comment() return $node
            case processing-instruction() return $node
            case attribute() return $node
            case element(a) return local:transform-a($node)
            case element(b) return local:transform-b($node)
            default return local:identity($node)
};

declare function local:transform-a($a as element(a)) as element(a) {
    element a {
        local:transform($a/(@* | node())), 
        if(not(exists($a/b))) then
            element b { root($a[1])/content/z }
        else ()
    }
};

declare function local:transform-b($b as element(b)) as element(b) {
    element b {
        local:transform($b/(@* | node())), 
        if(not(exists($b/z))) then 
          root($b[1])/content/z
        else ()
    }
};

declare function local:identity($node as element()*) as item()* {
    element {name($node)} {($node/@*, local:transform($node/node()))}
};

答案 1 :(得分:4)

在XSLT中这种事情要容易得多!

<xsl:mode on-no-match="shallow-copy"/>

<xsl:template match="a[not(b)]">
  <a>
    <xsl:apply-templates/>
    <b><xsl:copy-of select="preceding-sibling::z"/></b>
  </a>
</xsl:template>

<xsl:template match="b">
  <b>
    <xsl:apply-templates/>
    <xsl:copy-of select="../preceding-sibling::z"/>
  </b>
</xsl:template>

答案 2 :(得分:2)

如果您能够使用XQuery Update(我认为这需要Saxon的商业变体,但也有其他实现支持XQuery Update),只需使用转换并将<z/>节点插入每个<b/>节点:

let $z := //z
return
  copy $result := /root
  modify
    for $node in $result//b
    return insert node $z into $node
  return $result

如果没有,递归地遍历树并重新构建它,同时根据需要修改它是XQuery中常见的模式:

declare function local:insert-z($subtree as element(), $z as element()) as element() {
   element {node-name($subtree)}
      {$subtree/@*,
          for $node in $subtree/node()
              return
               if ($node instance of element())
                 then
                   (
                     local:insert-z($node, $z),
                     if ($subtree/self::b)
                     then $z
                     else ()
                   )
                 else $node
      }
};

local:insert-z(/root, //z)

有一个示例列表,如何在XQuery wikibook中为不同的用例使用此模式。